Compare commits

...

506 Commits

Author SHA1 Message Date
Vitor Pamplona
69099728b2 v1.07.3 2026-03-31 17:32:23 -04:00
Vitor Pamplona
190b99d55a Merge pull request #2060 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 17:31:30 -04:00
Crowdin Bot
95adb53640 New Crowdin translations by GitHub Action 2026-03-31 21:29:37 +00:00
davotoula
25a4b68958 update translations: CZ, DE, PT, SE 2026-03-31 23:26:48 +02:00
David Kaspar
e91c793f73 Merge pull request #2059 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 23:21:41 +02:00
David Kaspar
5b477c64cf Merge branch 'main' into l10n_crowdin_translations 2026-03-31 23:21:32 +02:00
Crowdin Bot
11ca2c14be New Crowdin translations by GitHub Action 2026-03-31 21:07:39 +00:00
Vitor Pamplona
1b39bf1d52 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-31 17:05:26 -04:00
Vitor Pamplona
a4e3625f26 Migrates to the new 10008 profile badges kind 2026-03-31 16:52:24 -04:00
Crowdin Bot
e75b5d173b New Crowdin translations by GitHub Action 2026-03-31 20:25:02 +00:00
Vitor Pamplona
027cc660e2 Merge pull request #2058 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 16:24:28 -04:00
Vitor Pamplona
49850e773f Adds a space after inserting the username to avoid issues in some keyboards. 2026-03-31 16:22:57 -04:00
Vitor Pamplona
44d100f60a Fixes file name 2026-03-31 16:11:45 -04:00
Crowdin Bot
f110436853 New Crowdin translations by GitHub Action 2026-03-31 19:47:41 +00:00
Vitor Pamplona
aebbe0974a Fixes wrong filter 2026-03-31 15:45:34 -04:00
Vitor Pamplona
a79ceb527f Adding filters by community and location to the new side feeds 2026-03-31 15:23:50 -04:00
Vitor Pamplona
29bb0d2682 Dynamic filters for Longs, Shorts, Polls and Pictures 2026-03-31 14:44:56 -04:00
Vitor Pamplona
794af6eb2b Adds Long Video Menu item 2026-03-31 13:24:55 -04:00
Vitor Pamplona
3e83762761 Adds new TopNav Filter settings 2026-03-31 13:24:37 -04:00
Vitor Pamplona
c6e1dc9d79 Fixes dal filters for Shorts 2026-03-31 12:58:53 -04:00
Vitor Pamplona
6354b32205 Adds a Shorts Feed 2026-03-31 12:22:33 -04:00
Vitor Pamplona
6618644ba3 No more pages in the video tab 2026-03-31 11:45:08 -04:00
Vitor Pamplona
329d133291 Merge pull request #2057 from vitorpamplona/claude/instagram-picture-feed-9sBzo
Add Pictures feed screen with NIP-68 picture event support
2026-03-31 10:45:00 -04:00
Vitor Pamplona
3fec18b9a8 Improvements to the old bookmarks screen 2026-03-31 10:40:24 -04:00
Vitor Pamplona
de395f5629 Merge pull request #2056 from vitorpamplona/claude/migrate-bookmark-event-86Biv
Add support for legacy NIP-51 bookmark list (kind 30001)
2026-03-31 10:22:24 -04:00
Claude
55a1626159 fix: merge old bookmarks into existing new bookmarks during migration
Instead of replacing the new bookmark list, the migration now checks for
an existing kind 10003 event and merges old bookmarks into it, skipping
duplicates that already exist.

https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
2026-03-31 13:31:29 +00:00
Vitor Pamplona
581f13eea8 This code is already running on IO 2026-03-31 09:25:41 -04:00
Claude
b960a4692a feat: migrate BookmarkListEvent from kind 30001 to 10003
Rename the existing BookmarkListEvent (kind 30001) to OldBookmarkListEvent
and create a new BookmarkListEvent with kind 10003 as per the updated spec.

- OldBookmarkListEvent (kind 30001): Kept as-is for backward compatibility,
  displayed as "Old Bookmarks" in the Bookmark Lists screen
- BookmarkListEvent (kind 10003): New replaceable event, all new bookmark
  operations (save/check/remove) use this kind
- Both kinds are displayed as separate items in the Bookmark Lists screen
- Old Bookmarks screen includes a "Move All to New Bookmarks" button that
  migrates public bookmarks to public and private to private
- Created PrivateReplaceableTagArrayEvent base class for replaceable events
  with encrypted private tags (kind 10003 is in the 10000-19999 range)
- Updated all relay filters, search queries, and profile views to fetch
  both kinds
- Updated Android and Desktop modules

https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
2026-03-31 13:24:27 +00:00
Vitor Pamplona
4256e3f3cf Merge pull request #2055 from vitorpamplona/claude/migrate-quartz-nip58-XQ8bw
Refactor NIP-58 Badge events with improved tag handling
2026-03-31 09:17:19 -04:00
Vitor Pamplona
f291bb126a Merge branch 'main' into claude/migrate-quartz-nip58-XQ8bw 2026-03-31 09:17:11 -04:00
Vitor Pamplona
e5c8e6dd7f Merge pull request #2054 from vitorpamplona/claude/edit-field-visual-transform-GDY9G
Refactor user mention parsing to support nprofile and nostr: URIs
2026-03-31 09:09:59 -04:00
Vitor Pamplona
bbeb2a66ee Fixes bad merge 2026-03-31 09:06:12 -04:00
Vitor Pamplona
7ec5357653 Lint 2026-03-31 09:04:42 -04:00
Vitor Pamplona
96a2645070 Merge pull request #2053 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 09:02:17 -04:00
Crowdin Bot
bc8b73e458 New Crowdin translations by GitHub Action 2026-03-31 13:01:46 +00:00
Vitor Pamplona
da96180a10 Merge pull request #2046 from vitorpamplona/claude/add-reaction-notifications-NzyLO
Add push notifications for post reactions
2026-03-31 09:01:43 -04:00
Vitor Pamplona
4dbd833c0d Merge pull request #2048 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-31 09:01:32 -04:00
Crowdin Bot
d5f27c7994 New Crowdin translations by GitHub Action 2026-03-31 13:01:19 +00:00
Claude
20425a7678 feat: add @Preview composables for picture feed UI components
Adds 6 preview functions covering:
- PictureCardCompose with title, without title, and multi-image
- PictureCardCaption with title, without title, and long content
- Uses ThemeComparisonColumn for light/dark theme comparison
- Uses mock PictureEvent JSON data consumed via LocalCache

https://claude.ai/code/session_01KtuzFmDZXk67tW8QxPqmMy
2026-03-31 13:00:29 +00:00
Vitor Pamplona
26798e2b57 Fixes assertion 2026-03-31 08:59:38 -04:00
Vitor Pamplona
9cb7f9cc21 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-31 08:57:49 -04:00
Vitor Pamplona
094adcd6f6 Merge branch 'main' into claude/add-reaction-notifications-NzyLO 2026-03-31 08:47:21 -04:00
Vitor Pamplona
3dbd79d879 Merge pull request #2052 from vitorpamplona/claude/fix-sorting-order-p23ZY
Improve sorting stability by adding secondary sort keys
2026-03-31 08:42:46 -04:00
Vitor Pamplona
126d58e2d1 Merge pull request #2051 from vitorpamplona/claude/fix-quote-cursor-position-QOw9L
Fix quote placement to begin at start of message
2026-03-31 08:41:54 -04:00
Vitor Pamplona
87d0d2e768 Merge pull request #2049 from vitorpamplona/claude/tor-exception-handling-2Qy1u
Add error handling to Tor service initialization and control
2026-03-31 08:29:03 -04:00
Vitor Pamplona
50470771b0 Merge pull request #2050 from vitorpamplona/claude/simplify-localcache-redirects-9vbb7
Refactor event consumption to use base methods directly
2026-03-31 08:28:30 -04:00
Claude
acd0a13abf fix: catch exceptions from Tor library to prevent app crashes
Wrap all Tor library interactions in try-catch blocks so that failures
in the Tor service (JNI, control connection, bind/unbind) are logged
and gracefully degrade to TorServiceStatus.Off instead of crashing
the entire app.

https://claude.ai/code/session_019JDZMTvVdJyc9z2WmoAimv
2026-03-31 12:07:09 +00:00
David Kaspar
bb6bd3f0ae Merge pull request #1910 from davotoula/chess-enhancements-and-bug-fixes
Chess enhancements and bug fixes
2026-03-31 09:06:36 +02:00
davotoula
4d1e9cb8d5 Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-31 07:26:39 +02:00
Claude
abeccb23a2 feat: support npub and nprofile with nostr: prefix in edit field visual transformation
The visual transformation for message edit fields now processes nostr:npub1...,
nostr:nprofile1..., and @nprofile1... mentions in addition to the existing @npub1...
pattern. Both UrlUserTagOutputTransformation (new API) and UrlUserTagTransformation
(old API) are updated to resolve these to user display names.

https://claude.ai/code/session_012ijrLmft5PjcQ64zDwXJWj
2026-03-31 04:23:50 +00:00
Claude
d57c03a01a feat: add Instagram-style picture feed for kind 20 events
Implements a dedicated picture feed screen with a custom card layout:
- Full-width image display with user avatar and name header
- Title and 3-line content preview below the image
- Reaction row (reply, boost, like, zap, share) at the bottom
- Top bar with follow list filter (same pattern as polls feed)
- Complete relay subscription pipeline for picture events
- Accessible from the navigation drawer

https://claude.ai/code/session_01KtuzFmDZXk67tW8QxPqmMy
2026-03-31 04:10:27 +00:00
Claude
be1ad9cdb8 refactor: remove ~94 redirect consume functions from LocalCache
Replace typed consume() overloads that simply forwarded to
consumeRegularEvent() or consumeBaseReplaceable() with direct calls
in the when dispatch block. This removes ~600 lines of boilerplate
without changing behavior.

https://claude.ai/code/session_017dM6dt1on3g3yhWSi69Pwq
2026-03-31 04:06:02 +00:00
Claude
3a850ba26c fix: place cursor at beginning of text field when quoting a note
When a user hits Quote, the new post screen opens with the nostr URI
pre-filled but the cursor was at the end (after the URI). This moves
the cursor to the beginning so the user can immediately start typing
their commentary.

https://claude.ai/code/session_01RPseKC9GJ8hs3GGBR85ezw
2026-03-31 04:01:05 +00:00
Claude
96c4092138 fix: ensure secondary sort by id ascending when sorting by createdAt descending
When events share the same createdAt timestamp, the tiebreaker sort by id
must be ascending to produce a stable, deterministic order. Fixed several
locations that either had no secondary sort, used descending id sort, or
used .reversed() which incorrectly flipped both sort directions.

https://claude.ai/code/session_01RvuyPf1x9wLf2DCgsSXLGz
2026-03-31 03:45:24 +00:00
Claude
a4bf093cb3 refactor: migrate NIP-58 badges to nip88Polls-style sub-package structure
Restructures the flat nip58Badges package into sub-packages following
the established nip88Polls pattern with proper tag classes, TagArrayExt,
and TagArrayBuilderExt for each event type:

- definition/ - BadgeDefinitionEvent (kind 30009) with NameTag, ImageTag,
  DescriptionTag, ThumbTag supporting dimensions per spec
- award/ - BadgeAwardEvent (kind 8) with build() method
- profiles/ - BadgeProfilesEvent (kind 30008) with AcceptedBadge tag
  for proper paired a+e tag parsing per NIP-58 spec

Adds build() companion methods to all three event types and full
spec compliance including image/thumb dimension support.

https://claude.ai/code/session_019Uvxfa7jJbjeprLxECoJ8s
2026-03-31 03:08:25 +00:00
Vitor Pamplona
c5edcab051 Fixes the loading of old labeled bookmarks 2026-03-30 22:03:51 -04:00
Claude
a7f60082f0 feat: add push notifications for reactions
Add a new notification channel for reactions (kind 7 events) so users
get notified when someone reacts to their posts. Follows the same
pattern as zap notifications with deduplication, time-window filtering,
and grouped summaries.

https://claude.ai/code/session_01J9rAA4oeFEfNcYD6GoJqqt
2026-03-30 21:51:03 +00:00
Vitor Pamplona
91f5dffc6d Merge pull request #2045 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 16:52:24 -04:00
Vitor Pamplona
8e4ee7a618 v1.07.2 2026-03-30 16:46:01 -04:00
Crowdin Bot
332bfcd88a New Crowdin translations by GitHub Action 2026-03-30 20:39:46 +00:00
Vitor Pamplona
1872a5879e Merge pull request #2042 from greenart7c3/main
Assign `rejected` based on the presence of the "rejected" key in intent extras rather than retrieving its specific boolean value
2026-03-30 16:39:13 -04:00
Vitor Pamplona
a293976501 Merge pull request #2043 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 16:38:44 -04:00
Vitor Pamplona
a1095b4c17 Merge pull request #2044 from vitorpamplona/claude/add-gif-keyboard-support-ahCJX
Add image paste support to message input field
2026-03-30 16:38:14 -04:00
Claude
95d678f62f feat: Add GIF keyboard support to short note post screen
Enable the message field to receive images/GIFs from Android's keyboard
using Compose Foundation's contentReceiver API. When content is received,
it's routed through the existing selectImage flow for upload.

https://claude.ai/code/session_019GfY3MtCPcYn3wqTHCMSNu
2026-03-30 20:33:55 +00:00
Crowdin Bot
b625ac6d2d New Crowdin translations by GitHub Action 2026-03-30 20:26:29 +00:00
Vitor Pamplona
f848ad20d8 Merge pull request #2041 from greenart7c3/fix_external_signer
fix: support external signers wrapped in NostrSignerWithClientTag
2026-03-30 16:24:56 -04:00
greenart7c3
198e049873 Assign rejected based on the presence of the "rejected" key in intent extras rather than retrieving its specific boolean value 2026-03-30 17:18:02 -03:00
greenart7c3
f2d007f9ba fix: support external signers wrapped in NostrSignerWithClientTag
The external signer registration logic was failing when the signer was
wrapped in a `NostrSignerWithClientTag`. Fix by:

1. Updating `ListenToExternalSignerIfNeeded` to unwrap the signer if it's
   a `NostrSignerWithClientTag` containing an `IActivityLauncher`
2. Using the unwrapped launcher for intent registration and response handling
3. Renaming the intent callback to `intentLauncher` to avoid shadowing the `ActivityResultLauncher` variable
2026-03-30 17:09:59 -03:00
Vitor Pamplona
4b9c3f239c Fixes version mismatch 2026-03-30 16:02:55 -04:00
Vitor Pamplona
3ac04cdc63 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 15:59:14 -04:00
Vitor Pamplona
f9d231063d Merge pull request #2040 from vitorpamplona/claude/migrate-textfield-state-6ezEZ
Migrate text fields from TextFieldValue to TextFieldState
2026-03-30 15:58:19 -04:00
Vitor Pamplona
4bca74888d Merge pull request #2039 from vitorpamplona/claude/relay-manager-user-search-jivZ2
Replace hex input with user search dialog in relay management
2026-03-30 15:46:03 -04:00
Vitor Pamplona
17ff27bac9 Merge pull request #2038 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 15:00:48 -04:00
Vitor Pamplona
3913384fcf Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 15:00:32 -04:00
Vitor Pamplona
b02b8c4ed9 new change logs 2026-03-30 14:57:13 -04:00
Crowdin Bot
ed31b12e3f New Crowdin translations by GitHub Action 2026-03-30 18:50:03 +00:00
Vitor Pamplona
3b63264927 Merge pull request #2036 from Nam0101/contribai/docs/undocumented-identity-based-comparison-i
Docs: Undocumented identity-based comparison in `equalImmutableLists`
2026-03-30 14:48:27 -04:00
Vitor Pamplona
640c47e29f v1.07.1 2026-03-30 13:53:25 -04:00
Vitor Pamplona
3dd1b949eb Merge pull request #2037 from vitorpamplona/claude/threadsafe-without-synchronized-Zw8Tx
Replace synchronized queue with Channel and AtomicBoolean
2026-03-30 13:51:20 -04:00
Claude
13bd492e32 refactor: replace synchronized with lock-free CAS in BleChunkAssembler
Use AtomicReference holding an immutable list with a compare-and-set loop,
eliminating the synchronized block while maintaining thread safety.

https://claude.ai/code/session_01KLRz7FJgWRtgCcZe3qD1VX
2026-03-30 17:48:58 +00:00
Claude
ef8b9be0e8 refactor: replace synchronized with Channel + AtomicBoolean in BLE relay classes
Use Channel(UNLIMITED) as a lock-free send queue and AtomicBoolean with
atomic exchange for the isSending flag, eliminating all synchronized blocks
while maintaining thread safety via a double-check pattern.

https://claude.ai/code/session_01KLRz7FJgWRtgCcZe3qD1VX
2026-03-30 17:37:23 +00:00
Vitor Pamplona
c44f59b1ce Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-30 12:30:54 -04:00
Vitor Pamplona
0c4e5be1ea v1.07.0 2026-03-30 12:29:41 -04:00
Nguyen Van Nam
bc491b154b docs: undocumented identity-based comparison in equalimmutablelists
`equalImmutableLists` is public and uses referential equality (`===`) per element rather than structural equality (`==`). Without documentation, callers may assume normal list equality and get false negatives for value-equal but distinct instances. This is a non-obvious contract and should be documented explicitly to prevent subtle logic bugs.


Affected files: ListUtils.kt

Signed-off-by: Nguyen Van Nam <nam.nv205106@gmail.com>
2026-03-30 22:48:41 +07:00
davotoula
a7259b21ee Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-30 17:40:40 +02:00
davotoula
06fae3ba31 remove unused imports 2026-03-30 17:26:21 +02:00
Vitor Pamplona
ea7a839568 Merge pull request #2035 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 11:13:44 -04:00
Crowdin Bot
6912a120f8 New Crowdin translations by GitHub Action 2026-03-30 15:12:36 +00:00
davotoula
3d0364ec5e update translations: CZ, DE, PT, SE 2026-03-30 17:09:39 +02:00
davotoula
f76fc23840 update logging to new style 2026-03-30 16:42:38 +02:00
davotoula
43914d655c chore: add .worktrees/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 15:52:02 +02:00
David Kaspar
e9a33a88ca Merge pull request #2032 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 15:11:05 +02:00
davotoula
40ce80786e update logging to new style 2026-03-30 14:50:42 +02:00
Crowdin Bot
4581233c10 New Crowdin translations by GitHub Action 2026-03-30 12:50:10 +00:00
Vitor Pamplona
f81631d95c Merge pull request #2034 from vitorpamplona/claude/simplify-relay-md-docs-GXKoW
Simplify RELAY.md documentation for clarity and brevity
2026-03-30 08:48:28 -04:00
Vitor Pamplona
5214470209 Merge pull request #2033 from vitorpamplona/claude/create-nostr-readme-BUScG
Add comprehensive Nostr client documentation guide
2026-03-30 08:48:14 -04:00
Claude
f65fd30728 docs: simplify RELAY.md by removing verbose sections
Remove architecture diagram, NIP support table, module table,
Live Subscriptions explanation, serve method details, production-ready
example, and excessive code comments. Keep the readable Quick Start,
Store options, and Policy documentation in a more concise form.

https://claude.ai/code/session_01U3iW3eRD7bfLwrM3L9gkDc
2026-03-30 12:46:38 +00:00
David Kaspar
4eb03f66f9 Merge pull request #2029 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-30 14:42:13 +02:00
Crowdin Bot
7ea9f72a0a New Crowdin translations by GitHub Action 2026-03-30 12:38:18 +00:00
Vitor Pamplona
a468f3b6d6 Merge pull request #2031 from vitorpamplona/claude/simplify-relay-api-Zq3o8
Add AutoCloseable support and simplify relay connection lifecycle
2026-03-30 08:36:44 -04:00
Claude
df88cab92d refactor: simplify relay API with AutoCloseable and serve() helper
- NostrServer and IEventStore implement AutoCloseable for .use {} support
- Add NostrServer.serve() to handle session lifecycle automatically
- IRelayPolicy + operator now returns PolicyStack instead of List
- Deprecate shutdown() in favor of close()
- Update RELAY.md guide to use simplified API patterns

https://claude.ai/code/session_013oL9PkQaFyNQHKVg2vw9qs
2026-03-30 12:35:27 +00:00
Claude
3d400d4197 docs: add CLIENT.md guide for building Nostr clients with Quartz
Companion to the existing RELAY.md, this guide covers the client side:
Ktor WebSocket transport implementation, subscribing to events (Flow
and callback APIs), publishing signed events, filters, key management,
and multi-relay patterns.

https://claude.ai/code/session_01KFos33kA9VoMhKdBPNLquF
2026-03-30 12:34:51 +00:00
Vitor Pamplona
b808f5a1f6 Merge pull request #2030 from vitorpamplona/claude/nip89-compliance-modernize-BWso3
Add PlatformLinkTag parsing and NIP-89 app handler extensions
2026-03-30 08:22:36 -04:00
Vitor Pamplona
5d7f37fb41 Merge pull request #2027 from nrobi144/fix/general-bugfixes
fix: desktop bugfixes — flaky test, repost rendering, reads feed
2026-03-30 08:21:52 -04:00
Claude
5f0fd52f26 refactor: remove nip88PollApps discovery package (out of scope)
https://claude.ai/code/session_013r2LXj11SieWa5PaLesKfC
2026-03-30 12:17:03 +00:00
Vitor Pamplona
e556f7716c Merge pull request #2028 from vitorpamplona/claude/create-relay-readme-6VEdI
Add comprehensive Nostr relay implementation guide (RELAY.md)
2026-03-30 08:11:25 -04:00
davotoula
17f071b0f4 Merge branch 'main' into chess-enhancements-and-bug-fixes
# Conflicts:
#	commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessEventBroadcaster.kt
2026-03-30 13:14:33 +02:00
nrobi144
0a41806d7e fix(desktop): feed loading issues — missing events, broken profile navigation
- Profile feed now includes reposts (kind 6/16), not just text notes
- Metadata consume returns false to avoid wasteful null note lookups
- Feed subscription limits raised from 50 to 200 for global/following
- Thread replies subscription fetches NIP-22 comments (kind 1111)
- Cache now handles CommentEvent for thread display
- Wire onNavigateToThread through UserProfileScreen so tapping notes opens threads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:33:18 +03:00
nrobi144
882c39fd0e fix(cache): use getOrCreateNote for reply linking to fix flaky thread test
Fixes #2001 — ThreadFilter test failed intermittently because
consumeTextNote used getNoteIfExists for reply-to linking. If the
reply event arrived before the root, the link was lost. Now uses
getOrCreateNote to create placeholders, same pattern as reposts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:44:24 +03:00
nrobi144
2b3005797e feat(desktop): render reposts and quoted notes in feed
- Extract GenericRepostLayout + BoostedMark to commons for cross-platform use
- Feed filters accept kind 6/16 reposts with deduplication by original note
- Relay subscriptions request kinds 1, 6, 16
- Cache consumes GenericRepostEvent (kind 16) and uses getOrCreateNote for
  repost originals to handle out-of-order arrival
- FeedNoteCard renders reposts with overlapping avatars + "Boosted" label
- QuotedNoteEmbed renders nostr:nevent/note references as embedded NoteCards
  with reactive metadata observation
- Direct relay subscriptions fetch missing referenced notes and author metadata
- FeedMetadataCoordinator routes all events (not just metadata) back to cache
- consumeMetadata invalidates note flows when author metadata arrives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:44:24 +03:00
nrobi144
0e8c6fa434 fix(cache): route ReadsScreen following-mode events through cache
Following-mode long-form feed was not calling consumeEvent(),
so LongTextNoteEvents never reached the cache. Back-navigation
showed empty reads because cache had nothing to seed from.
2026-03-30 11:44:23 +03:00
Claude
4ba92931f6 docs: add relay README for building with Ktor, NostrServer, and SQLite EventStore
Comprehensive guide covering the full relay stack: NostrServer setup,
SQLite EventStore configuration, Ktor WebSocket integration, policy
system (VerifyPolicy, FullAuthPolicy, PolicyStack), indexing strategies,
testing patterns, and NIP support matrix.

https://claude.ai/code/session_01CTyiKDNhgXdfCFBBBXf1NF
2026-03-30 04:13:53 +00:00
Claude
940e59b323 feat: NIP-89 compliance fixes, modernize package structure, link NIP-88 polls
Fix PlatformLinkTag.parse() off-by-one bug where tag indices were shifted
(platform/uri/entityType read from wrong positions). Add missing TagArrayExt
and EventExt files for definition and recommendation packages to align with
the modern pattern used by nip88Polls. Parameterize DiscoverNIP89FeedFilter
by targetKind and create NIP-89 poll app discovery infrastructure linking
NIP-88 polls (kind 1068) to the NIP-89 app handler discovery system.

https://claude.ai/code/session_013r2LXj11SieWa5PaLesKfC
2026-03-30 03:49:21 +00:00
Claude
4d8583eed9 feat: replace hex input with user search in relay manager allow/ban dialogs
Users can now search by name, npub, or NIP-05 identifier when adding
users to relay allow/ban lists, using the same UserSuggestionState
mechanism as the My List screens. The hex pubkey input was not
user-friendly since most users don't know their hex representation.

https://claude.ai/code/session_01SWReoziLKJKxrbwDWprQYs
2026-03-30 03:43:51 +00:00
Vitor Pamplona
35d677f531 Merge pull request #2025 from vitorpamplona/claude/nip90-dvm-kinds-itxXb
feat: add all NIP-90 DVM kind event classes from data-vending-machines spec
2026-03-29 23:28:15 -04:00
Claude
273af03c9f feat: add reader methods to all DVM request events for server processing
Each request event now exposes typed reader methods so DVM servers can
extract inputs, parameters, and configuration from incoming requests:
- inputs() returns List<InputTag> with value, type, relay, marker
- Kind-specific readers like language(), model(), searchQuery(), pow(), etc.
- Shared TagArrayExt with dvmParam(), dvmParamAll(), dvmParamValues()

Also adds readers to existing 5300/5301 events (dvmPubKey, user, relays).

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 03:21:19 +00:00
Claude
cd4b352bdf feat: add typed build parameters and shared DVM tag infrastructure
Add shared tag classes (InputTag, OutputTag) and TagArrayBuilder
extensions (inputUrl, inputText, inputEvent, inputJob, inputPrompt,
output, param) for all DVM kinds to reuse.

Update all 17 request event build() methods with typed parameters
matching their spec:
- 5000: inputUrl, outputMimeType, range, alignment
- 5001: eventIds, length, outputMimeType
- 5002: eventIds, language
- 5050: prompt, model, maxTokens, temperature, topK, topP, frequencyPenalty
- 5100: prompt, sourceImageUrl, model, lora, ratio, size, negativePrompt
- 5200: videoUrl, outputMimeType, range
- 5201: videoUrl, language, subtitle, range, outputFormat
- 5202: imageUrl
- 5250: text, language
- 5302: searchQuery, users, since, until, maxResults
- 5303: searchQuery, maxResults
- 5400: inputs, relays, groups, filterJson
- 5500: fileUrl
- 5900: eventId
- 5901: text
- 5905: eventJson, relays
- 5970: eventJson, pow

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 03:07:07 +00:00
Vitor Pamplona
ea2054fa56 Merge pull request #2024 from vitorpamplona/claude/update-nip-support-tUrxB
feat: update NIP support list to reflect current implementation status
2026-03-29 22:55:53 -04:00
Claude
fe9f2e71a0 feat: update NIP support list to reflect current implementation status
Cross-referenced the official NIP list from nostr-protocol/nips with
quartz package implementations. All NIPs are now supported except NIP-EE
(MLS Protocol). Added NIP-22, NIP-5A, NIP-B0. Removed merged NIPs
(NIP-12, NIP-16, NIP-20). Updated names to match official titles.

https://claude.ai/code/session_01TyGpa2VEryZ9aqmZvmE4v1
2026-03-30 02:47:10 +00:00
Claude
4f32776b24 feat: add all NIP-90 DVM kind event classes from data-vending-machines spec
Add request/response event classes for all 19 DVM kinds defined in
nostr-protocol/data-vending-machines: text extraction (5000/6000),
summarization (5001/6001), translation (5002/6002), text generation
(5050/6050), image generation (5100/6100), video conversion (5200/6200),
video translation (5201/6201), image-to-video (5202/6202), text-to-speech
(5250/6250), content search (5302/6302), people search (5303/6303),
event count (5400/6400), malware scanning (5500/6500), event timestamping
(5900/6900), OP_RETURN creation (5901/6901), event publish schedule
(5905/6905), and event PoW delegation (5970/6970).

Also fixes existing events:
- Content discovery response (6300) now parses both "a" and "e" tags
- User discovery response (6301) now has innerTags() to parse "p" tags

All new events registered in EventFactory.

https://claude.ai/code/session_017aamEDeRtQ2H9u5U9B5F1C
2026-03-30 02:45:54 +00:00
Vitor Pamplona
21488561e0 Merge pull request #2023 from vitorpamplona/claude/refactor-nip90-structure-ugeNj
Refactor NIP90 DVMs: reorganize packages and extract tag classes
2026-03-29 22:26:02 -04:00
Vitor Pamplona
570f7d787d Merge pull request #2022 from vitorpamplona/claude/nostr-ble-messaging-i2rrJ
Add NIP-BE Bluetooth Low Energy mesh networking support
2026-03-29 22:24:43 -04:00
Claude
6e7f6eccf1 refactor: reorganize NIP-BE into subpackages and add README
Restructured nipBEBle/ into logical subpackages:
- Root: public API surface (BleNostrMesh, BleConfig, BlePeer, BleRole)
- protocol/: wire format (BleMessageChunker, BleChunkAssembler)
- transport/: platform BLE abstraction (BleTransport, AndroidBleTransport)
- relay/: Nostr protocol over BLE (BleNostrClient, BleNostrServer, BleMeshManager)

Added README.md with quick start guide, package structure overview,
advanced usage examples, and wire protocol reference.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:23:49 +00:00
Claude
ce3de5caf7 refactor: restructure NIP90 DVMs to match NIP88 Polls pattern
Each event kind now gets its own subpackage with dedicated tag classes,
TagArrayExt for parsing, and TagArrayBuilderExt for building. Events use
the eventTemplate pattern instead of manual tag construction.

New structure:
- status/ (kind 7000) with StatusTag, AmountTag
- contentDiscoveryRequest/ (kind 5300) with RelaysTag, ParamTag
- contentDiscoveryResponse/ (kind 6300)
- userDiscoveryRequest/ (kind 5301)
- userDiscoveryResponse/ (kind 6301)

https://claude.ai/code/session_01JeB9nAK4SKuwKZBEd6EAPd
2026-03-30 02:22:54 +00:00
Claude
28f68382dd feat: add BleNostrMesh facade for simpler developer API
Before (4 steps, manual wiring):
  val transport = AndroidBleTransport(context)
  val mesh = BleMeshManager(transport, object : BleMeshListener { ... })
  transport.setListener(mesh)
  mesh.start()

After (2 steps, auto-wired):
  val mesh = BleNostrMesh(AndroidBleTransport(context))
  mesh.onEvent { event, peer -> saveEvent(event) }
  mesh.start()

The facade uses lambda callbacks instead of interface implementation,
and auto-wires the transport listener via AndroidBleTransportContract.
BleMeshManager remains available for advanced use cases.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:18:24 +00:00
Claude
57aec95e8d fix: align NIP-BE chunk format with KoalaSat/samiz reference implementation
Reviewed the samiz BLE implementation and fixed compatibility issues:

- Chunk index: changed from 2 bytes to 1 byte (matching samiz/NIP-BE example)
- Chunk overhead: 2 bytes total (1 index + 1 total count), not 3
- chunkSize parameter now means payload size (500), not total chunk size
- Android: TX power HIGH, advertise timeout indefinite (matching samiz)
- Android: request MTU 512 and CONNECTION_PRIORITY_HIGH on connect
- Android: discover services after MTU negotiation (samiz flow)
- Android: set WRITE_TYPE_DEFAULT on write characteristic
- MTU-to-chunkSize: properly accounts for ATT overhead (3) + chunk overhead (2)

These changes ensure wire-level compatibility with samiz devices.

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 02:12:52 +00:00
Vitor Pamplona
2d21529176 Merge pull request #2021 from vitorpamplona/claude/implement-nip-29-MEtLk
Add NIP-29 Relay Groups event types and tag support
2026-03-29 21:58:40 -04:00
Vitor Pamplona
bb7cdc973a Merge pull request #2019 from vitorpamplona/claude/implement-nip69-InF2y
Add NIP-69 P2P Order Events support
2026-03-29 21:58:33 -04:00
Claude
fb78c1a4c4 feat: implement NIP-BE Nostr BLE Communications Protocol in Quartz
Adds full NIP-BE implementation for BLE-based Nostr peer-to-peer
messaging and synchronization with KMP support across all targets.

Protocol layer (commonMain):
- BleConfig: NIP-BE constants (service UUID, characteristic UUIDs)
- BleMessageChunker: DEFLATE compression + chunk splitting/joining
- BleRole/assignRole: Role assignment based on UUID comparison
- BleTransport: Platform-agnostic BLE transport interface
- BleNostrClient: IRelayClient implementation over BLE
- BleNostrServer: Relay server over BLE with notification support
- BleMeshManager: High-level mesh manager with auto-discovery,
  role assignment, and event broadcasting
- BleChunkAssembler: Thread-safe chunk reassembly

Platform support:
- Deflate expect/actual for jvmAndroid, apple, and linux targets
- AndroidBleTransport: Full Android BLE implementation using GATT
  server/client APIs with scanning, advertising, and MTU negotiation

Tests: Deflate compression, message chunking, role assignment,
and chunk assembly (25 tests passing).

https://claude.ai/code/session_01Tz5E73Rj7tL48A3qUGS5DT
2026-03-30 01:58:26 +00:00
Vitor Pamplona
abd53c87a0 Merge pull request #2020 from vitorpamplona/claude/implement-nip-15-kMewH
Add NIP-15 Marketplace event types and data models
2026-03-29 21:58:26 -04:00
Claude
c5bd3ebcd3 feat: implement NIP-15 Nostr Marketplace protocol in Quartz
Adds support for the NIP-15 decentralized marketplace protocol with
all event kinds: StallEvent (30017), ProductEvent (30018),
MarketplaceEvent (30019), AuctionEvent (30020), BidEvent (1021),
and BidConfirmationEvent (1022). Follows NIP-88 Polls structure
with JSON content data models and tag builder extensions.

https://claude.ai/code/session_014tf8Fn7J5CNpEgRsyMnhAy
2026-03-30 01:56:01 +00:00
Vitor Pamplona
974b4fcfb8 Removing a hex scheme-less urldetector test to leave it for later. 2026-03-29 21:55:33 -04:00
Claude
f1547a020c feat: implement NIP-29 relay-based groups on Quartz
Add protocol-level support for NIP-29 relay-based groups with all
event kinds following the nip88Polls structure pattern.

Event kinds implemented:
- 9000 (put-user), 9001 (remove-user), 9002 (edit-metadata)
- 9005 (delete-event), 9007 (create-group), 9008 (delete-group)
- 9009 (create-invite), 9021 (join-request), 9022 (leave-request)
- 39000 (group-metadata), 39001 (group-admins)
- 39002 (group-members), 39003 (supported-roles)

Shared tags: GroupIdTag (h), PreviousTag, RoleTag, CodeTag, GroupAdminTag

https://claude.ai/code/session_016mcHkA5DUr4M2K5CddAGSq
2026-03-30 01:52:58 +00:00
Claude
0a25018f20 feat: implement NIP-69 P2P Order events (kind 38383) in Quartz
Add support for peer-to-peer order events as defined in NIP-69,
enabling unified liquidity pools across P2P trading platforms.

Implements P2POrderEvent as a BaseAddressableEvent with all required
tags (k, f, s, amt, fa, pm, premium, expires_at, expiration, y, z)
and optional tags (source, rating, network, layer, name, g, bond).
Follows the nip88Polls structure with per-tag classes, TagArrayExt,
and TagArrayBuilderExt. Reuses existing GeoHashTag and ExpirationTag.

