Commit Graph
17904 Commits
Author SHA1 Message Date
Claude 65e30c0acd fix: keep observeNotes list sorted while deduping addressables by idHex
Address review feedback: keep the incrementally-maintained, created_at-sorted
structure (a feed must stay sorted like a relay) instead of re-sorting a hash
map on every emission.

The root cause is unchanged: there is one Note instance per id/address
(LocalCache owns creation), but a note's sort key is mutable — a newer
replaceable event swaps the event on the SAME AddressableNote instance,
changing created_at in place. A sorted set ordered on that live value corrupts:
the moved node leaves the add()/remove() search path, so the same instance is
inserted twice and the emitted list carries a duplicate idHex, crashing the
App Recommendations LazyColumn (keyed on idHex).

Fix: snapshot the sort key into an immutable Entry when the note first enters,
order a ConcurrentSkipListSet on that snapshot (never read live again), and
index entries by the stable idHex (ConcurrentHashMap + putIfAbsent) so
membership stays unique and removal is reliable regardless of later created_at
changes. Ordering and "new versions do not update the list" are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 18:03:26 +00:00
Claude 0b0abeebbb fix: prevent duplicate LazyColumn key from observeNotes on addressable updates
NoteListMatchingFilter (backing LocalCache.observeNotes) stored notes in a
ConcurrentSkipListSet ordered by CreatedAtIdHexComparator. AddressableNotes are
mutable: when a newer replaceable event arrives, LocalCache swaps the event on
the SAME note instance (consumeBaseReplaceable -> loadEvent), changing its
createdAt in place, then re-notifies observers. A sorted set cannot survive a
member's sort key mutating underneath it — the moved node is no longer found on
the add() search path, so the same note gets inserted a second time and the
emitted list carries a duplicate idHex.

