Commit Graph

11549 Commits

Author SHA1 Message Date
nrobi144 f698b31eee fix(multi-account): account removal now switches or logs out properly
removeAccountFromStorage was calling loadSavedAccount() which reads
from legacy last_account.txt — didn't trigger feed/relay reload.

Now calls switchAccount(nextNpub) for proper relay reconnection and
feed reload, or logout(deleteKey=true) if no accounts remain.

Also cleans up bunker ephemeral keys on removal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:12:40 +03:00
nrobi144 ec3a4adb46 refactor(multi-account): in-app overlay dialog for Add Account
Replaced DialogWindow (OS native window with title bar, minimize/maximize)
with Dialog composable (in-app overlay with scrim). Matches the
ComposeNoteDialog pattern used elsewhere in the desktop app.

- Card wrapper with 480dp width, 24dp padding
- Title row with 'Add Account' + Close (✕) icon
- No OS window chrome — clean in-app feel
- Click outside or ✕ to dismiss

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:00:43 +03:00
nrobi144 09cb08e5c7 perf(cache): O(1) author lookup for metadata invalidation
consumeMetadata() was looping ALL notes in cache (O(N)) to find notes
by the author whose metadata changed. With hundreds of cached notes,
this caused lag on every metadata event.

Added notesByAuthor index (ConcurrentHashMap<HexKey, MutableSet<Note>>)
populated at loadEvent time. consumeMetadata now looks up notes by
author in O(1) instead of scanning the entire cache.

Before: ~500 notes × per metadata event = lag
After: ~3 notes per author × per metadata event = instant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 09:50:12 +03:00
nrobi144 9a586de6c7 fix(feed): key toNoteDisplayData on metadataState for reactive name updates
Root cause: toNoteDisplayData(localCache) wasn't keyed on metadataState,
so Compose could skip recomputing it when metadata arrived for items
that were composed before metadata was available (first ~2 items).

Fix: remember(event, metadataState) { event.toNoteDisplayData(localCache) }
ensures display data is recomputed when the note's metadata flow
invalidates. Applied to both regular and repost note paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 09:39:52 +03:00
nrobi144 22e50cc852 perf(feed): viewport-aware metadata loading with batched author filter
Two optimizations for feed metadata loading speed:

1. Batched author filter (FeedMetadataCoordinator.loadMetadataBatched):
   Single Filter(kind:0, authors:[all visible]) instead of individual
   per-pubkey subscriptions through rate limiter. Closes after EOSE.
   Follows ChessRelayFetchHelper one-shot pattern. Max 100 authors/filter.

2. Viewport-aware scroll observation (FeedScreen):
   snapshotFlow reads LazyListState.visibleItemsInfo (zero recomposition)
   with 500ms debounce + distinctUntilChanged. Only fetches metadata for
   visible notes ± 10 item buffer. Initial load batches first 30 notes.

Before: 100+ authors × 20/sec rate limit × 7 relays = 5+ seconds
After: ~20 visible authors × 1 batched sub × 7 relays = <1 second

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 07:15:27 +03:00
nrobi144 a826918293 fix(feed): observe metadata flow on regular notes for display name updates
Pre-existing bug: regular (non-repost) notes in the feed never observed
note.flow().metadata.stateFlow. When metadata arrived from relays and
invalidateData() was called, the composable didn't recompose — author
names and avatars stayed as hex fallbacks.

The repost path already observed metadata correctly (line 130). Added
the same observation to the regular note path (line 211).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 07:15:22 +03:00
nrobi144 4b1d468ed0 fix(multi-account): fetch metadata for all accounts, persist display names
Display names weren't showing because metadata was never requested for
account pubkeys. Now:

1. LaunchedEffect triggers loadMetadataForPubkeys() for all accounts
   in the switcher once relays connect
2. metadataVersion collector persists display names for ALL accounts
   (not just active one) when metadata arrives from relays
3. Names available immediately on next dropdown open

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:18:13 +03:00
nrobi144 f4308ac56d fix(multi-account): saveCurrentAccount now works for all account types
saveCurrentAccount() previously returned failure for read-only and
bunker accounts, preventing them from being added to multi-account
storage. Now:
- Read-only (npub): skips private key save, still saves to storage
- Bunker: saves to storage (private key already saved during login)
- Internal (nsec): saves private key + storage (unchanged)

This was the root cause of npub-only accounts not appearing in the
switcher and accounts being lost when adding via AddAccountDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:09:59 +03:00
nrobi144 4c832c008c perf(multi-account): cache metadata and encryption key in memory
DesktopAccountStorage now caches AccountMetadata in memory after first
read. Subsequent loadAccounts/saveAccount/setCurrentAccount serve from
cache, only hitting disk on writes. Also caches AES encryption key to
avoid repeated OS keychain lookups.

