Commit Graph

10262 Commits

Author SHA1 Message Date
Vitor Pamplona bba4d28e4b stable toast manager 2026-03-26 14:35:31 -04:00
Vitor Pamplona d0a3679778 Minimizes the size of the banner 2026-03-26 14:35:18 -04:00
Vitor Pamplona 423bc04dba Fixes logging 2026-03-26 13:54:46 -04:00
Vitor Pamplona 9f7672249b Otherwise, it is created by the main thread when the AccountViewModel Factory is run 2026-03-26 13:54:33 -04:00
Vitor Pamplona 79d66cca8f Merge pull request #1952 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 11:41:15 -04:00
Crowdin Bot 7cb9cc6cd5 New Crowdin translations by GitHub Action 2026-03-26 15:37:20 +00:00
Vitor Pamplona 7d6f1a539f Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
  fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
  refactor: remove Namecoin label from search results
  feat: show Namecoin label on .bit search results without ViewModel state
  refactor: remove Namecoin label from search results per review
  refactor: simplify .bit search resolution per review feedback
  feat: resolve bare .bit domains and show Namecoin resolution status in UI
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  refactor: rename trustAllCerts → usePinnedTrustStore
  fix: harden TLS for GrapheneOS and security-conscious Android ROMs
  doc: verify .onion ElectrumX cert via Tor, document pinning
  fix: bypass cert pinning for .onion ElectrumX servers
  feat: TOFU cert pinning for custom ElectrumX servers
  feat: add Test Connection diagnostics to Namecoin settings
  fix: pin ElectrumX server certs instead of trust-all TrustManager

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
2026-03-26 11:34:54 -04:00
Vitor Pamplona 155e57ab11 Defers most module creations to when they are needed via lambdas 2026-03-26 11:11:44 -04:00
Vitor Pamplona 7952f558eb Parallelizes the JSON parsing from stored account. 2026-03-26 11:11:21 -04:00
Vitor Pamplona 958a460ea9 Adds a try/catch to make sure this procedure unsubscribes from the req 2026-03-26 11:10:45 -04:00
Vitor Pamplona 3776f65179 Defers the creation of the geohashStateFlow until needed 2026-03-26 11:08:27 -04:00
Vitor Pamplona 44c39b01ea Defers NIP05 Resolver/NamecoinResolver Builder until needed 2026-03-26 11:07:56 -04:00
Vitor Pamplona d73dd662c6 Reduces calculations and state creations in the NavHost builder 2026-03-26 11:03:07 -04:00
Vitor Pamplona 4ef1ba1ac6 Fixes the bug of not sending NIP-17 reactions in the complete UI mode 2026-03-26 09:25:09 -04:00
Vitor Pamplona 276dc6589c Merge pull request #1937 from mstrofnone/fix/samsung-oneui7-namecoin-tls
fix: pin ElectrumX server certs for Samsung One UI 7 (Android 16) compatibility
2026-03-26 09:14:30 -04:00
Vitor Pamplona f9c381a113 Merge pull request #1951 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-03-26 08:38:12 -04:00
Crowdin Bot b77e6c1b6c New Crowdin translations by GitHub Action 2026-03-26 10:57:57 +00:00
davotoula 17eea1dca9 update translations: CZ, DE, PT, SE 2026-03-26 11:53:59 +01:00
M 7293b0985b fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
The local private WatchAndDisplayNip05Row uses NonClickableObserveAndDisplayNIP05
(plain text domain), while the shared one in ShowQRScreen uses the clickable
ObserveAndDisplayNIP05. They are not the same function — restore the local copy
to preserve the correct non-clickable behavior in user suggestions.
2026-03-26 17:36:53 +11:00
nrobi144 9c9c7d3f24 feat(cache): seed NotificationsScreen from cache on compose
- Add localCache param to NotificationsScreen
- Seed reactions and zaps from cache on initial compose
- Notifications now persist across tab navigation
- All screens now have cache seeding (Feed, Thread, Profile,
  Bookmarks, Reads, Notifications)
2026-03-26 07:20:22 +02:00
nrobi144 072e437277 feat(cache): wire State classes on DesktopIAccount for GC retention
- Add BookmarkListState, Kind3FollowListState, Nip65RelayListState
  to DesktopIAccount — pins important AddressableNotes via strong refs
- Create stub Kind3FollowListRepository + Nip65RelayListRepository
  (no persistence yet, null backups)