https://claude.ai/code/session_01VpvrrRMLdjpB5C9Pq4VupK
2026-03-30 01:52:15 +00:00
Vitor Pamplona
59ea94c248 Fixes PlatformLog's missing file on linux 2026-03-29 21:48:56 -04:00
Vitor Pamplona
9e21fcd2e8 Fixes lack of proxy access to download VLC files in claude's web environment. 2026-03-29 21:43:54 -04:00
Vitor Pamplona
6371f2e0d0 Fixes Url 2026-03-29 21:41:57 -04:00
Vitor Pamplona
207fc6beba removes Volatile (missing import) 2026-03-29 21:30:12 -04:00
Vitor Pamplona
7068263c00 Merge pull request #2018 from vitorpamplona/claude/implement-nip88-rendering-Ps1Ea
Implement NIP-C7 chat messages (kind 9)
2026-03-29 21:24:59 -04:00
Claude
092c057e6f feat: implement NIP-C7 chat messages (kind 9)
Add ChatEvent in quartz/nipC7Chats with support for kind 9 chat
messages and q-tag based replies per the NIP-C7 specification.
Wire up rendering in NoteCompose and NoteMaster (ThreadFeedView).

https://claude.ai/code/session_01BmAMHBCRKXG612i6zrhhP4
2026-03-30 01:17:28 +00:00
Vitor Pamplona
15c4fd61f1 Merge pull request #2017 from vitorpamplona/claude/implement-nip5a-rendering-5ioVq
feat: implement NIP-5A static website event rendering
2026-03-29 20:43:09 -04:00
Vitor Pamplona
c89a02eb0d Merge branch 'main' into claude/implement-nip5a-rendering-5ioVq 2026-03-29 20:43:02 -04:00
Claude
104b836bbe refactor: remove path listing from NIP-5A rendering
Users should click through to view the website in the browser
rather than seeing a list of file paths in the note card.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-30 00:41:00 +00:00
Claude
869628ae16 Revert "refactor: redesign NIP-5A rendering as website preview card"
This reverts commit 07f16c2efe.
2026-03-30 00:37:59 +00:00
Claude
07f16c2efe refactor: redesign NIP-5A rendering as website preview card
Replace the file-listing style with a UrlPreviewCard-like layout
showing nsite host URL, title, description, and optional favicon.
Remove path rendering and unused string resources.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-30 00:25:09 +00:00
Vitor Pamplona
365a526b12 Merge pull request #2016 from vitorpamplona/claude/implement-nip7d-rendering-d4zgy
feat: implement NIP-7D thread events (kind 11)
2026-03-29 20:19:18 -04:00
Vitor Pamplona
7f4e1f153a Merge pull request #2014 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 20:17:43 -04:00
Crowdin Bot
d9471f9531 New Crowdin translations by GitHub Action 2026-03-30 00:14:57 +00:00
Vitor Pamplona
5e20806229 Merge pull request #2015 from vitorpamplona/claude/implement-nip87-rendering-PBlES
Add NIP-87 Ecash support with Cashu and Fedimint event types
2026-03-29 20:13:32 -04:00
Vitor Pamplona
f91f68a74e Merge pull request #2013 from vitorpamplona/claude/refactor-url-detector-FCLCA
Refactor URL detection and path normalization for clarity
2026-03-29 19:16:05 -04:00
Claude
c4044858a9 feat: implement NIP-87 ecash mint discoverability
Add protocol support and UI rendering for NIP-87 which defines ecash
mint discovery via three event kinds: MintRecommendationEvent (38000),
CashuMintEvent (38172), and FedimintEvent (38173).

https://claude.ai/code/session_01JR2nFVPjPGG9jTV4Qq2PUW
2026-03-29 22:25:46 +00:00
Claude
776bc6d36f feat: implement NIP-5A static website event rendering
Add Quartz protocol support for NIP-5A Pubkey Static Websites with
RootSiteEvent (kind 15128) and NamedSiteEvent (kind 35128), including
tag parsers for path, server, title, description, and source. Wire up
rendering in NoteCompose and ThreadFeedView to display site metadata.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
2026-03-29 22:18:02 +00:00
Claude
8e66776ee1 feat: implement NIP-7D thread events (kind 11)
Add support for NIP-7D thread events with title rendering in both
NoteCompose (feed view) and ThreadFeedView (detail view). Replies
use existing NIP-22 kind 1111 comments.

https://claude.ai/code/session_01MR2hLpmq3pkzzT11t86dAt
2026-03-29 22:15:21 +00:00
David Kaspar
a170335db8 Merge pull request #2012 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 21:36:21 +02:00
Crowdin Bot
59c18f2c75 New Crowdin translations by GitHub Action 2026-03-29 18:39:41 +00:00
David Kaspar
6613d1cacd Merge pull request #2011 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 20:38:25 +02:00
Crowdin Bot
bed5f96f20 New Crowdin translations by GitHub Action 2026-03-29 14:53:06 +00:00
Vitor Pamplona
c624293661 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-03-29 10:51:11 -04:00
Vitor Pamplona
9dc208deba Removing internal runBlockings 2026-03-29 10:43:38 -04:00
Vitor Pamplona
decf7df2bb Merge pull request #2007 from davotoula/ci-build-stop-daemons-after-build
Added ./gradlew --stop (with if: always()) after the Gradle steps in both test and build-desktop jobs
2026-03-29 10:21:39 -04:00
Vitor Pamplona
ebbcb603fb Fixes lazy on trimming service 2026-03-29 10:20:06 -04:00
Vitor Pamplona
8613ae4f31 Speeding up start by parallelizing access to the preference files before they are needed. 2026-03-29 10:17:51 -04:00
Vitor Pamplona
56510e4213 Merge pull request #2010 from vitorpamplona/claude/nip43-relay-members-dWO19
Add NIP-43 relay members support with UI screens
2026-03-29 10:17:29 -04:00
Vitor Pamplona
cd17badaf4 Merge pull request #2008 from vitorpamplona/claude/add-nip-61-support-KTkuk
Add NIP-61 Nutzaps support with event types and tag handling
2026-03-29 10:17:18 -04:00
Vitor Pamplona
a4ce8439de Merge branch 'main' into claude/add-nip-61-support-KTkuk 2026-03-29 10:17:11 -04:00
Vitor Pamplona
2c5f80f613 Merge pull request #2009 from vitorpamplona/claude/add-nip60-support-DOf8B
Add NIP-60 Cashu wallet and spending history event support
2026-03-29 10:16:46 -04:00
Claude
cad0ff3d80 fix: Make add/remove member events support multiple p tags and add NIP-43 to NoteMaster
Changes memberPubKey() to memberPubKeys() returning a list of all p tags
in RelayAddMemberEvent and RelayRemoveMemberEvent. Updates renderers to
display multiple members. Adds NIP-43 event rendering to ThreadFeedView
(NoteMaster) alongside NoteCompose.

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 14:13:41 +00:00
Vitor Pamplona
ae99700cef Updates media 2026-03-29 10:02:52 -04:00
Vitor Pamplona
03c692a8cf Merge pull request #2005 from davotoula/Introduce-log-level-filtering
Introduce log level filtering
2026-03-29 09:56:21 -04:00
David Kaspar
efe0bde005 Merge pull request #2006 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-29 15:42:25 +02:00
davotoula
3f09150d80 Added ./gradlew --stop (with if: always()) after the Gradle steps in both test and build-desktop jobs 2026-03-29 15:41:40 +02:00
Crowdin Bot
bc367b2a7b New Crowdin translations by GitHub Action 2026-03-29 13:40:34 +00:00
davotoula
380026c7be optimise imports 2026-03-29 15:38:51 +02:00
davotoula
0b3bce5bc2 fixes for sonar 2026-03-29 15:13:43 +02:00
davotoula
c40c1ed383 perf: convert remaining 12 interpolated log calls to lambda overloads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 13:07:34 +02:00
davotoula
c17fb4641a feat: add find-non-lambda-logs skill for auditing log call hygiene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 13:00:21 +02:00
davotoula
6d89de9444 refactor: simplify PlatformLog actuals and convert remaining interpolated log calls to lambdas
- Extract private log() helper in JVM and iOS PlatformLog to reduce
  copy-paste branching
- Fix JVM formatter to be private and non-nullable
- Convert 60+ interpolated Log.w/e/i calls to lambda overloads,
  including hot-path Filter.kt toJson() and LocalCache event processing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:58:11 +02:00
davotoula
3b81731771 perf: convert 194 interpolated Log.d() calls to lambda overloads
Defers string construction until after the level check, avoiding
allocation when debug logging is filtered in release/benchmark builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:58:10 +02:00
davotoula
541b197a64 feat: add throwable support to Log.d/i and inline lambda overloads
- Add optional throwable parameter to d() and i() across all platforms
- Add inline lambda overloads for all log levels to defer message
  construction, avoiding string allocation when level is filtered
- Restore VoiceMessagePreview to pass throwable directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
250d89ca0a feat: set Log.minLevel in app entry points and migrate android.util.Log usages
- Set Log.minLevel based on BuildConfig.DEBUG in Amethyst.kt init
- Set Log.minLevel = DEBUG in Desktop Main.kt
- Migrate VideoCompressionHelper to quartz Log + LogLevel enum
- Migrate VoiceAnonymizer, VoiceMessagePreview, LiveStatusIndicator to quartz Log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
25b2cb42d0 feat: convert Log from expect/actual to filtered wrapper over PlatformLog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:56:18 +02:00
davotoula
65bc617222 feat: add PlatformLog actual implementations for all platforms
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:55:18 +02:00
davotoula
067938657f feat: add LogLevel enum and PlatformLog expect declaration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:55:18 +02:00
Claude
72106ea478 feat: add NIP-61 Nutzaps protocol support in quartz
Implements NIP-61 (Nutzaps) event types following the NIP-88 structure pattern:
- NutzapEvent (kind 9321): P2PK-locked Cashu token payments with proof, mint URL, unit tags
- NutzapInfoEvent (kind 10019): Replaceable event for recipient preferences (mints, relays, P2PK pubkey)
- NutzapRedemptionEvent (kind 7376): Token redemption history with NIP-44 encrypted content
- TokenEvent (kind 7375): Unspent proof storage with NIP-44 encrypted content

https://claude.ai/code/session_013tQRvZXdyh5VD11xENpNE5
2026-03-29 04:37:54 +00:00
Claude
32324e5bbe feat: Add NIP-60 Cashu wallet support in quartz
Implements the NIP-60 specification for cashu-based wallets with four event types:
- CashuWalletEvent (kind:17375) - replaceable wallet definition with encrypted mints/privkey
- CashuTokenEvent (kind:7375) - unspent cashu proofs with NIP-44 encrypted JSON content
- CashuSpendingHistoryEvent (kind:7376) - transaction history with encrypted tag array
- CashuMintQuoteEvent (kind:7374) - optional mint quote state with NIP-40 expiration

Follows the same structural patterns as the NIP-88 polls implementation.

https://claude.ai/code/session_018UfHPzrQCAFMwB1zB1ftDM
2026-03-29 04:34:41 +00:00
Claude
c3447e2a05 feat: Add @Preview annotations to NIP-43 UI components
Adds preview functions for RelayMemberEventCard variants (membership
list, add/remove member, join/leave request) and MembershipActions
states (not member, is member, join sent, leave sent).

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 04:22:33 +00:00
Claude
a462221ca6 refactor: improve URL detector performance and readability
- Replace 6 regex operations in Url.removeDotSegments() with simple
  string operations (startsWith/indexOf) using a when-expression
- Remove regex-based dropLastSegment() in favor of StringBuilder with
  lastIndexOf
- Extract duplicated hex/octal/decimal parsing in DomainNameReader
  into shared parseNumericLiteral() helper
- Deduplicate scheme matching by unifying findValidScheme* methods
  into findSchemeSuffix() using regionMatches (avoids lowercase allocation)
- Extract readPath() validity check into isPathValid() to remove
  duplicated 5-line condition
- Extract trySchemeNoSlashesOrUserPass() from readScheme() to
  eliminate duplicated branch logic
- Restructure readCurrent() with when-expression and extract
  resetDomainCounters() helper

https://claude.ai/code/session_017oGieyaUiLCxehNJ5aMFDK
2026-03-29 04:08:58 +00:00
Claude
b1b9707c5f feat: Implement NIP-43 relay access metadata and membership management
Adds full NIP-43 support with Quartz event classes (kinds 13534, 8000,
8001, 28934, 28935, 28936) following the nip88Polls structure pattern.
Includes relay membership list screen with join/leave actions and
NoteCompose rendering for NIP-43 events in the feed.

https://claude.ai/code/session_01PRqe4bBCb9u62oPh8u9sHx
2026-03-29 03:13:56 +00:00
Vitor Pamplona
48da76c6a3 Activates the decryption for all types of files if it matches the decryption url with the cipher info 2026-03-28 22:24:54 -04:00
Vitor Pamplona
a096d62cfb Adds relay discovery to the node master rendering list 2026-03-28 22:19:56 -04:00
Vitor Pamplona
8702ec4d96 Adds a scroll to the settings page. 2026-03-28 20:22:16 -04:00
Vitor Pamplona
4b5405c6b7 Dynamically adjust the preferred server to the first one when the list changes and the current default is not in the list of servers. 2026-03-28 20:21:57 -04:00
Vitor Pamplona
589bb041b2 Fixes refactoring issues by Claude. 2026-03-28 19:58:04 -04:00
Vitor Pamplona
ff9b619d26 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  fix: keep screen on during PiP playback and survive screen lock
  feat: add RelayDiscoveryEvent renderer for feed display
  feat: add OpenGraph preview to WebBookmark cards
  revert: keep INostrClient/NostrClient naming and restore onEose/isLive
  refactor: simplify NostrClient API for beginner-friendliness
2026-03-28 19:25:29 -04:00
Vitor Pamplona
14ff2fbf8c Sends Accept and Content-Type to the NIP86 request 2026-03-28 19:23:48 -04:00
Vitor Pamplona
7290f16d0d sorts pubkeys in the relay management and fixes issues on duplicated keys for the lazy column 2026-03-28 19:23:23 -04:00
Vitor Pamplona
567cfa33e8 Merge pull request #2000 from vitorpamplona/claude/add-relay-discovery-events-NqmlS
Add relay discovery event rendering UI component
2026-03-28 19:12:41 -04:00
Vitor Pamplona
31adfe80ff Merge pull request #1999 from vitorpamplona/claude/pip-wake-lock-investigation-XhK3D
Fix PiP video playback: keep screen on and improve lifecycle
2026-03-28 19:12:05 -04:00
Vitor Pamplona
dbd0fe03fa Adds another test case for NIP-11 2026-03-28 18:42:04 -04:00
Claude
1514de53c7 fix: keep screen on during PiP playback and survive screen lock
PiP video now keeps the screen on while playing (matching YouTube behavior)
and no longer kills the activity when the screen locks, allowing audio to
continue through the PlaybackService.

https://claude.ai/code/session_01QJzmFaabSA9YQ3oCQwEHTJ
2026-03-28 22:40:03 +00:00
Claude
0adea4f40a feat: add RelayDiscoveryEvent renderer for feed display
Add a composable to render NIP-66 RelayDiscoveryEvent (kind 30166)
in the note feed, displaying relay URL, RTT metrics with color-coded
chips, network type, relay type, requirements, supported NIPs,
accepted kinds, topics, and geohash location.

https://claude.ai/code/session_01EsPmtRFQgknkDV6zmKkeS2
2026-03-28 22:36:42 +00:00
Vitor Pamplona
23dcda55d3 Solves crashing when multiple relays with the same url are included in the resulting list. 2026-03-28 18:30:21 -04:00
Vitor Pamplona
d991af72ff Merge pull request #1986 from vitorpamplona/claude/simplify-nostrclient-api-ZpOTB
Refactor Nostr client API with clearer naming conventions
2026-03-28 18:09:12 -04:00
Vitor Pamplona
f96e2c02de Merge pull request #1997 from vitorpamplona/claude/add-opengraph-preview-uaOzk
Enhance web bookmarks with URL preview metadata
2026-03-28 18:08:59 -04:00
Vitor Pamplona
c596e16182 Improves layout of the Relay Monitor events. 2026-03-28 18:04:42 -04:00
Claude
9c361dbe1e feat: add OpenGraph preview to WebBookmark cards
Fetches URL metadata via the existing UrlCachedPreviewer to display
OG images, fallback titles, and descriptions in the bookmark list.

https://claude.ai/code/session_016igt51hduwQ7bXF9kNNHGD
2026-03-28 21:29:29 +00:00
Vitor Pamplona
485ed871d1 Finishing up the Poll Screen 2026-03-28 17:12:51 -04:00
Vitor Pamplona
20eb95ae57 Removing more warnings 2026-03-28 16:30:00 -04:00
Vitor Pamplona
3c01c17d59 Clearing more warnings 2026-03-28 15:46:59 -04:00
Vitor Pamplona
f3cefe0e13 resolving some more warnings 2026-03-28 15:35:45 -04:00
Vitor Pamplona
4db617aeef fixes the need for coroutine scopes 2026-03-28 15:26:29 -04:00
Claude
72bc7e0ed5 style: apply linter formatting to TextFieldState migration
https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:13:18 +00:00
Claude
f997586e4b feat: migrate remaining IMessageField implementors to TextFieldState
Completes the TextFieldState migration for ShortNotePostViewModel,
LongFormPostViewModel, and their associated post screens (GeoHash,
Hashtag, ShortNote, LongForm).

https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:11:09 +00:00
Claude
1d208edd33 feat: migrate ThinPaddingTextField to BasicTextField(TextFieldState) for GIF keyboard support
Migrates all new post screens from the old BasicTextField(TextFieldValue)
to the new BasicTextField(TextFieldState) API, which properly sets
EditorInfo.contentMimeTypes to enable GIF keyboard input.

Key changes:
- Add TextFieldState-based ThinPaddingTextField overload (keep old for readonly dropdowns)
- Create UrlUserTagOutputTransformation for the new OutputTransformation API
- Create TextFieldState extension functions (insertUrlAtCursor, replaceCurrentWord, currentWord)
- Update IMessageField interface to use TextFieldState + onMessageChanged()
- Migrate all IMessageField implementors: ChannelNewMessageViewModel,
  ChatNewMessageViewModel, NewProductViewModel, NewPublicMessageViewModel,
  CommentPostViewModel, ShortNotePostViewModel, LongFormPostViewModel
- Update PreviewState to accept String instead of TextFieldValue
- Add TextFieldState overload to UserSuggestionState.replaceCurrentWord()
- Update all post screen call sites

https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
2026-03-28 19:09:38 +00:00
Vitor Pamplona
c0212c09e5 Merge pull request #1996 from vitorpamplona/claude/migrate-autofill-apis-kQZDc
Replace deprecated autofill API with semantics-based approach
2026-03-28 14:58:16 -04:00
Vitor Pamplona
8c1ca8dc8b Merge branch 'main' into claude/migrate-autofill-apis-kQZDc 2026-03-28 14:58:01 -04:00
Vitor Pamplona
c44f155a82 Merge pull request #1995 from vitorpamplona/claude/migrate-clipboard-manager-QEupN
Update Compose clipboard API from LocalClipboardManager to LocalClipboard
2026-03-28 14:50:19 -04:00
Claude
0eb32ae4fb refactor: migrate LocalClipboardManager to LocalClipboard
Replace deprecated LocalClipboardManager with LocalClipboard across
all 21 files in both amethyst and desktopApp modules.

https://claude.ai/code/session_01FE2NjKxhUYUFRvhyqbYpUH
2026-03-28 18:46:02 +00:00
Vitor Pamplona
9b3e77e2d5 Merge pull request #1994 from vitorpamplona/claude/migrate-tabrow-material3-FlqJl
Replace TabRow and ScrollableTabRow with Secondary variants
2026-03-28 14:43:54 -04:00
Claude
61cf21a875 feat: migrate from deprecated LocalAutofillTree to semantics-based Autofill API
Replace manual AutofillNode/LocalAutofillTree/LocalAutofill usage with
the new Modifier.semantics { contentType = ContentType.Password } API
from androidx.compose.ui.autofill in KeyTextField, PasswordField, and
AccountBackupScreen.

https://claude.ai/code/session_018e5E6dFATZTEnGtTZbkKhP
2026-03-28 18:40:29 +00:00
Claude
6a13b2b7b7 feat: migrate TabRow/ScrollableTabRow to Material 3 Secondary variants
Replace deprecated material3 TabRow with SecondaryTabRow and
ScrollableTabRow with SecondaryScrollableTabRow across all 12 files.

https://claude.ai/code/session_01VBDx3qQ8n9AzQJV3dGWYff
2026-03-28 18:40:10 +00:00
Vitor Pamplona
72f6e47478 Merge pull request #1993 from vitorpamplona/claude/migrate-context-compat-activity-iAHrz
Replace ContextCompat.startActivity with Context.startActivity
2026-03-28 14:39:21 -04:00
Claude
8f1b10efa6 refactor: replace ContextCompat.startActivity with Context.startActivity
ContextCompat.startActivity is an unnecessary wrapper that just delegates
to Context.startActivity. Call the method directly on the context instead.

https://claude.ai/code/session_019gvXxtttDRZTnSE1x8F2EE
2026-03-28 18:36:14 +00:00
Vitor Pamplona
9231fa5457 Merge pull request #1990 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 14:35:53 -04:00
Crowdin Bot
496ee8a58c New Crowdin translations by GitHub Action 2026-03-28 18:27:56 +00:00
Vitor Pamplona
7b1c28c4b7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  increase android CI build to 45 minutes
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
2026-03-28 14:25:44 -04:00
Vitor Pamplona
cfa59dab01 removing more warnings 2026-03-28 14:24:13 -04:00
Crowdin Bot
61000e9662 New Crowdin translations by GitHub Action 2026-03-28 17:47:14 +00:00
David Kaspar
7c02dc3d2d Merge pull request #1989 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 18:46:43 +01:00
davotoula
d707900aa0 increase android CI build to 45 minutes 2026-03-28 18:45:54 +01:00
Crowdin Bot
9c5e5381a4 New Crowdin translations by GitHub Action 2026-03-28 17:43:09 +00:00
davotoula
b8b754a698 update translations: CZ, DE, PT, SE 2026-03-28 18:39:09 +01:00
Vitor Pamplona
aa7f480f14 Merge branch 'main' into claude/simplify-nostrclient-api-ZpOTB 2026-03-28 13:09:52 -04:00
Vitor Pamplona
e8ba9a2d2f Merge pull request #1987 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 12:48:43 -04:00
Crowdin Bot
b6184ad075 New Crowdin translations by GitHub Action 2026-03-28 16:46:57 +00:00
Vitor Pamplona
352a7f12a2 Merge pull request #1985 from vitorpamplona/claude/fix-quartz-deprecations-Y8BYY
Suppress deprecation warnings and improve code quality
2026-03-28 12:45:25 -04:00
Claude
4be617e64a revert: keep INostrClient/NostrClient naming and restore onEose/isLive
Reverts three naming changes from the previous refactor:
- Interface stays INostrClient (not NostrClient)
- Class stays NostrClient (not DefaultNostrClient)
- onEose stays onEose (not onCaughtUp)
- isLive stays isLive (not isRealTime)

All other renames from the API simplification are preserved:
subscribe, unsubscribe, publish, count, fetchAll, fetchFirst, etc.

https://claude.ai/code/session_01JPcYCcRx5eZN4GvgGxGwbf
2026-03-28 16:43:30 +00:00
Claude
4e2717c5c5 refactor: simplify NostrClient API for beginner-friendliness
Comprehensive rename of the NostrClient relay infrastructure to use
intuitive, self-documenting names instead of protocol-level jargon.

Interface/class renames:
- INostrClient → NostrClient (interface)
- NostrClient → DefaultNostrClient (implementation)
- IRequestListener → SubscriptionListener
- IRelayClientListener → RelayConnectionListener
- IOpenNostrRequest → SubscriptionHandle
- NostrClientStaticReq → StaticSubscription
- NostrClientDynamicReq → DynamicSubscription

Method renames:
- openReqSubscription() → subscribe()
- close(subId) → unsubscribe(subId)
- send() → publish()
- queryCount() → count()
- subscribe/unsubscribe(listener) → addConnectionListener/removeConnectionListener
- renewFilters() → syncFilters()

Callback renames:
- onEose → onCaughtUp
- isLive → isRealTime
- onStartReq → onSubscriptionStarted
- onCloseReq → onSubscriptionClosed

Extension function renames:
- query() → fetchAll()
- downloadFirstEvent() → fetchFirst()
- req() → subscribe()
- reqAsFlow() → subscribeAsFlow()
- reqUntilEoseAsFlow() → fetchAsFlow()
- sendAndWaitForResponse() → publishAndConfirm()
- queryCountSuspend() → count()
- queryCountMergedHll() → countMerged()
- reqBypassingRelayLimits() → fetchAllPages()
- updateFilter() → refresh() on SubscriptionHandle

https://claude.ai/code/session_01JPcYCcRx5eZN4GvgGxGwbf
2026-03-28 16:43:30 +00:00
Claude
610fcd7e0c fix: suppress deprecation and unchecked cast warnings in Quartz internal code
Deprecated APIs in Quartz are kept for library consumers but internal usages
now carry @Suppress annotations so the module builds warning-free. Also
replaces redundant .toInt() on hex Int literals in ChaCha20Core and migrates
deprecated java.net.URL to URI.resolve() in ServerInfoParser.

The spotless license header delimiter is updated to recognize @file: annotations.

https://claude.ai/code/session_01EwS56YAGGnnac5EuwhaUSs
2026-03-28 16:39:51 +00:00
Vitor Pamplona
0054c01921 Merge pull request #1984 from vitorpamplona/claude/parallelize-db-tests-I6D3g
Refactor forEachDB to use coroutines with parallel execution
2026-03-28 12:22:02 -04:00
Claude
d299b65c8a feat: make forEachDB action suspending, remove redundant runBlocking
Change the action lambda to `suspend (EventStore) -> Unit` so callers
can use suspend functions directly. Remove the now-unnecessary
runBlocking wrapper around delay() in ExpirationTest.

https://claude.ai/code/session_01TQRzVTAXBVWRGstcsapA5F
2026-03-28 16:15:23 +00:00
Claude
0889473c15 feat: parallelize BaseDBTest forEachDB using coroutines
Each EventStore uses an independent in-memory SQLite database, so tests
can safely run concurrently. This launches all 16 DB configurations in
parallel via coroutines on Dispatchers.Default instead of running them
sequentially.

https://claude.ai/code/session_01TQRzVTAXBVWRGstcsapA5F
2026-03-28 16:12:44 +00:00
Vitor Pamplona
009326ae0b Merge pull request #1982 from davotoula/fix-for-githooks-in-git-worktree
Handle git worktree where .git is a file, not a directory
2026-03-28 12:08:28 -04:00
davotoula
60686d0fef fix: resolves the .git file in worktrees by reading the gitdir: pointer and using the actual git directory for hooks installation 2026-03-28 16:59:01 +01:00
Vitor Pamplona
4c5606cd8d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  feat: complete NIP-85 Trusted Assertions implementation in quartz
2026-03-28 11:51:50 -04:00
Vitor Pamplona
dd2ec3f05f moves the initialization of the Secp lib to the setup of the test to make sure it doesn't affect the expiration timing tests. 2026-03-28 11:50:43 -04:00
Vitor Pamplona
b147e6d4c3 Fixes missing actual 2026-03-28 11:39:25 -04:00
Vitor Pamplona
2745b29bf7 removing some warnings 2026-03-28 11:39:15 -04:00
Vitor Pamplona
be62ca5374 Merge pull request #1981 from vitorpamplona/claude/implement-nip-85-hBlbp
Add NIP-85 trusted assertions support for events and addressables
2026-03-28 11:36:29 -04:00
Vitor Pamplona
b51efb5caf Merge branch 'main' into claude/implement-nip-85-hBlbp 2026-03-28 11:35:19 -04:00
Vitor Pamplona
5776e74bb5 Fixing some warnings 2026-03-28 11:33:03 -04:00
Vitor Pamplona
eaa45aee05 Fixes constructor issue in this BigDecimal Implementation 2026-03-28 11:31:14 -04:00
Vitor Pamplona
c11e41529e Merge pull request #1980 from davotoula/toast-for-downloads
Show toast instead of dialog on media download success
2026-03-28 10:36:06 -04:00
davotoula
cbf9b31a3f feat: show toast instead of dialog on media download success
Replace the interruptive AlertDialog with a brief Android Toast when
media downloads complete. Errors still show the full dialog for visibility.
2026-03-28 15:04:27 +01:00
Claude
d40dccde11 feat: complete NIP-85 Trusted Assertions implementation in quartz
Add full compliance with NIP-85 by implementing all four assertion event
kinds and their associated tag types in the nip85TrustedAssertions package:

- Kind 30382 (User Assertions): Add all 15 missing tag parsers and
  builders (first_created_at, post_cnt, reply_cnt, reactions_cnt, all
  zap/report/topic/active_hours tags) to ContactCardEvent
- Kind 30383 (Event Assertions): New EventAssertionEvent with rank,
  comment_cnt, quote_cnt, repost_cnt, reaction_cnt, zap_cnt, zap_amount
- Kind 30384 (Addressable Event Assertions): New
  AddressableAssertionEvent with same tags as 30383
- Kind 30385 (External ID Assertions): New ExternalIdAssertionEvent
  with rank, comment_cnt, reaction_cnt
- Add ProviderTypes for all new assertion kinds (30383/30384/30385)
- Register all new event kinds in EventFactory
- Add comprehensive tests for all assertion event types

https://claude.ai/code/session_01XZQsSnkHJ6tFiPS7fRLzPJ
2026-03-28 13:59:12 +00:00
Vitor Pamplona
e3c6c35688 Merge pull request #1973 from vitorpamplona/claude/review-quartz-multiplatform-00Oi0
feat: add linuxX64 KMP target and restructure native source sets
2026-03-28 09:54:11 -04:00
Vitor Pamplona
05e7b305a7 Merge pull request #1977 from vitorpamplona/claude/polls-feed-screen-DQ78j
Add Polls feed screen with filtering and discovery support
2026-03-28 09:53:50 -04:00
Vitor Pamplona
3b15242a02 Merge pull request #1978 from vitorpamplona/claude/nip-a4-compliance-mftpU
Add NIP-A4 support: k tag in zap requests and e tag filtering
2026-03-28 09:53:15 -04:00
Claude
5db9330306 fix: update test packages after nipA4PublicMessages rename
Move test files from experimental.publicMessages to nipA4PublicMessages
to match the package rename on main.

https://claude.ai/code/session_019JsSsTTXivnNWiAtE5DE1R
2026-03-28 13:52:02 +00:00
Claude
559ad40223 feat: add Polls feed screen with Global/Follows filter
Add a dedicated Polls-only feed screen (NIP-88 kind 1068, non-zap polls)
accessible from the left navigation drawer. The screen includes the
standard top nav filter bar for Global/Follows/etc filtering, reusing
the Discovery follow list settings.

New files:
- PollsFeedFilter: filters LocalCache for PollEvent only
- PollsFilterAssembler + PollsSubAssembler: relay subscriptions
- PollsScreen: main screen with pull-to-refresh feed
- PollsTopBar: top bar with Global/Follows filter spinner
- Relay filter subassemblies for authors, follows, global, hashtag

Wiring:
- Route.Polls added to navigation
- Polls entry in left drawer with ic_poll icon
- pollsFeed added to AccountFeedContentStates
- PollsFilterAssembler added to RelaySubscriptionsCoordinator

https://claude.ai/code/session_013JWrsYpQczmZtpW3WwRRfK
2026-03-28 13:51:51 +00:00
Vitor Pamplona
3ad42d2d8b Merge pull request #1979 from vitorpamplona/claude/nip24-compliance-check-WvjhO
Add birthday field support to UserMetadata
2026-03-28 09:48:35 -04:00
Claude
b465785c27 feat: NIP-A4 compliance - add k tag to zap requests and enforce e tag prohibition
- Add KindTag to LnZapRequestEvent.create() when zapping events, as required
  by NIP-A4 ("NIP-57 zaps MUST include the k tag to 24")