The App Recommendations screen keys its LazyColumn on note.idHex (an
AddressableNote's address, e.g. 31990:<pubkey>:nostr-dvm-labeler), so the
duplicate crashed with IllegalArgumentException: "Key ... was already used".

Dedupe by the immutable idHex instead of a createdAt-ordered set; ordering is
computed fresh on each emission. Adds a regression test reproducing the
multi-item corruption path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 17:23:50 +00:00
Vitor PamplonaandGitHub 84a96f1f86 Merge pull request #3763 from vitorpamplona/claude/fix-localcache-dropdown-warnings-y4952o
Refactor parameter names for clarity and update Material3 API
2026-07-28 11:50:54 -04:00
Claude 3ffa259cff fix: resolve LocalCache override, dropdown deprecation, and shadowed extension warnings
- Rename LocalCache Dao overrides to match supertype parameter names
  (getOrCreateUser: pubkey->hex, getOrCreateNote: idHex->hex,
  getOrCreateAddressableNote: key->address) so named-argument calls stay safe.
- Replace deprecated MenuAnchorType with ExposedDropdownMenuAnchorType in the
  Buzz dropdown composables.
- Remove the buzzChannelType() extension in BuzzChannelMetadata, now shadowed by
  the equivalent (stricter) member on GroupMetadataEvent, and drop its unused
  imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VBP6HGj9M2kzA4WKAivjC
2026-07-28 15:37:01 +00:00
Vitor Pamplona 775c76621a v1.13.0 v1.13.0 2026-07-28 10:52:38 -04:00
Vitor Pamplona 9497510733 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-07-28 10:46:18 -04:00
Vitor Pamplona 5424a62f93 Fixes the order of settings 2026-07-28 10:41:49 -04:00
David KasparandGitHub 2cbf57fe79 Merge pull request #3761 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-28 16:41:30 +02:00
vitorpamplonaandgithub-actions[bot] b8a9c0242c chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 14:38:25 +00:00
Vitor PamplonaandGitHub 414d3fdda0 Merge pull request #3762 from vitorpamplona/claude/bottom-nav-settings-move-fsytta
Reorganize settings catalog: move UI settings to dedicated section
2026-07-28 10:35:31 -04:00
Claude a0202618c1 refactor: move per-account setting entries into Account Settings
Move the bottom navigation bar entry out of App Settings and into the
Account Settings section of the All Settings catalog, since its state is
stored per account (AccountNavigationPreferencesInternal.bottomBarItems in
the account-synced settings blob).

While auditing the App Settings section for other per-account entries, two
more were purely per-account and moved alongside it:

- Reactions settings (reaction row actions) writes account-synced
  reactionRowItems and now sits next to the existing "Reactions" entry.
- Messages settings writes account-scoped chat feed toggles and view modes
  (account.settings.enabledChatFeeds / relayGroupViewMode / concordViewMode).

The remaining App Settings entries stay put: UI preferences, Home tabs,
Profile UI, Calendar reminder, OTS, Namecoin and Resource usage are all
device-global, and Compose/Notification settings are mixed (they hold
genuine app-global preferences alongside a few per-account toggles).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH88gCbbdTfdzp9wrr7buJ
2026-07-28 14:19:19 +00:00
Vitor PamplonaandGitHub 26a6cd37fb Merge pull request #3759 from davotoula/fix/playback-error-overlay-starves-browser-button
Keep the "open in browser" button alive in a short error box
2026-07-28 09:12:59 -04:00
David KasparandGitHub 93fd107ecf Merge pull request #3760 from davotoula/fix/workout-combined-sessions-plurals
Make the combined-sessions count a plurals resource
2026-07-28 10:33:11 +02:00
davotoula 416ac20f30 fix(workouts): make the combined-sessions count a plurals resource 2026-07-28 10:28:48 +02:00
David KasparandGitHub 58c88797f1 Merge pull request #3758 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-28 10:26:17 +02:00
davotoulaandgithub-actions[bot] e149f935e1 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 08:08:23 +00:00
davotoula c89382f767 update cs,sv,de,pt 2026-07-28 09:54:50 +02:00
davotoula 0b1c4894a0 Code review:
- scale the icon threshold so it can't slice the title
- make the button's survival structural, not threshold-tuned
2026-07-28 09:47:11 +02:00
davotoula 86d0434628 fix(video): keep the "open in browser" button alive in a short error box 2026-07-28 09:46:45 +02:00
Vitor PamplonaandGitHub 011d5f2fa5 Merge pull request #3757 from vitorpamplona/claude/nip84-highlight-marker
feat(highlights): render NIP-84 quotes with a highlighter-pen marker
2026-07-27 22:48:01 -04:00
Vitor PamplonaandClaude Opus 5 7d13f373ac feat(highlights): keep auto-translation on the marked quote
Replacing the markdown pipeline dropped TranslatableRichTextViewer, and
with it the ML Kit auto-translation of the quoted passage. Restore it
without giving up the marker.

A translation rewrites the passage, so the character offsets that locate
the quote inside its context stop pointing at anything. Translate the
quote as well and re-find it in the translated context: ML Kit works
sentence by sentence, so a quote that is one or more whole sentences --
the common case, since people highlight sentences -- comes back identical
whether it is translated alone or inside its paragraph.

  untranslated             -> context, original span marked
  translated + quote found -> translated context, translated span marked
  translated + not found   -> translated quote alone, fully marked

The fallback is free: HighlightQuote.of already returns the quote alone
when the needle is not in the haystack, so the not-found case needs no
special casing. Marking a guessed span, or claiming the whole paragraph
was highlighted, would both be worse than showing less.

That second translation must not draw its own status bar, so add
rememberTranslation() to both flavors: play reuses the existing
translateAndCache and its cache; fdroid, which ships no translation
service, returns the content unchanged.

Also indent the "Auto-translated from X to Y" line to the quote's own
15dp so it lines up with the text rather than the bar.

Verified on device: an English highlight renders as a fully translated
Portuguese paragraph with the quoted sentence marked inside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:39:57 -04:00
Vitor PamplonaandClaude Opus 5 057623e3ab feat(highlights): render NIP-84 quotes with a highlighter-pen marker
The renderer never drew a highlight. It synthesised a markdown string --
blockquote each line with "> ", wrap the quoted span in "**" -- and handed
it to the rich-text viewer, so a highlight arrived as bold text. That also
meant the quoted article prose was parsed as markdown, so any *, _, # or [
in it was interpreted as formatting rather than shown.

Drop the markdown round-trip and paint the marker behind the glyphs. The
stroke is drawn per visual line from the TextLayoutResult, so it follows
soft wraps and stops at real glyph edges. Per-line rounded rects rather
than SpanStyle(background), which can only ever be a hard full-line-height
rectangle -- that is what buys the rounded pen ends.

Size the stroke from the baseline and font size, not the line box, so
leading and stroke weight stay independent knobs.

Along the way:

- Locate the quote as an index range instead of context.replace(), which
  marked every occurrence when a quote repeated. Use the W3C
  TextQuoteSelector prefix -- already on the event, previously ignored --
  to disambiguate.
- Restore 1.35em leading. The markdown path forced 1.5em via
  MarkdownTextStyle; the ambient bodyLarge sets no lineHeight at all, so
  rendering plain text inherited the font's intrinsic ~1.2em.
- Indent the source attribution by the quote's own 15.dp so it lines up
  with the text rather than the bar, and space the comment, quote and
  attribution 8.dp apart -- they were flush at 0.dp.
- Clamp the stroke to the column so it cannot be clipped on full-width
  lines.

Light keeps a near-opaque yellow with dark glyphs reading through it. Dark
cannot do that, so it gets a translucent amber that glows rather than
covers. Not derived from the user's accent: a highlighter reads as yellow.

The quoted passage no longer routes through TranslatableRichTextViewer, so
it loses its auto-translate affordance; drawing the marker requires owning
the text layout. The author's own comment above the quote keeps it.

Verified on device in both themes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:03:28 -04:00
Vitor PamplonaandGitHub 329bfe95e7 Merge pull request #3752 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 21:40:02 -04:00
vitorpamplonaandgithub-actions[bot] 99f0443a98 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 01:37:22 +00:00
Vitor PamplonaandGitHub 9f7056165d Merge pull request #3755 from vitorpamplona/claude/bottom-nav-per-user-s5h6p4
Move bottom-bar persistence from app-global to per-account synced settings
2026-07-27 21:34:48 -04:00
Vitor PamplonaandGitHub 918cec6bf8 Merge pull request #3756 from vitorpamplona/claude/notification-settings-account-display-oz5ujd
Display user profiles in notification settings account list
2026-07-27 21:34:26 -04:00
Vitor PamplonaandGitHub d62a94f2eb Merge pull request #3753 from vitorpamplona/claude/changelog-1.13-audit
docs(changelog): tighten v1.13.0 highlights and audit against commits since v1.12.6
2026-07-27 20:02:46 -04:00
Claude 586346a68f fix: keep bottom-bar edits ordered to prevent a stale revert
Persisting a bottom-bar edit went through `launchSigner { account.change... }`,
which dispatches on a multi-threaded pool. Because the reactive StateFlow emit
happened inside that coroutine, two quick edits (e.g. tapping Add on several
catalog rows) could complete out of order: the older list would win the flow,
and the settings screen — which re-seeds its editable list from the flow via
`LaunchedEffect(savedItems) { syncFrom(...) }` — would visibly revert the newer
edit and publish the stale list in the NIP-78 event.

Apply the change to the in-memory flow synchronously on the caller (UI) thread
via `Account.applyBottomBarItems` (a non-suspending emit + local save, both
non-blocking) so rapid edits stay strictly ordered, and run only the
sign/encrypt/publish off-thread. Restores the ordering guarantee the previous
synchronous `tryEmit` had.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP
2026-07-27 23:58:13 +00:00
Claude 39c05fb538 refactor: use shared UserPicture/UsernameDisplay in notification settings
Replace the hand-rolled RobohashFallbackAsyncImage + observeUserInfo +
CreateTextWithEmoji in the background-accounts list with the app's standard
user components: LoadUser resolves the User behind each npub, and
ClickableUserPicture / UsernameDisplay render the picture and name. This
reuses the shared metadata observation, robohash fallback, custom-emoji
rendering, per-user nickname handling, and npub fallback instead of
duplicating that logic here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvB5t1ittHpw4PdJQYZYfp
2026-07-27 23:55:13 +00:00
Claude 7a41e21272 feat: make bottom navigation bar per-account via NIP-78
The bottom nav row configuration was an app-global setting stored in the
shared DataStore, so every account shared one bar. Move it into the
per-user NIP-78 app-specific data event (AppSpecificDataEvent) so each
account keeps its own bar and it syncs across the user's devices.

- Add `navigation.bottomBarItems` to AccountSyncedSettingsInternal (the
  serialized/encrypted synced-settings blob) and mirror it as a StateFlow
  in AccountSyncedSettings (seed / toInternal / updateFrom).
- Add AccountSettings.changeBottomBarItems, Account.changeBottomBarItems
  (republishes the NIP-78 event), and AccountViewModel.changeBottomBarItems
  / bottomBarItemsFlow().
- Remove bottomBarItems from the app-global UiSettings / UiSettingsFlow /
  UiSharedPreferences (including the now-unused encode/decode migration
  helpers).
- Point the live bar, navigation rail, preloaders, subscriptions and the
  Bottom Bar settings screen at the per-account flow.

No migration from the previous app-global setting: accounts start from the
default bar, matching the requested behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP
2026-07-27 23:30:17 +00:00
Vitor PamplonaandGitHub a9b22a927f Merge pull request #3754 from vitorpamplona/claude/finduserstest-metadata-failure-24a9ck
Fix flaky FindUsersTest by retaining users in cache
2026-07-27 19:28:27 -04:00
Claude 5be70e3a86 feat: show account picture and name in notification settings
The background-accounts list in the Notification Settings screen showed
each account by its npub only. Resolve the User behind each account and
observe its live metadata so the row displays the profile picture and
best display name (with the npub as a secondary line), falling back to
the short npub while metadata loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvB5t1ittHpw4PdJQYZYfp
2026-07-27 23:23:20 +00:00
Vitor Pamplona 70646f4609 adds gigi to changelog 2026-07-27 19:05:55 -04:00
Vitor Pamplona 8a587da45a updates dependencies 2026-07-27 19:05:45 -04:00
Claude 2d0ccb4d70 fix: stop FindUsersTest flaking when GC evicts weakly-cached users
DesktopLocalCache stores users in LargeSoftCache, which holds each User
via a WeakReference. FindUsersTest populated the cache but kept no strong
reference to the created users, so a GC between consumeMetadata and
findUsersStartingWith could collect them — LargeSoftCache.forEach then
skips (and evicts) the cleared entries, making the search return fewer
results. multipleUsersWithMetadata allocates the most objects and tripped
this most often (AssertionError at FindUsersTest.kt:115).

Verified with a forced-GC probe: without a strong reference all three
users were evicted (found=0); holding a reference kept all three (found=3).

Pin each test's users in a strong-reference list that stays reachable
through the assertions, mirroring how Notes/Account/follow lists keep
authors alive in the real app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X4BThbs8tz9egmFDYv2RPB
2026-07-27 23:04:04 +00:00
Vitor PamplonaandClaude Opus 5 f55b3cb6d9 docs(changelog): tighten v1.13.0 highlights and audit against commits
Highlights carried full feature descriptions that the sections below
already repeated. Reduce each to a single line and fold the detail down
into the section that owns it.

Audit the notes against all 1961 non-merge commits since v1.12.6:

- Drop the claim that WebSocket frame dispatch moved to a dedicated pool.
  It landed in a5c2a8b2c1's parent and was reverted 23 minutes later
  because the pool regressed. Replace it with the two receive-path wins
  that did ship (CachingEventDecoder, ParallelEventVerifier).
- Add the chat redesign, which had no section at all: bubbles,
  swipe-to-reply, name colors, jumbo emoji, day headers, the two-stage
  long-press sheet, and the new-conversation chooser.
- Add in-app podcast authoring, which the Podcasts section omitted in
  favour of consumption only.
- Promote the resource-usage ledger out of a single Wallet line into its
  own section, alongside the background-service master switch and
  memory-pressure trimming.
- Add large-screen support, NIP-85 nicknames, Birdstar and PS1 cards,
  compose signature, Marmot group icons, and other unreferenced work.

Remove the Upgrading section. No prior release has one, and all three of
its items were consequences of features documented further down, so they
now sit with the feature that causes them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:03:09 -04:00
Vitor PamplonaandGitHub 2beb75b59e Merge pull request #3751 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 18:23:37 -04:00
vitorpamplonaandgithub-actions[bot] a4ec34a96d chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 21:13:25 +00:00
Vitor PamplonaandGitHub 36792d09b1 Merge pull request #3750 from vitorpamplona/claude/update-changelog-3mwv4t
docs: update v1.13.00 changelog with new features
2026-07-27 17:10:10 -04:00
Claude 9df51641ea docs(changelog): split v1.13.0 fold-in notes into shorter sentences
Break the newly added BOLT12, Buzz Agent Work board, Blossom, Cashu,
NWC, search-indexing, and geode bullets into short verb-first sentences,
removing em-dash run-ons to match the changelog house style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hmbpUJPxrsN4m85Kwemcw
2026-07-27 21:06:24 +00:00
Claude 6c1412b7c9 docs(changelog): fold post-2026-07-24 work into v1.13.0 notes
Adds the BOLT12 payments & zaps (NIP-B1) work, the Buzz Agent Work
board / workflow runner, Concord message editing, the Blossom gallery
+ importer, Cashu P2PK-token claiming, NWC NIP-44/deep-link/notify
improvements, wider NIP-50 search indexing, and the geode release
pipeline + pluggable event store, plus assorted fixes. Credits dergigi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hmbpUJPxrsN4m85Kwemcw
2026-07-27 20:55:39 +00:00
Vitor PamplonaandGitHub 37759cf517 Merge pull request #3744 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-27 16:45:55 -04:00
vitorpamplonaandgithub-actions[bot] 1c581eb07a chore: sync Crowdin translations and seed translator npub placeholders 2026-07-27 20:22:29 +00:00
Vitor PamplonaandGitHub 0b73236e76 Merge pull request #3749 from vitorpamplona/claude/buzz-event-p-tags-7ud8bj
Resolve @mentions in message bodies to p-tags across all chat types
2026-07-27 16:19:46 -04:00
Claude 89add9d053 refactor(chat): require an explicit Dao on NewMessageTagger
Drop the `dao = LocalCache` default; the two callers without an
AccountViewModel now pass `dao = LocalCache` explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:17:07 +00:00
Claude 8283b22c2f refactor(chat): let LocalCache implement Dao directly
LocalCache already has getOrCreateUser/getOrCreateNote/getOrCreateAddressableNote,
so make it implement Dao and default NewMessageTagger's `dao` to LocalCache
itself — no wrapper object, no interface-default methods. Dao drops the
`suspend` modifiers (LocalCache's lookups are synchronous) so the object
satisfies the interface; AccountViewModel's delegating overrides follow suit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:13:05 +00:00
Claude 3562a62e0a refactor(chat): keep Dao abstract, implement LocalCache lookups in impls
Move the LocalCache calls off the Dao interface (back to a pure abstract
contract) and into the implementations: AccountViewModel keeps its own
overrides, and the default Dao used by callers without a ViewModel is an
anonymous implementation on NewMessageTagger's `dao` parameter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 20:04:25 +00:00
Claude b4da12300e refactor(chat): fold LocalCache Dao defaults into the Dao interface
Replace the standalone LocalCacheDao object with default method
implementations on the Dao interface itself, backed by LocalCache. The
NewMessageTagger `dao` parameter now defaults to a bare `object : Dao {}`,
so callers with no AccountViewModel (model-layer sends, the notification
receiver) just omit it, while UI callers keep passing their AccountViewModel
(which resolves to the same LocalCache calls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:57:09 +00:00
Claude d305c03e54 feat(chat): resolve mentions in minichat and notification quick-replies
The lightweight reply paths built their events from raw text, so a person
cited with `@name`/`nostr:` was neither resolved into a `nostr:` reference
nor tagged with `p`. Run NewMessageTagger on these paths too, backed by a
new LocalCache-based Dao so they work without an AccountViewModel:

- New `LocalCacheDao`: a `Dao` delegating to `LocalCache`, for mention
  resolution outside a ViewModel (model-layer sends, background receivers).
- `Account.sendMinichatReply` (kinds 9 + 1111): resolve mentions and emit
  `p` tags for cited users across the public-chat, Buzz, and NIP-29 group
  branches (parent author excluded to avoid a duplicate).
- `NotificationReplyReceiver.sendPublicReply` (kinds 1 + 1111): same
  resolution for the system notification quick-reply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:46:00 +00:00
Claude 11147aac10 fix(chat): add p tags for people cited in kind 9/11/1111 messages
Extends the mention p-tag fix to the shared chat/thread composers, which
resolved `@`/`nostr:` mentions via NewMessageTagger but then dropped the
cited users from the signed event:

- kind 9 (ChatEvent, NIP-29 group chat): the reply path tagged only the
  parent author and the plain build path tagged no one. Emit `p` tags for
  every cited body user (reply path keeps the parent's relay-hinted tag and
  excludes it from the extra set to avoid a duplicate).
- kind 1111 (CommentEvent) minichat reply: replyBuilder auto-tags the
  parent/root author but body mentions were dropped. Notify the other
  cited users (parent excluded to avoid a duplicate).
- kind 11 (ThreadEvent, NIP-29 group thread): the ShortNote thread branch
  added no `p` tags at all; emit them from the cited body users, matching
  the sibling Poll/ZapPoll branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcWSCNMTVcvKMXo7xA2hue
2026-07-27 19:36:57 +00:00