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
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).
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.
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.