- Enforce e tag prohibition in all PublicMessageEvent.build() methods by
  stripping any e tags from the tag builder output (NIP-A4: "e tags must not
  be used")
- Add comprehensive tests for both changes

https://claude.ai/code/session_019JsSsTTXivnNWiAtE5DE1R
2026-03-28 13:48:29 +00:00
Vitor Pamplona
9b483f903d Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  feat: implement NIP-32 Labeling protocol in quartz
2026-03-28 09:41:26 -04:00
Vitor Pamplona
0c7e5fddea Fixes test cases 2026-03-28 09:41:09 -04:00
Claude
46fd8c651c feat: add NIP-24 birthday field to UserMetadata
NIP-24 defines an optional birthday object with year, month, and day
fields for kind 0 metadata events. This was the only NIP-24 field
missing from Quartz's UserMetadata model.

https://claude.ai/code/session_01Sbj2DF5XDtEuDCeJ7yR6oS
2026-03-28 13:30:51 +00:00
Vitor Pamplona
7e1ff20e6d Merge pull request #1976 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 09:30:27 -04:00
Crowdin Bot
89b0064a11 New Crowdin translations by GitHub Action 2026-03-28 13:27:22 +00:00
Vitor Pamplona
b5a0c8b6d5 Merge pull request #1975 from vitorpamplona/claude/implement-nip-32-gWs2E
Add NIP-32 Labeling support with LabelEvent and tag parsing
2026-03-28 09:25:48 -04:00
Vitor Pamplona
943d46e01a Moves public messages out of experimental 2026-03-28 09:18:34 -04:00
Vitor Pamplona
5d281c34d5 Moves trusted assertions out of experimental 2026-03-28 09:16:44 -04:00
Vitor Pamplona
bfad08a4a7 Merge pull request #1974 from greenart7c3/claude/add-workflow-permissions-OO87O
Add explicit permissions to GitHub Actions workflows
2026-03-28 08:45:08 -04:00
Claude
8cbd11816e ci: add explicit permissions to workflows 2026-03-28 09:32:48 +00:00
Claude
aa15085f40 feat: implement NIP-32 Labeling protocol in quartz
Add kind 1985 LabelEvent with L (namespace) and l (label) tag support
for distributed moderation, content classification, and license assignment.
Includes tag parsers, builder extensions, self-reporting helpers, and tests.

https://claude.ai/code/session_011t5ZoP1BdgZTT5Cen9GH5z
2026-03-28 03:57:46 +00:00
Claude
5fc7526076 feat: add linuxX64 KMP target and restructure native source sets
Restructure quartz module's Kotlin/Native source set hierarchy for
proper multiplatform coverage. Adds linuxX64 target with complete
actual implementations for all expect declarations.

Source set hierarchy:
  commonMain
  ├── jvmAndroid → jvmMain, androidMain
  └── nativeMain (shared pure Kotlin)
      ├── appleMain (Apple APIs) → iosMain
      └── linuxMain (POSIX/OpenSSL) → linuxX64Main

nativeMain: Address, OptimizedJsonMapper, EventHasherSerializer,
  ChessEngine, Secp256k1Instance, BitSet, StringExt, io/ utilities

appleMain: Log (NSLog), SecureRandom (SecRandomCopyBytes),
  UriParser (NSURLComponents), BigDecimal (NSDecimalNumber),
  GZip (zlib), Sha256 (CC_SHA256), crypto (Apple provider),
  LargeCache (CacheMap), UrlEncoder, UnicodeNormalizer

linuxMain: Log (println), SecureRandom (/dev/urandom),
  UriParser (pure Kotlin), BigDecimal (pure Kotlin),
  GZip (zlib cinterop), Sha256 (OpenSSL), crypto (OpenSSL),
  LargeCache (AtomicReference<LinkedHashMap>), UrlEncoder

macOS targets deferred pending negentropy-kmp macOS artifacts.
macosMain/Platform.kt retained as scaffold for future activation.

https://claude.ai/code/session_01M9CAuPUV3TfPB2xShnsBhw
2026-03-28 02:55:20 +00:00
Vitor Pamplona
007a0b0d43 More descriptive counters 2026-03-27 21:42:35 -04:00
Vitor Pamplona
391a0bfab2 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  sonar fixes
  update translations: CZ, DE, PT, SE
  New Crowdin translations by GitHub Action
  update video compression library to latest
  update to use new limit short size bit more aggressive compression defaults
  add local maven repo for easier library development
2026-03-27 19:40:04 -04:00
Vitor Pamplona
ea8fbd4af9 Making a localhost:3030 string work correctly in the URL Detector 2026-03-27 19:34:55 -04:00
Vitor Pamplona
16dd14f403 Merge pull request #1972 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 19:30:35 -04:00
Crowdin Bot
8f3c9d2d93 New Crowdin translations by GitHub Action 2026-03-27 23:25:02 +00:00
davotoula
44b8ef5d8f sonar fixes 2026-03-28 00:23:31 +01:00
davotoula
c5a88bba97 update translations: CZ, DE, PT, SE 2026-03-28 00:19:38 +01:00
David Kaspar
6a6fadb493 Merge pull request #1971 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-28 00:06:55 +01:00
Crowdin Bot
4d38c1dfb0 New Crowdin translations by GitHub Action 2026-03-27 22:58:31 +00:00
Vitor Pamplona
da060a27fd Merge pull request #1970 from davotoula/update-video-compression-library
Update video compression library
2026-03-27 18:57:20 -04:00
Vitor Pamplona
49f0ca30b2 Fixes test case 2026-03-27 18:49:12 -04:00
Vitor Pamplona
e8c98f31e6 Reverting merge issues 2026-03-27 18:48:29 -04:00
Vitor Pamplona
ff8e5dd4e4 Fixes missing strings during merge 2026-03-27 18:43:06 -04:00
Vitor Pamplona
e444e59f66 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  feat: add NIP-66 monitor reports UI and string resources
  feat: register NIP-66 filter assembler in coordinator
  feat: add NIP-66 relay info subscription composable
  feat: add NIP-66 relay discovery filter sub-assembler
  feat: display NIP-66 relay monitor reports in relay information screen
2026-03-27 18:27:40 -04:00
davotoula
bb469700b7 update video compression library to latest 2026-03-27 23:25:55 +01:00
davotoula
3423065b94 update to use new limit short size
bit more aggressive compression defaults
2026-03-27 23:25:55 +01:00
Vitor Pamplona
549baa2ceb Ignore vlc temp files 2026-03-27 17:04:04 -04:00
Vitor Pamplona
c068fe38be Fixes tests 2026-03-27 16:36:42 -04:00
davotoula
acfb596647 add local maven repo for easier library development 2026-03-27 21:32:32 +01:00
Vitor Pamplona
323694761e Updating dependencies 2026-03-27 16:21:16 -04:00
Vitor Pamplona
540194e0ca Merge pull request #1969 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 16:18:57 -04:00
Crowdin Bot
6a99210633 New Crowdin translations by GitHub Action 2026-03-27 20:15:23 +00:00
Vitor Pamplona
d0a5e7d44b Merge pull request #1968 from vitorpamplona/claude/relay-info-nip66-GpWzf
Add relay monitor discovery events display to relay info screen
2026-03-27 16:13:49 -04:00
Vitor Pamplona
e0c569ea9b Merge pull request #1966 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 16:13:04 -04:00
Crowdin Bot
1696ad1248 New Crowdin translations by GitHub Action 2026-03-27 20:04:06 +00:00
Claude
77656ffc84 feat: add NIP-66 monitor reports UI and string resources
Add relay monitor report section to RelayInformationScreen showing
discovery events from multiple NIP-66 monitors. Add string resources
for the monitor reports UI.

https://claude.ai/code/session_01KWuXTp3Z7HST9cu6RWQ4G5
2026-03-27 19:58:26 +00:00
Vitor Pamplona
aa3202de4b feat: register NIP-66 filter assembler in coordinator 2026-03-27 15:56:19 -04:00
Vitor Pamplona
3387e119d5 feat: add NIP-66 relay info subscription composable 2026-03-27 15:55:25 -04:00
Vitor Pamplona
0a44bb4904 feat: add NIP-66 relay discovery filter sub-assembler 2026-03-27 15:55:19 -04:00
Vitor Pamplona
d65ffd1978 feat: display NIP-66 relay monitor reports in relay information screen
Subscribe to kind 30166 (RelayDiscoveryEvent) events for the relay being
viewed and display monitoring data from multiple providers. Shows RTT
metrics (open/read/write), network type, relay type, supported NIPs,
and access requirements reported by each monitor.

https://claude.ai/code/session_01KWuXTp3Z7HST9cu6RWQ4G5
2026-03-27 15:55:02 -04:00
Vitor Pamplona
4d426a011a Fixes typo 2026-03-27 15:50:15 -04:00
Vitor Pamplona
7ec1ec1a3f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: (26 commits)
  feat(cache): seed NotificationsScreen from cache on compose
  feat(cache): wire State classes on DesktopIAccount for GC retention
  feat(cache): extract Kind3FollowListState + Nip65RelayListState to commons
  feat(cache): extract BookmarkListState to commons/commonMain
  feat(cache): add periodic memory cleanup for Desktop
  feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
  refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
  feat(cache): seed Reads and Bookmarks screens from cache on compose
  fix(cache): don't reset followersCount to 0 on subscription restart
  feat(cache): cache profile metadata and follower/following counts
  fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions
  fix(cache): add missing relay subscriptions for FeedScreen and UserProfileScreen
  refactor(cache): P3 simplifications — remove dead code, reduce abstractions
  fix(cache): address review findings — subscription churn, O(n) size, atomicity
  feat(cache): ThreadScreen → DesktopFeedViewModel + DesktopThreadFilter
  feat(ui): relay health indicator in NavigationRail
  feat(cache): UserProfileScreen notes feed → DesktopFeedViewModel
  feat(cache): FeedScreen → DesktopFeedViewModel + Note-based FeedNoteCard
  feat(cache): Coordinator error handling + interaction subscriptions
  feat(cache): kind-based consumer registry + new event types
  ...
2026-03-27 15:47:56 -04:00
Vitor Pamplona
787b9ab9db Merge pull request #1905 from nrobi144/feat/desktop-cache-v2
feat(desktop): cache-centric architecture for desktop feeds
2026-03-27 15:45:34 -04:00
Vitor Pamplona
20f8d14d28 Final touches on rendering Zap Goals 2026-03-27 15:43:20 -04:00
Vitor Pamplona
58628dbc12 spotless apply 2026-03-27 15:36:14 -04:00
Vitor Pamplona
3a7402a62b Merge pull request #1964 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 15:34:28 -04:00
Vitor Pamplona
fe92deeb53 Merge pull request #1965 from vitorpamplona/claude/nip75-goals-feature-332LM
Add NIP-75 Zap Goals support with creation and rendering
2026-03-27 15:33:57 -04:00
Vitor Pamplona
25fee78296 Merge branch 'main' into claude/nip75-goals-feature-332LM 2026-03-27 15:33:50 -04:00
Vitor Pamplona
787eb01c2c Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  Get rid of libsodium related files.
2026-03-27 15:29:10 -04:00
Vitor Pamplona
1a2e34bdcf Adds a Pin icon to the post instead of making a separate section in user profiles 2026-03-27 15:27:50 -04:00
Crowdin Bot
a0bea7602e New Crowdin translations by GitHub Action 2026-03-27 19:19:32 +00:00
Vitor Pamplona
ddb23118a2 Merge pull request #1963 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 15:18:10 -04:00
Vitor Pamplona
0a62b898be Merge pull request #1962 from KotlinGeekDev/kmp-completeness
Remove libsodium files.
2026-03-27 15:17:59 -04:00
Crowdin Bot
37dad897cf New Crowdin translations by GitHub Action 2026-03-27 18:59:52 +00:00
Vitor Pamplona
b79cd53d34 Finishes vanish final touches. 2026-03-27 14:57:20 -04:00
KotlinGeekDev
d7930d4527 Get rid of libsodium related files. 2026-03-27 19:26:17 +01:00
Vitor Pamplona
face5ba6e6 Fixes long lists of vanish request relays 2026-03-27 14:18:32 -04:00
Vitor Pamplona
427423edda Adds the user's outbox relays to keep a copy of vanish requests. 2026-03-27 14:18:07 -04:00
Vitor Pamplona
153666b9b4 Improves vanishing tests 2026-03-27 13:59:55 -04:00
Vitor Pamplona
1ece456f41 Uses the new query method from quartz 2026-03-27 12:36:45 -04:00
Vitor Pamplona
a456517fbf Offers a new query method from quartz 2026-03-27 12:36:35 -04:00
Vitor Pamplona
d3ddb86dbb Allows users to pick relays on the request to vanish screen 2026-03-27 12:13:19 -04:00
Vitor Pamplona
0862be2761 Improvements to the layout of the kinds 2026-03-27 11:21:46 -04:00
Vitor Pamplona
cf28e799b7 Better wording 2026-03-27 11:00:47 -04:00
Vitor Pamplona
94c900e98f Moves manage action to the Relay Info header 2026-03-27 10:59:59 -04:00
Vitor Pamplona
e144eabf96 Merge pull request #1961 from vitorpamplona/claude/pinned-notes-in-tab-8y3TR
Integrate pinned notes into new threads tab
2026-03-27 10:25:26 -04:00
Vitor Pamplona
e209b9e0cc Merge pull request #1960 from vitorpamplona/claude/pubkey-user-mapping-vWcSL
Display user profiles in relay management pubkey lists
2026-03-27 10:16:12 -04:00
Claude
7618a5039d feat: show pinned notes at top of Notes tab instead of separate tab
Remove the dedicated Pinned Notes tab from the profile and display
pinned notes at the beginning of the Notes tab with a pin icon marker.

https://claude.ai/code/session_01ChamaudsEwwQ8mNtqnx4ze
2026-03-27 14:12:06 +00:00
Claude
9e871c5719 refactor: use flow.map for pubkey-to-user resolution, filter invalid pubkeys
Replace MutableStateFlow caches with Flow.map derived from bannedPubkeys
and allowedPubkeys. Use LocalCache.checkGetOrCreateUser to safely resolve
pubkeys, filtering out invalid ones (null). Remove raw pubkey display
from PubkeyUserCard since invalid pubkeys are already excluded.

https://claude.ai/code/session_018x2PcJX6VGyJmVuphkbK54
2026-03-27 14:08:31 +00:00
Vitor Pamplona
9be81ec1a5 Merge pull request #1959 from vitorpamplona/claude/auto-load-opengraph-bookmarks-XGj82
Add Open Graph preview fetching to web bookmarks dialog
2026-03-27 09:57:24 -04:00
Claude
438d7c9122 feat: map pubkeys to users with profile info in relay management
Resolve pubkeys to User objects via LocalCache in RelayManagementViewModel,
caching the mapping. Render banned/allowed pubkeys using SlimListItem with
user picture, name, and NIP-05 verification, with the remove action button
in trailingContent. Falls back to HexEntryCard for unknown pubkeys.

https://claude.ai/code/session_018x2PcJX6VGyJmVuphkbK54
2026-03-27 13:40:29 +00:00
Claude
c0a8da0e0c feat: auto-load OpenGraph data when adding web bookmarks
When creating a new web bookmark, automatically fetches OpenGraph
metadata (title, description) after the user leaves the URL field.
Only populates fields that are still empty, preserving any user input.

https://claude.ai/code/session_01RteQotL6WHqJtDr38fUHan
2026-03-27 13:28:41 +00:00
Vitor Pamplona
51c68bfaf1 use Users instead of Pubkeys as name of the tab 2026-03-27 09:22:20 -04:00
Vitor Pamplona
511fcb4c2a Fixes Web Bookmarks floating action button to circle 2026-03-27 08:37:21 -04:00
Vitor Pamplona
09e47d4174 Merge pull request #1958 from vitorpamplona/claude/nip62-relay-deletion-EN01M
Add NIP-62 Request to Vanish feature for data deletion
2026-03-27 08:29:51 -04:00
Vitor Pamplona
33b91050e1 Merge branch 'main' into claude/nip62-relay-deletion-EN01M 2026-03-27 08:29:43 -04:00
Vitor Pamplona
35e1c0b9c0 Merge pull request #1957 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-27 08:27:25 -04:00
Crowdin Bot
74b943336c New Crowdin translations by GitHub Action 2026-03-27 12:22:02 +00:00
Vitor Pamplona
d121d73bb7 Merge pull request #1954 from vitorpamplona/claude/nip86-relay-management-KuTru
Add NIP-86 relay management UI and client implementation
2026-03-27 08:20:33 -04:00
Vitor Pamplona
cbb8a345e2 Add NavigateIfIntentRequested to AppNavigation 2026-03-27 08:18:16 -04:00
Vitor Pamplona
143d8c30aa Merge branch 'main' into claude/nip86-relay-management-KuTru 2026-03-27 08:17:23 -04:00
Vitor Pamplona
515659791b Merge pull request #1955 from vitorpamplona/claude/add-negentropy-support-PcTpR
Add NIP-77 Negentropy set reconciliation protocol support
2026-03-27 08:15:31 -04:00
Vitor Pamplona
009b5655f9 Merge branch 'main' into claude/add-negentropy-support-PcTpR 2026-03-27 08:15:24 -04:00
Vitor Pamplona
bc7a7effb8 Merge pull request #1956 from vitorpamplona/claude/add-pinned-notes-njyQF
Add pinned notes feature with NIP-10001 support
2026-03-27 08:11:50 -04:00
Claude
c66d63dbb1 feat: add pinned notes support (NIP-51 kind 10001)
Implement pinned notes using NIP-51 kind 10001 event. This allows users
to pin notes to their profile and view pinned notes on other profiles.

- Rewrite PinListEvent from non-standard kind 33888 to NIP-51 kind 10001
  using e tags and BaseReplaceableEvent
- Add PinListState for reactive pin state management
- Add pin/unpin actions to Account, AccountViewModel, and 3-dot menu
- Add Pinned Notes tab to user profile page
- Add Pinned Notes tab to My Bookmarks screen
- Subscribe to kind 10001 in account metadata and profile relay filters

https://claude.ai/code/session_012GQzb2qcGfAcizC6jqZArD
2026-03-27 03:00:49 +00:00
Claude
be297b89b3 feat: Add NIP-75 Zap Goal rendering and creation screen
- Add GoalEvent renderer (Goal.kt) with progress bar showing zap
  funding status, goal image, summary, and closed status
- Add NoteCompose integration so GoalEvents render properly in feeds
- Add NewGoalScreen with form for description, amount, summary,
  image URL, website URL, and optional deadline
- Add NewGoalViewModel handling event creation via GoalEvent.build()
- Add Route.NewGoal and navigation registration
- Add string resources for goal UI

https://claude.ai/code/session_012dS1srrK9LjA4vEZ8WKDEk
2026-03-27 00:16:18 +00:00
Claude
2ed3de8d80 feat: add Vanish History screen with relay compliance testing
Add a new screen that fetches and displays all existing NIP-62 Request
to Vanish events from connected relays. Each entry shows the target
relays and event date. Per-relay "Test" buttons query the relay for
events older than the vanish date to check NIP-62 compliance - if
events are found, the relay is flagged as non-compliant.

https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
2026-03-26 23:58:10 +00:00
Vitor Pamplona
12a51f6e6d Merge pull request #1953 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 19:45:10 -04:00
Crowdin Bot
dfd959be4e New Crowdin translations by GitHub Action 2026-03-26 23:11:00 +00:00
Vitor Pamplona
75a8e461ad 20x Faster Rfc3986Normalizer and way less objects being created. 2026-03-26 19:04:18 -04:00
Vitor Pamplona
7c8d44559e Benchmark for the new Rfc3986 Normalizer 2026-03-26 17:32:07 -04:00
Vitor Pamplona
395b16050b Try catch to avoid crashes 2026-03-26 17:31:32 -04:00
Vitor Pamplona
4cb5b63631 Speeding up DrawerContent 2026-03-26 15:56:01 -04:00
Vitor Pamplona
e92d5c8a5c Simplifies navigation row 2026-03-26 15:48:47 -04:00
Vitor Pamplona
9cbcb3b39d Speedup rendering of the left bar by avoiding a textfield 2026-03-26 15:37:06 -04:00
Vitor Pamplona
85ebb7918b AsyncImage is faster than Image for resources 2026-03-26 15:22:03 -04:00
Vitor Pamplona
5d256bab2c stable subs 2026-03-26 15:21:08 -04:00
Vitor Pamplona
9171209d44 Kickstart LRUCache to void constructor blocking in main thread 2026-03-26 15:20:49 -04:00
Vitor Pamplona
3ecf761d68 Removes logs from animation 2026-03-26 14:37:43 -04:00
Vitor Pamplona
39e89834ba Moves gesture enable to inside the composable.
Creates a function just for navhost
2026-03-26 14:37:16 -04:00
Vitor Pamplona
bba4d28e4b stable toast manager 2026-03-26 14:35:31 -04:00
Vitor Pamplona
d0a3679778 Minimizes the size of the banner 2026-03-26 14:35:18 -04:00
Vitor Pamplona
423bc04dba Fixes logging 2026-03-26 13:54:46 -04:00
Vitor Pamplona
9f7672249b Otherwise, it is created by the main thread when the AccountViewModel Factory is run 2026-03-26 13:54:33 -04:00
Vitor Pamplona
79d66cca8f Merge pull request #1952 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 11:41:15 -04:00
Crowdin Bot
7cb9cc6cd5 New Crowdin translations by GitHub Action 2026-03-26 15:37:20 +00:00
Vitor Pamplona
7d6f1a539f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
  fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
  refactor: remove Namecoin label from search results
  feat: show Namecoin label on .bit search results without ViewModel state
  refactor: remove Namecoin label from search results per review
  refactor: simplify .bit search resolution per review feedback
  feat: resolve bare .bit domains and show Namecoin resolution status in UI
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  refactor: rename trustAllCerts → usePinnedTrustStore
  fix: harden TLS for GrapheneOS and security-conscious Android ROMs
  doc: verify .onion ElectrumX cert via Tor, document pinning
  fix: bypass cert pinning for .onion ElectrumX servers
  feat: TOFU cert pinning for custom ElectrumX servers
  feat: add Test Connection diagnostics to Namecoin settings
  fix: pin ElectrumX server certs instead of trust-all TrustManager

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
2026-03-26 11:34:54 -04:00
Vitor Pamplona
155e57ab11 Defers most module creations to when they are needed via lambdas 2026-03-26 11:11:44 -04:00
Vitor Pamplona
7952f558eb Parallelizes the JSON parsing from stored account. 2026-03-26 11:11:21 -04:00
Vitor Pamplona
958a460ea9 Adds a try/catch to make sure this procedure unsubscribes from the req 2026-03-26 11:10:45 -04:00
Vitor Pamplona
3776f65179 Defers the creation of the geohashStateFlow until needed 2026-03-26 11:08:27 -04:00
Vitor Pamplona
44c39b01ea Defers NIP05 Resolver/NamecoinResolver Builder until needed 2026-03-26 11:07:56 -04:00
Vitor Pamplona
d73dd662c6 Reduces calculations and state creations in the NavHost builder 2026-03-26 11:03:07 -04:00
Vitor Pamplona
4ef1ba1ac6 Fixes the bug of not sending NIP-17 reactions in the complete UI mode 2026-03-26 09:25:09 -04:00
Vitor Pamplona
276dc6589c Merge pull request #1937 from mstrofnone/fix/samsung-oneui7-namecoin-tls
fix: pin ElectrumX server certs for Samsung One UI 7 (Android 16) compatibility
2026-03-26 09:14:30 -04:00
Vitor Pamplona
f9c381a113 Merge pull request #1951 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 08:38:12 -04:00
Crowdin Bot
b77e6c1b6c New Crowdin translations by GitHub Action 2026-03-26 10:57:57 +00:00
davotoula
17eea1dca9 update translations: CZ, DE, PT, SE 2026-03-26 11:53:59 +01:00
M
7293b0985b fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
The local private WatchAndDisplayNip05Row uses NonClickableObserveAndDisplayNIP05
(plain text domain), while the shared one in ShowQRScreen uses the clickable
ObserveAndDisplayNIP05. They are not the same function — restore the local copy
to preserve the correct non-clickable behavior in user suggestions.
2026-03-26 17:36:53 +11:00
nrobi144
9c9c7d3f24 feat(cache): seed NotificationsScreen from cache on compose
- Add localCache param to NotificationsScreen
- Seed reactions and zaps from cache on initial compose
- Notifications now persist across tab navigation
- All screens now have cache seeding (Feed, Thread, Profile,
  Bookmarks, Reads, Notifications)
2026-03-26 07:20:22 +02:00
nrobi144
072e437277 feat(cache): wire State classes on DesktopIAccount for GC retention
- Add BookmarkListState, Kind3FollowListState, Nip65RelayListState
  to DesktopIAccount — pins important AddressableNotes via strong refs
- Create stub Kind3FollowListRepository + Nip65RelayListRepository
  (no persistence yet, null backups)
- Update followingKeySet() to read from Kind3FollowListState.flow
- Revert accidental long-form branch changes in DeckColumnContainer
2026-03-26 07:17:39 +02:00
nrobi144
2b95cc013a feat(cache): extract Kind3FollowListState + Nip65RelayListState to commons
- Add Kind3FollowListState to commons/commonMain with ICacheProvider
  + Kind3FollowListRepository interface for settings needs
- Add Nip65RelayListState to commons/commonMain with ICacheProvider
  + Nip65RelayListRepository interface with default relay sets
- Per-feature repository interfaces match existing EphemeralChatRepository
  / PublicChatListRepository pattern in commons
- Android originals unchanged — Desktop will use commons versions
- Static LocalCache.justConsumeMyOwnEvent calls replaced with cache param
2026-03-26 07:14:16 +02:00
nrobi144
7151d3052a feat(cache): extract BookmarkListState to commons/commonMain
- Move BookmarkListState from amethyst/ to commons/commonMain/
- Change cache param from LocalCache to ICacheProvider
- Android file becomes typealias to commons version
- No settings or decryptionCache dependencies (simplest State class)
- Pins bookmarkList AddressableNote via strong ref for GC retention
2026-03-26 07:14:16 +02:00
nrobi144
cf6a74b162 feat(cache): add periodic memory cleanup for Desktop
- Add cleanMemory() to DesktopLocalCache (sweeps stale WeakRef entries)
- Add startCleanupLoop() to Coordinator with heap monitoring
  (MemoryMXBean, checks every 30s, cleans at >75% heap or every 5min)
- cleanObservers() clears unused NoteFlowSets on notes + addressables
- Try-catch per operation so one failure doesn't skip the rest
- 2-minute startup grace period before first cleanup
- Cleanup job cancelled on clear() (account switch / logout)
2026-03-26 07:14:15 +02:00
nrobi144
3dbbc039b3 feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
- DesktopLocalCache now uses LargeSoftCache (WeakReference-based, GC-driven)
  instead of BoundedLargeCache (strong refs, 50k cap, arbitrary eviction)
- Delete BoundedLargeCache.kt — no longer needed
- Rewrite findUsersStartingWith to use forEach instead of values()
- Remove BoundedLargeCache eviction tests (no longer applicable)
- Notes now only disappear when nothing references them, not arbitrarily

Per Vitor's feedback: "this maximum size approach might not work well,
as things will just disappear"
2026-03-26 07:13:27 +02:00
nrobi144
00b06a0e2a refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
- Move LargeSoftCache from amethyst/model to commons/jvmAndroid/model/cache
  so both Android and Desktop share the same WeakReference cache implementation
- Change ICacheProvider return types from Any? to proper types (User?, Note?)
  since these model classes already live in commons
- Remove unnecessary casts in ThreadAssembler, ChatNewMessageState, SearchBarState
- Improve LargeSoftCache.cleanUp() to use single-pass iterator (zero allocation)
- Update Android imports in LocalCache, LargeSoftCacheAddressExt, and test

Per Vitor's feedback on PR #1905: BoundedLargeCache evicts arbitrarily.
LargeSoftCache uses WeakReferences so GC respects the reference graph.
2026-03-26 07:13:27 +02:00
nrobi144
3b489fb3cb feat(cache): seed Reads and Bookmarks screens from cache on compose
Both screens now show cached data instantly on navigation instead of
showing loading states while re-fetching from relays.

- BookmarksScreen: reads cached BookmarkListEvent from addressableNotes,
  seeds public bookmark events from notes cache
- ReadsScreen: seeds long-form notes (kind 30023) from notes cache

Relay subscriptions still run to fetch fresh data in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
f27c8811d5 fix(cache): don't reset followersCount to 0 on subscription restart
Cached value now stays visible until relay data exceeds it, preventing
the count jumping from cached→0→ticking back up on each navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
f2169e0236 feat(cache): cache profile metadata and follower/following counts
Profile screen now reads initial values from cache so navigating back
shows data instantly instead of re-fetching everything from scratch.

- Seed displayName/about/picture from User.metadataOrNull() on compose
- Add followerCounts/followingCounts maps to DesktopLocalCache
- Write counts on each update, read on profile open
- Follower count still ticks up live (good UX) but starts from cached value
- 4 new tests for profile count caching + metadata cache reads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
3e41bab5d5 fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions
Root cause of "0 relays, 0 notes, 0 follows": all screens used
connectedRelays as the relay set for subscriptions, but NostrClient
uses relay-on-demand — relays only connect when openReqSubscription
is called with their URL. This created a deadlock: screens waited for
connected relays, but relays only connect when screens subscribe.

Fix: use relayStatuses.keys (registered/available relays) which are
populated by addDefaultRelays() at startup. openReqSubscription
triggers connection on-demand.

Affected screens: FeedScreen, UserProfileScreen, ThreadScreen,
NotificationsScreen, BookmarksScreen, ReadsScreen, NewDmDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144
1e1bc1b172 fix(cache): add missing relay subscriptions for FeedScreen and UserProfileScreen
FeedScreen and UserProfileScreen used DesktopFeedViewModel (cache-backed)
but never subscribed to relays for kind 1/kind 3 events, causing 0 notes,
0 followed, and 0 relays on the home feed.

Adds rememberSubscription calls for:
- Contact list (kind 3) in FeedScreen → populates followedUsers
- Global/following feed (kind 1) in FeedScreen → populates cache
- Profile notes (kind 1) in UserProfileScreen → populates cache

Also adds 33 integration tests covering the full cache pipeline:
- DesktopCachePipelineTest: cache → filter → ViewModel (25 tests)
- CoordinatorPipelineTest: coordinator → cache → ViewModel (8 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
0621e8f7c2 refactor(cache): P3 simplifications — remove dead code, reduce abstractions
- Replace SubscriptionHealth map + data class with single lastEventAt
  Long? timestamp (only consumer was RelayHealthIndicator taking max())
- Replace consumer HashMap registry with when block (safe casts,
  compiler-checked, 9 kinds doesn't benefit from O(1) lookup)
- Remove dead loadReactionsForNotes() (superseded by requestInteractions)
- Remove unused BoundedLargeCache methods: containsKey, isEmpty,
  mapNotNull, forEach (zero external callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
4f5665a2bd fix(cache): address review findings — subscription churn, O(n) size, atomicity
P1 fixes from compound engineering review:
- Fix subscription churn flooding relays: DisposableEffect keyed on
  feedMode/noteId (stable) instead of feedState (changes every 250ms)
- BoundedLargeCache: AtomicInteger counter replaces O(n)
  ConcurrentSkipListMap.size() — eliminates 50K traversal per event
- Non-atomic updateHealth(): use MutableStateFlow.update{} for
  atomic compare-and-set instead of read-modify-write race

P2 fixes:
- DesktopThreadFilter: LinkedHashSet for O(1) containment checks,
  down from O(R²) with MutableList
- consumeContactList: createdAt guard ensures newer events always win
- consumeEvent error logging: include event ID and relay URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
c598c6135c feat(cache): ThreadScreen → DesktopFeedViewModel + DesktopThreadFilter
Replace EventCollectionState + 7 rememberSubscription blocks + inline
count maps with DesktopFeedViewModel + DesktopThreadFilter (graph walk).
Keep root note + replies relay subscriptions to populate cache. Use
FeedNoteCard for both root and reply notes (reads counts from Note.flowSet).
Remove ~200 lines of inline state management (zapsByEvent, reactionIdsByEvent,
replyIdsByEvent, repostIdsByEvent, bookmarkList, bookmark subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
8c1e54e147 feat(ui): relay health indicator in NavigationRail
Add RelayHealthIndicator component — shows elapsed time since last
relay event. Hidden when <30s (healthy), shows "45s ago" / "3m ago"
when stale. Placed above BunkerHeartbeatIndicator in NavigationRail.
Reads from Coordinator's subscriptionHealth StateFlow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
50eb03bc3e feat(cache): UserProfileScreen notes feed → DesktopFeedViewModel
Replace EventCollectionState + createUserPostsSubscription + manual cache
hydration with DesktopFeedViewModel + DesktopProfileFeedFilter. Profile
header subscriptions (metadata, contact list, followers) kept as-is since
they're not feed concerns. DisposableEffect cleanup on profile switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
c63ace1d9c feat(cache): FeedScreen → DesktopFeedViewModel + Note-based FeedNoteCard
Rewrite FeedScreen from inline EventCollectionState + 8 rememberSubscription()
calls to cache-centric DesktopFeedViewModel pattern:

- ViewModel keyed on feedMode with DisposableEffect cleanup (destroy())
- FeedState pattern matching (Loading/Loaded/Empty/Error)
- FeedNoteCard takes Note instead of Event, reads counts from Note.flowSet
- DisposableEffect for note.clearFlow() cleanup in LazyColumn
- DisposableEffect for Coordinator interaction subscription lifecycle
- LaunchedEffect for rate-limited metadata loading via Coordinator
- Extract FeedHeader into separate composable
- Add destroy() to DesktopFeedViewModel (ViewModel.clear() is internal in KMP)
- Update UserProfileScreen to look up Note from cache for FeedNoteCard

Removes ~350 lines of inline state management (EventCollectionState,
zapsByEvent, reactionIdsByEvent, replyIdsByEvent, repostIdsByEvent,
followedUsers, bookmarkList, 8 rememberSubscription blocks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144
e8ede21c17 feat(cache): Coordinator error handling + interaction subscriptions
Add try-catch in consumeEvent() so one bad event doesn't kill the
pipeline. Add requestInteractions()/releaseInteractions() with Job
tracking via ConcurrentHashMap for screen-scoped subscription lifecycle.
Add SubscriptionHealth tracking (lastEventReceivedAt, eoseReceived)
exposed as StateFlow for UI consumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
684ba158ab feat(cache): kind-based consumer registry + new event types
Replace when-dispatch with HashMap<Int, Handler> for O(1) event routing.
Add consume methods for: RepostEvent (kind 6), ContactListEvent (kind 3),
LongTextNoteEvent (kind 30023), BookmarkListEvent (kind 30001).
Replace @Volatile followedUsers with MutableStateFlow for thread safety
and Compose observability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
e834dbd1e5 fix(ui): show display names instead of npubs in feed
toNoteDisplayData() now reads User.toBestDisplayName() from cache
instead of always showing npub. Falls back to npub if no metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
e80473a7d9 feat(cache): Phase 3 — route all screen events through cache
All screens now pipe relay events into DesktopLocalCache via
coordinator.consumeEvent(). Existing per-screen EventCollectionState
kept for rendering (incremental migration), but data also enters
cache for persistence across navigation.

- FeedScreen: route global + following feed events
- ThreadScreen: route root note + reply events
- UserProfileScreen: route posts + add cache hydration on mount
  (LaunchedEffect queries cache for existing posts → instant display)
- BookmarksScreen: route public + private bookmark events
- ReadsScreen: route long-form feed events (add coordinator param)
- NotificationsScreen: route notification events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144
c198df57fd feat(cache): Phase 2+3 foundation — FeedFilters + DesktopFeedViewModel
Phase 2: 8 FeedFilter implementations:
- DesktopGlobalFeedFilter (kind 1, limit 2500)
- DesktopFollowingFeedFilter (kind 1 + followed pubkeys)
- DesktopThreadFilter (root + reply graph walk)
- DesktopProfileFeedFilter (notes by pubkey, limit 1000)
- DesktopBookmarkFeedFilter (notes by ID set)
- DesktopReadsFeedFilter (kind 30023, limit 500)
- DesktopNotificationFeedFilter (events tagging user)
- DesktopSearchFeedFilter (content text search)

All use AdditiveFeedFilter for incremental updates (O(batch)
after initial load). Limits are 5x Android's.

Phase 3 foundation: DesktopFeedViewModel subclass that calls
refreshSuspended() on init to load existing cache data —
fixes navigation persistence (back shows instant data).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
543f7a329c feat(cache): Phase 1 — cache-centric architecture foundation
- Add BoundedLargeCache: LargeCache wrapper with size enforcement
  (50k notes, 25k users, 10k addressable). Lock-free reads via
  ConcurrentSkipListMap, evicts 10% when cap exceeded.

- Switch DesktopLocalCache from ConcurrentHashMap to BoundedLargeCache.
  Exposes filterIntoSet, values, etc. matching Android's query patterns.

- Add consume methods for kinds 1 (TextNote), 7 (Reaction),
  9734 (ZapRequest), 9735 (Zap). Uses tagsWithoutCitations() for
  reply parsing, originalPost()+taggedAddresses() for reactions,
  zappedPost() for zaps. Follows Android LocalCache pattern.

- Add consumeEvent() bridge in coordinator with BasicBundledInsert
  (250ms batching). Routes relay onEvent callbacks to cache.

- Fix SharedFlow: extraBufferCapacity=64, DROP_OLDEST. Prevents
  blocking emitters and dropped events.

- Set -Xmx2g JVM arg for long desktop sessions.

- Clear cache on logout (coordinator first, then cache).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
61119e6916 docs: deepen cache plan — BoundedLargeCache, FeedNoteCard rewrite details
- Resolve LruCache vs LargeCache: use LargeCache (lock-free ConcurrentSkipListMap)
  with BoundedLargeCache wrapper for size enforcement on put()
- Confirm LargeCache available on desktop via quartz jvmAndroid source set
- Detail FeedNoteCard rewrite: Note model field mapping, 5 subscription removals
- Fix filter examples to use filterIntoSet (LargeCache API)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144
9c160006fa docs: desktop cache architecture brainstorm and plan
Cache-centric architecture for navigation persistence, mirroring
Android Amethyst's pattern. Three-phase incremental migration:
Phase 1 - Store events in DesktopLocalCache with LRU eviction
Phase 2 - Create FeedFilter implementations
Phase 3 - Migrate screens to FeedViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:45 +02:00
Claude
2f8e62fda5 feat: implement NIP-86 Relay Management API
Add quartz protocol package for NIP-86 JSON-RPC relay management with
NIP-98 HTTP authorization, and a management UI accessible from the
relay information screen when the relay advertises NIP-86 support.

Quartz (nip86RelayManagement/):
- Nip86Method: all 21 method constants from the spec
- Nip86Request: serializable JSON-RPC request with factory methods
- Nip86Response: response model with typed result parsing helpers
- Nip86Client: builds NIP-98 auth headers, serializes requests,
  parses responses for each result type (pubkeys, events, kinds, IPs)

Amethyst:
- Nip86Retriever: OkHttp-based HTTP executor for NIP-86 calls
- RelayManagementViewModel: state management for all NIP-86 operations
- RelayManagementScreen: tabbed UI (Pubkeys, Events, Kinds, IPs,
  Settings) with add/remove dialogs, moderation queue, and relay
  settings fields
- Route.RelayManagement added to navigation
- "Manage Relay" button shown in RelayInformationScreen top bar when
  relay's supported_nips includes "86"

https://claude.ai/code/session_01QckCAm1T8pJqqmURBiNtaQ
2026-03-26 04:32:43 +00:00
Claude
70baf55e1c feat: add NIP-77 negentropy sync support
Add support for NIP-77 (Negentropy Syncing) protocol messages using
the negentropy-kmp library for efficient set reconciliation between
client and relay.

New files:
- NegOpenCmd, NegMsgCmd, NegCloseCmd: Client-to-relay commands
- NegMsgMessage, NegErrMessage: Relay-to-client messages
- NegentropySession: Client-side reconciliation orchestrator
- NegentropyServerSession: Server-side reconciliation handler
- NegentropyManager: High-level sync manager with listener callbacks

Updated serializers:
- Jackson MessageSerializer/Deserializer for NEG-MSG, NEG-ERR
- Jackson CommandSerializer/Deserializer for NEG-OPEN, NEG-MSG, NEG-CLOSE
- Kotlin MessageKSerializer/CommandKSerializer for all NIP-77 types

Compatible with strfry relay's negentropy implementation via the
negentropy-kmp library which implements the same Protocol V1 spec.

https://claude.ai/code/session_01Dc6W1G1jURAAR9kzyVvk6g
2026-03-26 04:23:32 +00:00
Claude
463938768e feat: add NIP-62 Request to Vanish screen
Add a new screen allowing users to send NIP-62 relay deletion requests.
Users can select a specific relay or ALL RELAYS, pick a date (data
created before that date will be requested for deletion), and provide
an optional reason. Includes confirmation dialog with clear warnings
about the irreversible nature of vanish requests.

https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
2026-03-26 04:18:39 +00:00
M
ebbcfa26fb refactor: remove Namecoin label from search results
Per review: .bit resolution is just NIP-05, no special labeling needed.
2026-03-26 10:25:31 +11:00
M
e7cde18371 feat: show Namecoin label on .bit search results without ViewModel state
Derive the label from the existing searchTerm — if it ends with .bit,
show a chain-emoji label above each user result. No new StateFlow or
ViewModel state management needed.
2026-03-26 09:49:34 +11:00
M
1ca904b415 refactor: remove Namecoin label from search results per review
Remove namecoinResolvedUser StateFlow and the chain-emoji label
from search results. .bit resolution still works through the
existing NIP-05 path — just no special UI decoration.
2026-03-26 09:37:56 +11:00
Vitor Pamplona
e43047c027 removes warnings 2026-03-25 18:33:03 -04:00
M
67fd48c5b8 refactor: simplify .bit search resolution per review feedback
Address @vitorpamplona's review comments on the last commit:

1. Remove NamecoinResolutionState sealed class and status banners
   — the existing NIP-05 resolver already handles .bit, no need for
   separate Namecoin-specific UI state (spinner, chain emoji, error)

2. Remove separate toNip05IdOrNull() resolution path
   — widen the existing `if (term.contains('@'))` gate in both
   SearchBarViewModel and UserSuggestionState to also accept bare
   .bit domains (synthesize _@domain.bit → existing NIP-05 flow)

3. Remove d/ and id/ identifier support from search
   — per feedback, only NIP-05 style resolution (.bit) in search

4. Remove special Namecoin result rendering in ImportFollowList
   — just show UserLine for all results, no separation needed

5. Keep a lightweight 'Namecoin' label on resolved users in search
   — namecoinResolvedUser StateFlow tracks the .bit-resolved user,
   shown as a small label above the UserCompose row

Net: -405 lines, +50 lines across 6 files.
2026-03-26 08:43:39 +11:00
M
0d83af8912 feat: resolve bare .bit domains and show Namecoin resolution status in UI
Widen the existing NIP-05 resolution gate in UserSuggestionState and
SearchBarViewModel to also accept bare .bit domains and d//id/ Namecoin
identifiers. A synthesised Nip05Id is passed to the existing
nip05Client.get() path — which already routes .bit to the
NamecoinNameResolver — so no separate resolution logic is needed.

Add NamecoinResolutionState (Idle/Resolving/Resolved/Error) as a
StateFlow so the UI can show progress. ImportFollowListSelectUserScreen
and SearchScreen display an animated status banner (spinner while
resolving, chain emoji on success, warning on error) and label
Namecoin-resolved profiles in the results list.
2026-03-26 07:40:25 +11:00
Vitor Pamplona
46b75abc81 Merge pull request #1950 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 16:20:00 -04:00
Crowdin Bot
618edf9dd5 New Crowdin translations by GitHub Action 2026-03-25 20:17:28 +00:00
Vitor Pamplona
0de01ca295 Merge pull request #1949 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 16:16:54 -04:00
Vitor Pamplona
2e9beacb13 no message 2026-03-25 16:14:20 -04:00
Crowdin Bot
9db9283952 New Crowdin translations by GitHub Action 2026-03-25 20:07:46 +00:00
Vitor Pamplona
452d2accc9 Merge pull request #1948 from vitorpamplona/claude/add-web-bookmarks-qYwpU
feat: implement NIP-B0 Web Bookmarking (kind 39701)
2026-03-25 16:06:09 -04:00
Vitor Pamplona
bf8791088c Trying to resolve the build issue between spotless and vlcSetup without running vlcSetup when applying spotless from Claude. 2026-03-25 16:05:29 -04:00
Vitor Pamplona
15ccdf0c73 Merge pull request #1938 from vitorpamplona/claude/organize-favorite-relay-feeds-DL3o2
feat: separate favorite relay feeds into own category in nav popup
2026-03-25 15:44:59 -04:00
Vitor Pamplona
c903996cc2 Merge pull request #1939 from vitorpamplona/claude/nip89-client-tag-LZYhd
feat: NIP-89 client tag on all signed events
2026-03-25 15:42:31 -04:00
Vitor Pamplona
052fa22d59 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
2026-03-25 15:38:16 -04:00
Vitor Pamplona
e90945d201 Removing the solution to a warning about spotless dependencies because claude can't run the vlc step 2026-03-25 15:34:32 -04:00
M
2e02edc333 refactor: rename trustAllCerts → usePinnedTrustStore
The field no longer means 'trust all certificates' — since the Samsung
One UI 7 fix, it means 'use the pinned trust store (hardcoded +
TOFU-pinned certs + system CAs) instead of system-only trust.'

The old name was actively misleading and could cause a future
contributor to interpret the flag literally, potentially reintroducing
the trust-all pattern that Samsung Knox and GrapheneOS reject.

Renamed across all 4 files:
- ElectrumxServer.kt: field definition + updated KDoc
- ElectrumXClient.kt: createSocket() usage + comments
- NamecoinSettings.kt: parseServerString() usage + comments
- NamecoinSettingsTest.kt: test assertions
2026-03-26 06:33:43 +11:00
Claude
cbee188ca5 feat: implement NIP-B0 Web Bookmarking (kind 39701)
Add support for NIP-B0 web bookmarks with a complete protocol
implementation in Quartz and a full UI in Amethyst for adding,
editing, browsing, and deleting web bookmarks.

Quartz:
- WebBookmarkEvent (kind 39701) as an addressable event
- Tag builder/parser extensions for title, published_at, hashtags
- Registered in EventFactory

Amethyst:
- WebBookmarksScreen with FAB to add, and per-card edit/delete/open
- WebBookmarkEditDialog for add/edit with URL, title, description, tags
- WebBookmarkFeedFilter querying addressable notes by kind + pubkey
- Account.sendWebBookmark() and Account.deleteWebBookmark()
- LocalCache.consume() for WebBookmarkEvent
- Drawer navigation entry with Language icon
- Route.WebBookmarks and navigation registration

https://claude.ai/code/session_01UzfLJttwuJzovtb8HX5F9n
2026-03-25 19:29:55 +00:00
M
7f451ee7c7 fix: harden TLS for GrapheneOS and security-conscious Android ROMs
Address compatibility and security issues identified by reviewing
GrapheneOS source (hardened Conscrypt, strict TLS enforcement):

1. .onion: prefer pinned factory, fall back to trust-all

   GrapheneOS patches Conscrypt with stricter TLS enforcement that may
   reject no-op X509TrustManagers even for proxied sockets. The .onion
   server's cert is already in PINNED_ELECTRUMX_CERTS (same operator as
   electrumx.testls.space), so we now use cachedPinnedSslFactory() as
   the primary path. onionSslFactory() (trust-all) is kept as a fallback
   on SSLHandshakeException only — this handles cert rotation or unknown
   .onion servers gracefully.

2. TOFU: require explicit user confirmation before pinning

   Previously, Test Connection auto-pinned every cert on success. An
   attacker performing MITM during first test would get their cert
   permanently trusted. Now each new cert triggers an AlertDialog showing
   the full SHA-256 fingerprint. Users must explicitly accept ('Trust')
   or reject each cert — aligning with GrapheneOS's philosophy of
   explicit trust decisions.

3. Clarify trustAllCerts semantics

   The field name is misleading post-refactor: it now means 'use pinned
   trust store' not 'trust all certificates'. Added TODO to rename to
   usePinnedTrustStore and updated inline comments to prevent future
   misinterpretation.

Note: hostname verification on raw SSLSocket is a pre-existing gap
(not introduced by the Samsung fix PR) — SSLSocket.createSocket() uses
the host parameter for SNI only, not hostname verification. A follow-up
should add endpointIdentificationAlgorithm='HTTPS' or fingerprint-based
verification for pinned certs.
2026-03-26 06:26:18 +11:00
Vitor Pamplona
138428a233 Merge pull request #1947 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 15:24:15 -04:00
Crowdin Bot
fd86e3c045 New Crowdin translations by GitHub Action 2026-03-25 19:23:05 +00:00
Vitor Pamplona
283fb96bb7 Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  reuse hex methods.
  spotless
  delete voice files on failuer
  added progress and test plan to doc
  fix: delete abandoned compressed video when larger than original fix: eagerly delete intermediate temp files in upload pipeline fix: delete temp file from MediaCompressorFileUtils after image compression refactor: simplify temp file cleanup logic - Remove TOCTOU anti-pattern (file.exists() before file.delete()) - Consolidate double deleteTempUri calls into single conditional - Remove restating comments
  analysis for temporary files
  fix: clean up temp files after upload completes
  perf: optimize ChaCha20 and XChaCha20-Poly1305 for speed
  style: apply spotless formatting to ChaCha20/Poly1305 sources
  feat: replace libsodium with pure Kotlin ChaCha20/Poly1305 implementation
2026-03-25 15:20:28 -04:00
Vitor Pamplona
6db28dca0b Merge pull request #1908 from vitorpamplona/claude/remove-libsodium-dependency-DjtWa
Replace libsodium with pure Kotlin XChaCha20-Poly1305 implementation
2026-03-25 13:53:48 -04:00
Vitor Pamplona
2474ba1713 Fixes anon accounts becoming empty bubbles in the notifiy section of new posts. 2026-03-25 13:53:06 -04:00
Vitor Pamplona
fc560d2752 reuse hex methods. 2026-03-25 13:22:02 -04:00
Vitor Pamplona
2ead6645c0 Merge pull request #1925 from vitorpamplona/claude/review-cache-cleanup-Yr2P6
Add cleanup of temporary files in upload pipeline
2026-03-25 13:20:52 -04:00
Vitor Pamplona
138bb144ee Merge branch 'main' into claude/review-cache-cleanup-Yr2P6 2026-03-25 13:20:30 -04:00
Vitor Pamplona
e2ac534be0 spotless 2026-03-25 12:55:31 -04:00
davotoula
aa5dad9fac delete voice files on failuer 2026-03-25 17:42:05 +01:00
Vitor Pamplona
0d92c86459 Merge branch 'main' into claude/remove-libsodium-dependency-DjtWa 2026-03-25 11:50:07 -04:00
Vitor Pamplona
9e9f6c3164 Completing the transition from NIP-44 architecture specific tests to commontTest 2026-03-25 11:48:37 -04:00
Vitor Pamplona
d84d146981 Merge branch 'main' into claude/remove-libsodium-dependency-DjtWa 2026-03-25 11:24:55 -04:00
Vitor Pamplona
617db3613f Re-enables benchmark with native android ChaCha functions 2026-03-25 11:17:17 -04:00
Vitor Pamplona
60a44d25eb Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  feat: implement NIP-45 HyperLogLog for probabilistic event counting
2026-03-25 10:55:12 -04:00
Vitor Pamplona
2ffb920f75 Fixes NIP-46 decoding test 2026-03-25 10:51:31 -04:00
Vitor Pamplona
f8baf83146 Merge pull request #1941 from vitorpamplona/claude/implement-hyperloglog-nip45-Jai8I
feat: implement NIP-45 HyperLogLog support
2026-03-25 09:34:35 -04:00
Vitor Pamplona
08090f57b5 Merge pull request #1945 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 09:34:18 -04:00
Crowdin Bot
19ef2ca041 New Crowdin translations by GitHub Action 2026-03-25 13:30:02 +00:00
Vitor Pamplona
3a21396838 Merge pull request #1936 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-25 09:28:32 -04:00
Crowdin Bot
0705d070bd New Crowdin translations by GitHub Action 2026-03-25 12:40:37 +00:00
Vitor Pamplona
d6541e37c3 merge 2026-03-25 08:37:18 -04:00
Vitor Pamplona
7583f7f8e1 Moves some of the nip-44 based tests to commonTest 2026-03-25 08:33:04 -04:00
Vitor Pamplona
c654d2267c Protects against crashes when the signer sends an unverifiable payload back to Amethyst 2026-03-25 08:28:31 -04:00
Vitor Pamplona
0a0b6aefa0 Merge pull request #1943 from dadofsambonzuki/attestation-fix
Align attestation events with latest NIP format
2026-03-25 07:49:07 -04:00
Vitor Pamplona
a1ad460b90 Merge pull request #1944 from KotlinGeekDev/kmp-completeness
Kmp completeness: update feature parity table.
2026-03-25 07:47:38 -04:00
Vitor Pamplona
9f6684b044 Merge pull request #1942 from nrobi144/features/long-form-content
feat(desktop): highlight UX, shared stores, profile tabs, editor toolbar
2026-03-25 07:47:21 -04:00
KotlinGeekDev
ac60cbeb02 Update Quartz feature parity table. 2026-03-25 12:42:41 +01:00
Nathan Day
92ae288720 Align attestation tags with latest NIP format 2026-03-25 11:30:29 +00:00
Nathan Day
0b049c656c Merge branch 'vitorpamplona:main' into main 2026-03-25 11:01:43 +00:00
davotoula
42db03790a added progress and test plan to doc 2026-03-25 10:23:38 +01:00
davotoula
60b7c6bd0d fix: delete abandoned compressed video when larger than original
fix: eagerly delete intermediate temp files in upload pipeline
fix: delete temp file from MediaCompressorFileUtils after image compression
refactor: simplify temp file cleanup logic
- Remove TOCTOU anti-pattern (file.exists() before file.delete())
- Consolidate double deleteTempUri calls into single conditional
- Remove restating comments
2026-03-25 10:19:16 +01:00
davotoula
390edf311d analysis for temporary files 2026-03-25 09:29:48 +01:00
davotoula
26fb6fead4 Merge branch 'main-upstream' into claude/review-cache-cleanup-Yr2P6 2026-03-25 09:18:53 +01:00
davotoula
7871a7672b Merge branch 'main' into chess-enhancements-and-bug-fixes 2026-03-25 09:10:46 +01:00
nrobi144
e037254b39 fix(highlights): shared store, context menu, link rendering, publish, profile tabs
- Share DesktopHighlightStore and DesktopDraftStore at app level instead
  of per-column to fix cross-deck reactivity and draft persistence
- Replace broken clipboard-based highlight creation with
  LocalContextMenuRepresentation that piggybacks on Copy to get selected text
- Render highlights as highlight:// links instead of bold/italic markers
- Add collapsible highlights panel in article reader with edit/delete/publish
- Publish highlights to relays as NIP-84 events via HighlightPublishAction
- Add Reads tab (kind 30023) and Highlights tab (kind 9802) to profile
- Rewrite MarkdownToolbar with MarkdownEditorState for selection-aware
  toggle, Material icons, and keyboard shortcuts (Cmd+B/I/E/K)
- Wire DraftsScreen "New Draft" button to ArticleEditorScreen navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 06:59:41 +02:00
Claude
403351610b feat: implement NIP-45 HyperLogLog for probabilistic event counting
Add client-side HyperLogLog support as specified by NIP-45:
- HyperLogLog object with merge, estimate, encode/decode, and
  computeOffset for deterministic filter-based offset calculation
- Add hll ByteArray field to CountResult for relay HLL data
- Update CountResultKSerializer to serialize/deserialize hll hex field
- Add queryCountMergedHll extension for multi-relay HLL merging
- 20 unit tests covering HLL operations and serialization round-trips

https://claude.ai/code/session_01CQzygEkwckLCiXzSRPMpBa
2026-03-25 03:43:47 +00:00
Claude
36af9e2346 feat: add NIP-89 client tag to all signed events via NostrSignerWithClientTag decorator
Introduces a signer decorator that automatically appends the NIP-89
client tag to every event before signing, ensuring all events carry
client identification without modifying individual event creation sites.
Works transparently with internal, external (NIP-55), and remote (NIP-46) signers.

https://claude.ai/code/session_01WRtuk1ySCfqXzfFrieKsDs
2026-03-25 03:24:49 +00:00
Claude
c3c9bd016e feat: separate favorite relay feeds into their own category in nav popup
Relay feeds were previously grouped under the generic "Feeds" category
in the top nav bar filter dialog. This adds a dedicated "Relays" group
so users can more easily find their favorite relay feeds.

https://claude.ai/code/session_016gjbPgYRXrQCoY1zKPz5PX
2026-03-25 02:59:40 +00:00
M
a4953f628d doc: verify .onion ElectrumX cert via Tor, document pinning
Connected to the .onion hidden service via Tor SOCKS proxy and
confirmed it serves the SAME certificate as electrumx.testls.space:
  SHA-256: 53:65:D5:BB:26:19:F5:40:1C:D8:8E:FC:AF:FB:A5:B2:...

The .onion cert is already covered by the first pinned cert entry.
Added comments documenting this relationship and instructions for
fetching .onion certs via Tor for future updates.

The .onion still uses onionSslFactory() (trust-all) as the primary
path — this is correct since Tor provides its own authentication.
The pinned cert serves as defense-in-depth.
2026-03-25 12:13:49 +11:00
M
3afe0c9978 fix: bypass cert pinning for .onion ElectrumX servers
Tor hidden services are authenticated by their onion address (the
public key hash), making TLS certificate verification redundant.

The .onion server in TOR_ELECTRUMX_SERVERS was going through
pinnedSslFactory() but its cert isn't in the pinned list (and can't
be fetched without Tor). This would cause .onion connections to fail
with SSLHandshakeException when Tor mode is active.

Fix: .onion addresses now use a dedicated onionSslFactory() with
trust-all, which is safe because:
1. Tor provides end-to-end encryption
2. The onion address IS the server identity proof
3. Samsung Knox trust-all rejection doesn't apply to proxied sockets
2026-03-25 12:10:45 +11:00
M
b8b51db0b5 feat: TOFU cert pinning for custom ElectrumX servers
When a user adds a custom ElectrumX server and runs Test Connection,
the server's TLS certificate is automatically captured and pinned
(Trust On First Use). This allows custom servers with self-signed
certificates to work on Samsung/Xiaomi/OnePlus devices.

Changes:
- ElectrumXClient: addPinnedCert(), setDynamicCerts() for runtime
  cert management; testServer() now captures server cert PEM and
  SHA-256 fingerprint from SSL session
- NamecoinSharedPreferences: persist pinned certs to DataStore
- AppModules: load pinned certs on startup, sync to ElectrumXClient
- NamecoinSettings: custom servers always set trustAllCerts=true
  (ElectrumX servers almost universally use self-signed certs)
- UI: shows cert fingerprint in test results, auto-pins on success
- ServerTestResult: new serverCertPem + certFingerprint fields

Flow: Add server → Test Connection → cert auto-captured and pinned →
future connections trust that cert even on Samsung Knox devices.
2026-03-25 12:05:05 +11:00
Vitor Pamplona
f42881a48e v1.06.3 2026-03-24 20:55:04 -04:00
Vitor Pamplona
6fc72f47bb Highlights when arriving at the reply message 2026-03-24 20:53:22 -04:00
M
49698de99c feat: add Test Connection diagnostics to Namecoin settings
Add per-server connection testing with detailed error reporting:
- Test Connection button tests each ElectrumX server individually
- Shows streaming results:  success with response time,  failure
  with human-readable error (TLS handshake failed, connection refused,
  timeout, DNS failed, invalid response, etc.)
- Diagnostic card shows: last test timestamp + success count, device
  info (manufacturer/model/Android/API), TLS version negotiated
- ElectrumXClient.testServer() method for single-server diagnostics
  with TLS version capture from SSL session

This gives users (and bug reporters) immediate visibility into why
Namecoin resolution may be failing silently on their device.
2026-03-25 11:52:05 +11:00
Vitor Pamplona
041c788e21 Improvements to anonymous replies 2026-03-24 20:42:06 -04:00
M
15a2d77a0f fix: pin ElectrumX server certs instead of trust-all TrustManager
Samsung One UI 7 (Android 16) silently rejects TLS connections that use
a no-op X509TrustManager which accepts all certificates. This breaks
Namecoin resolution on all Samsung devices running One UI 7, including
Galaxy A15 (SM-A156E) and Galaxy S24 Ultra (SM-S938B).

Replace trustAllSslFactory() with pinnedSslFactory() that:
- Pins the actual self-signed certificates of known ElectrumX servers
- Also includes system CA certificates for servers with real certs
- Uses a proper TrustManagerFactory chain that Samsung Knox accepts

Additional OEM compatibility hardening:
- Cache the SSLSocketFactory (avoid expensive rebuild per connection)
- Request TLSv1.2 explicitly (Xiaomi MIUI/HyperOS and OnePlus ColorOS
  Conscrypt forks may default to TLS 1.0 for raw socket upgrades)
- Enforce TLSv1.2+ enabled protocols on the SSLSocket
- KeyStore fallback to PKCS12 if default type fails (Xiaomi)
- Defensive try/catch on system CA cert re-insertion (some OEMs return
  certs that cannot be added to a new KeyStore)

Tested on Android 16 (API 36) emulator — all ElectrumX servers connect
(hostname, IP-address, factory reuse) and .bit resolution works E2E.

Pinned certs:
- electrumx.testls.space:50002 (expires 2027-05-04)
- nmc2.bitcoins.sk:57002 (expires 2030-10-22)
2026-03-25 11:41:12 +11:00
davotoula
86fd92e505 feat(chess): add dismiss/clear-all for finished games in lobby
Persist dismissed game IDs locally (SharedPreferences on Android,
java.util.prefs on Desktop) so completed games can be permanently
hidden from the chess lobby. Adds per-game dismiss button and
"Clear all" action when 2+ finished games exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 21:30:46 +01:00
davotoula
1f34a79676 fix: prevent double-fetch and stop re-polling finished chess games
- Mark recentlyLoadedGames timestamp eagerly in refreshGame (before
  async fetch) to prevent concurrent fetches for the same game
- Clear focused game in removeGameId when the removed game matches,
  so finished games stop being re-polled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 19:41:20 +01:00
davotoula
5bdc684f21 fix: add event-level dedup and handleGameAccepted guard in chess lobby
- Dedup incoming events by ID (bounded LRU set of 500) to prevent
  redundant processing when multiple relays deliver the same event
- Filter non-chess kind 30 events early (isStart=false, isMove=false)
- Guard handleGameAccepted with recentlyLoadedGames check to prevent
  N concurrent relay fetches when N move events arrive for same game
2026-03-24 19:29:06 +01:00
davotoula
944fbcd000 add extensive [chessdebug] logging across all chess layers
Uses Log.d("chessdebug", ...) via quartz's multiplatform Log utility
(android.util.Log on Android, println on Desktop) for proper logcat
integration.

Layers instrumented:
- [Reconstructor] state reconstruction, move replay, desync detection
- [Collector/CollectorMgr] event ingestion, dedup, routing
- [GameLoader] game loading, live state conversion
- [Lobby] challenges, moves, resign, spectating, polling refresh
- [Polling] polling cycles, focused mode
- [Broadcaster] relay connections, event send/confirm
- [LiveGame] move validation, opponent moves, head event tracking
- [AndroidVM] incoming event parsing from subscriptions

Filter with: adb logcat | grep chessdebug
2026-03-24 15:43:17 +01:00
davotoula
da53001e65 Merge branch 'main' into chess-enhancements-and-bug-fixes 2026-03-24 13:21:41 +01:00
nrobi144
efa294ea72 feat(highlights): add article highlights and note-taking system
Phase 1: HighlightData model + DesktopHighlightStore (Preferences-based)
Phase 2: Text selection highlight via Cmd+H/Cmd+Shift+H, inline bold/italic
         rendering, HighlightAnnotationDialog, SelectionContainer wrapping
Phase 3: MyHighlightsScreen with grouped view, HighlightPublishAction for
         NIP-84 (kind 9802), deck/sidebar/menu wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:16:54 +02:00
nrobi144
c75176891f feat(reads): integrate long-form into deck layout with zoom and reactions
- Wire Article, Editor, Drafts into DeckColumnType + DeckColumnContainer
- Add Drafts to sidebar nav, Add Column dialog, and Window menu
- Add article navigation (onNavigateToArticle) in SinglePaneLayout
- Add NoteActionsRow to ReadsScreen LongFormCards (zaps, reactions, replies)
- Add Cmd+/Cmd- zoom in ArticleReaderScreen via fontScale on RenderMarkdown
- Add kind 30023 to ProfileSubscription for long-form in user profiles
- Add fontScale param to RenderMarkdown using scaled LocalDensity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:18:37 +02:00
nrobi144
328fcf86d7 refactor(reads): address all review findings — security, performance, simplicity
P1 Critical:
- Add 300ms debounce on editor preview to prevent AST re-parse per keystroke
- Hoist Regex constants in ReadingTimeCalculator and TableOfContents
- Add URI scheme allowlist to editor onLinkClick (was missing vs reader)
- Add MAX_CONTENT_BYTES (100KB) validation before publish

P2 Important:
- Delete MarkdownSpikeScreen (278 LOC spike artifact)
- Delete HighlightCreator (95 LOC, zero callers — YAGNI)
- Delete DraftLongTextNoteEvent (162 LOC, no consumers — YAGNI)
- Replace ArticleMediaRenderer interface with onLinkClick lambda
- Extract inline subscription to FilterBuilders.longFormByAddress()
- Replace SimpleDateFormat with thread-safe DateTimeFormatter
- Remove dead placeholders (bookmark button, reactions row, unused param)
- Cache draft index in memory to avoid redundant disk reads
- Add TODO for publish-before-relay-ack issue

P3 Nice-to-have:
- Remove timestamp from subscription subId for stability
- Remove redundant slug ".." removal (regex handles it)
- Add metadata field length limits (title 256, summary 1024)
- Fix identical isMacOS branches in editor
- Validate image URLs (http/https only) in ArticleHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:03:29 +02:00
nrobi144
4fcbffdb3b feat(reads): Phase 2 — editor, drafts, publish, highlights
- Create DraftLongTextNoteEvent (kind 30024) in quartz with EventFactory registration
- Create LongFormPublishAction for signing + publishing kind 30023 events
- Create split-pane ArticleEditorScreen with live markdown preview
- Create MarkdownToolbar (bold, italic, heading, link, image, code, quote)
- Create MetadataPanel (title, summary, banner, tags, slug)
- Create DesktopDraftStore with JSON index + .md files, atomic writes, slug sanitization
- Create DraftsScreen with draft list, delete, new draft button
- Create HighlightCreator dialog for kind 9802 highlight creation
- Add Drafts nav rail entry and Editor/Drafts screen routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:59:52 +02:00
nrobi144
5ef36363cd feat(reads): Phase 1 — article reader with markdown, ToC, reading time
- Add richtext-commonmark deps to commons for shared markdown rendering
- Create RenderMarkdown composable with URL scheme allowlist (security)
- Create ArticleMediaRenderer interface for platform-specific media
- Create ArticleHeader with banner, title, author, reading time metadata
- Create TableOfContents with heading extraction and scroll-spy
- Create ReadingTimeCalculator (238 WPM prose, 80 WPM code, image decay)
- Create ArticleReaderScreen with Medium-style typography (680dp, 1.58x)
- Add DesktopScreen.Article navigation with address tag routing
- Update ReadsScreen to navigate via address tags instead of event IDs
- Responsive ToC sidebar (shown when window > 1100dp)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:59:52 +02:00
Claude
cba65344b9 fix: clean up temp files after upload completes
MetadataStripper, MediaCompressor, and EncryptFiles create intermediate
temp files in cacheDir during the upload pipeline, but these were never
deleted after the upload finished. Over time this causes unbounded disk
growth. Wrap upload calls in try/finally to ensure all intermediate temp
files (compressed, stripped, encrypted) are deleted regardless of
success or failure.

https://claude.ai/code/session_01YS3dbZ1k84N2EHS3AruYRV
2026-03-24 04:37:41 +00:00
Claude
425fc24bc2 perf: optimize ChaCha20 and XChaCha20-Poly1305 for speed
ChaCha20Core.chaCha20Xor:
- Parse key/nonce once into initial state, reuse across blocks
- XOR at word level (4 bytes at a time) instead of byte-by-byte
- Reuse working IntArray across blocks instead of allocating per block

XChaCha20Poly1305:
- Add hChaCha20FromNonce24 to avoid nonce copyOfRange(0,16) allocation
- Add chaCha20PolyKey to generate only 32 bytes instead of full 64-byte block
- Use single result array instead of ciphertext + tag concatenation

Benchmark (98-byte padded message, JVM):
  Before: encrypt 2286 ns/op, decrypt 2062 ns/op
  After:  encrypt 1544 ns/op, decrypt 341 ns/op

https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-24 02:35:30 +00:00
davotoula
6c8021f219 code review fixes:
Deduplicate chess notify() into shared notifyChessEvent() helper using BaseChessEvent
Remove addCompletedGameDirectly, extend moveToCompleted with optional liveState param
Hoist currentUser lookup to top of ChessLobbyContent, remove 4 duplicate remembers
Add missing imports in desktop ChessScreen, replace all FQN usages with short names
Remove redundant distinctBy in completed games UI (state already prevents duplicates)
2026-03-23 20:39:54 +01:00
davotoula
a6205a29c4 fix(chess): add chess kinds to notification relay subscription
Chess event kinds 30065 (accept) and 30066 (move) were missing from the
notification relay subscription filters, so they were never fetched from
relays for the in-app notification feed. Also bypass the follow-list
filter for chess events since opponents may not be followed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
c86daa6950 fix(chess): filter own games from live list, dismissable errors, clickable avatars and completed games
- Filter user's own games from public "Live Games" list in lobby
- Fix broken addSpectatingGame filter (playerPubkey is always viewerPubkey)
- Add removeGame() to clean up stale entries when "Game not found"
- Make player avatars clickable to open profile on game screen
- Make completed game cards clickable to view final board state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
08d5f2ebb5 fix(chess): resolve TimeUtils compilation error in ChessLobbyState
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
478c4cc356 feat(chess): notifications for challenge accepted and opponent moves
Add Android push notifications for chess events:
- LiveChessGameAcceptEvent (kind 30065): notifies when opponent accepts challenge
- LiveChessMoveEvent (kind 30066): notifies when opponent makes a move
- New Chess notification channel alongside existing DM and Zap channels
- Chess kinds added to NotificationFeedFilter for in-app notification feed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
292c6de08a feat(chess): exit spectator mode with Leave Game button
Adds ability to leave a spectated game on both Android and Desktop:
- Added stopSpectating() to ChessLobbyLogic (removes from state + stops polling)
- Both ViewModels now delegate to logic.stopSpectating() instead of bypassing it
- LiveChessGameScreen accepts onLeaveSpectating callback shown in spectator info area
- Android: Leave Game button pops back stack and stops spectating
- Desktop: Leave Game button clears selectedGameId (returns to lobby) and stops spectating

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
ed9d942ce0 feat(chess): finished games section below active games in lobby
Adds a "Finished Games" section to the Android chess lobby screen,
showing up to 10 completed games with opponent avatars, result
(Won/Lost/Draw), and move count. The section only appears when
completedGames is non-empty, matching the Desktop implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:11 +01:00
davotoula
012e97494e feat(chess): add rank numbers (1-8) to board coordinates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
006a2229fe feat(chess): avatar vs avatar display on board and game list
Add ChessPlayerChip.kt with three shared composables:
- ChessPlayerChip: single player avatar + name with active border
- ChessPlayerVsHeader: mirrored White vs Black header above the board
- OverlappingAvatars: compact overlapping circular avatars for list cards

Wire avatars into:
- LiveChessGameScreen: vs header with active player green border
- DesktopChessGameLayout: vs header above the chess board
- Android & Desktop lobby cards: overlapping avatars in ActiveGameCard,
  ChallengeCard, and OutgoingChallengeCard avatar slots

Uses existing UserAvatar (coil3 AsyncImage + Robohash fallback) from
commons for KMP compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
8c69105fdb fix(chess): exclude own games from spectator list
Skip adding games to spectatingGames when the user is a participant
(playerPubkey or opponentPubkey matches userPubkey) or when the game
is already finished.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
638bdb7b2d fix(chess): finished games transition from active to completed
Wire onGameEndDismiss callback so the "Continue" button on game end
overlay moves the game to the completed list. Add auto-detection of
finished games during polling refresh so games transition even if the
user navigates away without clicking the button. Discovered games that
are already finished now go straight to completed instead of appearing
in the active list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
davotoula
2004caa53c fix(chess): board interactive on mobile at game start
The board was non-interactive for participants during pending challenges
on Android because canMakeMoves factored in isPendingChallenge. Desktop
passed isSpectator directly without the pending check, so it worked.

Remove isPendingChallenge from the board interactivity gate to match
Desktop behavior. The header still shows "Challenge Pending" status,
and this also fixes a secondary issue where the isPendingChallenge flag
could get stuck when replaceGameState preserved the old state via its
open-challenge workaround.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 19:31:10 +01:00
Claude
2e1bcf3e52 style: apply spotless formatting to ChaCha20/Poly1305 sources
https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-23 17:57:31 +00:00
Claude
d586a0dc25 feat: replace libsodium with pure Kotlin ChaCha20/Poly1305 implementation
Remove LazySodium/JNA/libsodium native dependencies and replace with
pure Kotlin implementations of ChaCha20, HChaCha20, XChaCha20,
Poly1305 MAC, and XChaCha20-Poly1305 AEAD. This eliminates platform-
specific native binaries while maintaining full NIP-44 compatibility.

- Add ChaCha20Core with RFC 8439 block function, IETF stream XOR,
  HChaCha20, and XChaCha20
- Add Poly1305 MAC (RFC 8439 §2.5)
- Add XChaCha20Poly1305 AEAD encrypt/decrypt
- Convert LibSodiumInstance from expect/actual to commonMain object
- Delete platform-specific LibSodiumInstance actuals (android/jvm/ios)
- Remove iOS native interop (cinterop, linker opts, .a libraries)
- Remove lazysodium-java, lazysodium-android, JNA from build deps
- Clean up ProGuard rules (remove JNA/LazySodium keep rules)
- Add RFC 8439 + libsodium test vectors for ChaCha20 and XChaCha20
- Update benchmark to use simplified ChaCha20 API

https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
2026-03-23 17:40:29 +00:00
Nathan Day
4491f3aedb Merge pull request #1 from dadofsambonzuki/fix/dm-swipe-back-signing
Fix swipe-back triggering unwanted draft deletion signing
2026-03-16 12:25:52 +00:00
1090 changed files with 53714 additions and 8048 deletions

View File

@@ -745,8 +745,8 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = 26 // Android 8.0 (Oreo)
targetSdk = 36 // Android 15
versionCode = 434
versionName = "1.06.2"
versionCode = 439
versionName = "1.07.3"
vectorDrawables {
useSupportLibrary = true

View File

@@ -0,0 +1,86 @@
---
name: find-non-lambda-logs
description: Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
---
# Find Non-Lambda Log Calls
## Overview
Locates `Log.d/i/w/e` calls that use string interpolation without the lambda overload, wasting string allocation when the log level is filtered out in release builds.
## When to Use
- After merging branches that add new logging
- Periodic audit of logging hygiene
- After migrating `android.util.Log` usages to the shared `Log` wrapper
## What to Flag
Calls with **string interpolation** (`$` in message) that do **not** pass a throwable:
```kotlin
// FLAG - interpolation without lambda, no throwable
Log.d("Tag", "Processing ${event.id}")
Log.w("Tag", "Failed for $url")
// IGNORE - passes throwable (lambda overload doesn't accept throwable)
Log.w("Tag", "Error: ${e.message}", e)
Log.e("Tag", "Failed for $url", throwable)
// IGNORE - no interpolation (no allocation benefit from lambda)
Log.d("Tag", "Initialization complete")
```
## Search Commands
**Important:** Tags can be string literals (`"Tag"`) or variables (`tag`, `LOG_TAG`). Run both patterns for each step.
### Step 1: Find interpolated Log.d/Log.i (highest priority — filtered in release)
```
pattern: Log\.(d|i)\("[^"]+",\s*"[^"]*\$
type: kotlin
```
```
pattern: Log\.(d|i)\(\w+,\s*"[^"]*\$
type: kotlin
```
### Step 2: Find interpolated Log.w/Log.e without throwable
```
pattern: Log\.(w|e)\("[^"]+",\s*"[^"]*\$
type: kotlin
```
```
pattern: Log\.(w|e)\(\w+,\s*"[^"]*\$
type: kotlin
```
Then **manually exclude** lines where a throwable is passed as third argument (ending with `, e)`, `, throwable)`, etc.). Check the actual line — a catch block catching `e` doesn't mean `e` is passed to the Log call.
### Step 3: Verify no android.util.Log leakage
```
pattern: android\.util\.Log\.(d|i|w|e|v)\(
type: kotlin
```
These bypass the `Log.minLevel` filter entirely. Exclude `PlatformLog.android.kt` which is the wrapper implementation.
## Fix Pattern
```kotlin
// Before
Log.d("Tag", "Processing event ${event.id} from ${relay.url}")
// After
Log.d("Tag") { "Processing event ${event.id} from ${relay.url}" }
```
## Do NOT Convert
- Calls passing a `Throwable` parameter - the lambda overload `(tag) { message }` has no throwable parameter
- Static string calls with no `$` interpolation - no allocation benefit
- Commented-out log calls

View File

@@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.06.2` (Maven Central)
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.07.3` (Maven Central)
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
**License**: MIT
@@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr
```toml
[versions]
quartz = "1.06.2"
quartz = "1.07.3"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
@@ -41,7 +41,7 @@ kotlin {
```kotlin
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
}
```

View File

@@ -3,7 +3,7 @@
## Current version
```
com.vitorpamplona.quartz:quartz:1.06.2
com.vitorpamplona.quartz:quartz:1.07.3
```
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
@@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua
```toml
[versions]
quartz = "1.06.2"
quartz = "1.07.3"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
@@ -55,7 +55,7 @@ kotlin {
```kotlin
// build.gradle.kts (app module)
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
}
```
@@ -70,7 +70,7 @@ plugins {
}
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.07.3")
// JNA needed for libsodium (NIP-44) on JVM
implementation("net.java.dev.jna:jna:5.18.1")
}

View File

@@ -17,7 +17,7 @@ The Quartz library was successfully converted from Android-only to full KMP supp
## Current artifact
```
com.vitorpamplona.quartz:quartz:1.06.2
com.vitorpamplona.quartz:quartz:1.07.3
```
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.

View File

@@ -12,8 +12,11 @@ echo "$JAVA_HOME"
echo "$(java -version)"
echo "Running test... "
./gradlew test --quiet
if [ "${CLAUDE_CODE_REMOTE:-}" = "true" ]; then
./gradlew test --quiet -x :desktopApp:test -x :desktopApp:upxDownload -x :desktopApp:vlcDownload
else
./gradlew test --quiet
fi
status=$?
if [ "$status" = 0 ] ; then

View File

@@ -6,6 +6,9 @@ on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -71,6 +74,10 @@ jobs:
- name: Test (gradle)
run: ./gradlew test --no-daemon
- name: Stop Gradle Daemon
if: always()
run: ./gradlew --stop
- name: Android Test Report
uses: asadmansr/android-test-report-action@v1.2.0
if: ${{ always() && matrix.os == 'ubuntu-latest' }}
@@ -85,7 +92,7 @@ jobs:
build-android:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -188,6 +195,10 @@ jobs:
- name: Build Desktop Distribution
run: ./gradlew :desktopApp:${{ matrix.task }}
- name: Stop Gradle Daemon
if: always()
run: ./gradlew --stop
- name: Upload Desktop Distribution
uses: actions/upload-artifact@v6
with:

View File

@@ -5,6 +5,9 @@ on:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest

View File

@@ -5,6 +5,10 @@ on:
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
branches: [ main ]
permissions:
contents: write
pull-requests: write
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest

4
.gitignore vendored
View File

@@ -23,6 +23,7 @@
/.idea/deviceManager.xml
/.idea/inspectionProfiles/
/.idea/migrations.xml
/desktopApp/vlc-temp/
/commons/.idea/gradle.xml
/commons/.idea/misc.xml
/commons/.idea/workspace.xml
@@ -162,3 +163,6 @@ TASKS.md
desktopApp/src/jvmMain/appResources/linux/
desktopApp/src/jvmMain/appResources/macos/
desktopApp/src/jvmMain/appResources/windows/
# Git worktrees
.worktrees/

View File

@@ -1,5 +1,214 @@
<a id="v1.07.3"></a>
# [Release v1.07.3: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.3) - 2026-03-31
- Migrates Shorts UI out of a paged design
- New edge-to-edge feed for Pictures only
- New edge-to-edge feed for Shorts only
- New edge-to-edge feed for long Videos only
- Migrates Badges to kind 10008
- Migrates Bookmarks to kind 10003
- Fixes AOSP keyboard auto-correction bug
- Fixes Poll's top nav filter
- Fixes loading of Bookmark sets
- Fixes sorting stability by including event ids
- Fixes cursor position in quote post field
- Fixes rendering of nostr: uris when composing notes
- Adds Basic support for reaction notifications
- Refactors NIP-58 Badges on Quartz
- Refactors LocalCache methods
- Improvements to the Chess engine (debug only)
- Add error handling to Tor service initialization and control
<a id="v1.07.2"></a>
# [Release v1.07.2: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.2) - 2026-03-30
- Adds GIF uploads support from Keyboard
- Migrates text fields and @ modifiers to the new Jetpack Compose states
- Fixes Bug that wasn't openning Amber to sign
- Fixes Bug on rejections using old Ambers
- Replaces hex input with user search dialog in relay management (allow/ban user)
<a id="v1.07.1"></a>
# [Release v1.07.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.1) - 2026-03-30
- New translations
- Fixes BLE's module need for synchronized events.
<a id="v1.07.0"></a>
# [Release v1.07.0: Vanish, Relay Management, Pin Notes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.0) - 2026-03-30
Amethyst:
- Adds support to Pin Notes
- Adds support to Polls feed screen
- Adds support for Requests to Vanish
- Adds support for Relay management (NIP-86) from Amethyst
- Adds support for Relay monitor assessments (NIP-66) in the Relay Info screen.
- Adds support for Relay member information (NIP-43) in the Relay Info screen
- Adds support for WebBookmarks
- Adds support for Zap Goals
Quartz:
- Rewords the NostrClient API for simpler commands.
- Adds NIP-15: Nostr Marketplace protocol — product listings, stalls, merchant events (#2020)
- Adds NIP-24: Birthday field support added to UserMetadata (#1979)
- Adds NIP-29: Relay-based groups — group events, member management, moderation (#2021)
- Adds NIP-32: Labeling protocol — LabelEvent and tag parsing (#1975)
- Adds NIP-43: Relay access metadata and membership management with UI screens (#2010)
- Adds NIP-60: Cashu wallet and spending history event support (#2009)
- Adds NIP-61: Nutzaps protocol support (#2008)
- Adds NIP-62: Request to Vanish feature — data deletion with relay compliance testing (#1958)
- Adds NIP-66: Relay monitor discovery events — UI, filter assembler, subscription composable (#1968)
- Adds NIP-69: P2P Order Events (kind 38383) (#2019)
- Adds NIP-75: Zap Goals support with creation and rendering (#1965)
- Adds NIP-77: Negentropy set reconciliation protocol support (#1955)
- Adds NIP-7D: Thread events (kind 11) rendering (#2016)
- Adds NIP-85: Trusted Assertions — assertions for events and addressables (#1981)
- Adds NIP-86: Relay management UI and client implementation (#1954)
- Adds NIP-87: Ecash mint discoverability — Cashu and Fedimint event types (#2015)
- Adds NIP-89: Compliance fixes, PlatformLinkTag parsing, app handler extensions (#2030)
- Adds NIP-90: All DVM kind event classes from data-vending-machines spec, restructured packages (#2023, #2025)
- Adds NIP-A4: Add k tag to zap requests and enforce e tag prohibition (#1978)
- Adds NIP-BE: Bluetooth Low Energy mesh networking / Nostr BLE Communications Protocol (#2022)
- Adds NIP-C7: Chat messages (kind 9) implementation (#2018)
- Adds NIP-5A: Static website event rendering (#2017)
- Adds NIP-51, kind 10001: Pinned notes feature (#1956)
Improvements and Fixes:
- Show toast instead of dialog on media download success
- Dynamically adjust preferred Blossom server when list changes
- Add relay discovery to node master rendering list
- Add scroll to settings page
- Solves crashing when multiple relays with the same url are included in the resulting list.
- Keep screen on during PiP playback and survive screen lock (#1999)
- Desktop feed loading — missing events, broken profile navigation (#2027)
- Use getOrCreateNote for reply linking to fix flaky thread test (#2027)
- Route ReadsScreen following-mode events through cache (#2027)
- Pin ElectrumX server certs for Samsung One UI 7 / Android 16 compatibility (#1937)
- Duplicate keys in relay management lazy column — sort pubkeys
- URL detector — fixes localhost:3030 strings, Japanese character URLs
- Web Bookmarks floating action button shape (circle) and open graph previews.
- NIP-86 requests now send Accept and Content-Type headers
- On DMs, activates decryption for all filetypes that match decryption url with the cipher info, not only binaries
- Adds a try/finally to subscriptions to make sure they close even in crashes.
- Protects against crashes when the signer sends an unverifiable payload back to Amethyst
Desktop:
- Cache-centric architecture for desktop feeds (#1905)
- Render reposts and quoted notes in feed (#2027)
Performance
- Faster startup procedures with less loading on the main thread.
- 20x Faster Rfc3986Normalizer and way less objects being created.
- Url Detector without using regex
- Parallelize preference file access at startup for faster launch
- Remove internal runBlocking calls
- Lazy loading the memory trimming service
- Speeding up DrawerContent rendering
- Eagerly delete intermediate temp files in upload pipeline
Refactoring:
- Simplify NostrClient API for beginner-friendliness (#1986)
- Simplify relay API with AutoCloseable and serve() helper (#2031)
- URL detector performance and readability improvements (#2013)
- Restructure NIP-90 DVMs to match NIP-88 Polls pattern (#2023)
- Reorganize NIP-BE into subpackages
- Move public messages and trusted assertions out of experimental
Migrations & Deprecation Fixes
- Migrate `LocalClipboardManager` to `LocalClipboard` (#1995)
- Migrate `LocalAutofillTree` to semantics-based Autofill API (#1996)
- Migrate `TabRow`/`ScrollableTabRow` to Material 3 Secondary variants (#1994)
- Replace `ContextCompat.startActivity` with `Context.startActivity` (#1993)
- Suppress deprecation and unchecked cast warnings in Quartz internal code (#1985)
Platform & Build
- Add linuxX64 target and restructure native source sets
- Add explicit permissions to GitHub Actions workflows
- Stop Gradle daemons after build steps
- Increase Android CI build timeout to 45 minutes
- Update video compression library to latest
- Update dependencies, add local maven repo for easier library development
- Remove libsodium files
- Git hooks installation in worktrees (.git file vs directory)
- Fixes lack of proxy access to download VLC files in claude's web environment.
- Log level filtering — LogLevel enum, PlatformLog, lambda overloads for 194+ Log.d() calls (#2005)
- Parallelize BaseDBTest forEachDB using coroutines
Documentation
- Add CLIENT.md guide for building Nostr clients with Quartz
- Add RELAY.md guide for building relays with Ktor, NostrServer, SQLite
Contributors
- @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z — Primary development
- @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef — Log level filtering, video compression, toast downloads, CI fixes, git worktree fix
- @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c — Desktop cache architecture, desktop bugfixes
- @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 — CI workflow permissions
- @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k — Remove libsodium files
- **mstrofnone** — ElectrumX cert pinning for Samsung One UI 7
Translations
- Czech, German, Swedish, and Portuguese by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef
- Hungarian by @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp
- French by @npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz
- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm
- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6
- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw
- Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
- Chinese by hypnotichemionus4 and @npub1gd8e0xfkylc7v8c5a6hkpj4gelwwcy99jt90lqjseqjj2t253s2s6ch58h
- Russian by Anton Zhao
<a id="v1.06.3"></a>
# [Release v1.06.3: Anon Posting](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.3) - 2026-03-24
Improvements to Anon Posting and Reply navigation on DMs
<a id="v1.06.2"></a>
# [Release v1.06.2: Multi-choice Polls](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.2) - 2026-03-24
- Add poll type selector for single and multiple choice polls
- Add drag-to-seek support on the video progress indicator
- Hide video controls on playback start
- Recording indicator bar stops recording when tapped, not just the small stop icon.
- Fixes non http uris in the references tag
- Makes sure quote tags only happen once in events.
- Removes the clickable NIP-05 URL in the @ tagging of users
- Reduces the need to compute naddr to index articles in bookmarks
- Checks if p tags are user pubkeys when loading drafts.
- Scroll to the replied message when clicking reply preview in chat
- Add an anonymous reply with a throwaway keypair
- Fixes pool rendering when one label is large and the other is small
- Use uri-reference-kmp in commonMain. Remove platform-specific implementations.
- Avoids crashing the app in URLs with japanese chars
- Sorts followers into a set to avoid LazyColumn key conflicts.
- Clickable relay rows on the profile page
- Move cache lookups from NavHost route lambdas into screen composables
- Improvements to the status of attestations. Validity first, then Processing status.
- Fixes Tor Manager flow value and improves Tor binding lifecycle.
- Simplifies the report feed in the user profile
- Clears some inconsistencies in translated strings
- Fixes client tags
- Fixes routes for AppDefinition events.
<a id="v1.06.1"></a>
# [Release v1.06.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.1) - 2026-03-23
- Improvements to the rendering of Polls.
- Solves some of the crashes of the concurrent modification exception
- Fixes URL parsers with Japanese chars
- Fixes Wallet import from Primal on Poco phones
- Improves the wording of the Last Seen
- Fixes for "Cannot disable reuse from root if it was caused by other groups"
- Fixes comparator to avoid Comparison method violates its general contract!
- Improves zap-store settings
<a id="v1.06.0"></a>
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2025-03-21
# [Release v1.06.0: Polls, Relay Feeds, Wallets and much more](https://github.com/vitorpamplona/amethyst/releases/tag/v1.06.0) - 2026-03-21
Polls:
- Adds support for creating and rendering NIP-85
@@ -271,13 +480,13 @@ Updated translations:
- Russian by Anton Zhao
<a id="v1.05.1"></a>
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
- Fixed mixed DMs between logged in users.
- Fixed draft screen click to edit post.
<a id="v1.05.0"></a>
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2026-01-08
#Amethyst v1.05.0: Bookmark Lists, Voice Notes, and WoT Scores

169
README.md
View File

@@ -39,107 +39,104 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
<img align="right" src="./docs/screenshots/home.png" data-canonical-src="./docs/screenshots/home.png" width="350px">
- [x] Events / Relay Subscriptions (NIP-01)
- [x] Basic protocol flow (NIP-01)
- [x] Follow List (NIP-02)
- [x] OpenTimestamps Attestations (NIP-03)
- [x] Private Messages (NIP-04 -- to be removed)
- [x] DNS Address (NIP-05)
- [x] Mnemonic seed phrase (NIP-06)
- [ ] WebBrowser Signer (NIP-07, Not applicable)
- [x] Old-style mentions (NIP-08)
- [x] Event Deletion (NIP-09)
- [x] Encrypted Direct Message (NIP-04)
- [x] DNS-based Identifiers (NIP-05)
- [x] Key Derivation from Mnemonic (NIP-06)
- [ ] window.nostr for Web Browsers (NIP-07, Not applicable)
- [x] Handling Mentions (NIP-08)
- [x] Event Deletion Request (NIP-09)
- [x] Text Notes and Threads (NIP-10)
- [x] Relay Information Document (NIP-11)
- [x] Generic Tag Queries (NIP-12)
- [x] Proof of Work Display (NIP-13)
- [ ] Proof of Work Calculations (NIP-13)
- [x] Events with a Subject (NIP-14)
- [ ] Marketplace (NIP-15)
- [x] Event Treatment (NIP-16)
- [x] Proof of Work (NIP-13)
- [x] Subject Tag in Text Events (NIP-14)
- [x] Nostr Marketplace (NIP-15)
- [x] Private Direct Messages (NIP-17)
- [x] Image/Video/Url/LnInvoice/Cashu Previews
- [x] Reposts, Quotes, Generic Reposts (NIP-18)
- [x] Bech32 Encoding support (NIP-19)
- [x] Command Results (NIP-20)
- [x] URI Support (NIP-21)
- [x] Long-form Content (NIP-23) (view only)
- [x] User Profile Fields / Relay list (NIP-24)
- [x] Reposts (NIP-18)
- [x] bech32-encoded Entities (NIP-19)
- [x] nostr: URI Scheme (NIP-21)
- [x] Comment (NIP-22)
- [x] Long-form Content (NIP-23)
- [x] Extra Metadata Fields and Tags (NIP-24)
- [x] Reactions (NIP-25)
- [ ] Delegated Event Signing (NIP-26, Will not implement)
- [x] Delegated Event Signing (NIP-26)
- [x] Text Note References (NIP-27)
- [x] Public Chats (NIP-28)
- [ ] Relay-based Groups (NIP-29)
- [x] Public Chat (NIP-28)
- [x] Relay-based Groups (NIP-29)
- [x] Custom Emoji (NIP-30)
- [x] Event alt descriptors (NIP-31)
- [ ] Labeling (NIP-32)
- [x] Git Stuff (NIP-34)
- [x] Dealing with Unknown Events (NIP-31)
- [x] Labeling (NIP-32)
- [x] git stuff (NIP-34)
- [x] Torrents (NIP-35)
- [x] Sensitive Content (NIP-36)
- [x] Drafts (NIP-37)
- [x] User Status Event (NIP-38)
- [x] External Identities (NIP-39)
- [x] Expiration Support (NIP-40)
- [x] Relay Authentication (NIP-42)
- [ ] Relay Access Metadata and Requests (NIP-43)
- [x] Versioned Encrypted Payloads (NIP-44)
- [x] Event Counts (NIP-45)
- [o] Nostr Connect (NIP-46)
- [o] Wallet Connect API (NIP-47)
- [ ] Proxy Tags (NIP-48, Not applicable)
- [x] Encryption for import/export (NIP-49)
- [x] Relay Search (NIP-50)
- [x] Draft Events (NIP-37)
- [x] User Statuses (NIP-38)
- [x] External Identities in Profiles (NIP-39)
- [x] Expiration Timestamp (NIP-40)
- [x] Authentication of Clients to Relays (NIP-42)
- [x] Relay Access Metadata and Requests (NIP-43)
- [x] Encrypted Payloads / Versioned (NIP-44)
- [x] Counting Results (NIP-45)
- [x] Nostr Remote Signing (NIP-46)
- [x] Nostr Wallet Connect (NIP-47)
- [x] Proxy Tags (NIP-48)
- [x] Private Key Encryption (NIP-49)
- [x] Search Capability (NIP-50)
- [x] Lists (NIP-51)
- [o] Calendar Events (NIP-52)
- [x] Live Activities & Live Chats (NIP-53)
- [x] Calendar Events (NIP-52)
- [x] Live Activities (NIP-53)
- [x] Wiki (NIP-54)
- [x] Android Signer (NIP-55)
- [x] Android Signer Application (NIP-55)
- [x] Reporting (NIP-56)
- [x] Lightning Tips
- [x] Zaps (NIP-57)
- [x] Lightning Zaps (NIP-57)
- [x] Zap Splits (NIP-57)
- [x] Private Zaps (NIP-57 / Draft)
- [x] Zapraiser (NIP-57 / Draft)
- [x] Private Zaps (NIP-57)
- [x] Zapraiser (NIP-57)
- [x] Badges (NIP-58)
- [x] Gift Wraps & Seals (NIP-59)
- [ ] Cashu Wallets (NIP-60)
- [ ] Nutzaps (NIP-61)
- [x] Gift Wrap (NIP-59)
- [x] Pubkey Static Websites (NIP-5A)
- [x] Cashu Wallet (NIP-60)
- [x] Nutzaps (NIP-61)
- [x] Request to Vanish (NIP-62)
- [x] Chess (NIP-64)
- [x] Chess / PGN (NIP-64)
- [x] Relay List Metadata (NIP-65)
- [x] Relay Discovery and Monitoring (NIP-66)
- [x] Picture-first feeds (NIP-68)
- [ ] Peer-to-peer Orders (NIP-69)
- [o] Protected Events (NIP-70)
- [x] Relay Discovery and Liveness Monitoring (NIP-66)
- [x] Picture-first Feeds (NIP-68)
- [x] Peer-to-peer Order Events (NIP-69)
- [x] Protected Events (NIP-70)
- [x] Video Events (NIP-71)
- [x] Moderated Communities (NIP-72)
- [x] External Content IDs (NIP-73)
- [ ] Zap Goals (NIP-75)
- [ ] Negentropy Syncing (NIP-77)
- [x] Arbitrary Custom App Data (NIP-78)
- [ ] NIP-29 Threads (NIP-7D)
- [x] Zap Goals (NIP-75)
- [x] Negentropy Syncing (NIP-77)
- [x] Application-specific Data (NIP-78)
- [x] Threads (NIP-7D)
- [x] Highlights (NIP-84)
- [x] Trusted Assertions (NIP-85)
- [ ] Relay Management API (NIP-86)
- [ ] ECash Mint Discoverability (NIP-87)
- [x] Relay Management API (NIP-86)
- [x] Ecash Mint Discoverability (NIP-87)
- [x] Polls (NIP-88)
- [x] Relay Notify Request
- [x] Recommended Application Handlers (NIP-89)
- [x] Data Vending Machine (NIP-90)
- [x] Inline Metadata (NIP-92)
- [x] Verifiable file URLs (NIP-94)
- [x] Data Vending Machines (NIP-90)
- [x] Media Attachments (NIP-92)
- [x] File Metadata (NIP-94)
- [x] Binary Blobs (NIP-95/Draft)
- [x] HTTP File Storage Integration (NIP-96)
- [x] HTTP Auth (NIP-98)
- [x] Classifieds (NIP-99)
- [x] Classified Listings (NIP-99)
- [x] Voice Messages (NIP-A0)
- [x] Public Messages (NIP-A4)
- [ ] Web Bookmarks
- [x] Web Bookmarks (NIP-B0)
- [x] Blossom (NIP-B7)
- [ ] Nostr BLE Communications Protocol (NIP-BE)
- [x] Nostr BLE Communications Protocol (NIP-BE)
- [x] Code Snippets (NIP-C0)
- [ ] NIP-29 Chats (NIP-C7)
- [x] Chats (NIP-C7)
- [ ] MLS Protocol (NIP-EE)
- [x] Audio Tracks (zapstr.live) (kind:31337)
- [x] Lightning Tips
- [x] Image/Video/Url/LnInvoice/Cashu Previews
- [x] Push Notifications (Google and Unified Push)
- [x] In-Device Automatic Translations
- [x] Hashtag Following and Custom Hashtags
@@ -405,24 +402,24 @@ to `onPause` methods.
### Feature Parity Table
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
| :--- | :--- | :---: | :---: | :--- |
| **Cryptography** | Secp256k1 (Schnorr, Keys) | Full | No | Core Nostr signing/verification is missing on iOS. |
| | LibSodium (ChaCha20, Poly1305) | Full | No | AEAD and stream ciphers are unimplemented. |
| | AES Encryption (CBC & GCM) | Full | No | `AESCBC` and `AESGCM` are stubs on iOS. |
| | Hashing (SHA-256, etc.) | Full | No | `DigestInstance` is unimplemented. |
| | MAC (HmacSHA256, etc.) | Full | No | `MacInstance` is unimplemented. |
| **Data & Serialization** | JSON Mapping (Optimized) | Full | No | `OptimizedJsonMapper` is a stub; cannot parse/serialize Events. |
| | GZip Compression | Full | No | `GZip` implementation is missing. |
| | BitSet | Full | No | `BitSet` utility is unimplemented. |
| | LargeCache | Full | No | `LargeCache` methods (get, keys, size, etc.) are stubs. |
| **NIP Support** | NIP-96 (File Storage Info) | Full | No | `ServerInfoParser` is unimplemented. |
| | NIP-46 (Remote Signer) | Full | Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
| | NIP-03 (OTS / Timestamps) | Full | No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
| **Utilities** | URL Encoding / Decoding | Full | No | `UrlEncoder` and `URLs.ios.kt` are unimplemented. |
| | Unicode Normalization | Full | No | `UnicodeNormalizer` is a stub. |
| | Platform Logging | Full | Full | iOS uses `NSLog`, Android uses standard Log. |
| | Current Time | Full | Full | Implemented using `NSDate` on iOS. |
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
|:-------------------------|:-------------------------------|:---------------------:|:-----------:|:-----------------------------------------------------------------------|
| **Cryptography** | Secp256k1 (Schnorr, Keys) | Full | Full | |
| | LibSodium (ChaCha20, Poly1305) | Full | Full | |
| | AES Encryption (CBC & GCM) | Full | Full | |
| | Hashing (SHA-256, etc.) | Full | Full | |
| | MAC (HmacSHA256, etc.) | Full | Full | |
| **Data & Serialization** | JSON Mapping (Optimized) | Full | Full | A fully custom implementation exists in `commonMain`. |
| | GZip Compression | Full | Full | |
| | BitSet | Full | Full | |
| | LargeCache | Full | Full | |
| **NIP Support** | NIP-96 (File Storage Info) | Full | Full | |
| | NIP-46 (Remote Signer) | Full | Partial | Some methods in `NostrSignerRemote` are unimplemented in `commonMain`. |
| | NIP-03 (OTS / Timestamps) | Full | No | `BitcoinExplorer` and `RemoteCalendar` have stubs in `commonMain`. |
| **Utilities** | URL Encoding / Decoding | Full | Full | |
| | Unicode Normalization | Full | Full | |
| | Platform Logging | Full | Full | iOS uses `NSLog`, Android uses standard Log. |
| | Current Time | Full | Full | Implemented using `NSDate` on iOS. |
## Contributing

View File

@@ -54,9 +54,9 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 434
versionName = generateVersionName("1.06.2")
buildConfigField "String", "RELEASE_NOTES_ID", "\"0b6af7660b44215b0edf9c39a1c9c0b4aafba7aba1ae28665ffcecb1a9717195\""
versionCode = 439
versionName = generateVersionName("1.07.3")
buildConfigField "String", "RELEASE_NOTES_ID", "\"12cd4bce977ed53502cf121ecba89a190ab02685333c8f230bac35b04f920eeb\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -70,9 +70,9 @@ class ThreadDualAxisChartAssemblerTest {
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
nwcFilterAssembler = NWCPaymentFilterAssembler(client),
otsResolverBuilder = EmptyOtsResolverBuilder,
geolocationFlow = { MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading) },
nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
cache = LocalCache,
client = client,
scope = scope,

View File

@@ -46,7 +46,7 @@ object PushDistributorHandler : PushDistributorActions {
fun setEndpoint(newEndpoint: String) {
endpointInternal = newEndpoint
Log.d("PushHandler", "New endpoint saved : $endpointInternal")
Log.d("PushHandler") { "New endpoint saved : $endpointInternal" }
}
fun removeEndpoint() {

View File

@@ -53,7 +53,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val messageStr = message.content.decodeToString()
Log.d(TAG, "New message $messageStr for Instance: $instance")
Log.d(TAG) { "New message $messageStr for Instance: $instance" }
scope.launch {
try {
parseMessage(messageStr)?.let {
@@ -61,7 +61,7 @@ class PushMessageReceiver : MessagingReceiver() {
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message could not be parsed: ${e.message}")
Log.d(TAG) { "Message could not be parsed: ${e.message}" }
}
}
}
@@ -87,7 +87,7 @@ class PushMessageReceiver : MessagingReceiver() {
) {
val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
Log.d(TAG) { "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint" }
pushHandler.setEndpoint(sanitizedEndpoint)
scope.launch(Dispatchers.IO) {
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
@@ -97,7 +97,7 @@ class PushMessageReceiver : MessagingReceiver() {
NotificationUtils.getOrCreateDMChannel(appContext)
}
} else {
Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint")
Log.d(TAG) { "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint" }
}
}
@@ -106,7 +106,7 @@ class PushMessageReceiver : MessagingReceiver() {
reason: FailedReason,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
Log.d(TAG) { "Registration failed for Instance: $instance" }
pushHandler.forceRemoveDistributor(context)
}
@@ -115,7 +115,7 @@ class PushMessageReceiver : MessagingReceiver() {
instance: String,
) {
val removedEndpoint = pushHandler.getSavedEndpoint()
Log.d(TAG, "Endpoint: $removedEndpoint removed for Instance: $instance")
Log.d(TAG) { "Endpoint: $removedEndpoint removed for Instance: $instance" }
Log.d(TAG, "App is unregistered. ")
pushHandler.forceRemoveDistributor(context)
pushHandler.removeEndpoint()

View File

@@ -35,7 +35,7 @@ object PushNotificationUtils {
accounts: List<AccountInfo>,
okHttpClient: (String) -> OkHttpClient,
) = with(Dispatchers.IO) {
if (!pushHandler.savedDistributorExists()) return
if (!pushHandler.savedDistributorExists()) return@with
val currentDistributor = PushDistributorHandler.getSavedDistributor()
PushDistributorHandler.saveDistributor(currentDistributor)

View File

@@ -23,10 +23,12 @@ package com.vitorpamplona.amethyst
import android.app.Application
import com.vitorpamplona.amethyst.service.logging.Logging
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.LogLevel
class Amethyst : Application() {
init {
Log.d("AmethystApp", "Creating App $this")
Log.minLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.ERROR
Log.d("AmethystApp") { "Creating App $this" }
}
companion object {
@@ -36,7 +38,7 @@ class Amethyst : Application() {
override fun onCreate() {
super.onCreate()
Log.d("AmethystApp", "onCreate $this")
Log.d("AmethystApp") { "onCreate $this" }
instance = AppModules(this)
if (isDebug) {
@@ -48,7 +50,7 @@ class Amethyst : Application() {
override fun onTerminate() {
super.onTerminate()
Log.d("AmethystApp", "onTerminate $this")
Log.d("AmethystApp") { "onTerminate $this" }
instance.terminate(this)
}
@@ -59,7 +61,7 @@ class Amethyst : Application() {
*/
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.d("AmethystApp", "onTrimMemory $level")
Log.d("AmethystApp") { "onTrimMemory $level" }
instance.trim()
}
}

View File

@@ -20,14 +20,15 @@
*/
package com.vitorpamplona.amethyst
import android.content.ContentResolver
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.UiSettings
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
@@ -63,9 +64,11 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.resourceCacheInit
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
@@ -87,6 +90,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -94,6 +98,7 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
class AppModules(
@@ -109,31 +114,54 @@ class AppModules(
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
// Pre-load both preference DataStores in parallel on IO threads.
// Both constructors use runBlocking internally, so starting them concurrently
// reduces total blocking time from (torPrefs + uiPrefs) to ~max(torPrefs, uiPrefs).
private val uiPrefsDeferred =
applicationIOScope.async {
val prefs = UiSharedPreferences.uiPreferences(appContext) ?: UiSettings()
UiSharedPreferences(prefs, appContext, applicationIOScope)
}
private val torPrefsDeferred =
applicationIOScope.async {
val prefs = TorSharedPreferences.torPreferences(appContext) ?: TorSettings()
TorSharedPreferences(prefs, appContext, applicationIOScope)
}
// Blocking load of UI Preferences to avoid theme/language blinking
val uiPrefs by lazy {
UiSharedPreferences(appContext, applicationIOScope)
Log.d("AppModules", "UiSharedPreferences Init")
runBlocking { uiPrefsDeferred.await() }
}
// Blocking load of Tor Settings to avoid connection leaks
val torPrefs by lazy {
TorSharedPreferences(appContext, applicationIOScope)
Log.d("AppModules", "TorSharedPreferences Init")
runBlocking { torPrefsDeferred.await() }
}
// Namecoin ElectrumX server preferences (global, like Tor settings)
val namecoinPrefs by lazy {
Log.d("AppModules", "NamecoinSharedPreferences Init")
NamecoinSharedPreferences(appContext, applicationIOScope)
}
// OTS blockchain explorer preferences (global, like Tor settings)
val otsPrefs by lazy {
Log.d("AppModules", "OtsSharedPreferences Init")
OtsSharedPreferences(appContext, applicationIOScope)
}
// App services that should be run as soon as there are subscribers to their flows
val locationManager = LocationState(appContext, applicationIOScope)
val locationManager by lazy {
Log.d("AppModules", "LocationManager Init")
LocationState(appContext, applicationIOScope)
}
val connManager = ConnectivityManager(appContext, applicationIOScope)
val uiState by lazy {
Log.d("AppModules", "UiSettingsState Init")
UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope)
}
@@ -158,40 +186,67 @@ class AppModules(
// Offers easy methods to know when connections are happening through Tor or not
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
// Custom fetcher that considers tor settings and avoids forwarding.
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
val electrumXClient by lazy {
Log.d("AppModules", "ElectrumXClient Init")
val client =
ElectrumXClient(
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
)
applicationIOScope.launch {
try {
val pinnedCerts = namecoinPrefs.loadPinnedCerts()
if (pinnedCerts.isNotEmpty()) {
client.setDynamicCerts(pinnedCerts)
}
} catch (_: Exception) {
// Non-fatal — defaults will still work
}
}
client
}
val namecoinResolver =
NamecoinNameResolver(
electrumxClient =
ElectrumXClient(
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
),
serverListProvider = {
// User-configured custom servers take priority
namecoinPrefs.customServersOrNull
?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
TOR_ELECTRUMX_SERVERS
} else {
DEFAULT_ELECTRUMX_SERVERS
}
},
)
val nip05Client = Nip05Client(nip05Fetcher, namecoinResolver)
val namecoinResolver by
lazy {
Log.d("AppModules", "Namecoin Resolver Init")
NamecoinNameResolver(
electrumxClient = electrumXClient,
serverListProvider = {
// User-configured custom servers take priority
namecoinPrefs.customServersOrNull
?: if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
TOR_ELECTRUMX_SERVERS
} else {
DEFAULT_ELECTRUMX_SERVERS
}
},
)
}
// Application-wide block height request cache
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
val nip05Client by
lazy {
Log.d("AppModules", "NIP05Client Init")
Nip05Client(
fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05),
namecoinResolverBuilder = { namecoinResolver },
)
}
val otsResolverBuilder: TorAwareOkHttpOtsResolverBuilder =
TorAwareOkHttpOtsResolverBuilder(
roleBasedHttpClientBuilder::okHttpClientForMoney,
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
otsBlockHeightCache,
customExplorerUrl = { otsPrefs.current.normalizedUrl() },
)
val otsResolverBuilder by
lazy {
Log.d("AppModules", "OtsResolverBuilder Init")
TorAwareOkHttpOtsResolverBuilder(
roleBasedHttpClientBuilder::okHttpClientForMoney,
roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations,
OtsBlockHeightCache(),
customExplorerUrl = { otsPrefs.current.normalizedUrl() },
)
}
// Application-wide ots verification cache
val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) }
val otsVerifCache by lazy {
Log.d("AppModules", "OtsCache Init")
VerificationStateCache(otsResolverBuilder)
}
val torEvaluatorFlow =
TorRelayState(
@@ -209,7 +264,7 @@ class AppModules(
scope = applicationIOScope,
)
// Connects the NostrClient class with okHttp
// Connects the INostrClient class with okHttp
val websocketBuilder =
OkHttpWebSocket.Builder { url ->
val useTor = torEvaluatorFlow.flow.value.useTor(url)
@@ -243,7 +298,12 @@ class AppModules(
val authCoordinator = AuthCoordinator(client, applicationIOScope)
// Tries to verify new OTS events when they arrive.
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationIOScope)
val otsEventVerifier =
IncomingOtsEventVerifier(
otsVerifCache = { otsVerifCache },
cache = cache,
scope = applicationIOScope,
)
// Tracks if it is possible to connect to relays.
val failureTracker = RelayOfflineTracker(client)
@@ -269,10 +329,10 @@ class AppModules(
// keeps all accounts live
val accountsCache =
AccountCacheState(
geolocationFlow = locationManager.geohashStateFlow,
nwcFilterAssembler = sources.nwc,
contentResolverFn = ::contentResolverFn,
otsResolverBuilder = otsResolverBuilder,
geolocationFlow = { locationManager.geohashStateFlow },
nwcFilterAssembler = { sources.nwc },
contentResolverFn = { appContext.contentResolver },
otsResolverBuilder = { otsResolverBuilder.build() },
cache = cache,
client = client,
)
@@ -280,8 +340,8 @@ class AppModules(
val sessionManager =
AccountSessionManager(
accountsCache = accountsCache,
nip05Client = nip05Client,
client = client,
nip05ClientBuilder = { nip05Client },
clientBuilder = { client },
localPreferences = LocalPreferences,
scope = applicationIOScope,
)
@@ -307,7 +367,8 @@ class AppModules(
}
}
val blossomResolver =
val blossomResolver by lazy {
Log.d("AppModules", "BlossomServerResolver Init")
BlossomServerResolver(
loggedInUsers = { listOfNotNull(sessionManager.loggedInAccount()?.pubKey) },
blossomServers = { addressesToSubscribe ->
@@ -323,45 +384,60 @@ class AppModules(
},
httpClientBuilder = roleBasedHttpClientBuilder,
)
}
// Organizes cache clearing
val trimmingService = MemoryTrimmingService(cache)
val trimmingService by
lazy {
MemoryTrimmingService(cache)
}
// as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator
// and reconnects relays if the configuration has been changed.
val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationIOScope)
// saves the .content of NIP-95 blobs in disk to save memory
val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) }
val nip95cache: File by lazy {
Log.d("AppModules", "NIP95 Cache Init")
Nip95CacheFactory.new(appContext)
}
// local video cache with disk + memory
val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) }
val videoCache: VideoCache by lazy {
Log.d("AppModules", "VideoCache Init")
VideoCacheFactory.new(appContext)
}
// image cache in disk for coil
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) }
val diskCache: DiskCache by lazy {
Log.d("AppModules", "ImageCacheFactory Init")
ImageCacheFactory.newDisk(appContext)
}
// image cache in memory for coil
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) }
val memoryCache: MemoryCache by lazy {
Log.d("AppModules", "MemoryCache Init")
ImageCacheFactory.newMemory(appContext)
}
// crash report storage
val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) }
val crashReportCache = CrashReportCache(appContext)
// cache for NIP-11 documents
val nip11Cache: Nip11CachedRetriever by lazy {
Log.d("AppModules", "Nip11CachedRetriever Init")
Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay)
}
fun contentResolverFn(): ContentResolver = appContext.contentResolver
fun setImageLoader() {
Log.d("AppModules", "ImageLoaderSetup Init")
ImageLoaderSetup.setup(
app = appContext,
diskCache = { diskCache },
memoryCache = { memoryCache },
blossomServerResolver = blossomResolver,
) { url ->
okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url))
}
blossomServerResolver = { blossomResolver },
callFactory = { okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(it)) },
)
}
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub)
@@ -380,14 +456,20 @@ class AppModules(
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// preloads tor preferences
torPrefs
// Sets Coil - Tor - OkHttp link
setImageLoader()
}
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Sets Coil - Tor - OkHttp link
setImageLoader()
uiState
}
// LRUCache should not be instanciated in the Main thread due to blocking
applicationIOScope.launch {
CachedRobohash
resourceCacheInit()
}
// registers to receive events
@@ -395,15 +477,10 @@ class AppModules(
// initializes diskcache on an IO thread.
applicationIOScope.launch {
// Sets Coil - Tor - OkHttp link
delay(3000)
// Prepares video cache later
delay(10_000)
videoCache
}
applicationIOScope.launch {
// Eagerly initialize OtsSharedPreferences off the main thread
otsPrefs
}
}
fun terminate(appContext: Context) {

View File

@@ -46,19 +46,19 @@ fun debugState(context: Context) {
val jvmHeapAllocatedMb = totalMemoryMb - freeMemoryMb
Log.d(STATE_DUMP_TAG, "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB")
Log.d(STATE_DUMP_TAG) { "Total Heap Allocated: $jvmHeapAllocatedMb/$maxMemoryMb MB" }
val nativeHeap = Debug.getNativeHeapAllocatedSize() / (1024 * 1024)
val maxNative = Debug.getNativeHeapSize() / (1024 * 1024)
Log.d(STATE_DUMP_TAG, "Total Native Heap Allocated: $nativeHeap/$maxNative MB")
Log.d(STATE_DUMP_TAG) { "Total Native Heap Allocated: $nativeHeap/$maxNative MB" }
val activityManager: ActivityManager? = context.getSystemService()
if (activityManager != null) {
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
Log.d(STATE_DUMP_TAG, "Memory Class $memClass MB (largeHeap $isLargeHeap)")
Log.d(STATE_DUMP_TAG) { "Memory Class $memClass MB (largeHeap $isLargeHeap)" }
}
Log.d(
@@ -68,14 +68,8 @@ fun debugState(context: Context) {
.size() + "/" + normalizedUrls.size(),
)
Log.d(
STATE_DUMP_TAG,
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(
STATE_DUMP_TAG,
"Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB",
)
Log.d(STATE_DUMP_TAG) { "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB" }
Log.d(STATE_DUMP_TAG) { "Image Memory Cache ${(Amethyst.instance.memoryCache.size) / (1024 * 1024)}/${(Amethyst.instance.memoryCache.maxSize) / (1024 * 1024)} MB" }
Log.d(
STATE_DUMP_TAG,
@@ -130,13 +124,12 @@ fun debugState(context: Context) {
LocalCache.ephemeralChannels.values().sumOf { it.notes.size() },
)
LocalCache.chatroomList.forEach { key, room ->
Log.d(
STATE_DUMP_TAG,
Log.d(STATE_DUMP_TAG) {
"Private Chats $key: " +
room.rooms.size() +
" / " +
room.rooms.sumOf { key, value -> value.messages.size },
)
room.rooms.sumOf { key, value -> value.messages.size }
}
}
Log.d(
STATE_DUMP_TAG,
@@ -173,10 +166,10 @@ fun debugState(context: Context) {
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory()?.toLong() ?: 0L })
qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB " }
}
qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
Log.d(STATE_DUMP_TAG, "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
Log.d(STATE_DUMP_TAG) { "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB " }
}
}
@@ -188,7 +181,7 @@ inline fun <T> logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage" }
}
result
} else {
@@ -203,7 +196,7 @@ inline fun <T> logTime(
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}" }
}
result
} else {

View File

@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
@@ -59,9 +58,11 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -96,6 +97,10 @@ private object PrefKeys {
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
const val DEFAULT_DISCOVERY_FOLLOW_LIST = "defaultDiscoveryFollowList"
const val DEFAULT_POLLS_FOLLOW_LIST = "defaultPollsFollowList"
const val DEFAULT_PICTURES_FOLLOW_LIST = "defaultPicturesFollowList"
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
const val DEFAULT_LONGS_FOLLOW_LIST = "defaultLongsFollowList"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_USER_METADATA = "latestUserMetadata"
const val LATEST_CONTACT_LIST = "latestContactList"
@@ -253,7 +258,7 @@ object LocalPreferences {
val prefsDir = File(prefsDirPath)
prefsDir.list()?.forEach {
if (it.contains(npub) && !File(prefsDir, it).delete()) {
Log.w("LocalPreferences", "Failed to delete preference file: $it")
Log.w("LocalPreferences") { "Failed to delete preference file: $it" }
}
}
}
@@ -281,7 +286,7 @@ object LocalPreferences {
*/
@SuppressLint("ApplySharedPref")
suspend fun deleteAccount(accountInfo: AccountInfo) {
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
Log.d("LocalPreferences") { "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}" }
withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
removeAccount(accountInfo)
@@ -330,6 +335,11 @@ object LocalPreferences {
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNotificationFollowList.value))
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, JsonMapper.toJson(settings.defaultDiscoveryFollowList.value))
putString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPollsFollowList.value))
putString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultPicturesFollowList.value))
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
putString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultLongsFollowList.value))
putOrRemove(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, settings.zapPaymentRequest.value?.denormalize())
putOrRemove(PrefKeys.LATEST_CONTACT_LIST, settings.backupContactList)
@@ -445,108 +455,155 @@ object LocalPreferences {
}
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
Log.d("LocalPreferences", "Load account from file $npub")
Log.d("LocalPreferences") { "Load account from file $npub" }
val result =
withContext(Dispatchers.IO) {
checkNotInMainThread()
return@withContext with(encryptedPreferences(npub)) {
Log.d("LocalPreferences") { "Load account from file $npub - opened file" }
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
val externalSignerPackageName =
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val externalSignerPackageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val defaultHomeFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_HOME_FOLLOW_LIST) ?: TopFilter.AllFollows
val defaultStoriesFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST) ?: TopFilter.Global
val defaultNotificationFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST) ?: TopFilter.Global
val defaultDiscoveryFollowList = parseOrNull<TopFilter>(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST) ?: TopFilter.Global
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
Log.d("LocalPreferences") { "Load account from file $npub - keys ready" }
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
val stripLocationOnUpload = getBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, true)
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
val latestIndexRelayList = parseEventOrNull<IndexerRelayListEvent>(PrefKeys.LATEST_INDEX_RELAY_LIST)
val latestRelayFeedsList = parseEventOrNull<RelayFeedsListEvent>(PrefKeys.LATEST_RELAY_FEEDS_LIST)
val latestBlockedRelayList = parseEventOrNull<BlockedRelayListEvent>(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
val latestTrustedRelayList = parseEventOrNull<TrustedRelayListEvent>(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
val latestChannelList = parseEventOrNull<ChannelListEvent>(PrefKeys.LATEST_CHANNEL_LIST)
val latestCommunityList = parseEventOrNull<CommunityListEvent>(PrefKeys.LATEST_COMMUNITY_LIST)
val latestHashtagList = parseEventOrNull<HashtagListEvent>(PrefKeys.LATEST_HASHTAG_LIST)
val latestGeohashList = parseEventOrNull<GeohashListEvent>(PrefKeys.LATEST_GEOHASH_LIST)
val latestEphemeralList = parseEventOrNull<EphemeralChatListEvent>(PrefKeys.LATEST_EPHEMERAL_LIST)
val latestTrustProviderList = parseEventOrNull<TrustProviderListEvent>(PrefKeys.LATEST_TRUST_PROVIDER_LIST)
val latestPaymentTargets = parseEventOrNull<PaymentTargetsEvent>(PrefKeys.LATEST_PAYMENT_TARGETS)
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null)
val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null)
val defaultNotificationFollowListStr = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null)
val defaultDiscoveryFollowListStr = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null)
val defaultPollsFollowListStr = getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null)
val defaultPicturesFollowListStr = getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null)
val defaultShortsFollowListStr = getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null)
val defaultLongsFollowListStr = getString(PrefKeys.DEFAULT_LONGS_FOLLOW_LIST, null)
val zapPaymentRequestServerStr = getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)
val defaultFileServerStr = getString(PrefKeys.DEFAULT_FILE_SERVER, null)
val pendingAttestationsStr = getString(PrefKeys.PENDING_ATTESTATIONS, null)
val latestUserMetadataStr = getString(PrefKeys.LATEST_USER_METADATA, null)
val latestContactListStr = getString(PrefKeys.LATEST_CONTACT_LIST, null)
val latestDmRelayListStr = getString(PrefKeys.LATEST_DM_RELAY_LIST, null)
val latestNip65RelayListStr = getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null)
val latestSearchRelayListStr = getString(PrefKeys.LATEST_SEARCH_RELAY_LIST, null)
val latestIndexRelayListStr = getString(PrefKeys.LATEST_INDEX_RELAY_LIST, null)
val latestRelayFeedsListStr = getString(PrefKeys.LATEST_RELAY_FEEDS_LIST, null)
val latestBlockedRelayListStr = getString(PrefKeys.LATEST_BLOCKED_RELAY_LIST, null)
val latestTrustedRelayListStr = getString(PrefKeys.LATEST_TRUSTED_RELAY_LIST, null)
val latestMuteListStr = getString(PrefKeys.LATEST_MUTE_LIST, null)
val latestPrivateHomeRelayListStr = getString(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, null)
val latestAppSpecificDataStr = getString(PrefKeys.LATEST_APP_SPECIFIC_DATA, null)
val latestChannelListStr = getString(PrefKeys.LATEST_CHANNEL_LIST, null)
val latestCommunityListStr = getString(PrefKeys.LATEST_COMMUNITY_LIST, null)
val latestHashtagListStr = getString(PrefKeys.LATEST_HASHTAG_LIST, null)
val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null)
val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null)
val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null)
val latestPaymentTargetsStr = getString(PrefKeys.LATEST_PAYMENT_TARGETS, null)
val lastReadPerRouteStr = getString(PrefKeys.LAST_READ_PER_ROUTE, null)
Log.d("LocalPreferences") { "Load account from file $npub - before parsing events" }
val defaultHomeFollowList = async { parseOrNull<TopFilter>(defaultHomeFollowListStr) ?: TopFilter.AllFollows }
val defaultStoriesFollowList = async { parseOrNull<TopFilter>(defaultStoriesFollowListStr) ?: TopFilter.Global }
val defaultNotificationFollowList = async { parseOrNull<TopFilter>(defaultNotificationFollowListStr) ?: TopFilter.Global }
val defaultDiscoveryFollowList = async { parseOrNull<TopFilter>(defaultDiscoveryFollowListStr) ?: TopFilter.Global }
val defaultPollsFollowList = async { parseOrNull<TopFilter>(defaultPollsFollowListStr) ?: TopFilter.Global }
val defaultPicturesFollowList = async { parseOrNull<TopFilter>(defaultPicturesFollowListStr) ?: TopFilter.Global }
val defaultShortsFollowList = async { parseOrNull<TopFilter>(defaultShortsFollowListStr) ?: TopFilter.Global }
val defaultLongsFollowList = async { parseOrNull<TopFilter>(defaultLongsFollowListStr) ?: TopFilter.Global }
val zapPaymentRequestServer = async { parseOrNull<Nip47WalletConnect.Nip47URI>(zapPaymentRequestServerStr) }
val defaultFileServer = async { parseOrNull<ServerName>(defaultFileServerStr) ?: DEFAULT_MEDIA_SERVERS[0] }
val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() }
val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) }
val latestContactList = async { parseEventOrNull<ContactListEvent>(latestContactListStr) }
val latestDmRelayList = async { parseEventOrNull<ChatMessageRelayListEvent>(latestDmRelayListStr) }
val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) }
val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) }
val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) }
val latestRelayFeedsList = async { parseEventOrNull<RelayFeedsListEvent>(latestRelayFeedsListStr) }
val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) }
val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) }
val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) }
val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) }
val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) }
val latestChannelList = async { parseEventOrNull<ChannelListEvent>(latestChannelListStr) }
val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) }
val latestHashtagList = async { parseEventOrNull<HashtagListEvent>(latestHashtagListStr) }
val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) }
val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) }
val latestTrustProviderList = async { parseEventOrNull<TrustProviderListEvent>(latestTrustProviderListStr) }
val latestPaymentTargets = async { parseEventOrNull<PaymentTargetsEvent>(latestPaymentTargetsStr) }
val lastReadPerRoute =
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
MutableStateFlow(it.value)
} ?: mapOf()
async {
parseOrNull<Map<String, Long>>(lastReadPerRouteStr)?.mapValues {
MutableStateFlow(it.value)
} ?: mapOf()
}
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
Log.d("LocalPreferences") { "Load account from file $npub - asyncs created" }
return@with AccountSettings(
keyPair = keyPair,
transientAccount = false,
externalSignerPackageName = externalSignerPackageName,
localRelayServers = MutableStateFlow(localRelayServers),
defaultFileServer = defaultFileServer,
defaultFileServer = defaultFileServer.await(),
stripLocationOnUpload = stripLocationOnUpload,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList.await()),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList.await()),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList.await()),
defaultPollsFollowList = MutableStateFlow(defaultPollsFollowList.await()),
defaultPicturesFollowList = MutableStateFlow(defaultPicturesFollowList.await()),
defaultShortsFollowList = MutableStateFlow(defaultShortsFollowList.await()),
defaultLongsFollowList = MutableStateFlow(defaultLongsFollowList.await()),
zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer.await()?.normalize()),
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
backupUserMetadata = latestUserMetadata,
backupContactList = latestContactList,
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
backupSearchRelayList = latestSearchRelayList,
backupIndexRelayList = latestIndexRelayList,
backupRelayFeedsList = latestRelayFeedsList,
backupBlockedRelayList = latestBlockedRelayList,
backupTrustedRelayList = latestTrustedRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupChannelList = latestChannelList,
backupCommunityList = latestCommunityList,
backupHashtagList = latestHashtagList,
backupGeohashList = latestGeohashList,
backupEphemeralChatList = latestEphemeralList,
backupTrustProviderList = latestTrustProviderList,
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
backupUserMetadata = latestUserMetadata.await(),
backupContactList = latestContactList.await(),
backupNIP65RelayList = latestNip65RelayList.await(),
backupDMRelayList = latestDmRelayList.await(),
backupSearchRelayList = latestSearchRelayList.await(),
backupIndexRelayList = latestIndexRelayList.await(),
backupRelayFeedsList = latestRelayFeedsList.await(),
backupBlockedRelayList = latestBlockedRelayList.await(),
backupTrustedRelayList = latestTrustedRelayList.await(),
backupPrivateHomeRelayList = latestPrivateHomeRelayList.await(),
backupMuteList = latestMuteList.await(),
backupAppSpecificData = latestAppSpecificData.await(),
backupChannelList = latestChannelList.await(),
backupCommunityList = latestCommunityList.await(),
backupHashtagList = latestHashtagList.await(),
backupGeohashList = latestGeohashList.await(),
backupEphemeralChatList = latestEphemeralList.await(),
backupTrustProviderList = latestTrustProviderList.await(),
lastReadPerRoute = MutableStateFlow(lastReadPerRoute.await()),
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
pendingAttestations = MutableStateFlow(pendingAttestations),
backupNipA3PaymentTargets = latestPaymentTargets,
pendingAttestations = MutableStateFlow(pendingAttestations.await()),
backupNipA3PaymentTargets = latestPaymentTargets.await(),
)
}
}
Log.d("LocalPreferences", "Loaded account from file $npub")
Log.d("LocalPreferences") { "Loaded account from file $npub" }
return result
}
private inline fun <reified T : Any> SharedPreferences.parseOrNull(key: String): T? {
val value = getString(key, null)
private inline fun <reified T : Any> parseOrNull(value: String?): T? {
if (value.isNullOrEmpty() || value == "null") {
return null
}
@@ -558,13 +615,12 @@ object LocalPreferences {
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
null
}
}
private inline fun <reified T> SharedPreferences.parseEventOrNull(key: String): T? {
val value = getString(key, null)
private inline fun <reified T> parseEventOrNull(value: String?): T? {
if (value.isNullOrEmpty() || value == "null") {
return null
}
@@ -572,7 +628,7 @@ object LocalPreferences {
Event.fromJson(value) as T?
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
Log.w("LocalPreferences", "Error Decoding ${T::class.java} from Preferences with value $value", e)
null
}
}

View File

@@ -56,6 +56,8 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
@@ -81,6 +83,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListD
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
import com.vitorpamplona.amethyst.model.nip62Vanish.VanishRequestsState
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
@@ -134,7 +137,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
@@ -143,7 +146,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
@@ -174,6 +177,7 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
@@ -185,6 +189,7 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
@@ -197,7 +202,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
@@ -213,6 +218,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
import com.vitorpamplona.quartz.utils.DualCase
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.containsAny
@@ -220,6 +226,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
@@ -234,9 +241,9 @@ import kotlin.coroutines.cancellation.CancellationException
class Account(
val settings: AccountSettings = AccountSettings(KeyPair()),
override val signer: NostrSigner,
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val otsResolverBuilder: OtsResolverBuilder,
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val otsResolverBuilder: () -> OtsResolver,
val cache: LocalCache,
val client: INostrClient,
val scope: CoroutineScope,
@@ -316,9 +323,13 @@ class Account(
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
val oldBookmarkState = OldBookmarkListState(signer, cache, scope)
val bookmarkState = BookmarkListState(signer, cache, scope)
val pinState = PinListState(signer, cache, scope)
val emoji = EmojiPackState(signer, cache, scope)
val vanish = VanishRequestsState(signer, cache, client, scope)
val appSpecific = AppSpecificState(signer, cache, scope, settings)
val blossomServers = BlossomServerListState(signer, cache, scope, settings)
@@ -372,10 +383,9 @@ class Account(
geohashCache = geohashListDecryptionCache,
)
// App-ready Feeds
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
fun topNavFilterFlow(listName: MutableStateFlow<TopFilter>) =
FeedTopNavFilterState(
feedFilterListName = settings.defaultHomeFollowList,
feedFilterListName = listName,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
@@ -387,56 +397,31 @@ class Account(
scope = scope,
).flow
// App-ready Feeds
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultHomeFollowList)
val liveHomeFollowListsPerRelay = OutboxLoaderState(liveHomeFollowLists, cache, scope).flow
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultStoriesFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
).flow
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultStoriesFollowList)
val liveStoriesFollowListsPerRelay = OutboxLoaderState(liveStoriesFollowLists, cache, scope).flow
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultDiscoveryFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
).flow
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultDiscoveryFollowList)
val liveDiscoveryFollowListsPerRelay = OutboxLoaderState(liveDiscoveryFollowLists, cache, scope).flow
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
FeedTopNavFilterState(
feedFilterListName = settings.defaultNotificationFollowList,
kind3Follows = kind3FollowList.flow,
allFollows = allFollows.flow,
locationFlow = geolocationFlow,
followsRelays = defaultGlobalRelays.flow,
blockedRelays = blockedRelayList.flow,
proxyRelays = proxyRelayList.flow,
caches = feedDecryptionCaches,
signer = signer,
scope = scope,
).flow
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultNotificationFollowList)
val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow
val livePollsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultPollsFollowList)
val livePollsFollowListsPerRelay = OutboxLoaderState(livePollsFollowLists, cache, scope).flow
val livePicturesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultPicturesFollowList)
val livePicturesFollowListsPerRelay = OutboxLoaderState(livePicturesFollowLists, cache, scope).flow
val liveShortsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultShortsFollowList)
val liveShortsFollowListsPerRelay = OutboxLoaderState(liveShortsFollowLists, cache, scope).flow
val liveLongsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultLongsFollowList)
val liveLongsFollowListsPerRelay = OutboxLoaderState(liveLongsFollowLists, cache, scope).flow
override fun isWriteable(): Boolean = settings.isWriteable()
suspend fun updateWarnReports(warnReports: Boolean): Boolean {
@@ -597,7 +582,7 @@ class Account(
onResponse: (Response?) -> Unit,
) {
val (event, relay) = nip47SignerState.sendNwcRequest(request, onResponse)
client.send(event, setOf(relay))
client.publish(event, setOf(relay))
}
suspend fun sendZapPaymentRequestFor(
@@ -606,7 +591,7 @@ class Account(
onResponse: (Response?) -> Unit,
) {
val (event, relay) = nip47SignerState.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
client.send(event, setOf(relay))
client.publish(event, setOf(relay))
}
suspend fun createZapRequestFor(
@@ -655,7 +640,7 @@ class Account(
myRelayList.addAll(it.relays)
}
client.send(deletionEvent, myRelayList)
client.publish(deletionEvent, myRelayList)
cache.justConsumeMyOwnEvent(deletionEvent)
}
}
@@ -669,7 +654,7 @@ class Account(
if (event.pubKey != signer.pubKey) return
val deletionEvent = signer.sign(DeletionEvent.build(listOf(event)))
client.send(deletionEvent, outboxRelays.flow.value + additionalRelays)
client.publish(deletionEvent, outboxRelays.flow.value + additionalRelays)
cache.justConsumeMyOwnEvent(deletionEvent)
}
@@ -692,7 +677,7 @@ class Account(
suspend fun boost(note: Note) {
RepostAction.repost(note, signer)?.let { event ->
client.send(event, computeMyReactionToNote(note, event))
client.publish(event, computeMyReactionToNote(note, event))
cache.justConsumeMyOwnEvent(event)
}
}
@@ -714,7 +699,7 @@ class Account(
event: Event,
relays: Set<NormalizedRelayUrl>,
) {
client.send(event, relays)
client.publish(event, relays)
cache.justConsumeMyOwnEvent(event)
}
@@ -930,7 +915,7 @@ class Account(
// download the event and send it.
noteEvent.host?.let { host ->
client
.downloadFirstEvent(
.fetchFirst(
filters =
note.relays.associateWith { relay ->
listOf(
@@ -943,11 +928,11 @@ class Account(
},
)?.let { downloadedEvent ->
val toRelays = computeRelayListToBroadcast(downloadedEvent)
client.send(downloadedEvent, toRelays)
client.publish(downloadedEvent, toRelays)
}
}
} else {
client.send(noteEvent, computeRelayListToBroadcast(note))
client.publish(noteEvent, computeRelayListToBroadcast(note))
}
}
}
@@ -995,7 +980,7 @@ class Account(
val relays = outboxRelays.flow.value + commEvent.relayUrls() + community.relays + (post.author?.inboxRelays() ?: emptyList())
cache.justConsumeMyOwnEvent(signedEvent)
client.send(signedEvent, relays)
client.publish(signedEvent, relays)
}
fun sendAutomatic(events: List<Event>) = events.forEach { sendAutomatic(it) }
@@ -1003,24 +988,49 @@ class Account(
fun sendAutomatic(event: Event?) {
if (event == null) return
cache.justConsumeMyOwnEvent(event)
client.send(event, computeRelayListToBroadcast(event))
client.publish(event, computeRelayListToBroadcast(event))
}
suspend fun sendWebBookmark(
url: String,
title: String?,
description: String,
hashtags: List<String> = emptyList(),
) {
if (!isWriteable()) return
val template = WebBookmarkEvent.build(url, title, description, tags = hashtags)
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
}
suspend fun deleteWebBookmark(event: WebBookmarkEvent) {
if (!isWriteable()) return
val template = DeletionEvent.build(listOf(event))
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
}
fun sendMyPublicAndPrivateOutbox(event: Event?) {
if (event == null) return
cache.justConsumeMyOwnEvent(event)
client.send(event, outboxRelays.flow.value)
client.publish(event, outboxRelays.flow.value)
}
fun sendMyPublicAndPrivateOutbox(events: List<Event>) {
events.forEach {
client.send(it, outboxRelays.flow.value)
client.publish(it, outboxRelays.flow.value)
cache.justConsumeMyOwnEvent(it)
}
}
fun sendLiterallyEverywhere(event: Event) {
client.send(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
client.publish(event, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
cache.justConsumeMyOwnEvent(event)
}
@@ -1037,7 +1047,7 @@ class Account(
cache.justConsumeMyOwnEvent(signedEvent)
client.send(signedEvent, computeRelayListToBroadcast(signedEvent))
client.publish(signedEvent, computeRelayListToBroadcast(signedEvent))
}
}
@@ -1073,10 +1083,10 @@ class Account(
val relayList = computeRelayListToBroadcast(signedEvent)
client.send(data, relayList = relayList)
client.publish(data, relayList = relayList)
cache.justConsumeMyOwnEvent(data)
client.send(signedEvent, relayList = relayList)
client.publish(signedEvent, relayList = relayList)
cache.justConsumeMyOwnEvent(signedEvent)
return cache.getNoteIfExists(signedEvent.id)
@@ -1097,8 +1107,8 @@ class Account(
signedEvent: FileStorageHeaderEvent,
relayList: Set<NormalizedRelayUrl>,
) {
client.send(data, relayList = relayList)
client.send(signedEvent, relayList = relayList)
client.publish(data, relayList = relayList)
client.publish(signedEvent, relayList = relayList)
}
fun sendHeader(
@@ -1106,7 +1116,7 @@ class Account(
relayList: Set<NormalizedRelayUrl>,
onReady: (Note) -> Unit,
) {
client.send(signedEvent, relayList = relayList)
client.publish(signedEvent, relayList = relayList)
cache.justConsumeMyOwnEvent(signedEvent)
cache.getNoteIfExists(signedEvent.id)?.let { onReady(it) }
@@ -1253,7 +1263,7 @@ class Account(
) {
val event = signer.sign(template)
cache.justConsumeMyOwnEvent(event)
client.send(event, relayList)
client.publish(event, relayList)
}
suspend fun <T : Event> signAndSendPrivatelyOrBroadcast(
@@ -1264,9 +1274,9 @@ class Account(
cache.justConsumeMyOwnEvent(event)
val relays = relayList(event)
if (!relays.isNullOrEmpty()) {
client.send(event, relays.toSet())
client.publish(event, relays.toSet())
} else {
client.send(event, computeRelayListToBroadcast(event))
client.publish(event, computeRelayListToBroadcast(event))
}
return event
}
@@ -1286,9 +1296,9 @@ class Account(
val relayList = computeRelayListToBroadcast(note)
client.send(event, relayList)
client.publish(event, relayList)
broadcast.forEach { client.send(it, relayList) }
broadcast.forEach { client.publish(it, relayList) }
return event
}
@@ -1300,10 +1310,19 @@ class Account(
val anonymousSigner = NostrSignerInternal(KeyPair())
val event = anonymousSigner.sign(template)
val relayList = nip65RelayList.outboxFlow.value.toSet()
cache.justConsumeMyOwnEvent(event)
val note =
if (event is AddressableEvent) {
cache.getOrCreateAddressableNote(event.address())
} else {
cache.getOrCreateNote(event.id)
}
client.send(event, relayList)
broadcast.forEach { client.send(it, relayList) }
val relayList = computeRelayListToBroadcast(note)
client.publish(event, relayList)
broadcast.forEach { client.publish(it, relayList) }
return event
}
@@ -1335,7 +1354,7 @@ class Account(
extraNotesToBroadcast: List<Event>,
) {
cache.justConsumeMyOwnEvent(event)
extraNotesToBroadcast.forEach { client.send(it, relays) }
extraNotesToBroadcast.forEach { client.publish(it, relays) }
}
suspend fun createAndSendDraftIgnoreErrors(
@@ -1367,9 +1386,9 @@ class Account(
val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet()
if (relayList.isNotEmpty()) {
client.send(draftEvent, relayList)
client.publish(draftEvent, relayList)
broadcast.forEach {
client.send(it, relayList.toSet())
client.publish(it, relayList.toSet())
}
}
}
@@ -1396,8 +1415,8 @@ class Account(
cache.justConsumeMyOwnEvent(deletionEvent)
if (relayList.isNotEmpty()) {
client.send(deletedDraft, relayList)
client.send(deletionEvent, relayList)
client.publish(deletedDraft, relayList)
client.publish(deletionEvent, relayList)
}
}
@@ -1420,9 +1439,9 @@ class Account(
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
if (relayList.isNotEmpty()) {
client.send(event, relayList + noteRelays)
client.publish(event, relayList + noteRelays)
} else {
client.send(event, outboxRelays.flow.value + noteRelays)
client.publish(event, outboxRelays.flow.value + noteRelays)
}
cache.justConsumeMyOwnEvent(event)
}
@@ -1446,9 +1465,9 @@ class Account(
val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value
if (relayList.isNotEmpty()) {
client.send(event, relayList + noteRelays)
client.publish(event, relayList + noteRelays)
} else {
client.send(event, outboxRelays.flow.value + noteRelays)
client.publish(event, outboxRelays.flow.value + noteRelays)
}
cache.justConsumeMyOwnEvent(event)
}
@@ -1509,9 +1528,9 @@ class Account(
} else {
val it = signer.sign(template)
cache.justConsumeMyOwnEvent(it)
client.send(it, relayList = relayList)
client.publish(it, relayList = relayList)
mapEntitiesToNotes(quotes).forEach { it.event?.let { client.send(it, relayList = relayList) } }
mapEntitiesToNotes(quotes).forEach { it.event?.let { client.publish(it, relayList = relayList) } }
}
}
@@ -1554,9 +1573,9 @@ class Account(
} else {
val it = signer.sign(template)
cache.justConsumeMyOwnEvent(it)
client.send(it, relayList = relayList)
client.publish(it, relayList = relayList)
broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } }
broadcastNotes.forEach { it.event?.let { client.publish(it, relayList = relayList) } }
}
}
@@ -1581,8 +1600,8 @@ class Account(
val newEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(newEvent)
client.send(newEvent, relayList = relays)
client.send(bountyEvent, relayList = relays)
client.publish(newEvent, relayList = relays)
client.publish(bountyEvent, relayList = relays)
}
suspend fun sendEdit(
@@ -1609,9 +1628,9 @@ class Account(
val note = cache.getOrCreateNote(event.id)
val relayList = computeRelayListToBroadcast(note)
client.send(event, relayList = relayList)
client.publish(event, relayList = relayList)
broadcast.forEach { client.send(it, relayList) }
broadcast.forEach { client.publish(it, relayList) }
}
override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) {
@@ -1622,7 +1641,7 @@ class Account(
val destinationRelays = recipient?.let { cache.getOrCreateUser(it).dmInboxRelays() } ?: emptyList()
cache.justConsumeMyOwnEvent(newEvent)
client.send(newEvent, outboxRelays.flow.value + destinationRelays)
client.publish(newEvent, outboxRelays.flow.value + destinationRelays)
}
override suspend fun sendNip17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) {
@@ -1640,7 +1659,7 @@ class Account(
override suspend fun sendGiftWraps(wraps: List<GiftWrapEvent>) {
wraps.forEach { wrap ->
val relayList = computeRelayListToBroadcast(wrap)
client.send(wrap, relayList)
client.publish(wrap, relayList)
}
}
@@ -1661,7 +1680,7 @@ class Account(
}
val relayList = computeRelayListToBroadcast(wrap)
client.send(wrap, relayList)
client.publish(wrap, relayList)
}
}
@@ -1774,6 +1793,86 @@ class Account(
cache.justConsumeMyOwnEvent(event)
}
suspend fun migrateOldBookmarksToNew() {
if (!isWriteable()) return
val oldList = oldBookmarkState.getBookmarkList() ?: return
val oldPublic = oldList.publicBookmarks()
val oldPrivate = oldList.privateBookmarks(signer) ?: emptyList()
if (oldPublic.isEmpty() && oldPrivate.isEmpty()) return
val existingNewList = bookmarkState.getBookmarkList()
val newEvent =
if (existingNewList != null) {
val existingPublic = existingNewList.publicBookmarks()
val existingPrivate = existingNewList.privateBookmarks(signer) ?: emptyList()
val existingPublicIds = existingPublic.map { it.toTagIdOnly().toList() }.toSet()
val existingPrivateIds = existingPrivate.map { it.toTagIdOnly().toList() }.toSet()
val newPublic = oldPublic.filter { it.toTagIdOnly().toList() !in existingPublicIds }
val newPrivate = oldPrivate.filter { it.toTagIdOnly().toList() !in existingPrivateIds }
if (newPublic.isEmpty() && newPrivate.isEmpty()) return
val mergedPublic = existingPublic + newPublic
val mergedPrivate = existingPrivate + newPrivate
BookmarkListEvent.create(
publicBookmarks = mergedPublic,
privateBookmarks = mergedPrivate,
signer = signer,
)
} else {
BookmarkListEvent.create(
publicBookmarks = oldPublic,
privateBookmarks = oldPrivate,
signer = signer,
)
}
sendMyPublicAndPrivateOutbox(newEvent)
}
suspend fun addPin(note: Note) {
if (!isWriteable() || note.isDraft()) return
sendMyPublicAndPrivateOutbox(pinState.addPin(note))
}
suspend fun removePin(note: Note) {
if (!isWriteable() || note.isDraft()) return
val event = pinState.removePin(note)
if (event != null) {
sendMyPublicAndPrivateOutbox(event)
}
}
suspend fun createAddPinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
if (!isWriteable() || note.isDraft()) return null
val event = pinState.addPin(note)
val relays = outboxRelays.flow.value
return event to relays
}
suspend fun createRemovePinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
if (!isWriteable() || note.isDraft()) return null
val event = pinState.removePin(note) ?: return null
val relays = outboxRelays.flow.value
return event to relays
}
fun consumePinEvent(event: Event) {
cache.justConsumeMyOwnEvent(event)
}
suspend fun createAuthEvent(
relay: NormalizedRelayUrl,
challenge: String,
@@ -1803,7 +1902,7 @@ class Account(
onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit,
) {
val relays = nip65RelayList.inboxFlow.value.toSet()
val request = NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer)
val request = signer.sign<NIP90ContentDiscoveryRequestEvent>(NIP90ContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays))
val relayList =
dvmPublicKey.inboxRelays()?.toSet()?.ifEmpty { null }
@@ -1812,7 +1911,7 @@ class Account(
cache.justConsumeMyOwnEvent(request)
onReady(request)
delay(100)
client.send(request, relayList)
client.publish(request, relayList)
}
fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event)
@@ -1970,6 +2069,31 @@ class Account(
suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays))
suspend fun requestToVanish(
relays: List<NormalizedRelayUrl>,
reason: String,
createdAt: Long,
) {
if (!isWriteable() || relays.isEmpty()) return
val template = RequestToVanishEvent.build(relays, reason, createdAt)
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
client.publish(signedEvent, outboxRelays.flow.value + relays.toSet())
}
suspend fun requestToVanishFromEverywhere(
reason: String,
createdAt: Long,
) {
if (!isWriteable()) return
val template = RequestToVanishEvent.buildVanishFromEverywhere(reason, createdAt)
val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent)
client.publish(signedEvent, followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value)
}
suspend fun sendNip65RelayList(relays: List<AdvertisedRelayInfo>) = sendLiterallyEverywhere(nip65RelayList.saveRelayList(relays))
suspend fun sendBlossomServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers))

