The parent message shown above a reply in the MessagingStyle notification was
unconditionally labeled "Me" with the account's avatar. That's wrong whenever
the account is notified as the *root* author of a thread but the direct parent
belongs to someone else — e.g. Vitor starts a thread, fiatjaf replies, a third
person replies to fiatjaf: fiatjaf's note was rendered as Vitor. This surfaced
through the NIP-22 comment and public-chat reply paths, which notify on
root-authorship, not just direct-parent authorship.
ReplyNotification now receives the parent Note (not a bare content string) and
resolves its actual author: attributed to the MessagingStyle `me` Person only
when the parent truly is the account's, otherwise shown as the real author with
their name + avatar, observed for enrichment like the replier. postConversation
reuses the `me` Person for a self-authored parent so it still renders as you.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
When a mention/article body contains an inline `http(s)` image link, show the
image as the notification's big picture (BigPictureStyle) instead of leaving the
raw URL in the text. NotificationContent.renderNoteText() now pulls the first
image URL out of the content — scanning all lines, since images usually sit on
their own line — using the cheap RichTextParser.isImageUrl extension check, and
strips that link from the excerpt. Videos are left in the text (Coil can't load
them as a still). The Mention and Article renderers pass the extracted URL as
bigPictureUrl; Reply stays MessagingStyle (a big picture doesn't fit a chat
bubble) and keeps its text as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Notification bodies previously showed raw `nostr:npub1…` / `nostr:nprofile1…`
tokens for anyone cited in the text — the name never filled in even after the
cited user's kind:0 loaded, because the excerpt was a static substring taken
once and never re-resolved against the metadata cache.
Add NotificationContent.resolveMentions(), which rewrites each cited npub/
nprofile token to `@<best display name>` and returns the cited Users so the
renderer can add them to its enrichment window. Event references (nevent/note/
naddr) are left verbatim — they have no name to show. Mention, Reply, Media and
Article renderers now recompute the body inside the observable build closure and
observe the cited users, so the text flips from `@npub1abc…` to `@RealName` in
place, matching the author-name enrichment the title already had.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Concord fetched a channel's messages only when its screen was open (the history
pager mounts on the channel screen), so an un-opened channel showed "No messages
yet" in the community list and never appeared in the Messages inbox — unlike
NIP-28 / NIP-29, which preload a last-message per room. Add a one-shot warm
drain, `Account.warmConcordChannelPreviews`, triggered on community-screen open
and app-wide per subscribed community from the account preload (both debounced
so the cold-boot fold burst warms once).
Per channel (`ConcordSubscriptionPlanner.channelPreviewFilters`):
- never read -> the newest `previewLimit` (10) wraps: a preview plus a rough
sense of how busy the channel is, without pulling the whole backlog.
- read -> everything `since lastRead - 1` (capped at `catchUpLimit`): the unread
badge is accurate and the missed messages are cached for on-open; the `-1`
re-includes the last-read message (its created_at == lastRead) so a caught-up
channel still shows a preview, and unread stays exact (the count is strict `>`).
Filters group by relay into one REQ per relay (one filter per channel); the
wraps ingest through the normal cache path, and the always-on plane subscription
keeps them fresh afterward. Full history still pages in on open.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A public channel carries no delivered key, so a writer lists it under an
entry's `channels` with only {id, epoch, name}. `WireChannel.key` was a
required field, so kotlinx.serialization threw MissingFieldException — and
`decodeDocument`'s catch-all turned that one bad channel into an empty list,
silently dropping EVERY joined community from the kind-13302 list (communities
"won't load" at all).
- Default `WireChannel.key = ""` so a keyless (public) channel no longer throws.
- Decode entries one at a time and keep any we still can't parse verbatim in
`ConcordListResidue.unparsedEntries`, re-emitted on write — so one malformed
entry can never wipe the whole list, and a read-modify-write never deletes a
membership this version can't model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- run.sh: add a Buzz-style bare reaction check (kind-7 with an `e` tag but no `p`
tag) asserting it still lands on the Reactions channel, and a note that Buzz DM
isn't auto-triggered.
- README: coverage row for the bare reaction, plus a "Buzz DM (manual)" section
with the channel-setup steps (kind-39000 `t=dm` + participant, then post
kind-9/40002) since participant-routing needs cached channel metadata.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Buzz DM messages (NIP-29 relay-group StreamMessageV2Event kind 40002 / ChatEvent
kind 9 in a `t=dm` channel) carry no `p` tag — participation is the relevance
signal — so main added them to the in-app feed only. This wires them into push:
- BuzzDmNotification renderer: MessagingStyle on the Private Messages channel,
channel name as the conversation title, "sender: text" body, sender avatar,
deep-links to the relay-group chatroom via the channel's kind-39000 naddr
(reusing the existing naddr → Route.RelayGroup path). Honors the
"show messages in notifications" toggle and enriches the sender observably.
- Dispatcher: adds kinds 40002/9 to NOTIFICATION_KINDS and gates them
EXCLUSIVELY on Buzz-DM membership (buzzDmChannelForMe) so ordinary kind-9
chats (Concord / NIP-C7) don't leak into the tray.
- NotificationRoutes.relayGroupUri for the channel deep-link.
Also closes a mute-parity gap the audit surfaced: the push muted-thread check
covered Reaction/LnZap but not Repost/GenericRepost (the feed mutes all four) —
so a bare Buzz repost of your note on a muted thread now stays muted in push too.
Buzz reactions/reposts (bare no-`p`-tag likes) already route to the existing
Reaction/Repost renderers, which are correct for Buzz (plain kind-7/6/16, same
target resolution and emoji semantics) — verified, no change needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
A Buzz like is a bare ["e", <id>] reaction with no p tag, so the push gate's
(isTaggedUser || publicChatReply) pre-clause rejected it even though
tagsAnEventByUser already recognizes a reaction/repost whose reacted note is
mine. Relax the pre-clause for reaction/repost kinds; tagsAnEventByUser keeps it
scoped to reactions on my own note. Reuses the existing Reaction/Repost
renderers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Cancelling a proof-of-work mining job used to silently discard the post
(the sign+broadcast continuation only ran on a successfully mined
template). Users had no way to publish the note un-mined once they'd
started waiting.
Now abandoning a template post asks what to do instead of discarding:
- The × on the mining banner opens a dialog with "Send without PoW",
"Discard post", or tap-away to keep mining. Only shown for jobs that
carry a plain un-mined fallback (template posts); opaque work jobs
(reactions, reposts, anonymous posts, gift wraps) keep the direct
cancel since they have no template to fall back to.
- The mining foreground-service notification gains a "Send now" action
that publishes every eligible queued post without proof of work.
Implementation: the queue keeps the un-mined publish continuation
alongside the miner. sendWithoutPow() sets a flag the worker picks up on
its next isActive poll; the miner aborts and the plain template is
published through the same sign+broadcast path the mined template would
have used, off the worker pool. PoWJobState exposes canSendWithoutPow so
the UI knows which jobs support it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012kLVFV7ps4HfXDDJPNqi82
Kotlin/Native marks the stdlib `assert()` with `@ExperimentalNativeApi`, so
the iOS test target (`compileTestKotlinIosSimulatorArm64`) failed to compile
without an opt-in — while JVM/Android were fine. Swap it for `assertTrue`
from `kotlin.test`, which needs no opt-in on any target and matches the
assertions already used in this file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
The two workspace-owner actions (Add people to this workspace, Create invite
link) sat as an inline button row above the channel list. Move them into a
3-dot overflow (MoreVert) menu in the top-right of the workspace top bar,
matching the Concord community pattern. Fold the invite-mint flow (spinner,
result Copy/Share dialog, error dialog) into the new BuzzWorkspaceOverflowMenu
and drop the now-obsolete BuzzInviteMintButton.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
A Buzz like is a bare kind-7 `["e", <msg-id>]` — no `p` tag (unlike NIP-25,
which p-tags the reacted author) and no `h` tag. The notifications filter's
p-tag gate and follow filter therefore both dropped it, so a like on my Buzz
message never surfaced even in Global mode, despite the reaction being loaded
(it rendered as a chip in the open conversation).
Recognize a reaction/repost that carries NO `p` tag whose reacted target — the
last `e` tag, already loaded — is my own note, and let it bypass the follow
filter and satisfy the p-tag gate (like a Concord reaction). Scoped to the
no-`p`-tag case so well-formed NIP-25 reactions keep their normal routing, and
to an already-loaded target so it can't accept blindly. The per-kind gate
already resolves the same target author, so it needs no change.
Verified on emulator: a 👍 + ❤️ on my Buzz DM message now appears on the
Curated notifications tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The group-notification query (reactions/zaps/reposts on my messages inside a
NIP-29 group, kind-7 etc. scoped #p=me + #h on the host relay) sourced its #h
set from the published kind-10009 group list — which deliberately excludes Buzz
DM channels (membership is server-side, tracked in BuzzDmChannels). So a
reaction on my DM message was only ever loaded as a chip inside the open
conversation, never surfacing on the Notifications tab.
Include DM channels (from BuzzDmChannels, minus hidden ones) alongside the
joined groups in both the live-tail (AccountNotificationsEoseFromInboxRelays-
Manager) and backward-history (AccountNotificationsHistoryEoseManager) queries,
reusing the same filterGroupNotificationsToPubkey builder, and re-subscribe when
a DM is discovered/hidden. Regular-channel reactions already worked via the
joined-group path; this closes the DM gap the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs(dm): note the profile Reports tab also reads reportsNamingUser
- refactor(dm): push report-tag typing to quartz and simplify the warning stack
- fix(dm): narrow report indexing and address final review findings
- perf(dm): resolve a 1:1 chat row's counterpart once per row
fix(dm): dedupe reporter avatars and align warning card styling
feat(dm): warn in the room when the counterpart is reported by a follow
feat(dm): expose a report-warning flow per user
refactor(reports): extract reusable reportTypeLabel composable
feat(reports): index author-named reports even when they target an event
feat(reports): add pure DM report-warning classifier
feat(reports): add additive reportsNamingUser index to UserReportCache
Batch of correctness, performance, and code-quality fixes across the Buzz
workspace feature surfaced by a full audit of the branch's modified files:
- ChannelNewMessageViewModel: agent auto-invite ran inside createTemplate(),
which sendDraftSync() calls per keystroke — so @mentioning a member
published real kind-9000 invites while still typing. Move the invite to the
send path (sendPostSync) and stage the pending mentions instead.
- AgentConsoleViewModel: fix a decryptCache read/reload race by guarding
reloadFromCache (not refresh) under the mutex; bound observerSeen growth.
- BuzzNewDmViewModel: broaden start()'s catch so signer/IO/timeout failures
surface as an error instead of leaving Start stuck on "Sending"; rethrow
CancellationException.
- BuzzPresenceState / BuzzAgentActivityState: replace per-record full-map
copies with persistent maps (structural sharing) to cut GC churn; the
StateFlow now holds the map directly.
- RelayGroupChannelListScreen / RelayGroupMembersScreen / RelayGroupTopBar /
RenderBuzzNotes: correct remember/LaunchedEffect keys so state rebinds when
the relay or channel changes.
- RelayGroupDiscoveryFeedFilter: hoist joinedGroupIds() out of the per-item
matches() loop.
- RelayGroupChannel / BuzzWorkspaceStates: @Volatile the fields read off relay
dispatcher threads.
- BuzzInviteMinter: build the request URL through HttpUrl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Follow-up to the Buzz-DMs-in-Notifications work: a Buzz DM on the Notifications
tab rendered like a public post and its actions didn't fit a DM.
- Render as a conversation: new RenderRelayGroupMessage draws a
RelayGroupChannelHeader (titled by the other participant for a DM) above the
message body — the group/DM analog of RenderChatMessage/RenderChannelMessage.
NoteCompose routes a group-scoped kind-9 (ChatEvent with a RelayGroupChannel
gatherer) and kind-40002 (StreamMessageV2Event) to it. The in-conversation
chat screen renders via RefreshingChatroomFeedView, not NoteCompose, so its
bubbles are unaffected.
- Reply goes into the conversation, not a public kind:1111: routeReplyTo now
detects the gathering RelayGroupChannel (mirroring the Marmot/Concord cases)
and opens the group/DM instead of falling through to Route.GenericCommentPost.
- Action row matches the card: a relay-group message (incl. a Buzz DM) hides
Repost and Share — both would reference a membership-gated group event that
non-members can't fetch, and a DM shouldn't be rebroadcast. Reply/like/zap stay.
Verified on emulator: the DM card shows the participant header + body + a
reply/like/zap-only row, and tapping reply opens the DM conversation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Buzz DM is a relay-authoritative NIP-29 group whose messages carry no `p`
tag, so nothing made them eligible for the Notifications tab and nothing
fetched them app-wide (discovery was scoped to the open DM inbox, which only
pulls 44100 + 39000 — never the message bodies). Two halves fix that:
- NotificationFeedFilter now early-accepts a group chat message (kind-9 or
kind-40002 — the deployed relay uses both) when it resolves to a `t=dm`
channel whose 39000 participants include me, honoring the same "Messages in
notifications" toggle and never notifying for my own message. LocalCache
gains `getRelayGroupChannelForContent`, the read-only reverse-lookup this
needs (same serving-relay-then-single-channel keying as the consume path).
- An always-on discovery (BuzzDmDiscoveryPreload) subscribes 44100 #p=me across
joined workspaces into the new BuzzDmChannels registry and fetches each DM's
39000 directory; BuzzDmJoinedChatTailFilterAssembler then keeps those
channels' recent messages warm app-wide (reusing the joined-group #h tail),
excluding hidden DMs. Both mount in LoggedInPage. This is what makes a Buzz
DM show on Notifications / in push without opening the conversation.
Tests: BuzzDmChannels registry; and a LocalCache resolution test proving a
40002 and a kind-9 message both resolve back to their DM channel (and a
non-dm channel does not).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three owner/admin add-people paths (the relay enforces the role gate):
- Channel add-member: an "Add member" FAB on the channel members screen
opens a user-search dialog that publishes a NIP-29 kind-9000 put-user for
the picked pubkey (plain member).
- Community add-member: an "Add people to this workspace" action in the Buzz
community view publishes the Buzz relay-admin add-member command (kind
9030) so the relay updates its NIP-43 membership list (13534). Wires the
existing quartz RelayAdminAddMember/RemoveMember events into Account +
AccountViewModel (addCommunityMember / removeCommunityMember).
- Invite-link mint: BuzzInviteMinter does the Buzz-specific, NIP-98-signed
POST /api/invites on the relay host (via HTTPAuthorizationEvent + the
trusted-relay OkHttp client) and returns {code, url, expires_at}; a
"Create invite link" button surfaces the link with Copy / Share.
The user search is extracted into a reusable BuzzAddPeopleDialog shared by
the channel and community add flows. /api/invites is Buzz-proprietary (only
NIP-98 is standard); community add uses Buzz's 9030 admin command, since
NIP-43 itself has no owner-initiated add.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
The home feed always emitted a live-bubbles item followed by a 5dp
Spacer, even when the live section (live streams, geohash/ephemeral
chats) was empty — which is the common case. With no bubbles to draw,
that Spacer reserved dead vertical space at the very top of the list,
stacking on top of the LazyColumn's FeedPadding and the first note's own
top padding. The result was an oversized gap between the top-bar divider
and the first author's picture, most noticeable when the New Threads /
Replies tab row is hidden (single tab) so the divider sits right under
the top bar.
Move the row and its trailing Spacer inside a non-empty check so they
only render when there are actual bubbles. When the live section is
empty the item now collapses to zero height, bringing the home feed's
top gap in line with every other feed screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTFiQykXfCShHUCA3yM2pN
Give the donation card a bolder, more exciting look:
- Add a gradient hero header (bitcoin orange → amethyst purple) with a
glowing circular bolt badge and a larger, white title
- Round the card corners (16dp) and add elevation for a modern, floating feel
- Overlay the close button on the hero and keep the description, version
credit, zap splits and donate button in a clean body section
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bM2CKE5Mnao4FG85HPuAV
An agent-runnable script that drives the whole push-notification pipeline on a
real device/emulator: a second identity publishes each notification kind through
`amy` to the account on the phone, and the script reads back the notification
shade over `adb` to assert the right notification on the right channel — then
exercises cold-push metadata enrichment (title flips from raw pubkey to display
name in place) and dismiss-on-read (opening the note clears the tray entry).
Covers mention, reply, reaction, repost, picture, and DM; prints PASS/WARN/FAIL
and exits non-zero on any hard failure. tools/notification-e2e/{run.sh,README.md}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Concord threads chat messages with kind-1111 minichat comments, but Buzz relays
reject kind-1111 — so Buzz replies fell back to inline 40002 and rendered as flat
quotes, never opening a thread.
Buzz already threads over 40002 NIP-10 markers (matching block/buzz): a reply-marked,
non-`broadcast` 40002 is a thread reply; a `broadcast=1` one is an inline timeline
sibling. Teach Amethyst's minichat to speak that dialect:
- Add `isMinichatReply(event)`: kind-1111 CommentEvent, OR a Buzz 40002 thread reply
(reply-marked, non-broadcast). Used by all three read sites so they agree.
- `LocalCache.computeReplyTo`: link a 40002 thread reply into its parent's replies.
- Timeline filter, minichat reply-count, and minichat feed now key on that predicate,
so 40002 thread replies leave the timeline, get counted on the "N replies" chip, and
render inside the minichat.
- Send: an INLINE reply on Buzz sets `broadcast=1` (stays inline); MINICHAT omits it
(threads). `sendMinichatReply` posts a 40002 thread reply on Buzz instead of kind-1111.
Verified on device: a message shows an "N replies" chip, opens the Thread view with the
root + replies + composer, replies post and increment the count, and thread replies no
longer appear inline in the main chat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NIP-64 chess is debug-only for now (see the isDebug gate on the drawer entry),
so its notifications and settings channel shouldn't appear in release.
- ChessNotification.notify early-returns when !isDebug.
- NotificationChannels omits the Chess channel from the settings list (and thus
never creates it) in release builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
The Relay Groups discovery feed listed every kind-39000, including Buzz DM channels
— private 1:1 conversations that belong in the DM section, not a browsable group
list. Exclude channels whose 39000 carries the NIP-29 `hidden` tag (which Buzz sets
on t=dm channels) from both the constraint match and the "My Groups" path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deep-audit follow-up on the per-kind notification rework. Fixes two correctness
bugs and cuts the enrichment path's CPU/battery/IPC cost.
Correctness:
- Cold-push loss: the immediate notification post ran on a detached
applicationIOScope coroutine OUTSIDE any wakelock (the dispatcher's wakelock
was already released), so in Doze the CPU could sleep before it reached the
tray. The enrichment wakelock now wraps the initial render too.
- Resurrection: enrichment re-posts a notification as metadata arrives; if the
user read/dismissed the post in-app mid-window it came back seconds later.
dismissNotificationForEvent now records the event id, postStandard/
postConversation skip a dismissed id, and the enrichment window stops early
once dismissed.
Performance / battery:
- Channel creation is cached per-process again (was 2 Binder IPCs on every post
AND every re-render; now once, matching the original field-caching).
- Re-renders are debounced (250ms), collapsing the StateFlow initial-value burst
and rapid relay arrivals into one rebuild instead of ~3+ back-to-back — cutting
redundant bitmap loads/crops, notify() calls, and group-summary re-posts.
- Concurrent enrichment windows are capped (Semaphore of 4) so a push burst can't
hold N simultaneous 25s relay windows + wakelocks + subscriptions.
Behavioral audit found no regressions: routing is a strict superset of the
original and all gates/deep-links are preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Rebuilds the Android tray-notification layer around a per-kind design system so
every Nostr notification reads at a glance and looks native to modern Android.
Framework:
- NotificationCategory: one table-driven entry per kind carrying its channel,
accent color, monochrome status-bar icon, importance, group key, and settings
icon. Reuses existing channel ids (no orphaned user settings) and adds
Reposts/Media/Articles/Code/Badges channels, organized into system-settings
channel groups (Messages/Social/Payments/Content/Developer/Games) so users can
silence a single kind or a whole family.
- NotificationUtils: rewritten as a category-driven builder — postStandard
(BigText, or BigPicture for media/badges) and postConversation (MessagingStyle
for DMs/replies/chat/group). Adds setColor accents, circular avatars, colorized
zaps, kept the NIP-30 emoji badge overlay, and setOnlyAlertOnce so enrichment
updates replace silently.
- NotificationEnricher: makes notifications observable — renders immediately from
cache, then subscribes to the involved npubs (author/zapper/chat members) and
notes via userFinder/eventFinder, re-rendering in place as names, pictures,
content, and post images arrive over a bounded relay window.
One file per notification (service/notifications/renderers/): DirectMessage,
GroupMessage (+welcome), Reply, Mention, Reaction, Zap (Lightning+nutzap+onchain),
Repost, Media, Article/Highlight, Code (git), Badge, Chess. EventNotificationConsumer
is now a slim policy dispatcher (account match + shared gates) delegating to them.
Closes push-vs-feed parity gaps: nutzaps (9321), onchain zaps (8333), reposts
(6/16), badge awards (8), and git PRs/updates (1618/1619) now render.
Dismiss-on-read is preserved (per-event id keying) so reading a post in-app
clears its tray notification. Adds monochrome status-bar drawables and the new
channel/title/group strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122uQ8BLHLeHDni81RBP26r
Adds amethyst/plans/2026-07-23-push-notification-redesign.md: a rendering/UX
design for the Android tray notifications that gives each Nostr notification
kind (zap, reaction, reply, mention, DM, repost, nutzap, media, git, badge,
chess) its own accent color, status-bar icon, and notification style
(MessagingStyle / BigPictureStyle / colorized zap card), plus aggregation,
Conversations/Bubbles, and richer actions. Also documents the spec+builder
refactor, channel restructuring, icon/string work, parity gaps to close
(nutzap/onchain/repost/badge), a phased rollout, and testing.
Indexes the plan in amethyst/plans/README.md.
A Buzz DM channel showed the generic "DM" metadata name and offered Forum-threads
+ Share actions that don't fit a private 1:1 conversation.
- Title the DM by the OTHER participant's display name (from the 39000 inlined `p`
tags), reactively resolved, falling back to the channel name until it loads.
- Hide the Forum/Threads button and the Share button when the channel is a DM
(isBuzzDm): a DM has no forum, and its private, membership-gated naddr is
meaningless to share.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match Buzz's reference forum thread and fix insets:
- Add a "{N} replies" count header and a "No replies yet" empty state (flat root +
replies, mirroring block/buzz's ForumThreadPanel — replies are NOT hierarchical).
- Use DisappearingScaffold instead of a plain Scaffold so the composer + list get the
app's shared imePadding + navigation-bar insets (same as RelayGroupChatScreen); the
composer is content at the bottom of the Column, not a Scaffold bottomBar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The console did a single fetch on bind and read the cache back immediately,
which races the NIP-42 auth handshake: the warm-auth fetch can return before
the authenticated read actually delivers, so personas (30175) and turn
metrics (44200) landed in LocalCache a moment after the one-and-only
reloadFromCache — leaving both tabs empty with no way to refresh (the VM's
bind guard blocks a re-fetch on re-entry).
Adds a live subscription (startWatching/stopWatching, mounted for the
console's whole lifetime) for 44200 #p=me + 30175 authors=me on the
community relay; each arriving batch re-derives both tabs from LocalCache.
The relay re-serves the subscription once the connection authenticates — the
same path that unlocks the channel roster — so the console now populates on
its own instead of showing nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Mentioning someone who isn't yet a member of a Buzz workspace channel now
adds them (kind-9000 put-user) before the message is published, so the
`@`-mention resolves to a real member — mirroring Buzz's own composer, which
is how you pull a bot into a channel by naming it.
Gated on the sender being able to moderate (only a moderator can issue
kind-9000; it's a silent no-op otherwise), skips self and already-present
members, and is best-effort so a failed add never blocks the message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Brings the Buzz community + member screens closer to Buzz's own clients:
- Presence dots (BuzzPresenceState → online green / away amber) overlaid
on DM-row and member-list avatars; offline/unknown shows nothing rather
than implying we track a peer we don't.
- Community view sections (Channels / Forums) are now collapsible, each
channel row carries an unread dot (relayGroupChannelHasUnreadFlow) and a
star toggle; starred channels float to the top. Stars persist
device-globally (BuzzChannelStars + BuzzChannelStarPreferences), mirroring
the joined-workspaces store.
- Canvas editing: the canvas screen gains an edit mode that publishes a
fresh kind-40100 CanvasEvent to the channel's host relay (last-write-wins);
the top-bar canvas button now shows on any Buzz channel so an empty one can
be created.
- Bot "Working…" indicator: a process-wide BuzzAgentActivityState, fed by a
members-screen observer (24200) subscription, lights a live "Working…" line
next to an agent currently emitting frames; tapping opens the community's
Agent Console. Owner-scoped by nature (frames are #p=owner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
The forum thread's reply field was a bare OutlinedTextField. Replace it with the
same rich composer chats use (EditFieldRow): emoji + @mention suggestions, media
attach, drafts, and the typing indicator, with the standard send button styling.
Reuse works by making ChannelNewMessageViewModel.createTemplate() protected/open
and adding ForumReplyNewMessageViewModel, which overrides only the built event so
a send publishes a kind-45003 ForumCommentEvent scoped to the open thread.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Buzz forum roots (kind 45001) and comments (45003) were consumed onto the chat
timeline (consumeBuzzTimelineEvent → addNote), so a forum post showed up as a
kind-9 chat bubble and replies leaked into the chat feed.
Route them like the content they are:
- 45001 → the group's Threads collection (addThread) via a new consumeBuzzForumPost,
so it surfaces in the forum/Threads view; 45003/45002 are now store-only.
- ThreadRow renders a ForumPostEvent (body-as-heading, no title) alongside kind-11.
- The Threads REQ (RELAY_GROUP_THREAD_KINDS) now also fetches 45001/45003.
- A forum-type channel (channel_type=forum) opens the Threads view directly from
the community Forums section instead of the chat view.
- New BuzzForumThreadScreen: the root post + its 45003 replies + a reply composer
(ForumCommentEvent.build). Own replies aren't re-delivered on the open sub, so a
send restarts the REQ to re-fetch the thread.
Verified on device: forum posts list as threads, open to root+replies, replies post
and appear; the forum channel's chat no longer shows the posts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Buzz community member (kind 13534 roster / 8000-8001 deltas) can read and post
across every channel on the relay, but `RelayGroupChannel.membershipOf()` read
only the per-channel NIP-29 roster (39001/39002), so a community member/admin who
wasn't explicitly added to a private channel resolved as NONE and was wrongly
gated (Join lock, hidden composer).
Add `BuzzCommunityMembership`, a per-relay registry fed by the relay-signed NIP-43
events, and consult it as a Buzz-only fallback in `membershipOf` (mapped to MEMBER
only, never channel ADMIN, to avoid over-granting per-channel moderation). Wire
`LocalCache.consume` for kinds 13534/8000/8001 to update the registry (LWW on
created_at) and poke the relay's channels so the gate re-renders live.
This complements the open-channel fix: open channels no longer require membership
at all, and members of private channels are now recognized community-wide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>