- Update followingKeySet() to read from Kind3FollowListState.flow
- Revert accidental long-form branch changes in DeckColumnContainer
2026-03-26 07:17:39 +02:00
nrobi144 2b95cc013a feat(cache): extract Kind3FollowListState + Nip65RelayListState to commons
- Add Kind3FollowListState to commons/commonMain with ICacheProvider
  + Kind3FollowListRepository interface for settings needs
- Add Nip65RelayListState to commons/commonMain with ICacheProvider
  + Nip65RelayListRepository interface with default relay sets
- Per-feature repository interfaces match existing EphemeralChatRepository
  / PublicChatListRepository pattern in commons
- Android originals unchanged — Desktop will use commons versions
- Static LocalCache.justConsumeMyOwnEvent calls replaced with cache param
2026-03-26 07:14:16 +02:00
nrobi144 7151d3052a feat(cache): extract BookmarkListState to commons/commonMain
- Move BookmarkListState from amethyst/ to commons/commonMain/
- Change cache param from LocalCache to ICacheProvider
- Android file becomes typealias to commons version
- No settings or decryptionCache dependencies (simplest State class)
- Pins bookmarkList AddressableNote via strong ref for GC retention
2026-03-26 07:14:16 +02:00
nrobi144 cf6a74b162 feat(cache): add periodic memory cleanup for Desktop
- Add cleanMemory() to DesktopLocalCache (sweeps stale WeakRef entries)
- Add startCleanupLoop() to Coordinator with heap monitoring
  (MemoryMXBean, checks every 30s, cleans at >75% heap or every 5min)
- cleanObservers() clears unused NoteFlowSets on notes + addressables
- Try-catch per operation so one failure doesn't skip the rest
- 2-minute startup grace period before first cleanup
- Cleanup job cancelled on clear() (account switch / logout)
2026-03-26 07:14:15 +02:00
nrobi144 3dbbc039b3 feat(cache): replace BoundedLargeCache with LargeSoftCache on Desktop
- DesktopLocalCache now uses LargeSoftCache (WeakReference-based, GC-driven)
  instead of BoundedLargeCache (strong refs, 50k cap, arbitrary eviction)
- Delete BoundedLargeCache.kt — no longer needed
- Rewrite findUsersStartingWith to use forEach instead of values()
- Remove BoundedLargeCache eviction tests (no longer applicable)
- Notes now only disappear when nothing references them, not arbitrarily

Per Vitor's feedback: "this maximum size approach might not work well,
as things will just disappear"
2026-03-26 07:13:27 +02:00
nrobi144 00b06a0e2a refactor(cache): move LargeSoftCache to commons/jvmAndroid, fix ICacheProvider types
- Move LargeSoftCache from amethyst/model to commons/jvmAndroid/model/cache
  so both Android and Desktop share the same WeakReference cache implementation
- Change ICacheProvider return types from Any? to proper types (User?, Note?)
  since these model classes already live in commons
- Remove unnecessary casts in ThreadAssembler, ChatNewMessageState, SearchBarState
- Improve LargeSoftCache.cleanUp() to use single-pass iterator (zero allocation)
- Update Android imports in LocalCache, LargeSoftCacheAddressExt, and test

Per Vitor's feedback on PR #1905: BoundedLargeCache evicts arbitrarily.
LargeSoftCache uses WeakReferences so GC respects the reference graph.
2026-03-26 07:13:27 +02:00
nrobi144 3b489fb3cb feat(cache): seed Reads and Bookmarks screens from cache on compose
Both screens now show cached data instantly on navigation instead of
showing loading states while re-fetching from relays.

- BookmarksScreen: reads cached BookmarkListEvent from addressableNotes,
  seeds public bookmark events from notes cache
- ReadsScreen: seeds long-form notes (kind 30023) from notes cache

Relay subscriptions still run to fetch fresh data in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144 f27c8811d5 fix(cache): don't reset followersCount to 0 on subscription restart
Cached value now stays visible until relay data exceeds it, preventing
the count jumping from cached→0→ticking back up on each navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144 f2169e0236 feat(cache): cache profile metadata and follower/following counts
Profile screen now reads initial values from cache so navigating back
shows data instantly instead of re-fetching everything from scratch.

