Commit Graph

11546 Commits

Author SHA1 Message Date
Claude 7637596835 Merge remote-tracking branch 'origin/main' into claude/fix-scaffold-scrolling-NHdRa 2026-04-20 14:19:18 +00:00
Vitor Pamplona 5551f2f7d2 Merge pull request #2456 from vitorpamplona/claude/nip51-interest-sets-p0f6r
Add Interest Sets feature for organizing hashtags
2026-04-20 10:17:31 -04:00
Claude 4c82011a25 fix(interest-sets): consume kind 30015 locally + empty-state polish
- 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.
2026-04-20 14:11:06 +00:00
Vitor Pamplona 282b3a436b Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-20 09:53:18 -04:00
Vitor Pamplona 0e077c02d0 linting 2026-04-20 09:41:58 -04:00
Vitor Pamplona 83837ad090 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-20 09:38:12 -04:00
davotoula 5eb91145df style: strip trailing whitespace and fix indent flagged by spotless
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:37:56 +02:00
Vitor Pamplona 2f3d035d3c Fixes failing tests due to dispatcher misconfiguration 2026-04-20 09:34:48 -04:00
Claude 198e950f37 feat(interest-sets): NIP-51 Interest Sets (kind 30015) UI + TopNav chips
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.
2026-04-20 13:28:51 +00:00
Vitor Pamplona 516115cc1c Fixes test cases for the CallManagerTest 2026-04-20 09:21:01 -04:00
Vitor Pamplona fab435509b Merge pull request #2454 from vitorpamplona/claude/add-thumbhash-support-ZfNS1
Add ThumbHash support for image placeholders
2026-04-20 08:51:36 -04:00
Claude 63da850e3b perf(thumbhash): cache cosine tables and flatten hot loop in decoder
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.
2026-04-20 00:33:26 +00:00
Claude b2f297b3bb feat: add ThumbHash support alongside BlurHash across events, uploads, and UI
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
2026-04-20 00:12:28 +00:00
Claude 5b8bbbaa42 merge: origin/main (BadgesScreen + banner features)
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
2026-04-19 23:46:54 +00:00
Vitor Pamplona 3bfeac885a Merge pull request #2452 from vitorpamplona/claude/badge-system-amethyst-J07UM
Add comprehensive badge system with creation, awarding, and profile management
2026-04-19 17:41:10 -04:00
Vitor Pamplona f43b334d93 Merge branch 'main' into claude/badge-system-amethyst-J07UM 2026-04-19 17:41:02 -04:00
Claude 35cab28543 feat(badges): add accept controls to notification card
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.
2026-04-19 21:35:04 +00:00
Claude 8eeeae9601 refactor(badges/new): show form first with an upload placeholder
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.
2026-04-19 21:20:38 +00:00
Claude 29518dbf19 feat(badges/new): image-first creation flow with upload + auto UUID
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.
2026-04-19 20:53:24 +00:00
Claude a346ae86de refactor(badges): default Badges feed filter to Mine 2026-04-19 20:20:55 +00:00
Vitor Pamplona 2d5097e8b0 Merge pull request #2451 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-19 16:13:40 -04:00
Crowdin Bot af27ad4673 New Crowdin translations by GitHub Action 2026-04-19 19:37:16 +00:00
davotoula f15e280862 updated cz,sv,pt,de 2026-04-19 21:31:56 +02:00
Vitor Pamplona fd49ee27ad Merge pull request #2443 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-19 15:04:49 -04:00
Vitor Pamplona bb1817c67c Merge pull request #2449 from vitorpamplona/claude/add-favorite-dvm-filter-DfveW
Add NIP-90 DVM favorite list support with content discovery
2026-04-19 15:04:42 -04:00
Claude f99fc4c333 refactor: rename FavoriteDvm* -> FavoriteAlgoFeed* throughout amethyst
The wire-level class was renamed to FavoriteAlgoFeedsListEvent in a
prior commit. Finish the rename through the rest of the feature so
the internal vocabulary (packages, classes, properties, routes, DSL
helpers, TopFilter variants, composables) matches what users see.

Packages:
- model.nip51Lists.favoriteDvmLists -> favoriteAlgoFeedsLists
- model.dvms                         -> algoFeeds
- model.topNavFeeds.favoriteDvm      -> favoriteAlgoFeeds
- home.datasource.nip90Dvms          -> nip90AlgoFeeds