Before: 3 decrypts + 1 encrypt + 4 keychain calls per login/switch
After: 1 decrypt + 1 keychain call on first access, then memory only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:07:56 +03:00
nrobi144 5a1b9d445c fix(multi-account): sequential account save in AddAccountDialog, debounced metadata
Root cause of accounts not persisting:
ensureCurrentAccountInStorage() was fire-and-forget (scope.launch),
so loginWithKey() ran before the old account was saved. The old account
was lost. Now all steps run sequentially in one coroutine.

UI hanging fix:
metadataVersion LaunchedEffect fired on every metadata event, doing
encrypted file I/O each time. Now debounced with 2s delay so it only
writes once after a batch of metadata settles.

Also:
- loadInternalAccount falls back to read-only (test updated)
- bunker/nostrconnect paths also await ensureCurrentAccountInStorage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 10:04:23 +03:00
nrobi144 156391ec0a fix(multi-account): persist display names in encrypted storage, fix npub-only fallback
Three fixes:

1. Display names now stored in AccountInfo/AccountInfoDto and persisted
   in encrypted storage. Populated when metadata arrives from relays via
   metadataVersion LaunchedEffect. Available immediately on next open.

2. loadInternalAccount falls back to loadReadOnlyAccount when no private
   key found in SecureKeyStorage — fixes npub-only accounts that were
   saved as SignerType.Internal from earlier sessions.

3. Dropdown uses persisted displayName first, cache lookup as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:54:08 +03:00
nrobi144 1032db0c05 fix(multi-account): npub-only login stores ViewOnly type, reactive display names
Root cause of npub-only switch failure:
loginWithKey() with pubkey created AccountState with signerType=Internal
(default). switchAccount then called loadInternalAccount which requires
a private key from SecureKeyStorage → failed. Now sets ViewOnly.

Display names not showing:
- resolveDisplayName() ran at composition time but metadata hadn't
  loaded from relays yet. Dropdown never recomposed when it arrived.
- Added metadataVersion counter to DesktopLocalCache, incremented on
  each consumeMetadata(). Dropdown collects it to trigger recomposition
  when user names become available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:48:16 +03:00
nrobi144 29aa1d6c9c fix(multi-account): npub-only switching + reactive display names
- Add loadReadOnlyAccount() for ViewOnly signer type switching
  (was mapping to loadInternalAccount which requires private key)
- Remove remember() from display name resolution — now re-evaluates
  each recomposition so names appear once metadata loads from relays
- Only show subtitle (npub row) when display name is available;
  if no display name, show npub as title only
- Signer type badge shown as subtitle when no display name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:41:51 +03:00
nrobi144 250bb5a1ad feat(multi-account): display names, middle-truncated npub, npub-only account fix
Account switcher dropdown improvements:
- Two-row display: Display Name on top, npub (middle-truncated) below
  e.g. 'Alice' / 'npub1abc...wxyz · Bunker'
- Middle-truncation for npub: shows first 10 + last 6 chars
- Resolves display names from DesktopLocalCache user metadata
- Confirmation dialog also shows display name
- npub-only (view-only) accounts now persist to encrypted storage
  (ensureCurrentAccountInStorage called in onLoginSuccess)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 09:35:33 +03:00
nrobi144 66310ca336 fix(multi-account): reset lastContactListCreatedAt on cache clear
Root cause of 'follows at 0 after 2+ switches':

DesktopLocalCache.clear() reset _followedUsers to empty but did NOT
reset lastContactListCreatedAt. On re-subscribe after account switch,
the contact list event arrived with the same timestamp, was rejected
by the 'event.createdAt <= lastContactListCreatedAt' guard, and
followedUsers stayed empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 15:00:21 +03:00
nrobi144 c6b2618fd6 fix(multi-account): clear subscriptions and cache on account switch
When switching accounts, the feed showed stale data from the previous
account because subscriptions and LocalCache weren't cleared.

Now tracks previousAccountPubKey and when it changes:
1. Clears all relay subscriptions (subscriptionsCoordinator.clear())
2. Clears local event cache (localCache.clear())
3. Restarts subscriptions coordinator

Feed composables then resubscribe with the new account's pubkey
via their existing rememberSubscription(account, ...) keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:23:15 +03:00
nrobi144 0fb7c659f2 fix(multi-account): persist loaded account to encrypted storage on startup
loadSavedAccount() reads from legacy files (last_account.txt) but never
wrote to the encrypted multi-account storage, so the account switcher
dropdown was always empty after restart.

Now calls ensureCurrentAccountInStorage() + refreshAccountList() after
successful load so the current account appears in the dropdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:19:04 +03:00
nrobi144 573b478bbc fix(multi-account): preserve existing account when adding new one
- ensureCurrentAccountInStorage(): saves current account metadata to
  encrypted storage before switching to new account via AddAccountDialog
- AddAccountDialog calls ensureCurrentAccountInStorage() before each
  login method to prevent losing the previous account