- Seed displayName/about/picture from User.metadataOrNull() on compose
- Add followerCounts/followingCounts maps to DesktopLocalCache
- Write counts on each update, read on profile open
- Follower count still ticks up live (good UX) but starts from cached value
- 4 new tests for profile count caching + metadata cache reads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144 3e41bab5d5 fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions
Root cause of "0 relays, 0 notes, 0 follows": all screens used
connectedRelays as the relay set for subscriptions, but NostrClient
uses relay-on-demand — relays only connect when openReqSubscription
is called with their URL. This created a deadlock: screens waited for
connected relays, but relays only connect when screens subscribe.

Fix: use relayStatuses.keys (registered/available relays) which are
populated by addDefaultRelays() at startup. openReqSubscription
triggers connection on-demand.

Affected screens: FeedScreen, UserProfileScreen, ThreadScreen,
NotificationsScreen, BookmarksScreen, ReadsScreen, NewDmDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:27 +02:00
nrobi144 1e1bc1b172 fix(cache): add missing relay subscriptions for FeedScreen and UserProfileScreen
FeedScreen and UserProfileScreen used DesktopFeedViewModel (cache-backed)
but never subscribed to relays for kind 1/kind 3 events, causing 0 notes,
0 followed, and 0 relays on the home feed.

Adds rememberSubscription calls for:
- Contact list (kind 3) in FeedScreen → populates followedUsers
- Global/following feed (kind 1) in FeedScreen → populates cache
- Profile notes (kind 1) in UserProfileScreen → populates cache