Classes/composables renamed (selected):
- FavoriteDvmListState/DecryptionCache -> FavoriteAlgoFeedsListState/DecryptionCache
- FavoriteDvmOrchestrator / FavoriteDvmSnapshot -> FavoriteAlgoFeedsOrchestrator / FavoriteAlgoFeedsSnapshot
- FavoriteDvmTopNavFilter{,PerRelayFilter{,Set}}, FavoriteDvmFeedFlow ->
  FavoriteAlgoFeedTopNavFilter{,PerRelayFilter{,Set}}, FavoriteAlgoFeedFlow
- AllFavoriteDvmsTopNavFilter / AllFavoriteDvmsFeedFlow / AllFavoriteDvmsBanner ->
  AllFavoriteAlgoFeedsTopNavFilter / AllFavoriteAlgoFeedsFlow / AllFavoriteAlgoFeedsBanner
- FavoriteDvmName -> FavoriteAlgoFeedName
- FavoriteDvmToggle -> FavoriteAlgoFeedToggle
- FavoriteDvmListScreen -> FavoriteAlgoFeedsListScreen
- HomeDvmStatusBanner / SingleDvmBanner / DvmStatusBanner ->
  HomeAlgoFeedStatusBanner / SingleAlgoFeedBanner / AlgoFeedStatusBanner
- filterHomePostsByDvmIds -> filterHomePostsByAlgoFeedIds
- TopFilter.FavoriteDvm / TopFilter.AllFavoriteDvms ->
  TopFilter.FavoriteAlgoFeed / TopFilter.AllFavoriteAlgoFeeds
- Route.EditFavoriteDvms -> Route.EditFavoriteAlgoFeeds

Account / AccountSettings / AccountViewModel properties & mutators
renamed consistently: favoriteDvmList -> favoriteAlgoFeedsList,
backupFavoriteDvmList -> backupFavoriteAlgoFeedsList, follow/unfollow/
isFavoriteDvm -> follow/unfollow/isFavoriteAlgoFeed, refreshFavoriteDvm
-> refreshFavoriteAlgoFeed, dvmAddress kwargs -> feedAddress,
dvmNote locals -> feedNote, etc.

Persisted TopFilter.FavoriteAlgoFeed/AllFavoriteAlgoFeeds `code` strings
changed (old "FavoriteDvm/…" / " All Favourite DVMs " values don't
round-trip anymore) — kotlinx-serialization polymorphism keys the
sealed class by class name, so cold-start of pre-existing installs
will fall back to AllFollows for this filter. Acceptable since this
is a new feature only on this branch.

Kept unchanged: ui/screen/loggedIn/dvms/ folder (hosts generic
DvmContentDiscoveryScreen / DvmTopBar / DvmPaymentActions), quartz
nip90Dvms package (NIP-90 wire-protocol naming). XML string resource
keys (favorite_dvms_title, dvm_home_* etc) kept stable; only their
values were updated previously.

Build + tests green on both modules.
2026-04-19 17:56:16 +00:00
Claude d2ddedc871 fix(badges/profile): freeze list order across Switch toggles
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.
2026-04-19 17:49:09 +00:00
Claude c20bb75590 fix(ui): wrap DisappearingScaffold in a Surface (restore LocalContentColor)
The custom SubcomposeLayout that replaced Scaffold inherited Material's
theme but not its implicit Surface, which is what provides
`LocalContentColor = onBackground`. Without that provider,
`LocalContentColor` falls back to `Color.Black` — which in the dark
theme is invisible against the dark background, making all feed text
unreadable.

Fixed by wrapping the root modifier chain in a `Surface(color =
MaterialTheme.colorScheme.background, contentColor =
MaterialTheme.colorScheme.onBackground)`, matching what M3 Scaffold
does internally. Extracted the SubcomposeLayout into a small private
`ScaffoldLayout` composable so the Surface wraps it cleanly.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-19 17:39:34 +00:00
Claude 72598026a3 refactor(quartz): rename FavoriteDvmListEvent -> FavoriteAlgoFeedsListEvent
The list event only stores content-discovery feeds (kind-5300 DVMs),
not every DVM, so the old name was misleading. Rename the wire-level
type to reflect what's actually in it.