- LoginScreen onLoginSuccess now calls refreshAccountList() so the
  switcher dropdown picks up the initial account immediately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 14:16:47 +03:00
nrobi144 b1098368a1 feat(multi-account): add account switcher to single-pane layout, scrollable dropdown
- Account switcher now appears at bottom of NavigationRail in single-pane mode
  (after tor indicator), matching placement in deck sidebar
- AddAccountDialog wired in single-pane mode too
- Dropdown scrollable with 400dp max height for many accounts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 13:12:55 +03:00
nrobi144 82ec892296 feat(multi-account): add AddAccountDialog and per-account logout
- AddAccountDialog: DialogWindow (480×600) wrapping existing LoginCard
  with back button, "Import Account" title. Supports nsec, npub, bunker,
  nostrconnect. New account becomes active immediately after login.
- Per-account logout: ✕ button on each account row in dropdown, with
  AlertDialog confirmation before removal.
- Wired both into DeckSidebar and Main.kt.

Matches Android's AddAccountDialog pattern (full-screen dialog with
LoginOrSignupScreen) adapted for desktop (DialogWindow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 12:26:00 +03:00
nrobi144 0da51d5ae3 refactor(multi-account): remove legacy migration code
Migration from single-account files didn't work reliably.
Removed migrateFromLegacyFiles() and migrateAndLoadAccounts().
Account list now populated only via saveCurrentAccount/saveBunkerAccount
when user logs in — no automatic migration from old format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:18:06 +03:00
nrobi144 43b6d80ea6 fix(multi-account): address review findings — per-account bunker keys, resource cleanup, simplifications
Fixes from code-simplicity and architecture reviews:

Critical fixes:
- Per-account bunker ephemeral key alias (bunker_ephemeral_<npub>) instead of
  shared alias. Prevents wrong-identity handshake when switching bunker accounts.
  Falls back to legacy alias for existing single-account setups.
- Resource cleanup in switchAccount(): closes NIP-46 subscription, stops
  heartbeat, disconnects NIP-46 client before transitioning to new account.
- Manages signer connection state on switch (Connected for bunker, NotRemote otherwise).

Simplifications:
- Removed unused createWithStorage() factory (zero callers)
- Removed loadViewOnlyAccount() dead code (no UI path to create view-only accounts)
- Removed unused allAccounts collection at Window scope (duplicated in MainContent)
- Files.move with REPLACE_EXISTING for cross-platform atomic rename
- Remote signer with empty bunkerUri falls back to Internal (prevents corrupt state)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:36:10 +03:00
nrobi144 0a378c7c4a feat(multi-account): add account switcher dropdown in sidebar
Phase 4: account switcher UI.

- AccountSwitcherDropdown composable (person icon → dropdown menu)
- Shows all accounts with active checkmark and signer type label
- "Add Account" button at bottom with divider
- DropdownMenu with DpOffset(48dp) to expand right of sidebar
- DeckSidebar now takes account params (activeNpub, allAccounts, callbacks)
- Replaces "A" logo text with account switcher button
- Migration called on startup to populate account list
- Account switch wired through to AccountManager.switchAccount()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:20:10 +03:00
nrobi144 9c6502b089 feat(multi-account): add account switching, storage, and migration to AccountManager
Phase 3: wire multi-account lifecycle into desktop AccountManager.

- Add DesktopAccountStorage field and allAccounts StateFlow
- switchAccount(): loads new account BEFORE cancelling old state
  (prevents unrecoverable partial failure per coroutines review)
- addAccountToStorage / removeAccountFromStorage for CRUD
- loadViewOnlyAccount() for npub-only accounts
- migrateAndLoadAccounts() for legacy file migration on startup
- saveCurrentAccount / saveBunkerAccount now save to encrypted storage
- Updated test mock setup to support SecureKeyStorage key store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:15:50 +03:00
nrobi144 197db2361d feat(multi-account): add DesktopAccountStorage with AES-256-GCM encryption
Phase 2: encrypted persistence for multi-account metadata.

- DesktopAccountStorage implements AccountStorage interface
- AES-256-GCM encryption with random key stored in OS keychain
- Jackson DTOs for flat serialization (no polymorphic complexity)
- Migration helper from legacy single-account files
- Atomic writes via temp file + rename
- POSIX file permissions (owner-only read/write)
- 11 unit tests covering roundtrip, encryption, deletion, migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 07:34:07 +03:00
nrobi144 aa33b7d271 feat(multi-account): extract SignerType, AccountInfo, AccountStorage to commons
Phase 1 of multi-account support. Move account types to commons/commonMain
so both Android and Desktop share the same data model:

- SignerType sealed class (Internal, Remote, ViewOnly)
- AccountInfo data class (npub, signerType, isTransient)
- AccountStorage interface (loadAccounts, saveAccount, etc.)

Desktop AccountManager now imports SignerType from commons instead of
defining its own. All existing tests updated with new import path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 07:29:52 +03: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 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 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