Also adds 33 integration tests covering the full cache pipeline:
- DesktopCachePipelineTest: cache → filter → ViewModel (25 tests)
- CoordinatorPipelineTest: coordinator → cache → ViewModel (8 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 0621e8f7c2 refactor(cache): P3 simplifications — remove dead code, reduce abstractions
- Replace SubscriptionHealth map + data class with single lastEventAt
  Long? timestamp (only consumer was RelayHealthIndicator taking max())
- Replace consumer HashMap registry with when block (safe casts,
  compiler-checked, 9 kinds doesn't benefit from O(1) lookup)
- Remove dead loadReactionsForNotes() (superseded by requestInteractions)
- Remove unused BoundedLargeCache methods: containsKey, isEmpty,
  mapNotNull, forEach (zero external callers)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 4f5665a2bd fix(cache): address review findings — subscription churn, O(n) size, atomicity
P1 fixes from compound engineering review:
- Fix subscription churn flooding relays: DisposableEffect keyed on
  feedMode/noteId (stable) instead of feedState (changes every 250ms)
- BoundedLargeCache: AtomicInteger counter replaces O(n)
  ConcurrentSkipListMap.size() — eliminates 50K traversal per event
- Non-atomic updateHealth(): use MutableStateFlow.update{} for
  atomic compare-and-set instead of read-modify-write race

P2 fixes:
- DesktopThreadFilter: LinkedHashSet for O(1) containment checks,
  down from O(R²) with MutableList
- consumeContactList: createdAt guard ensures newer events always win
- consumeEvent error logging: include event ID and relay URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 c598c6135c feat(cache): ThreadScreen → DesktopFeedViewModel + DesktopThreadFilter
Replace EventCollectionState + 7 rememberSubscription blocks + inline
count maps with DesktopFeedViewModel + DesktopThreadFilter (graph walk).
Keep root note + replies relay subscriptions to populate cache. Use
FeedNoteCard for both root and reply notes (reads counts from Note.flowSet).
Remove ~200 lines of inline state management (zapsByEvent, reactionIdsByEvent,
replyIdsByEvent, repostIdsByEvent, bookmarkList, bookmark subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 8c1e54e147 feat(ui): relay health indicator in NavigationRail
Add RelayHealthIndicator component — shows elapsed time since last
relay event. Hidden when <30s (healthy), shows "45s ago" / "3m ago"
when stale. Placed above BunkerHeartbeatIndicator in NavigationRail.
Reads from Coordinator's subscriptionHealth StateFlow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 50eb03bc3e feat(cache): UserProfileScreen notes feed → DesktopFeedViewModel
Replace EventCollectionState + createUserPostsSubscription + manual cache
hydration with DesktopFeedViewModel + DesktopProfileFeedFilter. Profile
header subscriptions (metadata, contact list, followers) kept as-is since
they're not feed concerns. DisposableEffect cleanup on profile switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 c63ace1d9c feat(cache): FeedScreen → DesktopFeedViewModel + Note-based FeedNoteCard
Rewrite FeedScreen from inline EventCollectionState + 8 rememberSubscription()
calls to cache-centric DesktopFeedViewModel pattern:

- ViewModel keyed on feedMode with DisposableEffect cleanup (destroy())
- FeedState pattern matching (Loading/Loaded/Empty/Error)
- FeedNoteCard takes Note instead of Event, reads counts from Note.flowSet
- DisposableEffect for note.clearFlow() cleanup in LazyColumn
- DisposableEffect for Coordinator interaction subscription lifecycle
- LaunchedEffect for rate-limited metadata loading via Coordinator
- Extract FeedHeader into separate composable
- Add destroy() to DesktopFeedViewModel (ViewModel.clear() is internal in KMP)
- Update UserProfileScreen to look up Note from cache for FeedNoteCard

Removes ~350 lines of inline state management (EventCollectionState,
zapsByEvent, reactionIdsByEvent, replyIdsByEvent, repostIdsByEvent,
followedUsers, bookmarkList, 8 rememberSubscription blocks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:26 +02:00
nrobi144 e8ede21c17 feat(cache): Coordinator error handling + interaction subscriptions
Add try-catch in consumeEvent() so one bad event doesn't kill the
pipeline. Add requestInteractions()/releaseInteractions() with Job
tracking via ConcurrentHashMap for screen-scoped subscription lifecycle.
Add SubscriptionHealth tracking (lastEventReceivedAt, eoseReceived)
exposed as StateFlow for UI consumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144 684ba158ab feat(cache): kind-based consumer registry + new event types
Replace when-dispatch with HashMap<Int, Handler> for O(1) event routing.
Add consume methods for: RepostEvent (kind 6), ContactListEvent (kind 3),
LongTextNoteEvent (kind 30023), BookmarkListEvent (kind 30001).
Replace @Volatile followedUsers with MutableStateFlow for thread safety
and Compose observability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144 e834dbd1e5 fix(ui): show display names instead of npubs in feed
toNoteDisplayData() now reads User.toBestDisplayName() from cache
instead of always showing npub. Falls back to npub if no metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144 e80473a7d9 feat(cache): Phase 3 — route all screen events through cache
All screens now pipe relay events into DesktopLocalCache via
coordinator.consumeEvent(). Existing per-screen EventCollectionState
kept for rendering (incremental migration), but data also enters
cache for persistence across navigation.

- FeedScreen: route global + following feed events
- ThreadScreen: route root note + reply events
- UserProfileScreen: route posts + add cache hydration on mount
  (LaunchedEffect queries cache for existing posts → instant display)
- BookmarksScreen: route public + private bookmark events
- ReadsScreen: route long-form feed events (add coordinator param)
- NotificationsScreen: route notification events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:13:25 +02:00
nrobi144 c198df57fd feat(cache): Phase 2+3 foundation — FeedFilters + DesktopFeedViewModel
Phase 2: 8 FeedFilter implementations:
- DesktopGlobalFeedFilter (kind 1, limit 2500)
- DesktopFollowingFeedFilter (kind 1 + followed pubkeys)
- DesktopThreadFilter (root + reply graph walk)
- DesktopProfileFeedFilter (notes by pubkey, limit 1000)
- DesktopBookmarkFeedFilter (notes by ID set)
- DesktopReadsFeedFilter (kind 30023, limit 500)
- DesktopNotificationFeedFilter (events tagging user)
- DesktopSearchFeedFilter (content text search)

All use AdditiveFeedFilter for incremental updates (O(batch)
after initial load). Limits are 5x Android's.

Phase 3 foundation: DesktopFeedViewModel subclass that calls
refreshSuspended() on init to load existing cache data —
fixes navigation persistence (back shows instant data).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144 543f7a329c feat(cache): Phase 1 — cache-centric architecture foundation
- Add BoundedLargeCache: LargeCache wrapper with size enforcement
  (50k notes, 25k users, 10k addressable). Lock-free reads via
  ConcurrentSkipListMap, evicts 10% when cap exceeded.

- Switch DesktopLocalCache from ConcurrentHashMap to BoundedLargeCache.
  Exposes filterIntoSet, values, etc. matching Android's query patterns.

- Add consume methods for kinds 1 (TextNote), 7 (Reaction),
  9734 (ZapRequest), 9735 (Zap). Uses tagsWithoutCitations() for
  reply parsing, originalPost()+taggedAddresses() for reactions,
  zappedPost() for zaps. Follows Android LocalCache pattern.

- Add consumeEvent() bridge in coordinator with BasicBundledInsert
  (250ms batching). Routes relay onEvent callbacks to cache.

- Fix SharedFlow: extraBufferCapacity=64, DROP_OLDEST. Prevents
  blocking emitters and dropped events.

- Set -Xmx2g JVM arg for long desktop sessions.

- Clear cache on logout (coordinator first, then cache).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144 61119e6916 docs: deepen cache plan — BoundedLargeCache, FeedNoteCard rewrite details
- Resolve LruCache vs LargeCache: use LargeCache (lock-free ConcurrentSkipListMap)
  with BoundedLargeCache wrapper for size enforcement on put()
- Confirm LargeCache available on desktop via quartz jvmAndroid source set
- Detail FeedNoteCard rewrite: Note model field mapping, 5 subscription removals
- Fix filter examples to use filterIntoSet (LargeCache API)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144 9c160006fa docs: desktop cache architecture brainstorm and plan
Cache-centric architecture for navigation persistence, mirroring
Android Amethyst's pattern. Three-phase incremental migration:
Phase 1 - Store events in DesktopLocalCache with LRU eviction
Phase 2 - Create FeedFilter implementations
Phase 3 - Migrate screens to FeedViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:45 +02:00
Claude 2f8e62fda5 feat: implement NIP-86 Relay Management API
Add quartz protocol package for NIP-86 JSON-RPC relay management with
NIP-98 HTTP authorization, and a management UI accessible from the
relay information screen when the relay advertises NIP-86 support.

Quartz (nip86RelayManagement/):
- Nip86Method: all 21 method constants from the spec
- Nip86Request: serializable JSON-RPC request with factory methods
- Nip86Response: response model with typed result parsing helpers
- Nip86Client: builds NIP-98 auth headers, serializes requests,
  parses responses for each result type (pubkeys, events, kinds, IPs)

Amethyst:
- Nip86Retriever: OkHttp-based HTTP executor for NIP-86 calls
- RelayManagementViewModel: state management for all NIP-86 operations
- RelayManagementScreen: tabbed UI (Pubkeys, Events, Kinds, IPs,
  Settings) with add/remove dialogs, moderation queue, and relay
  settings fields
- Route.RelayManagement added to navigation
- "Manage Relay" button shown in RelayInformationScreen top bar when
  relay's supported_nips includes "86"

https://claude.ai/code/session_01QckCAm1T8pJqqmURBiNtaQ
2026-03-26 04:32:43 +00:00
Claude 70baf55e1c feat: add NIP-77 negentropy sync support
Add support for NIP-77 (Negentropy Syncing) protocol messages using
the negentropy-kmp library for efficient set reconciliation between
client and relay.

New files:
- NegOpenCmd, NegMsgCmd, NegCloseCmd: Client-to-relay commands
- NegMsgMessage, NegErrMessage: Relay-to-client messages
- NegentropySession: Client-side reconciliation orchestrator
- NegentropyServerSession: Server-side reconciliation handler
- NegentropyManager: High-level sync manager with listener callbacks

Updated serializers:
- Jackson MessageSerializer/Deserializer for NEG-MSG, NEG-ERR
- Jackson CommandSerializer/Deserializer for NEG-OPEN, NEG-MSG, NEG-CLOSE
- Kotlin MessageKSerializer/CommandKSerializer for all NIP-77 types

Compatible with strfry relay's negentropy implementation via the
negentropy-kmp library which implements the same Protocol V1 spec.

https://claude.ai/code/session_01Dc6W1G1jURAAR9kzyVvk6g
2026-03-26 04:23:32 +00:00
Claude 463938768e feat: add NIP-62 Request to Vanish screen
Add a new screen allowing users to send NIP-62 relay deletion requests.
Users can select a specific relay or ALL RELAYS, pick a date (data
created before that date will be requested for deletion), and provide
an optional reason. Includes confirmation dialog with clear warnings
about the irreversible nature of vanish requests.

https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
2026-03-26 04:18:39 +00:00
M ebbcfa26fb refactor: remove Namecoin label from search results
Per review: .bit resolution is just NIP-05, no special labeling needed.
2026-03-26 10:25:31 +11:00
M e7cde18371 feat: show Namecoin label on .bit search results without ViewModel state
Derive the label from the existing searchTerm — if it ends with .bit,
show a chain-emoji label above each user result. No new StateFlow or
ViewModel state management needed.
2026-03-26 09:49:34 +11:00