- Quartz: package nip51Lists.favoriteDvmList -> favoriteAlgoFeedsList;
  class FavoriteDvmListEvent -> FavoriteAlgoFeedsListEvent.
- DSL helpers renamed: favoriteDvm/favoriteDvms builder extensions ->
  favoriteAlgoFeed/favoriteAlgoFeeds; TagArray.favoriteDvmList/Set ->
  favoriteAlgoFeedsList/Set.
- create/add/remove parameter names dvm -> feed, publicDvms/privateDvms
  -> publicFeeds/privateFeeds; public/private accessors
  publicFavoriteDvms/privateFavoriteDvms -> publicFavoriteAlgoFeeds/
  privateFavoriteAlgoFeeds. ALT string updated.
- EventFactory + LocalCache dispatch branches + AccountSettings backup
  field type + FavoriteDvmListState + FavoriteDvmListDecryptionCache all
  import the new type. Internal amethyst-side classes
  (FavoriteDvmListState, FavoriteDvmListDecryptionCache), the top-nav
  filter classes (FavoriteDvm*, AllFavoriteDvms*) and the orchestrator
  keep their names — they still deal with content-discovery DVMs
  specifically, and the narrower rename here is scoped to the Nostr
  wire format the user was asking about.
- Quartz test renamed + rewired. Build + tests green on both modules.
2026-04-19 17:37:55 +00:00
Claude 6dd373556e fix(badges): stop losing accepted-set entries on rapid toggles
Two interacting bugs were still eating badges when the user toggled
switches in the profile-badges settings page:

1. TimeUtils.now() returns seconds, and LocalCache.consumeBaseReplaceable
   only accepts an update whose createdAt is strictly greater than the
   one already stored. Two toggles within the same second produced
   equal-timestamp events, so the second was silently dropped from
   the cache — the UI reverted after a beat and the change looked
   like it had never happened.

2. launchSigner coroutines run on Dispatchers.IO so two concurrent
   toggles could both read the same pre-state, each write its own
   fragment, and whichever landed last clobbered the other.

Wrap the read-modify-write with a Mutex and bump the outgoing
createdAt to maxOf(now, latestCachedCreatedAt + 1) so every write is
strictly newer than whatever sits in cache. The sign + local consume
happen under the lock; network publish stays outside it.
2026-04-19 17:31:10 +00:00
Crowdin Bot 63ef6dae6c New Crowdin translations by GitHub Action 2026-04-19 17:27:09 +00:00
Vitor Pamplona ce6741ce92 Merge pull request #2450 from vitorpamplona/claude/add-pdf-preview-DIUJE
Add PDF viewing support with preview cards and full-page viewer
2026-04-19 13:25:47 -04:00
Claude 496cc9876f fix(pdf): revert RGB_565 — PdfRenderer requires ARGB_8888
PdfRenderer.Page.render() only accepts ARGB_8888 bitmaps; RGB_565 is
silently rejected (the renderer produces no output), which is why the
preview card and viewer both stopped showing anything. Go back to
ARGB_8888 and update the memory estimates in the comments. The other
perf wins from 49e913b (FilterQuality.Medium, 4096 hi-res cap, 1.5x
threshold, remembered ImageBitmap wrapper) are unaffected and stay.
2026-04-19 17:19:36 +00:00
Claude d942a624eb fix(badges): preserve NIP-58 a/e pair order, clickable rows, accepted-first sort
Three fixes to the Profile-badges flow:

1. TagArrayBuilder groups tags by name, so calling acceptedBadges() on
   the builder scrambled [a1, e1, a2, e2] into [a1, a2, e1, e2] when
   serializing. AcceptedBadge.parseAll expects adjacent (a, e) pairs,
   so only one mangled badge survived, making each Accept toggle
   appear to replace the previous one. Rewrite the build() of both
   ProfileBadgesEvent (kind 10008) and AcceptedBadgeSetEvent (kind
   30008) to collect the prefix tags (d / alt / initializer) via the
   builder, then append AcceptedBadge.assemble(...) verbatim to keep
   the pair order intact.