View File

@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -57,6 +56,7 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -165,6 +165,10 @@ class AccountSettings(
val defaultStoriesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultNotificationFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultDiscoveryFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultPollsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultLongsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val zapPaymentRequest: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?> = MutableStateFlow(null),
var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
@@ -321,6 +325,50 @@ class AccountSettings(
}
}
fun changeDefaultPollsFollowList(name: FeedDefinition) {
changeDefaultPollsFollowList(name.code)
}
fun changeDefaultPollsFollowList(name: TopFilter) {
if (defaultPollsFollowList.value != name) {
defaultPollsFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultPicturesFollowList(name: FeedDefinition) {
changeDefaultPicturesFollowList(name.code)
}
fun changeDefaultPicturesFollowList(name: TopFilter) {
if (defaultPicturesFollowList.value != name) {
defaultPicturesFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultShortsFollowList(name: FeedDefinition) {
changeDefaultShortsFollowList(name.code)
}
fun changeDefaultShortsFollowList(name: TopFilter) {
if (defaultShortsFollowList.value != name) {
defaultShortsFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultLongsFollowList(name: FeedDefinition) {
changeDefaultLongsFollowList(name.code)
}
fun changeDefaultLongsFollowList(name: TopFilter) {
if (defaultLongsFollowList.value != name) {
defaultLongsFollowList.tryEmit(name)
saveAccountSettings()
}
}
// ---
// language services
// ---

View File

@@ -87,7 +87,7 @@ class AntiSpamFilter {
val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay))
val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
// Log down offenders
val spammer = logOffender(hash, event)
@@ -114,7 +114,7 @@ class AntiSpamFilter {
val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay))
val link2 = njumpLink(NEvent.create(event.id, null, null, relay))
Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2")
Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" }
// Log down offenders
val spammer = logOffender(hash, event)

View File

@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.cache.CacheCollectors

View File

@@ -151,19 +151,19 @@ class UiSettingsFlow(
}
companion object {
fun build(torSettings: UiSettings): UiSettingsFlow =
fun build(uiSettings: UiSettings): UiSettingsFlow =
UiSettingsFlow(
MutableStateFlow(torSettings.theme),
MutableStateFlow(torSettings.preferredLanguage),
MutableStateFlow(torSettings.automaticallyShowImages),
MutableStateFlow(torSettings.automaticallyStartPlayback),
MutableStateFlow(torSettings.automaticallyShowUrlPreview),
MutableStateFlow(torSettings.automaticallyHideNavigationBars),
MutableStateFlow(torSettings.automaticallyShowProfilePictures),
MutableStateFlow(torSettings.dontShowPushNotificationSelector),
MutableStateFlow(torSettings.dontAskForNotificationPermissions),
MutableStateFlow(torSettings.featureSet),
MutableStateFlow(torSettings.gallerySet),
MutableStateFlow(uiSettings.theme),
MutableStateFlow(uiSettings.preferredLanguage),
MutableStateFlow(uiSettings.automaticallyShowImages),
MutableStateFlow(uiSettings.automaticallyStartPlayback),
MutableStateFlow(uiSettings.automaticallyShowUrlPreview),
MutableStateFlow(uiSettings.automaticallyHideNavigationBars),
MutableStateFlow(uiSettings.automaticallyShowProfilePictures),
MutableStateFlow(uiSettings.dontShowPushNotificationSelector),
MutableStateFlow(uiSettings.dontAskForNotificationPermissions),
MutableStateFlow(uiSettings.featureSet),
MutableStateFlow(uiSettings.gallerySet),
)
}
}

View File

@@ -31,8 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -44,10 +45,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class AccountCacheState(
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val contentResolverFn: () -> ContentResolver,
val otsResolverBuilder: OtsResolverBuilder,
val otsResolverBuilder: () -> OtsResolver,
val cache: LocalCache,
val client: INostrClient,
) {
@@ -91,9 +92,11 @@ class AccountCacheState(
val cached = accounts.value[signer.pubKey]
if (cached != null) return cached
val signerWithClientTag = NostrSignerWithClientTag(signer, CLIENT_TAG_NAME)
return Account(
settings = accountSettings,
signer = signer,
signer = signerWithClientTag,
geolocationFlow = geolocationFlow,
nwcFilterAssembler = nwcFilterAssembler,
otsResolverBuilder = otsResolverBuilder,
@@ -122,4 +125,8 @@ class AccountCacheState(
emptyMap()
}
}
companion object {
const val CLIENT_TAG_NAME = "Amethyst"
}
}

View File

@@ -91,7 +91,7 @@ class PrivateStorageRelayListState(
init {
settings.backupPrivateHomeRelayList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved private home relay list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -101,7 +101,7 @@ class PrivateStorageRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start")
getPrivateOutboxRelayListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Private Home Relay List for ${signer.pubKey}" }
(noteState.note.event as? PrivateOutboxRelayListEvent)?.let {
settings.updatePrivateHomeRelayList(it)
}

View File

@@ -132,7 +132,7 @@ class UserMetadataState(
init {
settings.backupUserMetadata?.let {
Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved user metadata ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
@@ -142,7 +142,7 @@ class UserMetadataState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
getUserMetadataFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 0 ${user.toBestDisplayName()}")
Log.d("AccountRegisterObservers") { "Updating Kind 0 ${user.toBestDisplayName()}" }
(it.note.event as? MetadataEvent)?.let {
settings.updateUserMetadata(it)
}

View File

@@ -162,7 +162,7 @@ class Kind3FollowListState(
init {
settings.backupContactList?.let {
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
Log.d("AccountRegisterObservers") { "Loading saved ${it.tags.size} contacts" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
@@ -172,7 +172,7 @@ class Kind3FollowListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Kind 3 Collector Start")
getFollowListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Kind 3 ${signer.pubKey}" }
(it.note.event as? ContactListEvent)?.let {
settings.updateContactListTo(it)
}

View File

@@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
class IncomingOtsEventVerifier(
private val otsVerifCache: VerificationStateCache,
private val otsVerifCache: () -> VerificationStateCache,
private val cache: LocalCache,
private val scope: CoroutineScope,
) {
@@ -52,7 +52,7 @@ class IncomingOtsEventVerifier(
suspend fun consume(note: Note) {
note.event?.let { event ->
if (event is OtsEvent) {
otsVerifCache.cacheVerify(event)
otsVerifCache().cacheVerify(event)
}
}
}

View File

@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
@@ -37,7 +37,7 @@ import java.util.Base64
class OtsState(
val signer: NostrSigner,
val cache: LocalCache,
val otsResolver: OtsResolverBuilder,
val otsResolver: () -> OtsResolver,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
@@ -55,10 +55,10 @@ class OtsState(
}
suspend fun updateAttestations(): List<OtsEvent> {
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
Log.d("Pending Attestations") { "Updating ${settings.pendingAttestations.value.size} pending attestations" }
return settings.pendingAttestations.value.toList().mapNotNull { (key, value) ->
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build())
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver())
if (otsState != null) {
val hint = cache.getNoteIfExists(key)?.toEventHint<Event>()
@@ -96,7 +96,7 @@ class OtsState(
Base64.getEncoder().encodeToString(
OtsEvent.stamp(
id,
otsResolver.build(),
otsResolver(),
),
),
)

View File

@@ -46,7 +46,7 @@ fun loadRelayInfo(
value = it
},
onError = { url, errorCode, exceptionMessage ->
Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage")
Log.e("RelayInfo") { "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage" }
},
)
}

View File

@@ -88,7 +88,7 @@ class DmRelayListState(
init {
settings.backupDMRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved DM Relay List ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(it)
@@ -98,7 +98,7 @@ class DmRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start")
getDMRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating DM Relay List for ${signer.pubKey}" }
(it.note.event as? ChatMessageRelayListEvent)?.let {
settings.updateDMRelayList(it)
}

View File

@@ -64,7 +64,7 @@ import kotlinx.coroutines.launch
*/
class NwcSignerState(
val signer: NostrSigner,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val cache: LocalCache,
val scope: CoroutineScope,
val nip47Setup: MutableStateFlow<Nip47WalletConnect.Nip47URINorm?>,
@@ -163,11 +163,13 @@ class NwcSignerState(
relay = walletService.relayUri,
)
nwcFilterAssembler.subscribe(filter)
val assembler = nwcFilterAssembler()
assembler.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000)
nwcFilterAssembler.unsubscribe(filter)
assembler.unsubscribe(filter)
}
cache.consume(event, null, true, walletService.relayUri) {
@@ -204,11 +206,13 @@ class NwcSignerState(
relay = walletService.relayUri,
)
nwcFilterAssembler.subscribe(filter)
val assembler = nwcFilterAssembler()
assembler.subscribe(filter)
scope.launch(Dispatchers.IO) {
delay(60000) // waits 1 minute to complete payment.
nwcFilterAssembler.unsubscribe(filter)
assembler.unsubscribe(filter)
}
cache.consume(event, zappedNote, true, walletService.relayUri) {

View File

@@ -20,282 +20,6 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
typealias BookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
@Stable
class BookmarkListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
class BookmarkList(
val public: List<Note> = emptyList(),
val private: List<Note> = emptyList(),
)
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val bookmarkList = cache.getOrCreateAddressableNote(getBookmarkListAddress())
fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey)
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
fun publicBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.publicBookmarks() ?: emptyList()
}
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.privateBookmarks(signer) ?: emptyList()
}
@OptIn(FlowPreview::class)
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
publicBookmarks(noteState.note)
}.onStart {
emit(publicBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
@OptIn(FlowPreview::class)
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
getBookmarkListFlow()
.map { noteState ->
privateBookmarks(noteState.note)
}.onStart {
emit(privateBookmarks(bookmarkList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkEventIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val publicBookmarkAddressIdSet =
publicBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkEventIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is EventBookmark) it.eventId else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val privateBookmarkAddressIdSet =
privateBookmarks
.map { bookmark ->
bookmark
.mapNotNull {
if (it is AddressBookmark) it.address else null
}.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun bookmarkList(
privateBookmarks: List<BookmarkIdTag>,
publicBookmarks: List<BookmarkIdTag>,
): BookmarkList =
BookmarkList(
public =
publicBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
private =
privateBookmarks
.mapNotNull {
when (it) {
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
}
}.reversed(),
)
@OptIn(FlowPreview::class)
val bookmarks: StateFlow<BookmarkList> =
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
emit(bookmarkList(private, public))
}.onStart {
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
BookmarkList(),
)
fun isInPrivateBookmarks(note: Note): Boolean {
if (!signer.isWriteable()) return false
return if (note is AddressableNote) {
privateBookmarkAddressIdSet.value.contains(note.address)
} else {
privateBookmarkEventIdSet.value.contains(note.idHex)
}
}
fun isInPublicBookmarks(note: Note): Boolean =
if (note is AddressableNote) {
publicBookmarkAddressIdSet.value.contains(note.address)
} else {
publicBookmarkEventIdSet.value.contains(note.idHex)
}
suspend fun addBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent {
val bookmarkList = getBookmarkList()
return if (bookmarkList == null) {
if (note is AddressableNote) {
BookmarkListEvent.create(
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.create(
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
if (note is AddressableNote) {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.add(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
}
}
suspend fun removeBookmark(
note: Note,
isPrivate: Boolean,
): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
isPrivate = isPrivate,
signer = signer,
)
}
} else {
null
}
}
suspend fun removeBookmark(note: Note): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
signer = signer,
)
}
} else {
null
}
}
}
typealias OldBookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState

View File

@@ -0,0 +1,130 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip51Lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@Stable
class PinListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
val pinList = cache.getOrCreateAddressableNote(PinListEvent.createPinAddress(signer.pubKey))
fun getPinListFlow(): StateFlow<NoteState> = pinList.flow().metadata.stateFlow
fun getPinList(): PinListEvent? = pinList.event as? PinListEvent
fun pinnedEvents(note: Note): List<EventBookmark> {
val noteEvent = note.event as? PinListEvent
return noteEvent?.pinnedEvents() ?: emptyList()
}
@OptIn(FlowPreview::class)
val pinnedNotes: StateFlow<List<EventBookmark>> =
getPinListFlow()
.map { noteState ->
pinnedEvents(noteState.note)
}.onStart {
emit(pinnedEvents(pinList))
}.debounce(100)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
val pinnedEventIdSet: StateFlow<Set<String>> =
pinnedNotes
.map { pins ->
pins.map { it.eventId }.toSet()
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptySet(),
)
@OptIn(FlowPreview::class)
val pinnedNotesList: StateFlow<List<Note>> =
pinnedNotes
.map { pins ->
pins.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed()
}.onStart {
emit(
pinnedNotes.value.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed(),
)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun isPinned(note: Note): Boolean = pinnedEventIdSet.value.contains(note.idHex)
suspend fun addPin(note: Note): PinListEvent {
val currentList = getPinList()
val pin = EventBookmark(note.idHex, note.relayHintUrl())
return if (currentList == null) {
PinListEvent.create(
pin = pin,
signer = signer,
)
} else {
PinListEvent.add(
earlierVersion = currentList,
pin = pin,
signer = signer,
)
}
}
suspend fun removePin(note: Note): PinListEvent? {
val currentList = getPinList() ?: return null
val pin = EventBookmark(note.idHex, note.relayHintUrl())
return PinListEvent.remove(
earlierVersion = currentList,
pin = pin,
signer = signer,
)
}
}

View File

@@ -93,7 +93,7 @@ class BlockedRelayListState(
init {
settings.backupBlockedRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Blocked relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
@@ -101,7 +101,7 @@ class BlockedRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start")
getBlockedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Blocked Relay List for ${signer.pubKey}" }
(it.note.event as? BlockedRelayListEvent)?.let {
settings.updateBlockedRelayList(it)
}

View File

@@ -106,7 +106,7 @@ class GeohashListState(
init {
settings.backupGeohashList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Geohash list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -116,7 +116,7 @@ class GeohashListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Geohash List Collector Start")
getGeohashListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Geohash List for ${signer.pubKey}" }
(noteState.note.event as? GeohashListEvent)?.let {
settings.updateGeohashListTo(it)
}

View File

@@ -106,7 +106,7 @@ class HashtagListState(
init {
settings.backupHashtagList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Hashtag list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -116,7 +116,7 @@ class HashtagListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Hashtag List Collector Start")
getHashtagListFlow().collect {
Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Hashtag List for ${signer.pubKey}" }
(it.note.event as? HashtagListEvent)?.let {
settings.updateHashtagListTo(it)
}

View File

@@ -103,7 +103,7 @@ class IndexerRelayListState(
init {
settings.backupIndexRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved index relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved index relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
@@ -111,7 +111,7 @@ class IndexerRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Index Relay List Collector Start")
getIndexerRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Index Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Index Relay List for ${signer.pubKey}" }
(it.note.event as? IndexerRelayListEvent)?.let {
settings.updateIndexRelayList(it)
}

View File

@@ -141,7 +141,7 @@ class MuteListState(
init {
settings.backupMuteList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved mute list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -151,7 +151,7 @@ class MuteListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Mute List Collector Start")
getMuteListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Mute List for ${signer.pubKey}" }
(it.note.event as? MuteListEvent)?.let {
settings.updateMuteList(it)
}

View File

@@ -115,7 +115,7 @@ class RelayFeedListState(
init {
settings.backupRelayFeedsList?.let {
Log.d("AccountRegisterObservers", "Loading saved relay feeds list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved relay feeds list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
@@ -123,7 +123,7 @@ class RelayFeedListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Relay feeds list Collector Start")
getRelayFeedsListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Relay feeds list for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Relay feeds list for ${signer.pubKey}" }
(it.note.event as? RelayFeedsListEvent)?.let {
settings.updateRelayFeedList(it)
}

View File

@@ -42,6 +42,7 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
suspend fun relays(event: T) = cachedPrivateLists.mergeTagList(event).relaySet()
@Suppress("UNCHECKED_CAST")
fun fastStartValueForRelayList(note: Note): RelayListCard {
val noteEvent = note.event as? T
return if (noteEvent != null) {
@@ -52,6 +53,7 @@ open class GenericRelayListCache<T : PrivateTagArrayEvent>(
}
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("UNCHECKED_CAST")
fun observeDecryptedRelayList(note: Note): Flow<RelayListCard> =
note
.flow()

View File

@@ -103,7 +103,7 @@ class SearchRelayListState(
init {
settings.backupSearchRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved search relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
}
@@ -111,7 +111,7 @@ class SearchRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Search Relay List Collector Start")
getSearchRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Search Relay List for ${signer.pubKey}" }
(it.note.event as? SearchRelayListEvent)?.let {
settings.updateSearchRelayList(it)
}

View File

@@ -90,7 +90,7 @@ class TrustedRelayListState(
init {
settings.backupTrustedRelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Trusted relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
}
@@ -98,7 +98,7 @@ class TrustedRelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start")
getTrustedRelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating Trusted Relay List for ${signer.pubKey}" }
(it.note.event as? TrustedRelayListEvent)?.let {
settings.updateTrustedRelayList(it)
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip62Vanish
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchFirst
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
@Stable
data class VanishEventItem(
val event: RequestToVanishEvent,
val relays: List<NormalizedRelayUrl>,
val isAllRelays: Boolean,
val complianceResults: MutableStateFlow<Map<NormalizedRelayUrl, ComplianceStatus>> =
MutableStateFlow(
relays.associateWith { ComplianceStatus.UNTESTED },
),
)
enum class ComplianceStatus {
UNTESTED,
TESTING,
COMPLIANT,
NON_COMPLIANT,
ERROR,
}
class VanishRequestsState(
val signer: NostrSigner,
val cache: LocalCache,
val client: INostrClient,
val scope: CoroutineScope,
) {
val noteFlow =
cache
.observeNotes(
Filter(
kinds = listOf(RequestToVanishEvent.KIND),
authors = listOf(signer.pubKey),
),
).stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
val testableFlow =
noteFlow
.map { notes ->
notes.mapNotNull {
val noteEvent = it.event
if (noteEvent is RequestToVanishEvent) {
VanishEventItem(noteEvent, noteEvent.vanishFromRelays(), noteEvent.vanishFromAllRelays())
} else {
null
}
}
}.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(10000),
initialValue = emptyList(),
)
suspend fun testVanishCompliance(
item: VanishEventItem,
relay: NormalizedRelayUrl,
) {
item.complianceResults.update {
it + (relay to ComplianceStatus.TESTING)
}
try {
val foundEvent =
withContext(Dispatchers.IO) {
client.fetchFirst(
relay = relay,
filter =
Filter(
authors = listOf(item.event.pubKey),
until = item.event.createdAt - 1,
limit = 1,
),
)
}
item.complianceResults.update {
it + (
relay to
if (foundEvent != null) {
ComplianceStatus.NON_COMPLIANT
} else {
ComplianceStatus.COMPLIANT
}
)
}
} catch (_: Exception) {
item.complianceResults.update {
it + (relay to ComplianceStatus.ERROR)
}
}
}
}

View File

@@ -144,7 +144,7 @@ class Nip65RelayListState(
init {
settings.backupNIP65RelayList?.let {
Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved nip65 relay list ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
}
@@ -152,7 +152,7 @@ class Nip65RelayListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start")
getNIP65RelayListFlow().collect {
Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating NIP-65 List for ${signer.pubKey}" }
(it.note.event as? AdvertisedRelayListEvent)?.let {
settings.updateNIP65RelayList(it)
}

View File

@@ -155,7 +155,7 @@ class CommunityListState(
init {
settings.backupCommunityList?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved Community list ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -165,7 +165,7 @@ class CommunityListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "Community List Collector Start")
getCommunityListFlow().collect {
Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Community List for ${signer.pubKey}" }
(it.note.event as? CommunityListEvent)?.let {
settings.updateCommunityListTo(it)
}

View File

@@ -65,7 +65,7 @@ class AppSpecificState(
init {
if (settings.isWriteable()) {
settings.backupAppSpecificData?.let { event ->
Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved app specific data ${event.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) {
LocalCache.justConsumeMyOwnEvent(event)
@@ -84,7 +84,7 @@ class AppSpecificState(
Log.d("AccountRegisterObservers", "AppSpecificData Collector Start")
getAppSpecificDataFlow().collect {
try {
Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating AppSpecificData for ${signer.pubKey}" }
(it.note.event as? AppSpecificDataEvent)?.let {
val decrypted = signer.decrypt(it.content, it.pubKey)
try {

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip86RelayManagement
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.coroutines.executeAsync
private const val CONTENT_TYPE_STRING = "application/nostr+json+rpc"
private val CONTENT_TYPE = CONTENT_TYPE_STRING.toMediaType()
class Nip86Retriever(
val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient,
) {
suspend fun execute(
client: Nip86Client,
request: Nip86Request,
): Nip86Response {
val jsonBody = client.serializeRequest(request)
val bodyBytes = jsonBody.encodeToByteArray()
val authToken = client.buildAuthHeader(bodyBytes)
val httpRequest =
Request
.Builder()
.url(client.httpUrl)
.header("Content-Type", CONTENT_TYPE_STRING)
.header("Accept", CONTENT_TYPE_STRING)
.header("Authorization", authToken)
.post(bodyBytes.toRequestBody(CONTENT_TYPE))
.build()
val httpClient = okHttpClient(client.relayUrl)
return withContext(Dispatchers.IO) {
try {
httpClient.newCall(httpRequest).executeAsync().use { response ->
val body = response.body.string()
if (response.code == 401) {
Nip86Response(error = "Unauthorized: relay rejected authentication")
} else if (!response.isSuccessful) {
Nip86Response(error = "HTTP ${response.code}: $body")
} else {
try {
client.parseResponse(body)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Nip86Retriever", "Failed to parse response ${client.httpUrl}: $body", e)
Nip86Response(error = "Failed to parse response: ${e.message}")
}
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Nip86Retriever", "Failed to reach relay ${client.relayUrl.url}", e)
Nip86Response(error = "Failed to reach relay: ${e.message}")
}
}
}
}

View File

@@ -46,7 +46,7 @@ class NipA3PaymentTargetsState(
init {
settings.backupNipA3PaymentTargets?.let {
Log.d("AccountRegisterObservers", "Loading saved nipA3 Payment targets ${it.toJson()}")
Log.d("AccountRegisterObservers") { "Loading saved nipA3 Payment targets ${it.toJson()}" }
@OptIn(DelicateCoroutinesApi::class)
scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) }
}
@@ -54,7 +54,7 @@ class NipA3PaymentTargetsState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "nipA3 Payment targets Collector Start")
getNipA3PaymentTargetsFlow().collect {
Log.d("AccountRegisterObservers", "Updating nipA3 Payment targets for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "Updating nipA3 Payment targets for ${signer.pubKey}" }
(it.note.event as? PaymentTargetsEvent)?.let { paymentTargetsEvent ->
settings.updateNIPA3PaymentTargets(paymentTargetsEvent)
}

View File

@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@@ -87,6 +88,10 @@ class BlossomServerListState(
mergeServerList(blossoms)
}.onStart {
emit(mergeServerList(flow.value))
}.onEach { servers ->
if (servers.none { it == settings.defaultFileServer }) {
settings.changeDefaultFileServer(servers.firstOrNull() ?: DEFAULT_MEDIA_SERVERS[0])
}
}.flowOn(Dispatchers.IO)
.stateIn(
scope,

View File

@@ -32,8 +32,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlin.coroutines.cancellation.CancellationException
@@ -58,17 +57,21 @@ class NamecoinSharedPreferences(
companion object {
val KEY_ENABLED = booleanPreferencesKey("namecoin.enabled")
val KEY_CUSTOM_SERVERS = stringPreferencesKey("namecoin.customServers")
val KEY_PINNED_CERTS = stringPreferencesKey("namecoin.pinnedCerts")
}
/**
* Current settings, loaded synchronously at init to avoid races.
*/
private val _settings =
MutableStateFlow(
runBlocking { loadFromDisk() ?: NamecoinSettings.DEFAULT },
)
private val _settings = MutableStateFlow(NamecoinSettings.DEFAULT)
val settings: StateFlow<NamecoinSettings> = _settings
init {
scope.launch {
_settings.tryEmit(loadFromDisk() ?: NamecoinSettings.DEFAULT)
}
}
/** Synchronous snapshot — safe to call from `serverListProvider` lambdas. */
val current: NamecoinSettings get() = _settings.value
@@ -99,8 +102,48 @@ class NamecoinSharedPreferences(
suspend fun reset() {
persist(NamecoinSettings.DEFAULT)
clearPinnedCerts()
}
/**
* Store a PEM-encoded certificate that the user accepted via Test Connection.
* The cert is appended to the existing list and synced to the ElectrumXClient.
*/
suspend fun addPinnedCert(pem: String) {
val existing = loadPinnedCertsFromDisk()
val updated = (existing + pem).distinct()
savePinnedCerts(updated)
}
/** Load all user-pinned certs from disk (for startup sync). */
suspend fun loadPinnedCerts(): List<String> = loadPinnedCertsFromDisk()
private suspend fun clearPinnedCerts() = savePinnedCerts(emptyList())
private suspend fun savePinnedCerts(certs: List<String>) {
try {
context.sharedPreferencesDataStore.edit { prefs ->
prefs[KEY_PINNED_CERTS] = json.encodeToString(certs)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("NamecoinPrefs") { "Error writing pinned certs: ${e.message}" }
}
}
private suspend fun loadPinnedCertsFromDisk(): List<String> =
try {
val prefs = context.sharedPreferencesDataStore.data.first()
val certsJson = prefs[KEY_PINNED_CERTS]
if (certsJson != null) {
json.decodeFromString<List<String>>(certsJson)
} else {
emptyList()
}
} catch (_: Exception) {
emptyList()
}
// ── Internal ───────────────────────────────────────────────────────
private suspend fun persist(settings: NamecoinSettings) {
@@ -115,7 +158,7 @@ class NamecoinSharedPreferences(
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("NamecoinPrefs", "Error writing DataStore: ${e.message}")
Log.e("NamecoinPrefs") { "Error writing DataStore: ${e.message}" }
}
}
@@ -137,7 +180,7 @@ class NamecoinSharedPreferences(
NamecoinSettings(enabled = enabled, customServers = servers)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("NamecoinPrefs", "Error reading DataStore: ${e.message}")
Log.e("NamecoinPrefs") { "Error reading DataStore: ${e.message}" }
null
}
}

View File

@@ -86,7 +86,7 @@ class OtsSharedPreferences(
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("OtsPrefs", "Error writing DataStore: ${e.message}")
Log.e("OtsPrefs") { "Error writing DataStore: ${e.message}" }
}
}
@@ -97,7 +97,7 @@ class OtsSharedPreferences(
OtsSettings(customExplorerUrl = url)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("OtsPrefs", "Error reading DataStore: ${e.message}")
Log.e("OtsPrefs") { "Error reading DataStore: ${e.message}" }
null
}
}

View File

@@ -40,14 +40,31 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.cancellation.CancellationException
@Stable
class TorSharedPreferences(
prefs: TorSettings,
val context: Context,
val scope: CoroutineScope,
) {
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
val value = TorSettingsFlow.build(prefs)
@OptIn(FlowPreview::class)
val saving =
value.propertyWatchFlow
.debounce(1000)
.distinctUntilChanged()
.onEach {
save(it, context)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
companion object {
// loads faster when individualized
val TOR_TYPE_KEY = stringPreferencesKey("tor.torType")
@@ -63,74 +80,58 @@ class TorSharedPreferences(
val MONEY_OPERATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.moneyOperationsViaTor")
val NIP05_VERIFICATIONS_VIA_TOR_KEY = booleanPreferencesKey("tor.nip05VerificationsViaTor")
val MEDIA_UPLOADS_VIA_TOR_KEY = booleanPreferencesKey("tor.mediaUploadsViaTor")
}
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
val value =
runBlocking {
TorSettingsFlow.build(torPreferences() ?: TorSettings())
}
@OptIn(FlowPreview::class)
val saving =
value.propertyWatchFlow
.debounce(1000)
.distinctUntilChanged()
.onEach(::save)
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
suspend fun torPreferences(): TorSettings? =
try {
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
TorSettings(
torType = preferences[TOR_TYPE_KEY]?.let { TorType.valueOf(it) } ?: TorType.INTERNAL,
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 9050,
onionRelaysViaTor = preferences[ONION_RELAYS_VIA_TOR_KEY] ?: true,
dmRelaysViaTor = preferences[DM_RELAYS_VIA_TOR_KEY] ?: true,
newRelaysViaTor = preferences[NEW_RELAYS_VIA_TOR_KEY] ?: true,
trustedRelaysViaTor = preferences[TRUSTED_RELAYS_VIA_TOR_KEY] ?: false,
urlPreviewsViaTor = preferences[URL_PREVIEWS_VIA_TOR_KEY] ?: false,
profilePicsViaTor = preferences[PROFILE_PICS_VIA_TOR_KEY] ?: false,
imagesViaTor = preferences[IMAGES_VIA_TOR_KEY] ?: false,
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
mediaUploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
null
}
suspend fun save(torSettings: TorSettings) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[TOR_TYPE_KEY] = torSettings.torType.name
preferences[EXTERNAL_SOCKS_PORT_KEY] = torSettings.externalSocksPort
preferences[ONION_RELAYS_VIA_TOR_KEY] = torSettings.onionRelaysViaTor
preferences[DM_RELAYS_VIA_TOR_KEY] = torSettings.dmRelaysViaTor
preferences[NEW_RELAYS_VIA_TOR_KEY] = torSettings.newRelaysViaTor
preferences[TRUSTED_RELAYS_VIA_TOR_KEY] = torSettings.trustedRelaysViaTor
preferences[URL_PREVIEWS_VIA_TOR_KEY] = torSettings.urlPreviewsViaTor
preferences[PROFILE_PICS_VIA_TOR_KEY] = torSettings.profilePicsViaTor
preferences[IMAGES_VIA_TOR_KEY] = torSettings.imagesViaTor
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.mediaUploadsViaTor
suspend fun torPreferences(context: Context): TorSettings? =
try {
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
TorSettings(
torType = preferences[TOR_TYPE_KEY]?.let { TorType.valueOf(it) } ?: TorType.INTERNAL,
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 9050,
onionRelaysViaTor = preferences[ONION_RELAYS_VIA_TOR_KEY] ?: true,
dmRelaysViaTor = preferences[DM_RELAYS_VIA_TOR_KEY] ?: true,
newRelaysViaTor = preferences[NEW_RELAYS_VIA_TOR_KEY] ?: true,
trustedRelaysViaTor = preferences[TRUSTED_RELAYS_VIA_TOR_KEY] ?: false,
urlPreviewsViaTor = preferences[URL_PREVIEWS_VIA_TOR_KEY] ?: false,
profilePicsViaTor = preferences[PROFILE_PICS_VIA_TOR_KEY] ?: false,
imagesViaTor = preferences[IMAGES_VIA_TOR_KEY] ?: false,
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
mediaUploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences") { "Error reading DataStore preferences: ${e.message}" }
null
}
suspend fun save(
torSettings: TorSettings,
context: Context,
) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[TOR_TYPE_KEY] = torSettings.torType.name
preferences[EXTERNAL_SOCKS_PORT_KEY] = torSettings.externalSocksPort
preferences[ONION_RELAYS_VIA_TOR_KEY] = torSettings.onionRelaysViaTor
preferences[DM_RELAYS_VIA_TOR_KEY] = torSettings.dmRelaysViaTor
preferences[NEW_RELAYS_VIA_TOR_KEY] = torSettings.newRelaysViaTor
preferences[TRUSTED_RELAYS_VIA_TOR_KEY] = torSettings.trustedRelaysViaTor
preferences[URL_PREVIEWS_VIA_TOR_KEY] = torSettings.urlPreviewsViaTor
preferences[PROFILE_PICS_VIA_TOR_KEY] = torSettings.profilePicsViaTor
preferences[IMAGES_VIA_TOR_KEY] = torSettings.imagesViaTor
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.mediaUploadsViaTor
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
}
}
}

View File

@@ -49,36 +49,18 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.cancellation.CancellationException
val Context.sharedPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "shared_settings")
@Stable
class UiSharedPreferences(
prefs: UiSettings,
val context: Context,
val scope: CoroutineScope,
) {
companion object {
// loads faster when individualized
val UI_THEME = stringPreferencesKey("ui.theme")
val UI_LANGUAGE = stringPreferencesKey("ui.language")
val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images")
val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback")
val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview")
val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars")
val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures")
val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector")
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
}
// UI Preferences. Makes sure to wait for it to avoid blinking themes and language preferences
val value =
runBlocking {
UiSettingsFlow.build(uiPreferences() ?: UiSettings())
}
val value = UiSettingsFlow.build(prefs)
val languageUpdate =
value.preferredLanguage
@@ -98,68 +80,87 @@ class UiSharedPreferences(
value.propertyWatchFlow
.debounce(1000)
.distinctUntilChanged()
.onEach(::save)
.flowOn(Dispatchers.IO)
.onEach {
save(it, context)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
value.toSettings(),
)
suspend fun uiPreferences(): UiSettings? =
try {
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
UiSettings(
theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM,
preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null },
automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false,
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
companion object {
// loads faster when individualized
val UI_THEME = stringPreferencesKey("ui.theme")
val UI_LANGUAGE = stringPreferencesKey("ui.language")
val UI_SHOW_IMAGES = stringPreferencesKey("ui.show_images")
val UI_START_PLAYBACK = stringPreferencesKey("ui.start_playback")
val UI_SHOW_URL_PREVIEW = stringPreferencesKey("ui.show_url_preview")
val UI_HIDE_NAVIGATION_BARS = stringPreferencesKey("ui.hide_navigation_bars")
val UI_SHOW_PROFILE_PICTURES = stringPreferencesKey("ui.show_profile_pictures")
val UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR = booleanPreferencesKey("ui.dont_show_push_notification_selector")
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
suspend fun uiPreferences(context: Context): UiSettings? =
try {
val oldVersion = LocalPreferences.loadSharedSettings()
if (oldVersion != null) {
save(oldVersion)
}
oldVersion
// Get the preference flow and take the first value.
val preferences = context.sharedPreferencesDataStore.data.first()
UiSettings(
theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM,
preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null },
automaticallyShowImages = preferences[UI_SHOW_IMAGES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyStartPlayback = preferences[UI_START_PLAYBACK]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyShowUrlPreview = preferences[UI_SHOW_URL_PREVIEW]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
automaticallyHideNavigationBars = preferences[UI_HIDE_NAVIGATION_BARS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS,
dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false,
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences") { "Error reading DataStore preferences: ${e.message}" }
suspend fun save(sharedSettings: UiSettings) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[UI_THEME] = sharedSettings.theme.name
preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: ""
preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name
preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name
preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name
preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name
preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name
preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
try {
val oldVersion = LocalPreferences.loadSharedSettings()
if (oldVersion != null) {
save(oldVersion, context)
}
oldVersion
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
suspend fun save(
sharedSettings: UiSettings,
context: Context,
) {
try {
context.sharedPreferencesDataStore.edit { preferences ->
preferences[UI_THEME] = sharedSettings.theme.name
preferences[UI_LANGUAGE] = sharedSettings.preferredLanguage ?: ""
preferences[UI_SHOW_IMAGES] = sharedSettings.automaticallyShowImages.name
preferences[UI_START_PLAYBACK] = sharedSettings.automaticallyStartPlayback.name
preferences[UI_SHOW_URL_PREVIEW] = sharedSettings.automaticallyShowUrlPreview.name
preferences[UI_HIDE_NAVIGATION_BARS] = sharedSettings.automaticallyHideNavigationBars.name
preferences[UI_SHOW_PROFILE_PICTURES] = sharedSettings.automaticallyShowProfilePictures.name
preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] = sharedSettings.dontShowPushNotificationSelector
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences") { "Error saving DataStore preferences: ${e.message}" }
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// Log any errors that occur while reading the DataStore.
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
}
}
}

View File

@@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
@Suppress("UNCHECKED_CAST")
class MergedFollowListsState(
val kind3List: Kind3FollowListState,
val peopleList: PeopleListsState,

View File

@@ -51,7 +51,7 @@ class FeedTopNavFilterState(
val feedFilterListName: MutableStateFlow<TopFilter>,
val kind3Follows: StateFlow<Kind3FollowListState.Kind3Follows>,
val allFollows: StateFlow<MergedFollowListsState.AllFollows>,
val locationFlow: StateFlow<LocationState.LocationResult>,
val locationFlow: () -> StateFlow<LocationState.LocationResult>,
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
@@ -78,7 +78,7 @@ class FeedTopNavFilterState(
}
TopFilter.AroundMe -> {
AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays)
AroundMeFeedFlow(locationFlow(), followsRelays, proxyRelays)
}
TopFilter.Chess -> {

View File

@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.torState
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
@@ -35,6 +36,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import okhttp3.OkHttpClient
@Stable
class TorRelayState(
val okHttpClient: DualHttpClientManager,
val torSettingsFlow: TorSettingsFlow,

View File

@@ -20,11 +20,11 @@
*/
package com.vitorpamplona.amethyst.model.trustedAssertions
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviderSet
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviders
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviderSet
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviders
class TrustProviderListDecryptionCache(
val signer: NostrSigner,

View File

@@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ProviderTypes
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ProviderTypes
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -124,7 +124,7 @@ class TrustProviderListState(
scope.launch(Dispatchers.IO) {
Log.d("AccountRegisterObservers", "TrustProviderList Collector Start")
getTrustProviderListFlow().collect { noteState ->
Log.d("AccountRegisterObservers", "TrustProviderList List for ${signer.pubKey}")
Log.d("AccountRegisterObservers") { "TrustProviderList List for ${signer.pubKey}" }
(noteState.note.event as? TrustProviderListEvent)?.let {
settings.updateTrustProviderListTo(it)
}

View File

@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.broadcast
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
@@ -85,12 +85,12 @@ class BroadcastTracker {
// Add to active broadcasts and cache event for retries
_activeBroadcasts.update { (it + broadcast).toImmutableList() }
Log.d(TAG, "Starting broadcast $trackingId (kind ${event.kind}) to ${relays.size} relays")
Log.d(TAG) { "Starting broadcast $trackingId (kind ${event.kind}) to ${relays.size} relays" }
val resultChannel = Channel<RelayResponse>(UNLIMITED)
val subscription =
object : IRelayClientListener {
object : RelayConnectionListener {
override fun onCannotConnect(
relay: IRelayClient,
errorMessage: String,
@@ -102,7 +102,7 @@ class BroadcastTracker {
result = RelayResult.Error(errorMessage),
),
)
Log.d(TAG, "[$trackingId] Cannot connect to ${relay.url}: $errorMessage")
Log.d(TAG) { "[$trackingId] Cannot connect to ${relay.url}: $errorMessage" }
}
}
@@ -114,7 +114,7 @@ class BroadcastTracker {
result = RelayResult.Error("Relay disconnected before completion"),
),
)
Log.d(TAG, "[$trackingId] Disconnected from ${relay.url}")
Log.d(TAG) { "[$trackingId] Disconnected from ${relay.url}" }
}
}
@@ -135,62 +135,65 @@ class BroadcastTracker {
RelayResult.Error(msg.message)
}
resultChannel.trySend(RelayResponse(relay.url, result))
Log.d(TAG, "[$trackingId] Response from ${relay.url}: success=${msg.success} message=${msg.message}")
Log.d(TAG) { "[$trackingId] Response from ${relay.url}: success=${msg.success} message=${msg.message}" }
}
}
}
}
}
client.subscribe(subscription)
try {
client.addConnectionListener(subscription)
val finalBroadcast =
coroutineScope {
val resultCollector =
async {
val receivedRelays = mutableSetOf<NormalizedRelayUrl>()
var currentBroadcast = broadcast
val finalBroadcast =
coroutineScope {
val resultCollector =
async {
val receivedRelays = mutableSetOf<NormalizedRelayUrl>()
var currentBroadcast = broadcast
withTimeoutOrNull(TIMEOUT_SECONDS * 1000) {
while (receivedRelays.size < relays.size) {
val response = resultChannel.receive()
withTimeoutOrNull(TIMEOUT_SECONDS * 1000) {
while (receivedRelays.size < relays.size) {
val response = resultChannel.receive()
// Skip if already received (don't override success)
if (response.relay in receivedRelays) continue
// Skip if already received (don't override success)
if (response.relay in receivedRelays) continue
receivedRelays.add(response.relay)
currentBroadcast = currentBroadcast.withResult(response.relay, response.result)
receivedRelays.add(response.relay)
currentBroadcast = currentBroadcast.withResult(response.relay, response.result)
// Update active broadcasts with new progress
_activeBroadcasts.update { list ->
list.map { if (it.id == trackingId) currentBroadcast else it }.toImmutableList()
// Update active broadcasts with new progress
_activeBroadcasts.update { list ->
list.map { if (it.id == trackingId) currentBroadcast else it }.toImmutableList()
}
}
}
// Mark remaining relays as timeout
relays.filter { it !in receivedRelays }.forEach { relay ->
currentBroadcast = currentBroadcast.withResult(relay, RelayResult.Timeout)
}
currentBroadcast
}
// Mark remaining relays as timeout
relays.filter { it !in receivedRelays }.forEach { relay ->
currentBroadcast = currentBroadcast.withResult(relay, RelayResult.Timeout)
}
// Send after setting up listener
client.publish(event, relays)
currentBroadcast
}
resultCollector.await()
}
// Send after setting up listener
client.send(event, relays)
resultChannel.close()
resultCollector.await()
// Remove from active, emit to completed
_activeBroadcasts.update { list ->
list.map { if (it.id == trackingId) finalBroadcast else it }.toImmutableList()
}
client.unsubscribe(subscription)
resultChannel.close()
// Remove from active, emit to completed
_activeBroadcasts.update { list ->
list.map { if (it.id == trackingId) finalBroadcast else it }.toImmutableList()
Log.d(TAG) { "Broadcast $trackingId complete: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success" }
} finally {
client.removeConnectionListener(subscription)
}
Log.d(TAG, "Broadcast $trackingId complete: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success")
}
/**
@@ -263,7 +266,7 @@ class BroadcastTracker {
val resultChannel = Channel<RelayResponse>(UNLIMITED)
val subscription =
object : IRelayClientListener {
object : RelayConnectionListener {
override fun onCannotConnect(
relay: IRelayClient,
errorMessage: String,
@@ -275,7 +278,7 @@ class BroadcastTracker {
result = RelayResult.Error(errorMessage),
),
)
Log.d(TAG, "[${broadcast.id}] Retry cannot connect to ${relay.url}: $errorMessage")
Log.d(TAG) { "[${broadcast.id}] Retry cannot connect to ${relay.url}: $errorMessage" }
}
}
@@ -287,7 +290,7 @@ class BroadcastTracker {
result = RelayResult.Error("Relay disconnected before completion"),
),
)
Log.d(TAG, "[${broadcast.id}] Retry disconnected from ${relay.url}")
Log.d(TAG) { "[${broadcast.id}] Retry disconnected from ${relay.url}" }
}
}
@@ -308,14 +311,14 @@ class BroadcastTracker {
RelayResult.Error(msg.message)
}
resultChannel.trySend(RelayResponse(relay.url, result))
Log.d(TAG, "[${broadcast.id}] Retry response from ${relay.url}: success=${msg.success}")
Log.d(TAG) { "[${broadcast.id}] Retry response from ${relay.url}: success=${msg.success}" }
}
}
}
}
}
client.subscribe(subscription)
client.addConnectionListener(subscription)
val finalBroadcast =
coroutineScope {
@@ -367,12 +370,12 @@ class BroadcastTracker {
currentBroadcast.copy(status = newStatus)
}
client.send(event, relaysToRetry)
client.publish(event, relaysToRetry)
resultCollector.await()
}
client.unsubscribe(subscription)
client.removeConnectionListener(subscription)
resultChannel.close()
// Update in active broadcasts
@@ -380,7 +383,7 @@ class BroadcastTracker {
list.map { if (it.id == broadcast.id) finalBroadcast else it }.toImmutableList()
}
Log.d(TAG, "Retry complete for ${broadcast.id}: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success")
Log.d(TAG) { "Retry complete for ${broadcast.id}: ${finalBroadcast.successCount}/${finalBroadcast.totalRelays} success" }
return finalBroadcast
}

View File

@@ -48,7 +48,7 @@ class ConnectivityFlow(
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d("ConnectivityFlow", "onAvailable ${network.networkHandle}")
Log.d("ConnectivityFlow") { "onAvailable ${network.networkHandle}" }
connectivityManager.getNetworkCapabilities(network)?.let {
trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData()))
}
@@ -60,13 +60,13 @@ class ConnectivityFlow(
) {
super.onCapabilitiesChanged(network, networkCapabilities)
val isMobile = networkCapabilities.isMeteredOrMobileData()
Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile")
Log.d("ConnectivityFlow") { "onCapabilitiesChanged ${network.networkHandle} $isMobile" }
trySend(ConnectivityStatus.Active(network.networkHandle, isMobile))
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d("ConnectivityFlow", "onLost ${network.networkHandle} ")
Log.d("ConnectivityFlow") { "onLost ${network.networkHandle} " }
trySend(ConnectivityStatus.Off)
}
}

View File

@@ -41,12 +41,12 @@ import kotlin.coroutines.cancellation.CancellationException
class BlossomFetcher(
private val options: Options,
private val data: Uri,
private val blossomServerResolver: BlossomServerResolver,
private val blossomServerResolver: () -> BlossomServerResolver,
private val networkFetcher: (url: String) -> Fetcher,
) : Fetcher {
override suspend fun fetch(): FetchResult? =
try {
val urlResult = blossomServerResolver.findServers(data.toString())
val urlResult = blossomServerResolver().findServers(data.toString())
networkFetcher(urlResult?.serverUrl ?: data.toString()).fetch()
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -55,7 +55,7 @@ class BlossomFetcher(
@OptIn(ExperimentalCoilApi::class)
class Factory(
val blossomServerResolver: BlossomServerResolver,
val blossomServerResolver: () -> BlossomServerResolver,
val networkClient: (url: String) -> Call.Factory,
) : Fetcher.Factory<Uri> {
private val connectivityCheckerLazy = singleParameterLazy(::ConnectivityChecker)

View File

@@ -64,7 +64,7 @@ class ImageLoaderSetup {
app: Context,
diskCache: () -> DiskCache,
memoryCache: () -> MemoryCache,
blossomServerResolver: BlossomServerResolver,
blossomServerResolver: () -> BlossomServerResolver,
callFactory: (url: String) -> Call.Factory,
) {
SingletonImageLoader.setUnsafe(

View File

@@ -188,7 +188,7 @@ class LightningAddressResolver {
}
if (errorMessage == null) {
Log.d("LightningAddressResolver", "Error parsing LNResponse: $body")
Log.d("LightningAddressResolver") { "Error parsing LNResponse: $body" }
}
return errorMessage

View File

@@ -48,13 +48,13 @@ class LocationFlow(
val locationCallback =
LocationListener { location ->
Log.d("LocationFlow", "onLocationChanged $location")
Log.d("LocationFlow") { "onLocationChanged $location" }
launch { send(location) }
}
locationManager.allProviders.forEach {
val location = locationManager.getLastKnownLocation(it)
Log.d("LocationFlow", "Last Known location is $location")
Log.d("LocationFlow") { "Last Known location is $location" }
if (location != null) {
send(location)
}

View File

@@ -65,7 +65,7 @@ class LocationState(
}
@OptIn(ExperimentalCoroutinesApi::class)
val geohashStateFlow =
val geohashStateFlow by lazy {
hasLocationPermission
.transformLatest {
if (it) {
@@ -92,4 +92,5 @@ class LocationState(
SharingStarted.WhileSubscribed(5000),
latestLocation,
)
}
}

View File

@@ -29,6 +29,7 @@ import androidx.annotation.RequiresApi
import com.vitorpamplona.quartz.utils.Log
import java.io.IOException
@Suppress("DEPRECATION")
class ReverseGeolocation {
companion object {
fun execute(
@@ -52,18 +53,18 @@ class ReverseGeolocation {
val locationCallback =
object : Geocoder.GeocodeListener {
override fun onGeocode(addresses: List<Address>) {
Log.d("ReverseGeoLocation", "Found ${addresses.size} new addresses")
Log.d("ReverseGeoLocation") { "Found ${addresses.size} new addresses" }
onReady(addresses)
}
override fun onError(errorMessage: String?) {
super.onError(errorMessage)
Log.w("ReverseGeoLocation", "Failure $errorMessage")
Log.w("ReverseGeoLocation") { "Failure $errorMessage" }
onReady(null)
}
}
Log.d("ReverseGeoLocation", "Execute Async $location")
Log.d("ReverseGeoLocation") { "Execute Async $location" }
Geocoder(context).getFromLocation(
location.latitude,
location.longitude,
@@ -76,7 +77,7 @@ class ReverseGeolocation {
location: Location,
context: Context,
): List<Address>? {
Log.d("ReverseGeoLocation", "Execute Sync $location")
Log.d("ReverseGeoLocation") { "Execute Sync $location" }
return try {
Geocoder(context).getFromLocation(
location.latitude,

View File

@@ -41,7 +41,7 @@ object ChoreographerHelper {
if (diff > 35) {
// Follow the frame number
val droppedCount = (diff / 16.6).toInt()
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
Log.w("block-canary") { "Dropped $droppedCount frames. Skipped $diff ms" }
}
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)

View File

@@ -60,7 +60,7 @@ class LogMonitor : Printer {
val endTime = System.currentTimeMillis()
if (x.indexOf("com.vitorpamplona.amethyst") > 0) {
Log.d("block-canary", "Looper ${endTime - mStartTimestamp}ms for $x")
Log.d("block-canary") { "Looper ${endTime - mStartTimestamp}ms for $x" }
}
mPrintingStarted = false

View File

@@ -66,7 +66,7 @@ data class NamecoinSettings(
* TLS is the default protocol. Append `:tcp` for plaintext
* (useful for `.onion` addresses and local servers).
*
* `.onion` addresses automatically get `trustAllCerts = true`
* `.onion` addresses automatically get `usePinnedTrustStore = true`
* since certificate verification is meaningless over Tor.
*/
fun parseServerString(s: String): ElectrumxServer? {
@@ -77,11 +77,15 @@ data class NamecoinSettings(
if (host.isEmpty() || port <= 0 || port > 65535) return null
val useSsl = parts.getOrNull(2)?.trim()?.lowercase() != "tcp"
val isOnion = host.endsWith(".onion")
// All custom servers use the pinned trust store. ElectrumX
// servers almost universally use self-signed certs, so we
// route them through our pinned SSLSocketFactory (hardcoded
// defaults + TOFU-pinned certs + system CAs).
return ElectrumxServer(
host = host,
port = port,
useSsl = useSsl,
trustAllCerts = isOnion || !useSsl,
usePinnedTrustStore = true,
)
}

View File

@@ -29,7 +29,9 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendChessNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReactionNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.stringRes
@@ -43,10 +45,14 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip64Chess.baseEvent.BaseChessEvent
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
@@ -67,14 +73,14 @@ class EventNotificationConsumer(
LocalPreferences.allSavedAccounts().forEach {
if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner)) {
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { acc ->
Log.d(TAG, "New Notification Testing if for ${it.npub}")
Log.d(TAG) { "New Notification Testing if for ${it.npub}" }
try {
val account = Amethyst.instance.accountsCache.loadAccount(acc)
consumeIfMatchesAccount(event, account)
matchAccount = true
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message was not for user ${it.npub}: ${e.message}")
Log.d(TAG) { "Message was not for user ${it.npub}: ${e.message}" }
}
}
}
@@ -94,20 +100,43 @@ class EventNotificationConsumer(
account: Account,
) {
val consumed = LocalCache.hasConsumed(notificationEvent)
Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.signer.pubKey} consumed= $consumed")
Log.d(TAG) { "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.signer.pubKey} consumed= $consumed" }
if (!consumed) {
Log.d(TAG, "New Notification was verified")
if (!notificationManager().areNotificationsEnabled()) return
Log.d(TAG, "Notifications are enabled")
unwrapAndConsume(notificationEvent, account.signer)?.let { innerEvent ->
Log.d(TAG, "Unwrapped consume ${innerEvent.javaClass.simpleName}")
Log.d(TAG) { "Unwrapped consume ${innerEvent.javaClass.simpleName}" }
when (innerEvent) {
is PrivateDmEvent -> notify(innerEvent, account)
is LnZapEvent -> notify(innerEvent, account)
is ChatMessageEvent -> notify(innerEvent, account)
is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, account)
is PrivateDmEvent -> {
notify(innerEvent, account)
}
is LnZapEvent -> {
notify(innerEvent, account)
}
is ChatMessageEvent -> {
notify(innerEvent, account)
}
is ChatMessageEncryptedFileHeaderEvent -> {
notify(innerEvent, account)
}
is ReactionEvent -> {
notify(innerEvent, account)
}
is LiveChessGameAcceptEvent -> {
notifyChessEvent(innerEvent, account, R.string.app_notification_chess_challenge_accepted)
}
is LiveChessMoveEvent -> {
notifyChessEvent(innerEvent, account, R.string.app_notification_chess_your_turn)
}
}
}
}
@@ -124,14 +153,14 @@ class EventNotificationConsumer(
LocalPreferences.allSavedAccounts().forEach {
if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner) && it.npub in npubs) {
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { accountSettings ->
Log.d(TAG, "New Notification Testing if for ${it.npub}")
Log.d(TAG) { "New Notification Testing if for ${it.npub}" }
try {
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
consumeNotificationEvent(event, account)
matchAccount = true
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.d(TAG, "Message was not for user ${it.npub}: ${e.message}")
Log.d(TAG) { "Message was not for user ${it.npub}: ${e.message}" }
}
}
}
@@ -365,7 +394,7 @@ class EventNotificationConsumer(
account: Account,
) {
Log.d(TAG, "New Zap to Notify")
Log.d(TAG, "Notify Start ${event.toNostrUri()}")
Log.d(TAG) { "Notify Start ${event.toNostrUri()}" }
LocalCache.getNoteIfExists(event.id) ?: return
Log.d(TAG, "Notify Not Notified Yet")
@@ -378,7 +407,7 @@ class EventNotificationConsumer(
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
Log.d(TAG, "Notify ZapRequest $noteZapRequest zapped $noteZapped")
Log.d(TAG) { "Notify ZapRequest $noteZapRequest zapped $noteZapped" }
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
@@ -387,11 +416,11 @@ class EventNotificationConsumer(
if (event.isTaggedUser(account.signer.pubKey)) {
val amount = showAmount(event.amount)
Log.d(TAG, "Notify Amount $amount")
Log.d(TAG) { "Notify Amount $amount" }
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
decryptZapContentAuthor(event, account.signer)?.let { decryptedEvent ->
Log.d(TAG, "Notify Decrypted if Private Zap ${event.id}")
Log.d(TAG) { "Notify Decrypted if Private Zap ${event.id}" }
val author = LocalCache.getOrCreateUser(decryptedEvent.pubKey)
val senderInfo = Pair(author, decryptedEvent.content.ifBlank { null })
@@ -428,7 +457,7 @@ class EventNotificationConsumer(
.hexToByteArray()
.toNpub()
Log.d(TAG, "Notify ${event.id} $content $title $noteUri")
Log.d(TAG) { "Notify ${event.id} $content $title $noteUri" }
notificationManager()
.sendZapNotification(
@@ -463,7 +492,7 @@ class EventNotificationConsumer(
.hexToByteArray()
.toNpub()
Log.d(TAG, "Notify ${event.id} $title $noteUri")
Log.d(TAG) { "Notify ${event.id} $title $noteUri" }
notificationManager()
.sendZapNotification(
@@ -481,6 +510,111 @@ class EventNotificationConsumer(
}
}
private suspend fun notify(
event: ReactionEvent,
account: Account,
) {
Log.d(TAG, "New Reaction to Notify")
// old event being re-broadcast
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return
// don't notify for own reactions
if (event.pubKey == account.signer.pubKey) return
// only notify if the reaction is for the current user
if (!event.isTaggedUser(account.signer.pubKey)) return
val reactedPostId = event.originalPost().firstOrNull() ?: return
val reactedNote = LocalCache.checkGetOrCreateNote(reactedPostId)
val author = LocalCache.getOrCreateUser(event.pubKey)
val user = author.toBestDisplayName()
val userPicture = author.profilePicture()
val reactionContent = event.content
val reactionSymbol =
when {
reactionContent == ReactionEvent.LIKE || reactionContent.isBlank() -> "\uD83E\uDD19"
reactionContent == ReactionEvent.DISLIKE -> "\uD83D\uDC4E"
else -> reactionContent
}
val title = "$reactionSymbol $user"
val reactedContent =
reactedNote
?.event
?.content
?.split("\n")
?.firstOrNull() ?: ""
val content =
if (reactedContent.isNotBlank()) {
stringRes(
applicationContext,
R.string.app_notification_reactions_channel_message_for,
reactedContent,
)
} else {
stringRes(
applicationContext,
R.string.app_notification_reactions_channel_message,
user,
)
}
val noteUri =
"notifications$ACCOUNT_QUERY_PARAM" +
account.signer.pubKey
.hexToByteArray()
.toNpub()
notificationManager()
.sendReactionNotification(
event.id,
content,
title,
event.createdAt,
userPicture,
noteUri,
applicationContext,
)
}
private suspend fun notifyChessEvent(
event: BaseChessEvent,
account: Account,
contentStringRes: Int,
) {
if (
event.createdAt > TimeUtils.fifteenMinutesAgo() &&
event.pubKey != account.signer.pubKey
) {
val author = LocalCache.getOrCreateUser(event.pubKey)
val user = author.toBestDisplayName()
val userPicture = author.profilePicture()
val title = stringRes(applicationContext, R.string.app_notification_chess_channel_name)
val content = stringRes(applicationContext, contentStringRes, user)
val noteUri =
"notifications$ACCOUNT_QUERY_PARAM" +
account.signer.pubKey
.hexToByteArray()
.toNpub()
notificationManager()
.sendChessNotification(
event.id,
content,
title,
event.createdAt,
userPicture,
noteUri,
applicationContext,
)
}
}
fun notificationManager(): NotificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
as NotificationManager

View File

@@ -85,7 +85,7 @@ class NotificationReplyReceiver : BroadcastReceiver() {
notificationManager.cancel(notificationId)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("NotificationReply", "Failed to send reply: ${e.message}")
Log.e("NotificationReply") { "Failed to send reply: ${e.message}" }
} finally {
pendingResult.finish()

View File

@@ -45,8 +45,14 @@ import kotlinx.coroutines.withContext
object NotificationUtils {
private var dmChannel: NotificationChannel? = null
private var zapChannel: NotificationChannel? = null
private var reactionChannel: NotificationChannel? = null
private var chessChannel: NotificationChannel? = null
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION"
private const val REACTION_GROUP_KEY = "com.vitorpamplona.amethyst.REACTION_NOTIFICATION"
private const val CHESS_GROUP_KEY = "com.vitorpamplona.amethyst.CHESS_NOTIFICATION"
const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
const val KEY_REPLY_TEXT = "key_reply_text"
@@ -56,6 +62,8 @@ object NotificationUtils {
private const val DM_SUMMARY_ID = 0x10000
private const val ZAP_SUMMARY_ID = 0x20000
private const val REACTION_SUMMARY_ID = 0x40000
private const val CHESS_SUMMARY_ID = 0x30000
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
if (dmChannel != null) return dmChannel!!
@@ -99,6 +107,104 @@ object NotificationUtils {
return zapChannel!!
}
fun getOrCreateReactionChannel(applicationContext: Context): NotificationChannel {
if (reactionChannel != null) return reactionChannel!!
reactionChannel =
NotificationChannel(
stringRes(applicationContext, R.string.app_notification_reactions_channel_id),
stringRes(applicationContext, R.string.app_notification_reactions_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description =
stringRes(applicationContext, R.string.app_notification_reactions_channel_description)
}
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(reactionChannel!!)
return reactionChannel!!
}
fun getOrCreateChessChannel(applicationContext: Context): NotificationChannel {
if (chessChannel != null) return chessChannel!!
chessChannel =
NotificationChannel(
stringRes(applicationContext, R.string.app_notification_chess_channel_id),
stringRes(applicationContext, R.string.app_notification_chess_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
).apply {
description =
stringRes(applicationContext, R.string.app_notification_chess_channel_description)
}
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(chessChannel!!)
return chessChannel!!
}
suspend fun NotificationManager.sendReactionNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
getOrCreateReactionChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_reactions_channel_id)
sendNotification(
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
pictureUrl = pictureUrl,
uri = uri,
channelId = channelId,
notificationGroupKey = REACTION_GROUP_KEY,
category = NotificationCompat.CATEGORY_SOCIAL,
summaryId = REACTION_SUMMARY_ID,
summaryText = stringRes(applicationContext, R.string.app_notification_reactions_summary),
applicationContext = applicationContext,
)
}
suspend fun NotificationManager.sendChessNotification(
id: String,
messageBody: String,
messageTitle: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
) {
getOrCreateChessChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_chess_channel_id)
sendNotification(
id = id,
messageBody = messageBody,
messageTitle = messageTitle,
time = time,
pictureUrl = pictureUrl,
uri = uri,
channelId = channelId,
notificationGroupKey = CHESS_GROUP_KEY,
category = NotificationCompat.CATEGORY_SOCIAL,
summaryId = CHESS_SUMMARY_ID,
summaryText = stringRes(applicationContext, R.string.app_notification_chess_summary),
applicationContext = applicationContext,
)
}
suspend fun NotificationManager.sendZapNotification(
id: String,
messageBody: String,

View File

@@ -69,7 +69,7 @@ class PokeyReceiver : BroadcastReceiver() {
) {
if (intent.action == POKEY_ACTION) { // it's best practice to verify intent action before performing any operation
val eventStr = intent.getStringExtra("EVENT")
Log.d(TAG, "New Pokey Notification Arrived $eventStr")
Log.d(TAG) { "New Pokey Notification Arrived $eventStr" }
if (eventStr == null) return

View File

@@ -78,7 +78,7 @@ class RegisterAccounts(
accounts
.mapNotNull { account ->
if (account.hasPrivKey || account.loggedInWithExternalSigner) {
Log.d(tag, "Register Account ${account.npub}")
Log.d(tag) { "Register Account ${account.npub}" }
val acc = LocalPreferences.loadAccountConfigFromEncryptedStorage(account.npub)
if (acc != null && acc.isWriteable()) {
@@ -87,10 +87,10 @@ class RegisterAccounts(
if (isDebug) {
val readRelays = nip65Read.joinToString(", ") { it.url }
Log.d(tag, "Register Account ${account.npub} NIP65 Reads $readRelays")
Log.d(tag) { "Register Account ${account.npub} NIP65 Reads $readRelays" }
val dmRelays = nip17Read.joinToString(", ") { it.url }
Log.d(tag, "Register Account ${account.npub} NIP17 Reads $dmRelays")
Log.d(tag) { "Register Account ${account.npub} NIP17 Reads $dmRelays" }
}
val relays = (nip65Read + nip17Read)
@@ -132,7 +132,7 @@ class RegisterAccounts(
val client = client(url)
client.newCall(request).executeAsync().use { response ->
Log.i(tag, "Server registration ${response.isSuccessful}")
Log.i(tag) { "Server registration ${response.isSuccessful}" }
}
}

View File

@@ -33,9 +33,9 @@ class EncryptedBlobInterceptor(
val body = peekBody(Long.MAX_VALUE)
// Only tries to decrypt if the content-type is a byte array
if (body.contentType().toString() != "application/octet-stream") {
return null
}
// if (body.contentType().toString() != "application/octet-stream") {
// return null
// }
val bytes = body.bytes()

View File

@@ -41,7 +41,7 @@ class LoggingInterceptor : Interceptor {
val response: Response = chain.proceed(request)
val t2 = System.nanoTime()
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
Log.d("OkHttpLog") { "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms" }
return response
}

View File

@@ -47,7 +47,7 @@ fun GetVideoController(
keepPlaying = mediaItem.src.keepPlaying,
context = context,
).onEach { state ->
Log.d("PlaybackService", "Controller instance: ${state.controller}")
Log.d("PlaybackService") { "Controller instance: ${state.controller}" }
if (BackgroundMedia.isPlaying()) {
// There is a video playing, start this one on mute.
@@ -57,7 +57,7 @@ fun GetVideoController(
// There is no other video playing. Use the default mute state to
// decide if sound is on or not.
state.controller.volume = if (muted) 0f else 1f
Log.d("PlaybackService", "OnEach $muted")
Log.d("PlaybackService") { "OnEach $muted" }
}
if (play) {

View File

@@ -24,7 +24,6 @@ import androidx.annotation.OptIn
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

View File

@@ -27,8 +27,6 @@ import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
@SuppressLint("UnsafeOptInUsageError")
@@ -42,20 +40,18 @@ class VideoCache {
lateinit var cacheDataSourceFactory: CacheDataSource.Factory
suspend fun initFileCache(
fun initFileCache(
context: Context,
cachePath: File,
) {
exoDatabaseProvider = StandaloneDatabaseProvider(context)
withContext(Dispatchers.IO) {
simpleCache =
SimpleCache(
cachePath,
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
}
simpleCache =
SimpleCache(
cachePath,
leastRecentlyUsedCacheEvictor,
exoDatabaseProvider,
)
}
// This method should be called when proxy setting changes.

View File

@@ -22,18 +22,15 @@ package com.vitorpamplona.amethyst.service.playback.diskCache
import android.content.Context
import com.vitorpamplona.amethyst.service.safeCacheDir
import kotlinx.coroutines.runBlocking
class VideoCacheFactory {
companion object {
fun new(app: Context): VideoCache {
val newCache = VideoCache()
runBlocking {
newCache.initFileCache(
app,
app.safeCacheDir().resolve("exoplayer"),
)
}
newCache.initFileCache(
app,
app.safeCacheDir().resolve("exoplayer"),
)
return newCache
}
}

View File

@@ -60,14 +60,22 @@ class PipVideoActivity : ComponentActivity() {
}
}
override fun onStop() {
super.onStop()
finishAndRemoveTask()
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (!isInPictureInPictureMode) {
// User dismissed PiP (swiped away or expanded).
finishAndRemoveTask()
}
}
override fun finish() {
finishAndRemoveTask()
super.finish()
override fun onStop() {
super.onStop()
if (!isInPictureInPictureMode) {
// Only finish if we're not in PiP mode.
// When the screen locks while in PiP, we stay alive
// so the PlaybackService can continue audio playback.
finishAndRemoveTask()
}
}
companion object {

View File

@@ -32,7 +32,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.content.ContextCompat
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.state.rememberMuteButtonState
@@ -51,6 +53,8 @@ fun RenderPipVideo(
controller: MediaControllerState,
waveformData: WaveformData?,
) {
KeepScreenOnWhilePlaying(controller)
val modifier =
remember {
val ratio =
@@ -78,6 +82,31 @@ fun RenderPipVideo(
}
}
@Composable
fun KeepScreenOnWhilePlaying(controller: MediaControllerState) {
val view = LocalView.current
DisposableEffect(controller.controller, view) {
val listener =
object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (view.keepScreenOn != isPlaying) {
view.keepScreenOn = isPlaying
}
}
}
// Set initial state
view.keepScreenOn = controller.controller.isPlaying
controller.controller.addListener(listener)
onDispose {
controller.controller.removeListener(listener)
view.keepScreenOn = false
}
}
}
@Composable
fun RegisterControllerReceiver(controllerState: MediaControllerState) {
val context = LocalContext.current

View File

@@ -67,14 +67,14 @@ object PlaybackServiceClient {
.setConnectionHints(bundle)
.buildAsync()
Log.d("PlaybackService", "Preparing Controller $id $videoUri")
Log.d("PlaybackService") { "Preparing Controller $id $videoUri" }
controllerFuture.addListener(
{
try {
val controller = controllerFuture.get(5, TimeUnit.SECONDS)
Log.d("PlaybackService", "Controller Ready $id $videoUri")
Log.d("PlaybackService") { "Controller Ready $id $videoUri" }
// checks if the player is still active before engaging further
trySend(
@@ -92,11 +92,11 @@ object PlaybackServiceClient {
)
awaitClose {
Log.d("PlaybackService", "Releasing Controller $id $videoUri")
Log.d("PlaybackService") { "Releasing Controller $id $videoUri" }
try {
MediaController.releaseFuture(controllerFuture)
} catch (e: Exception) {
Log.e("Playback Client", "Failed to release Playback Client for $id $videoUri ${e.message}")
Log.e("Playback Client") { "Failed to release Playback Client for $id $videoUri ${e.message}" }
}
}
}

View File

@@ -74,21 +74,29 @@ class RelayProxyClientConnector(
if (it.connectivity is ConnectivityStatus.StartingService) {
// ignore
} else if (it.connectivity is ConnectivityStatus.Off) {
Log.d("ManageRelayServices", "Connectivity Off: Pausing Relay Services ${it.connectivity}")
Log.d("ManageRelayServices") { "Connectivity Off: Pausing Relay Services ${it.connectivity}" }
if (client.isActive()) {
client.disconnect()
}
if (it.torStatus is TorServiceStatus.Active) {
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT)
Log.d("ManageRelayServices", "Pausing Tor Activity")
try {
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT)
Log.d("ManageRelayServices", "Pausing Tor Activity")
} catch (e: Exception) {
Log.e("ManageRelayServices") { "Failed to signal Tor dormant: ${e.message}" }
}
}
} else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) {
Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services")
if (it.torStatus is TorServiceStatus.Active) {
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE)
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM)
Log.d("ManageRelayServices", "Resuming Tor Activity with new nym")
try {
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE)
it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM)
Log.d("ManageRelayServices", "Resuming Tor Activity with new nym")
} catch (e: Exception) {
Log.e("ManageRelayServices") { "Failed to signal Tor active: ${e.message}" }
}
}
// only calls this if the client is not active. Otherwise goes to the else below

View File

@@ -80,7 +80,7 @@ class AuthCoordinator(
if (account == null) return
if (isDebug) {
Log.d("AuthCoordinator", "Watch $account")
Log.d("AuthCoordinator") { "Watch $account" }
}
authWithAccounts.add(account)
@@ -91,7 +91,7 @@ class AuthCoordinator(
if (account == null) return
if (isDebug) {
Log.d("AuthCoordinator", "Unwatch $account")
Log.d("AuthCoordinator") { "Unwatch $account" }
}
authWithAccounts.remove(account)

View File

@@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -68,7 +68,7 @@ abstract class PerUniqueIdEoseManager<T, U : Any>(
open fun newSub(key: T): Subscription =
requestNewSubscription(
object : IRequestListener {
object : SubscriptionListener {
override fun onEose(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,

View File

@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -69,7 +69,7 @@ abstract class PerUserAndFollowListEoseManager<T, U : Any>(
open fun newSub(key: T): Subscription =
requestNewSubscription(
object : IRequestListener {
object : SubscriptionListener {
override fun onEose(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,

View File

@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -67,7 +67,7 @@ abstract class PerUserEoseManager<T>(
open fun newSub(key: T): Subscription =
requestNewSubscription(
object : IRequestListener {
object : SubscriptionListener {
override fun onEose(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,

View File

@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -41,7 +41,7 @@ abstract class SingleSubNoEoseCacheEoseManager<T>(
) : BaseEoseManager<T>(client, allKeys) {
val sub =
requestNewSubscription(
object : IRequestListener {
object : SubscriptionListener {
override fun onEose(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,

View File

@@ -37,8 +37,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
@@ -80,9 +85,15 @@ class RelaySubscriptionsCoordinator(
val hashtags = HashtagFilterAssembler(client)
val geohashes = GeoHashFilterAssembler(client)
val relayFeed = RelayFeedFilterAssembler(client)
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
val followPacks = FollowPackFeedFilterAssembler(client)
val chess = ChessFilterAssembler(client)
val polls = PollsFilterAssembler(client)
val pictures = PicturesFilterAssembler(client)
val shorts = ShortsFilterAssembler(client)
val longs = LongsFilterAssembler(client)
// active when sending zaps via NWC
val nwc = NWCPaymentFilterAssembler(client)
@@ -93,6 +104,10 @@ class RelaySubscriptionsCoordinator(
chatroomList,
video,
discovery,
polls,
pictures,
shorts,
longs,
channelFinder,
eventFinder,
userFinder,
@@ -105,6 +120,7 @@ class RelaySubscriptionsCoordinator(
hashtags,
geohashes,
relayFeed,
relayInfoNip66,
chess,
nwc,
)

View File

@@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.users
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.BundledUpdate
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState
@@ -35,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineT
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -107,7 +106,7 @@ class AccountFollowsLoaderSubAssembler(
val sub =
orchestrator.requestNewSubscription(
if (isDebug) logTag + newSubId() else newSubId(),
object : IRequestListener {
object : SubscriptionListener {
override fun onEose(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,

View File

@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metada
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -43,6 +42,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent

View File

@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -38,6 +37,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent

View File

@@ -24,13 +24,21 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
val ReportsAndBookmarksFromKeyKinds =
listOf(
ReportEvent.KIND,
BookmarkListEvent.KIND,
OldBookmarkListEvent.KIND,
LabeledBookmarkListEvent.KIND,
PinListEvent.KIND,
RequestToVanishEvent.KIND,
)
fun filterBookmarksAndReportsFromKey(

View File

@@ -30,14 +30,16 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent
import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent
import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
val FollowAndMutesFromKeyKinds =
listOf(
PeopleListEvent.KIND,
FollowListEvent.KIND,
MuteListEvent.KIND,
BadgeProfilesEvent.KIND,
AcceptedBadgeSetEvent.KIND,
ProfileBadgesEvent.KIND,
EmojiPackSelectionEvent.KIND,
EphemeralChatListEvent.KIND,
ChannelListEvent.KIND,

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