Replaces the plain stacked-form EmojiPackMetadataScreen with the badge-
definition layout: a large square hero preview at the top, Name and
Description fields below, and a single Create/Save action.
Picking an image now launches the gallery directly (no URL paste step).
If the user submits with a freshly picked local image, the ViewModel
uploads to the account's default file server first, then publishes the
EmojiPackEvent with the uploaded URL in `image`. Existing remote URLs
keep rendering as the hero until replaced. The signer contract and
ownedEmojiPacks create/update calls are unchanged.
Add a goalEventId() accessor on LiveActivitiesEvent that reads the
zap.stream `["goal", "<hex>"]` tag. When a stream channel's 30311 event
references a goal, fetch the kind 9041 event and its zap receipts
(#e=<goalId>) so the existing goal-progress aggregation works.
Render a compact LiveStreamGoalHeader above the chat feed showing the
goal title, the existing GoalProgressBar, and a one-tap ZapReaction
that targets the goal event so viewers can contribute directly to the
fundraiser without leaving the stream.
https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi
Add a Quartz LiveActivitiesClipEvent for zap.stream's kind 1313 clip
convention, parsing the `a` tag (stream address), `p` tag (host), `r`
tag (video URL), and `title` tag. Register it in EventFactory, subscribe
in the live-activity chat filter, and route clips into the source
LiveActivitiesChannel cache. Render clips as an accent-colored card
showing "X created a clip" with the title and an inline video player
for the clip's playable URL.
https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi
The edit/create top bar was keyed on model.isEditing(), which reads
existingDTag set by loadFrom(). loadFrom runs in a LaunchedEffect after
the first composition, so the first render of the edit screen briefly
showed the "Create" button before flipping to "Save".
Derive the top bar from the `editing: Address?` parameter instead — it
is known synchronously at composition, so edit mode shows "Save" on
frame one.
Add a Quartz LiveActivitiesRaidEvent for zap.stream's kind 1312 raid
convention with root/mention a-tag markers. Wire it into the event
factory, subscribe to it in the live-activity chat filter, and route
received raids into both source and target LiveActivitiesChannel caches
so viewers on either side see the notification. Render raids as an
accent-colored pill showing "X is raiding Y" that navigates to the
target stream when tapped.
https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi
RenderCommunitiesThumb was observing the full NoteState, which
recomposes on every reaction, zap, boost or metadata tick. Each
recomposition rebuilt the CommunityCard, re-parsing the definition
tags and calling moderatorKeys().toImmutableList() — producing a
brand-new ImmutableList instance. That new list identity then
invalidated LaunchedEffect(key = moderators) in LoadModerators,
re-running the expensive ParticipantListBuilder traversal (author +
all replies + boosts + zaps + reactions + public-chat notes) per
tick.
Fixes:
- Switch to observeNoteEvent<CommunityDefinitionEvent> so the
thumb only recomposes when the definition event itself changes
(including edits). Reactions/zaps are handled separately inside
LikeReaction/ZapReaction.
- remember(event.id) { CommunityCard(...) } so the card and its
moderator list identity are allocated once per event version.
- Stabilize LoadModerators' LaunchedEffect with (baseNote.idHex,
moderators). Convert the hosts list to a Set for O(1) filtering,
reuse a single ParticipantListBuilder instance, and skip the
redundant .minus(hosts) call on the new set.
Name display
- CommunityCard, CommunityName spinner option, FeedFilterSpinner
renderer, and DisplayFollowingCommunityInPost now all prefer the
NIP-72 `name` tag and fall back to the `d` tag. Previously they
rendered the raw UUID d-tag for communities created by this app,
which made them show up as /n/<hex> instead of the configured name.
- DisplayCommunity is now reactive via observeNote so the chip in
NoteCompose recomposes when the community definition arrives.
Edit flow
- New Route.EditCommunity(kind, pubKeyHex, dTag) and
EditCommunityScreen that reuses the create form via a shared
CommunityFormScreen composable.
- NewCommunityModel gains loadFrom(CommunityDefinitionEvent) to
preload name/description/rules/moderators/relays and to remember
the existing d-tag + image URL. publish() reuses the existing
d-tag so the replaceable kind-34550 event updates in place.
- Cover preview on the form supports the already-uploaded image
(AsyncImage with remove button) and falls back to the placeholder
if cleared.
- Owner (note author == signer) sees an "Edit" FilledTonalIconButton
in LongCommunityActionOptions that opens the edit screen.
- Top bar becomes "Edit Community" with a Save button when editing.
Adds a gallery-picker icon as the leadingIcon of the URL field in
AddEmojiDialog. The uploader uses the same NIP-96/Blossom pathway as
BookmarkGroupMetadataViewModel — the upload function and progress
state live on EmojiPackViewModel so the dialog stays composable-only.
On upload success the returned URL is written back to the dialog's
local url state, leaving the user's shortcode entry and private
toggle intact, so the existing validation flow (EmojiUrlTag.isValidShortcode
+ supportingText error) keeps working.
https://claude.ai/code/session_01SNG3nj8ZZDChggTsg1qznn
Reuses the NIP-96/Blossom uploader plumbing from BookmarkGroupMetadata —
the cover image field now shows a gallery-picker icon on its leading
side and populates the URL on upload success. The manual URL input
still works as before.
https://claude.ai/code/session_01SNG3nj8ZZDChggTsg1qznn
Subscribe to kind 9735 alongside kind 1311 against the stream's #a tag,
route host-directed zaps into the LiveActivitiesChannel cache, and render
them as a centered, lightning-accented row with the zapper, amount, and
optional zap message. Matches zap.stream's chat behavior for
NIP-53 + NIP-57 interop.
https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi
addMember/removeMember/updateGroupMetadata were publishing the raw TLS
Commit struct inside the kind:445 outer ChaCha20 layer, so receivers
that started with MlsMessage.decodeTls read the first two bytes of the
Commit as the MLS protocol version and aborted with "Unsupported MLS
version: <garbage>" (e.g. 16704 = 0x4140).
Wrap commits produced by MlsGroup.commit in a PublicMessage carrying
the sender leaf index and confirmation_tag, then in an MlsMessage
envelope, and expose those bytes via CommitResult.framedCommitBytes so
MarmotManager uses them instead of the raw commit bytes. The internal
CommitResult.commitBytes field stays raw so MlsGroup.processCommit and
existing tests that exercise it directly keep working.
Also mark the committer's own published kind:445 as already processed
in MarmotInboundProcessor so that the relay echo of our own commit is
treated as a duplicate rather than re-applied on top of the already-
advanced local epoch.
https://claude.ai/code/session_01NR6JgYRimVb142T2nkChuh
Introduces a dedicated "Autoplay Videos" (Always/Never) preference that
controls whether videos start playing automatically when visible in the
feed. The pre-existing "Video Playback" preference actually gates
auto-downloading, so its description is corrected to say "Automatically
load videos and GIFs".
https://claude.ai/code/session_01T4MHiQHK6moNu8e6Rx3Nao
- Large tap-to-pick cover image at the top (Badge-style): shows an
aspect-16:9 placeholder that opens the gallery, uploads to the user's
chosen media server, and previews via ShowImageUploadGallery.
- Moderators section: lists the current user as pinned "Owner" plus
each added moderator (picture + display name + NIP-05) with a
remove icon. The add field is powered by ShowUserSuggestionList so
names, npubs and NIP-05s all search live.
- Relays section: each row uses BasicRelaySetupInfoClickableRow so
users see the full relay stats (icon, users, event counts, status)
just like the AllRelayList screen. Below each row a FilterChip group
sets the NIP-72 marker (any / author / requests / approvals).
A RelayUrlEditField at the bottom adds new relays with live search
suggestions.
- Reworked NewCommunityModel: dedicated mutableStateListOf for
moderators and relays, proper image upload via MultiOrchestrator,
CommunityRelayEntry data class, publish() now uploads then signs
kind 34550 and follows it (so the new community appears in "Mine").
- Updated strings and localized resources.
Adds a dump_daemon_diagnostics helper (relays per type, wn debug health,
relay-control-state, pending invites, wnd stderr tail) and wires it into
Test 02 and Test 04 on both sides of the poll (pre-invite baseline +
post-timeout). wait_for_invite now prints a ~10s heartbeat with the
pending-invite count and the most recent welcome/giftwrap line from wnd
stderr. On timeout the script also prompts the operator to paste the
Amethyst-side MarmotDbg logcat so both ends of the gift-wrap pipe end up
in one log file.
Adds MyEmojiListScreen under emojipacks/membershipManagement/, reachable
from the "My Emoji List" header row in ListOfEmojiPacksScreen (which
previously had a no-op click handler).
The screen enumerates every pack address referenced by the user's kind
10030 selection event and renders each with title, author name, pack
thumbnail, and a preview of its emojis. A trailing delete icon calls
account.removeEmojiPack(note) which republishes the 10030 without that
pack's `a` tag; downstream consumers (reaction menu, `:` autocomplete in
composers) refresh automatically since they share the same flow.
Tapping a row routes to Route.EmojiPackView(dTag) when the pack is
self-authored, and to Route.Note(addressTag) otherwise. EmojiPackView is
backed by OwnedEmojiPacksState, which filters to the logged-in user's
authored packs, so selected packs authored by other users must use the
generic thread viewer (which already renders kind 30030 via
RenderEmojiPack).
Reordering is intentionally deferred: it would require a new
EmojiPackSelectionEvent.reorder(...) builder in Quartz (none exists
today) and a drag affordance that doesn't conflict with tap-to-view /
tap-to-delete. The header's `openMyEmojiList` callback remains the only
nav entry point, so adding reorder later is a pure follow-up.
Adds a FilterChip toggle to AddEmojiDialog letting users choose whether a
new custom emoji is written to the public `emoji` tags or to the encrypted
`.content` (NIP-51 private tags) of their kind 30030 EmojiPackEvent.
Because the downstream consumers (`:` autocomplete via EmojiSuggestionState
and the reaction menu via RenderEmojiPack) currently read public tags only
through EmojiPackState.mergePack / EmojiPackEvent.taggedEmojis(), private
emojis are NOT surfaced end-to-end yet. Rather than silently shipping a
half-broken surface (which would also only work for self-owned packs since
foreign packs cannot be decrypted anyway), the dialog now shows an honest
explainer describing exactly what "private" means today: stored encrypted,
visible only to the pack owner in this screen.
EmojiPackScreen already rendered both lists; the grid now distinguishes
private entries with a small lock badge overlay and updates the
long-press deletion path to pass isPrivate through so removeEmoji removes
from the correct location (encrypted content vs public tags).
Follow-up to the earlier MIP-spec alignment commit, addressing the
remaining audit items:
- MIP-00 (KeyPackageUtils.isValid): now performs every strict tag-level
check that MDK does — d tag is exactly 64 hex chars, mls_protocol_version
is exactly "1.0", mls_ciphersuite is exactly "0x0001", mls_extensions
contains both 0xf2ee + 0x000a, mls_proposals contains 0x000a, encoding
is "base64", content non-empty, i tag present. Adds
`isCryptographicallyValid` for deep checks: i tag matches the computed
KeyPackageRef (RFC 9420 §5.2), Basic credential identity equals the
event's pubkey, KeyPackage signature verifies.
- MIP-01 (MarmotGroupData): encoder now omits the v3-only
`disappearing_message_secs` field when version < 3, so v2 output
matches MDK's `tls_codec` 0.4 byte-for-byte. Adds byte-level fixture
tests pinned to the Rust reference (encode + decode in both directions).
- MIP-02 (WelcomeGiftWrap): replaces the redundant sign-then-rumorize
step with `RumorAssembler.assembleRumor`, producing a true unsigned
rumor as NIP-59 requires (no wasted secp256k1 signature).
- MIP-05 kind 449 (TokenRemovalEvent.build): no longer accepts a tag
initializer. Per the MIP-05 spec the token-removal event MUST have
empty tags; allowing arbitrary caller-supplied tags risks metadata
leakage and rejection by strict validators (e.g. the MDK reference).
- RFC 9420 §6 PublicMessage framing: encoder/decoder now branch on
`contentType`. Application messages keep the `opaque<V>` length prefix;
Proposal/Commit bodies are emitted/parsed as their typed structs (no
outer length prefix), per the MLS RFC. Previously encoder used
`putBytes` raw and decoder used `readOpaqueVarInt`, so neither could
roundtrip and Proposal/Commit PublicMessages from other MLS clients
would mis-parse.
- KeyPackageUtilsTest fixtures: switch tiny "0"/"1"/"lr" slot ids to
realistic 64-char hex slot ids, since `isValid` now enforces the
MIP-00 d-tag format.
Three-dots menu on an EmojiPackEvent now offers Add/Remove from my emoji
list (kind 10030) and navigates to EmojiPackSelection. The regular
Manage bookmarks row is hidden for emoji packs so they can't end up in
the bookmark list by mistake. Closes#2426.
Add screens under ui/screen/loggedIn/emojipacks/ mirroring the bookmarkgroups
layout:
- list/ListOfEmojiPacksScreen plus EmojiPackItem for the pack feed, with a
"My Emoji List" row at the top that surfaces kind 10030 selection count.
- list/metadata/EmojiPackMetadataScreen(+ViewModel) for create/edit form.
- display/EmojiPackScreen(+ViewModel) rendering the emoji grid, FAB to add,
long-press to delete. AddEmojiDialog validates the shortcode live against
EmojiUrlTag.isValidShortcode.
- membershipManagement/EmojiPackSelectionScreen providing a single toggle
for the user's kind 10030 selection.
New routes EmojiPacks, EmojiPackView, EmojiPackMetadataEdit, and
EmojiPackSelection wired through AppNavigation. A Manage Emoji Packs row is
added to the drawer. English-only strings added to strings.xml.
Mirrors the LabeledBookmarkListsState structure: observes all authored
EmojiPackEvents in the local cache, exposes a sorted StateFlow<List<OwnedEmojiPack>>
for the UI, and provides suspend helpers for create/update/addEmoji/removeEmoji/
deletePack. Pack deletion publishes a kind 5 deletion event. Account gains
pass-through methods so AccountViewModel.launchSigner can invoke them.
Add a unit test for the OwnedEmojiPack data class.
The build DSL was typed as TagArrayBuilder<GitRepositoryEvent>, which made
signer.sign(template) return the wrong event subclass. Retype it to
EmojiPackEvent and add local TagArrayBuilder extensions for title,
description, and image tags. Also add title()/description()/image()
accessors on EmojiPackEvent to match the LabeledBookmarkListEvent pattern.
Quartz (NIP-72 compliance)
- Fix CommunityDefinition image() builder to use the NIP-72 ImageTag
(was wrongly pointing at the NIP-23 ImageTag, which prevented the
builder from attaching `<W>x<H>` dimensions).
- Fix CommunityPostApproval notifyAuthor() to emit a `p` tag for the
post author as required by NIP-72 (was duplicating the `e` tag).
- Add RelayTag.MARKER_AUTHOR/REQUESTS/APPROVALS constants.
Amethyst - Communities screen
- New top-level Communities screen with FeedFilterSpinner TopNav that
supports the Mine option (filters to communities authored by the
logged-in user).
- New Route.Communities + drawer entry.
- New CommunitiesFeedFilter (subclass of DiscoverCommunityFeedFilter
using defaultCommunitiesFollowList + Mine handling).
- New CommunitiesListFilterAssembler/SubAssembler with dedicated
filterCommunitiesMine relay query.
- Account.liveCommunitiesFollowLists + settings.defaultCommunitiesFollowList.
- TopNavFilterState.communityRoutes (includes Mine).
Amethyst - Create Community
- Route.NewCommunity + FAB on the Communities screen.
- NewCommunityModel + Material3 form: name, description, image URL,
rules, moderator pubkeys, relay requests/approvals URLs.
- Signs a kind 34550 CommunityDefinitionEvent and auto-follows it so
it appears under "Mine".
- Account.sendCommunityDefinition helper.
Misc
- Extracted parseTopFilterOrDefault helper in LocalPreferences to keep
the existing inner load method under the JVM method-size limit.
Previously ChatroomListKnownFeedFilter dropped any group whose
newestMessage was null, so groups the user just created and groups
received via Welcome events without any activity yet never appeared
in the Messages list.
Emit a stable placeholder Note (carrying the chatroom as a gatherer)
per empty group, route it through the existing Marmot row path, and
invalidate dmKnown on MarmotGroupList.groupListChanges so newly added
or promoted groups rebuild the feed.
Brings Amethyst's Marmot code into wire-level interoperability with the
reference MDK (Rust) implementation by resolving four spec violations and
tightening the epoch lookback window:
- MIP-01 (NostrGroupData extension): switch to QUIC-style VarInt length
prefixes (RFC 9000 §16) instead of fixed uint16. MIP-01 mandates this
encoding (the one produced by the Rust `tls_codec` crate v0.4+), so
Amethyst's previous uint16 framing could not round-trip with any MDK
peer. admin_pubkeys, relays (outer + each inner), name, description,
image_*, and disappearing_message_secs now all use VarInt prefixes.
- MIP-01: enforce "admin_pubkeys MUST NOT contain duplicates" in the
MarmotGroupData constructor.
- MIP-05 (Push Notifications): token plaintext padded size was 220 (→ 280
encrypted); spec MUSTs are exactly 1024 bytes plaintext (1084 encrypted).
A server expecting MIP-05 tokens would reject Amethyst's frames outright.
- MIP-05: HKDF IKM was sha256(shared_point); spec requires the raw 32-byte
ECDH x-coordinate. The extra sha256 produced a different PRK, so token
ciphertext authenticated only within Amethyst. Removed the hash; ECDH
already returns the x-only compact form via pubKeyTweakMulCompact.
- MIP-03: bump EPOCH_RETENTION_WINDOW from 2 to 5 to match MDK's
DEFAULT_EPOCH_LOOKBACK, so late-arriving GroupEvents whose ChaCha20
outer key was derived from a prior epoch's exporter secret can still
be decrypted after a Commit advances the group.
Hand-crafted MarmotGroupData test blobs were updated to emit VarInt
length prefixes.
Replaces the comma-separated relay text on the Marmot Group Info
screen with a FlowRow of 55dp relay avatars that open RelayInfo on
tap and copy the URL on long-press, matching the relay-icon pattern
used elsewhere in the app.
To surface whether each configured relay is actually carrying traffic
for the group, MarmotGroupChatroom now tracks the most recent
kind:445 event timestamp observed from each delivering relay and the
info screen renders a green dot when a relay has delivered a group
event within the last 7 days (gray otherwise).
Makes the API safer: callers can't accidentally pass an unparsed or
malformed identifier. EmojiUrlTag.emojiSet is now Address?, serialized
via toValue() and parsed via Address.parse() (which rejects anything
that isn't a well-formed kind:pubkey:dTag).
The "h" tag is optional in MIP-02 Welcome events — senders like
whitenoise-rs omit it. Instead of failing when the h-tag is absent,
derive nostrGroupId from the NostrGroupData extension embedded in the
Welcome's GroupContext (the authoritative MLS source). An h-tag hint,
if present, is still validated against the MLS-derived value.
Expose the member list as MutableStateFlow<List<GroupMemberInfo>> on
MarmotGroupChatroom, populated by MarmotManager.syncMetadataTo alongside
the existing memberCount. MarmotGroupInfoScreen and RemoveMemberScreen
now observe it via collectAsStateWithLifecycle, so both remote commits
(already routed through syncMetadataTo in DecryptAndIndexProcessor) and
local mutations refresh the UI without manual re-queries.
Account.addMarmotGroupMember and removeMarmotGroupMember now call
syncMetadataTo right after the MLS state is mutated, mirroring the
pattern already used by updateMarmotGroupMetadata, so the local change
is visible immediately without waiting for the relay round-trip.