2. Rows in ProfileBadgesScreen were not clickable. Thread nav through
   AwardRow / StaticAwardRow and wrap the row body in Modifier.clickable
   that routes to the badge definition's thread. The Switch keeps
   consuming its own clicks, so toggling still works.

3. Sort accepted badges to the top of the list, then the rest by
   createdAt desc, so the user can see at a glance which badges are
   currently shown on the profile.
2026-04-19 17:00:06 +00:00
Claude 49e913bec4 perf(pdf): reduce pan/zoom jitter in viewer dialog
The main cost during a live pan/zoom is the GPU re-sampling the page
bitmap every frame under the zoomable modifier's transform. Three
changes bring that cost down substantially:

- Bitmap.Config.RGB_565 instead of ARGB_8888. PDFs are opaque, so the
  alpha channel is unused. Halves texture memory and GPU upload
  bandwidth; visually identical.
- HI_RES_MAX_DIM_PX lowered from 6144 to 4096. 4096 stays within the
  common GPU texture-size limit, keeping rendering hardware-accelerated
  on mid-range devices. Peak bitmap is now ~24 MB (A4, RGB_565) instead
  of ~107 MB (ARGB_8888, 6144).
- FilterQuality.Medium (bilinear) instead of High (bicubic/Mitchell).
  High is recomputed per frame during pan/zoom and is the main source
  of jitter on multi-megapixel sources. At 3072-4096 px base resolution
  bilinear is effectively indistinguishable.
- HI_RES_ZOOM_THRESHOLD raised from 1.2 to 1.5 so we don't trigger a
  costly re-render for marginal zoom levels where the base is fine.
- Cache the ImageBitmap wrapper via remember to avoid per-recomposition
  allocations.
