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.
- 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.
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.
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.
Per NIP-30 the emoji tag is ["emoji", <shortcode>, <url>, <emoji-set-address>]
with the fourth field optional. EmojiUrlTag only exposed the first three.
Adds emojiSet as an optional field, preserves existing positional callers
via default null, and introduces isValidShortcode() so callers can validate
user input against the spec (alphanumeric, hyphens, underscores).
Replace LaunchedEffect with LifecycleResumeEffect so the members list
(and chatroom.memberCount) is re-queried every time MarmotGroupInfoScreen
resumes, not just when nostrGroupId changes. Previously, after adding a
member via the AddMemberScreen and popping back, the members count
displayed in the header and chat top bar stayed stale until the screen
was fully recreated.
- LocalCache.consume dispatcher now handles InterestSetEvent via
consumeBaseReplaceable, so the event created by the user is stored
and flows through newEventBundles → interestSets.newNotes → listFeedFlow
refresh. Without this, the just-signed event sat in the sendMyPublicAndPrivateOutbox
call but the UI never saw the update.
- List screen: icon + centered text for the empty state and dividers
between rows.
Add Android UI for managing NIP-51 Interest Sets and surface them as chips
in the TopNav feed filter alongside followed hashtags. InterestSetEvent was
already implemented in Quartz; this wires it into the app.
- InterestSetsState mirrors LabeledBookmarkListsState: listFeedFlow with
versioned refresh, decrypted hashtag cache keyed by dTag for the feed
filter pipeline, and suspend helpers for create/rename/delete/clone/add
hashtag/remove hashtag/move (public<->private).
- New screens under ui/screen/loggedIn/interestSets/: list, metadata edit
(create/rename, title-only since InterestSetEvent has no image field),
and display screen with add/remove + public/private toggle per hashtag.
- TopFilter.InterestSet(address) feeds into a new MultiHashtagFeedFlow
that reuses HashtagTopNavFilter with Set<String>.
- TopNavFilterState.mergeInterests emits interest-set chips; FeedFilterSpinner
groups them under a new "Interest Sets" category.
- Drawer entry + HomeScreen FAB branch.
The reference port recomputed the inverse DCT cosine tables once per output
pixel — for a 32x24 decode that's ~43k redundant cos() calls. This change
lifts the tables out of the inner loop and caches them across decodes
keyed by (size, componentCount) so subsequent placeholders at the same
target size skip cosine evaluation entirely.
Additional wins:
- AC coefficients unpack into pre-sized DoubleArrays (no ArrayList<Double>
boxing, no post-hoc copy)
- truncated hashes are rejected up-front via a single length check instead
of mid-stream
- LPQA -> sRGB uses an inline branch clamp instead of a
min/max/round/coerceIn chain
Tests: 12 unit tests cover determinism across repeated decodes, cache
clearing, alpha preservation, aspect-ratio preservation both directions,
average-color drift bounds, truncated input rejection, and round-tripping
base64 vs. raw bytes. All green.
Bench: new ThumbHashBenchmark exercises opaque decode, alpha decode,
warm- and cold-cache decode, aspect-ratio probe, and the full
decodeKeepAspectRatio pipeline so the perf delta is visible on device.
Adds a parallel ThumbHash placeholder everywhere BlurHash is already used.
Remote events now carry both hashes; renderers prefer thumbhash when
available and fall back to blurhash. The MIP-04 `thumbhash` imeta field
that was previously reserved-but-unused is now wired end-to-end.
- New ThumbHash encoder/decoder in commons/commonMain (no new Gradle dep)
- New ThumbhashTag and thumbhash accessors/builders across NIP-17, 68,
71, 94, 95, 99, the experimental profile gallery, and MIP-04
- RichTextParser + MediaContentModels carry a thumbhash field alongside
blurhash so every downstream composable can pick its preferred placeholder
- New ThumbHashFetcher (Android + Desktop) registered with Coil, plus a
small placeholderModel(thumbhash, blurhash) helper centralising the
"prefer thumbhash, fall back to blurhash" rule
- Rename BlurhashMetadataCalculator -> PreviewMetadataCalculator; the
calculator decodes the bitmap / video thumbnail once and runs both
encoders on the same pixels to keep upload cost flat
- DesktopMediaMetadata, MediaUploadResult, and FileHeader now surface
both hashes through the NIP-96, Blossom, NIP-95, MIP-04, NIP-17 DM,
Classifieds, picture, video, and long-form upload paths
- Round-trip unit test for the ThumbHash port
Resolves conflicts between our CompositionLocal-based scaffold padding
and main's new features:
Conflicts resolved:
- HomeScreen: main added `HomeAlgoFeedStatusBanner` floated on top of
the feed with a Box wrapper. Kept main's Box + banner, but dropped
the `Modifier.padding(paddingValues)` + `HorizontalPager
(contentPadding = PaddingValues(0.dp))` — inner LazyColumns now pull
scaffold padding via LocalDisappearingScaffoldPadding, and the banner
aligns TopCenter with `padding(top = paddingValues.calculateTopPadding())`
so it sits below the top bar even when the feed scrolls behind it.
- PinnedNotesScreen / BookmarkListScreen / OldBookmarkListScreen: main
added `DeletedItemsBanner` that was stacked above the feed inside a
padded Column. Same pattern as Home — feed fills the Box, banner
overlays at TopCenter with the scaffold's top padding so items scroll
under the bar while the banner stays pinned.
- `DeletedItemsBanner` gained a `modifier: Modifier = Modifier` param
so callers can position it.
New-on-main screens audited and migrated:
- BadgesScreen: dropped `Column(Modifier.padding(paddingValues))`
wrapper; LazyColumn now extends behind the bars via the
CompositionLocal.
- BookmarkGroupScreen: SecondaryTabRow `containerColor =
Color.Transparent` changed to `MaterialTheme.colorScheme.background`
to match the other opaque tab rows (items scrolling underneath
would otherwise bleed through).
- AwardBadgeScreen, NewBadgeScreen, ProfileBadgesScreen,
AllSettingsScreen, FavoriteAlgoFeedsListScreen: none use
DisappearingScaffold (stock `Scaffold`) — no migration needed.
Unit tests still green (10/10). Full amethyst module compiles.
https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
The notification card BadgeCompose rendered the awarded badge via
BadgeDisplay (definition-only) and stopped there, so a recipient
viewing a fresh badge in their inbox had no way to add it to their
profile without navigating to the award's thread first.
Expose AcceptBadgeControls (was private inside Badge.kt) and call it
from BadgeCompose under the BadgeDisplay row when the underlying note
is a BadgeAwardEvent.
Other surfaces are unchanged:
- RenderBadgeAward (Badge feed / threads) keeps its own call to the
same composable; behaviour is identical because the controls were
already package-private.
- BadgeCompose has only one caller (CardFeedView, the notifications
feed), so the new row only appears there.
The FAB now opens the new-badge dialog directly. The dialog renders a
big bordered "Upload an image" placeholder where the picture will go;
tapping it opens the gallery. Once the user picks an image, the
placeholder is replaced by the existing ShowImageUploadGallery preview
and tapping the preview lets them pick a different image.
Lets the user see the whole form (name, description, server, quality,
strip-metadata) immediately instead of being thrown into the picker
the moment they hit the FAB.
Replace the form-first create screen with a picker-first media pipeline
that matches the other upload screens in the app.
- NewBadgeButton (FAB, AddPhotoAlternate icon) opens GallerySelect
directly.
- After the image is picked, NewBadgeDialog mirrors the NewMediaView /
ImageVideoPost pattern: a thumbnail strip, name + description fields,
server picker, compression-quality slider, and strip-metadata switch.
- NewBadgeModel drives the upload through the shared MultiOrchestrator.
Only on a successful upload does it reach into Account.sendBadgeDefinition
with an auto-generated UUID d-tag, the uploaded URL + dimensions, and
the uploaded URL reused as the NIP-58 thumb (a separate thumbnail
upload can land as a follow-up).
The Cancel / Post buttons use the CreatingTopBar so "Create" reads
right on the primary action. Submit is disabled until the name is
non-empty, an image is staged, and a server is selected.
Drops the old route-based NewBadgeScreen / NewBadgeViewModel and the
Route.NewBadge entry.
The accepted-first sort was keyed on acceptedAwardIds, so every toggle
changed that set and the list jumped around. Key the remember on a
"loaded" flag (first time either the 10008 or the legacy 30008 event
arrives) plus the existing bundle tick; acceptedAwardIds is read from
the enclosing scope at the moment of recomputation but isn't part of
the key.
Result: the list loads with accepted badges on top, new awards flowing
in during the session trigger a re-sort, but plain Switch toggles leave
the visual order intact.