2026-04-19 16:59:23 +00:00
Claude 640848a2f6 fix(pdf): always scale render to target, never keep 72-DPI native size
PdfRenderer.Page.getWidth()/getHeight() return the page size in
PostScript points (1/72"), so for a standard A4 that's 595 x 842
"pixels" passed to cappedRenderSize. The old early-return kept native
dimensions whenever longest <= targetDim (which was always), so the
base bitmap was 595 x 842 regardless of what we asked for — effectively
72 DPI. Zoom-aware re-renders hit the same early return and also
stayed tiny, which is why double-tap produced no visible improvement.

Remove the early return: since PDFs are vector, always rescale the
point dimensions so the longest side equals targetDim. Base renders
now produce ~3072 px bitmaps and the hi-res path actually goes to
6144 px when zoomed.
2026-04-19 16:45:17 +00:00
Claude fc284e3cfe feat(dvm-favorites): rename user-facing to "Favorite Feed Algorithms" + fix cold-start race
- User-facing rename. All user-visible strings switch from "favourite"
  to "favorite" (American spelling) and from "DVM(s)" to "Feed
  Algorithm(s)": settings entry, spinner group label, empty state,
  banner messages, menu accessibility labels. Code identifiers still
  reference NIP-90's "DVM" protocol name since that's the wire spec.
- Cold-start race. When the app relaunches with a persisted
  TopFilter.FavoriteDvm, the AppDefinitionEvent may not be in cache
  yet. mergeInterests was filtering out any favorite whose
  AppDefinitionEvent didn't yet advertise kind 5300, so the spinner
  didn't include a chip matching the persisted selection — it
  showed "Select an option" while the orchestrator quietly fired the
  RPC with no UI to ground it. Always include every favorite; the
  5300 check at add time (in FavoriteDvmToggle) is enough.
2026-04-19 16:43:46 +00:00
Claude 154adcf111 refactor(ui): CompositionLocal for scaffold padding + scaffold hygiene
Follow-up polish to the disappearing-scaffold migration addressing
the review items:

1. Replace the `scaffoldPadding: PaddingValues` sprawl across ~15 feed
   entry points with a single `LocalDisappearingScaffoldPadding`
   CompositionLocal published by the scaffold, plus a
   `rememberFeedContentPadding(inner)` helper that merges it with the
   list's own baseline padding. The contract becomes implicit at the
   call site ("inner LazyColumn uses rememberFeedContentPadding") and
   there's no more optional parameter for a future dev to forget.
   `rememberMergedPadding` stays available for manual use.

2. Fix the stale-closure bug in `DisappearingScaffold`: the NSC is
   `remember`'d and keeps its captured `canScroll` lambda across
   recompositions, but the lambda was reading `allowBarHide`,
   `isActive`, and `accountViewModel` by closure — so later parameter
   updates wouldn't be visible. Wrapped them in `rememberUpdatedState`
   so the NSC's `canScroll` always sees current values.

3. Gate `ResetBarsOnResume` and the `Modifier.nestedScroll(connection)`
   install on `allowBarHide`. Chat detail screens (pinned chrome) no
   longer wire up a lifecycle observer or dispatch scroll events
   through the NSC for bars that never move.

4. Unify header-slot naming: `SearchScreen.DisplaySearchResults` now
   uses `headerContent` to match `CardFeedView.RenderCardFeed`.

5. Add `DisappearingBarNestedScrollTest` covering:
   - onPostScroll never consumes
   - hide/reveal deltas applied to both bars
   - per-bar clamping at their independent limits
   - consumed + available sum (so overscroll works)
   - canScroll=false freezes the bars
   - reverseLayout inverts the delta sign
   - setting a smaller heightLimit clamps the current offset
   - onPostFling returns Velocity.Zero to swallow phantom velocity

All 41 consumer / shared-helper files touched to remove the now-unused
`scaffoldPadding` parameter, replaced with `rememberFeedContentPadding(
FeedPadding)` at the innermost LazyColumn / LazyVerticalGrid. Behaviour
is unchanged; API surface is smaller.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-19 16:32:55 +00:00
Claude 2e4a985b36 feat(images): load full-resolution source in the image dialog
SubcomposeAsyncImage was being called with just the URL (or local
File), so Coil auto-sized the decode target to the composable's
layout bounds. In the feed that's desirable — a small thumbnail
decode — but in the zoomable dialog the composable is fillMaxWidth
(~screen width), and pinch-zooming then GPU-upscales the already
downsampled bitmap, producing the same blurriness the PDF viewer
had before.

Add a fullResolution: Boolean = false flag to UrlImageView and
LocalImageView. When true, swap the model for an ImageRequest with
size(Size.ORIGINAL) so Coil decodes at the image's native
dimensions. Feed call sites keep the default (fast, sampled). Both
dialog call sites in ZoomableContentDialog now pass
fullResolution = true, so pinch-zoom shows native pixels.

The existing blurhash/aspect-ratio placeholder path already covers
the brief high-res load window in the dialog.
2026-04-19 16:32:34 +00:00
Claude d1fc49dc51 feat(badges): render feed items with author + reactions chrome
Revert of 93cf9f1 restored the BadgeCard chrome that works well when
embedded inside another NoteCompose (e.g. a BadgeAwardEvent's body).
But the badge feed was still bypassing NoteCompose's author header and
ReactionsRow because BadgeDefinitionEvent was hoisted out of
CheckNewAndRenderNote up at the top of NoteCompose.

Move BadgeDefinitionEvent into RenderNoteRow next to BadgeAwardEvent
so it flows through the same chrome path. Feed items now get:
- the author avatar / name / timestamp header
- the existing BadgeDisplay card body
- the standard ReactionsRow (reply / repost / zap / like)

Other call sites of BadgeDisplay are untouched:
- RenderBadgeAward still embeds BadgeDisplay as a child card
- BadgeCompose notifications still embed BadgeDisplay
- DisplayBadges profile strip uses BadgeThumb, not BadgeDisplay
2026-04-19 16:31:06 +00:00
Claude ffa55a30f3 Revert "refactor(badges): drop OutlinedCard chrome from feed items"
This reverts commit 93cf9f1d6e.
2026-04-19 16:26:38 +00:00
Claude 0c88b83c3f feat(pdf): wire double-tap to toggle zoom in viewer
The zoomable library's onDoubleTap callback is not wired by default,
so double-tapping the PDF page did nothing — no scale change, no
hi-res re-render. Pass an onDoubleTap handler that calls
zoomState.toggleScale(2.5x, tapPosition). The scale animation runs
through the same Animatable snapshotFlow already observes, so the
debounced hi-res render kicks in automatically once the animation
settles.
2026-04-19 16:14:43 +00:00
Vitor Pamplona 5aa63c96a6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-19 12:11:12 -04:00
Vitor Pamplona a9fba25acd ignores the state when running marmot tests 2026-04-19 12:09:53 -04:00
Vitor Pamplona e5a87a83ae Adds a quick exit to the address parser 2026-04-19 12:09:35 -04:00
Vitor Pamplona be24dc34e4 Merge pull request #2448 from vitorpamplona/claude/unpin-deleted-posts-gTD4S
Add deleted items banner to bookmarks, pins, and bookmark groups
2026-04-19 11:55:43 -04:00
Claude b0f75d4dd4 fix(home-banner): float DVM status banner over the feed instead of in the top bar
Putting HomeDvmStatusBanner inside the top-bar Column meant the
SecondaryTabRow shifted up/down every time the banner appeared or
disappeared (filter selection, refresh, response arrival). Annoying.

Move the banner into a Box that wraps the HorizontalPager and align
it to TopCenter so it floats over the feed:
- topBar Column is back to just HomeTopBar + SecondaryTabRow; tabs
  no longer reflow when the banner toggles.
- BannerCard takes a Modifier and gets tonalElevation + shadowElevation
  + a slightly stronger surface tint so it reads as a floating overlay
  rather than part of the scaffold chrome.
2026-04-19 15:53:23 +00:00
Claude e244271bd0 feat(pdf): zoom-aware hi-res re-render for crisp pinch-zoom
Base bitmap stays at 3072 px for the initial render. When the user
zooms past 1.2x and scale settles for 200 ms, asynchronously re-render
the current page at (VIEWER_MAX_DIM_PX * scale) capped at 6144 px
(~100 MB peak for an A4 page). Swap the hi-res bitmap in while zoomed;
revert to base when scale drops back under threshold or the page
leaves composition. Only the currently-focused page gets the hi-res
treatment.

Also apply FilterQuality.High to the Image composable in both the
viewer and the preview card so any residual GPU upscaling uses
bicubic-ish sampling instead of bilinear.

Factored the render path into a shared renderPageCatching() helper.
2026-04-19 15:46:25 +00:00
Claude 93cf9f1d6e refactor(badges): drop OutlinedCard chrome from feed items
Each badge was wrapped in its own rounded OutlinedCard, which reads as
an out-of-place boxed widget in the feed since every other feed item
is a flat row separated by the standard HorizontalDivider drawn by
FeedLoaded.

Replace the Card with a plain Column + clickable + padding. The
feed's own divider handles item separation and the UI now matches
Notes, Articles, Pictures, etc.
2026-04-19 15:40:30 +00:00
Claude 3730d2fb73 fix(ui): keep chrome visible on chat detail screens
Chat detail screens (DMs, public chat, live-activity chat, ephemeral
chat, marmot group) have a compound layout that doesn't play well with
the scroll-under-bars model:

  Column {
      (optional) relay-warning header
      weight(1) Column { reverseLayout LazyColumn }
      Spacer
      PrivateMessageEditFieldRow   // stays above the keyboard, has
                                   // its own IME / nav-inset handling
  }

Making the messages scroll behind the top bar while the input field
stays planted above the keyboard (and the warning header sits below
the bar when present) requires a Box overlay + measured-input-height
trick that's incompatible with the existing Column + weight layout.
Most chat apps (Telegram, Signal, WhatsApp, etc.) keep chrome pinned
on a conversation screen anyway, so the simple and correct answer is
to just not hide the chrome here.

- New `allowBarHide: Boolean = true` parameter on `DisappearingScaffold`.
  When false, `canScroll` returns false and the NSC no-ops, which keeps
  the top bar pinned regardless of `isImmersiveScrollingActive()`.
- All 6 chat detail screens pass `allowBarHide = false`:
  - ChatroomScreen
  - ChatroomByAuthorScreen
  - PublicChatChannelScreen
  - EphemeralChatScreen
  - LiveActivityChannelScreen
  - MarmotGroupChatScreen

The existing `Column(Modifier.padding(it)...)` layout is correct when
the bars are pinned (no strip because the bar never translates), so no
other changes needed inside the chat views themselves.

https://claude.ai/code/session_01M3Bj24jLc9aVhMuvn55jXa
2026-04-19 15:37:21 +00:00