The four families that read one relay-set state off `Account` now carry that set as a `StateFlow` on their key, the way `SearchQueryState` does: `ProfileAppRecommendationsQueryState(outboxRelays, defaultGlobalRelays)`, `ProfileBadgesQueryState(notificationRelays)`, `ConnectedAppsQueryState(homeRelays)` and `ThreadQueryState(defaultRelays)`. Thread also takes the cache: `ThreadFilterAssembler(cache, client)`, and its two sub-assemblers run `ThreadAssembler(cache)`. The home feed moves whole. Its `FilterHomePosts*` dispatchers were already import-clean; `HomeOutboxEventsEoseManager` becomes a `TopNavFeedSubAssembler<HomeQueryState>` whose key carries the new-threads and replies floors plus `enabledHomeFeedTypes`. The Settings › Home toggle stays an unsampled invalidator (still `drop(1)`) via `extraInvalidators`, and the disabled-kinds stripping is unchanged. Same deltas as the other feeds: watchers on the screen scope, follows sampler 500 ms (was 1000 ms). The commented-out alternative sub-assemblers in `HomeFilterAssembler` are dropped. The app keeps one `*Subscription.kt` per feature, building the key from `AccountViewModel`. The algo-feed filter test moves to commons commonTest on `kotlin.test`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J836W9ZjSUJUQ1d23TpiJo
54 KiB
Full sweep: amethyst/ → :commons migration candidates
Execution status (updated 2026-08-30, same branch): Waves 0-1 are DONE on this branch — the 12 shim deletions, the 38-file relayClient batch (with
AccountScopedQuerygeneralized toIAccount), the okhttp stack →commons/service/http, 37 model/service singles, theIFeedTopNavFilter→ICacheProvidersignature fix,TopFilterextracted out ofAccountSettings.kt, and 34 topNavFeeds files reunited with their commons half. All app/desktop/cli targets compile. Corrections found while executing are folded into the sections below; the biggest one: import-graph analysis under-counts blockers — same-package files useAccount/LocalCache/each other without imports (extension receivers included), so several "clean" files (AccountMarmotActions, EventBroadcaster, ParticipantListBuilder, UnexpectedCrashSaver, the Blossom fetchers) are actually Account/LocalCache-coupled and stayed. Theui/theme+ui/layoutsbatch also does NOT move mechanically:Theme.ktis Android-coupled (Activity, UiModeManager, app font/theme prefs enums) and the layouts sit on app-side theme constants +stringRes— that whole cluster belongs to the strings/theme wave.Refined
LocalCacherecipe (next big step, needs maintainer input): the move-group isLocalCache+AntiSpamFilter(android LruCache → androidx.collection) +CachePruner+CacheSearch+MiniFhir+OnchainZapResolver, into commons jvmAndroid (which legalizes itsjava.io.FileNIP-95 spill as-is). Seams to cut:Amethyst.instance(2 sites → injected scope), appisDebug(2 → settable flag),checkNotInMainThread(→ settable hook or expect),ui.note.dateFormatter(1 log line). The open design question:CachePruner/CacheSearchcallAccount.isFollowing(...)and readaccount.hiddenUsers.flow.value. hiddenWordsCase—IAccountalready hasisHidden/hiddenWordsCasebut lacksisFollowing, so eitherIAccountgrows it (DesktopIAccount must implement) or the pruner/search take a narrower ISP interface.
Date: 2026-08-30
Scope: every Kotlin source file in the amethyst Android module
(amethyst/src/main, 2,347 files), audited for "can and should move to
:commons", cross-checked against what commons/ and desktopApp/ already
contain.
Method
- Static classification of all 2,347 files by imports:
- Android-dirty = imports
android.*,com.google.*, orandroidx.*other than the KMP-safe set (compose,lifecycle,annotation,paging,collection), or usesandroidx.navigation. - R-dirty = imports
com.vitorpamplona.amethyst.Ror referencesR.string/R.drawable/R.raw(incl. viastringRes).
- Android-dirty = imports
- Transitive closure over the module-internal import graph: a file is Tier A iff it and everything it references inside the module are clean of both. Tier B = clean once its strings move to commons Compose resources.
- Six deep area audits (model, service, service/relayClient+okhttp, ui shared components, ui/screen, napplet/connectedApps/misc) verifying verdicts file-by-file and hunting for desktopApp reimplementations.
Headline numbers
| Metric | Count |
|---|---|
Files in amethyst/src/main |
2,347 |
| Directly Android/R-clean | 1,324 |
| Tier A — transitively clean, movable now | 509 |
| Tier B — movable once their strings migrate | +21 |
| Files that genuinely touch an Android API | ~14% of ui/ |
| ViewModels app-side vs migrated to commons | 138 vs 11 |
| Strings migrated to commons Compose resources | 152 / 4,417 |
The dominant blockers are not Android APIs. They are five hub types that are themselves Android-import-free but still live in the app module, plus the string-resource bridge:
| Blocker | Files it transitively blocks | Nature |
|---|---|---|
model.Account |
254 | Android-clean god-object (184 vals, 218 funs) — decompose, don't move |
ui.screen.loggedIn.AccountViewModel |
180 | genuinely Android-coupled (9 android imports, 36 R refs) — shrink, don't move |
ui.navigation.navs.INav / routes.Route |
173 | platform nav — correctly stays |
model.LocalCache |
115 | Android-clean; only 3 trivial dirty deps |
ui.stringRes (StringResourceCache) |
~210 direct users | needs a Compose-resources twin |
model.TopFilter (declared inside AccountSettings.kt!) |
39 | clean sealed class in the wrong file |
The five highest-leverage edits
Each of these is small and unlocks a large batch:
service/relays/EOSE.kt→ finish the move. 4 of its 6 declarations are already typealiases intocommons/relays;SincePerRelayMap(androidx.collection.LruCache— KMP-safe) andEOSEAccountKeyremain. Moving them unblocks ~145subassemblies/filter files at once (97% of that layer is already Tier A).IFeedTopNavFiltersignature fix.model/topNavFeeds/IFeedTopNavFilter.kt:34,36hard-codescache: LocalCacheintoPerRelayFlow/startValue, poisoning all 23*TopNavFilterimplementors. Change to the existing commonsICacheProviderport → the app half oftopNavFeedsreunites with the 25-file half already incommons/model/topNavFeeds/.- Move
LocalCache(3,980 lines). It'sobject LocalCache : ILocalCache, ICacheProvider, Daowith zero Android imports. Its entire dirty-dep set:MainThreadChecker(Looper →expectno-op on JVM), adateFormatterlog line,Amethyst.instancescope (inject),BundledInsert(commonsBundledUpdatealready exists), NIP-95java.io.Fileblob spill (expectsink), and hoisting the inlineILocalCacheinterface. This deletesDesktopLocalCache.kt(1,173 lines) — the single largest duplication in the repo. - Extract
TopFilter+ aListBackupStoreport out ofAccountSettings.kt.AccountSettings(1,900 lines) mixes thebackupXList/updateXListpersistence port with nav-shape types (NavBarItem,DrawerItemVisibility) and theTopFiltersealed class (line 102). The port +ICacheProviderunlock all ofmodel/nip51Lists(31 files) and ~18 one-filemodel/nipNNpackages — the commons copies ofNip65RelayListStateetc. prove the recipe is purely mechanical (LocalCache→ICacheProvider, dropAccountSettings). - A Compose-resources
stringRestwin.ui/StringResourceCache.ktdefines the sixstringRes()overloads +painterResonandroid.util.LruCache+Rids. commons already has the translatedcomposeResourcespipeline (152 strings so far). This is the gate on ~210 otherwise-clean composables/ViewModels.
MOVE-NOW batches (no prerequisites)
These are Tier A today; grouped as reviewable PRs. Verified per-file by the area audits.
Batch 1 — relay client (highest confidence, zero risk)
service/relayClient/eoseManagers/:PerUserEoseManager,PerUserAndFollowListEoseManager,PerUniqueIdEoseManager,AccountScopedSingleSubNoEoseCacheEoseManager→commons/relayClient/eoseManagers(they extend commonsBaseEoseManageralready; depsUser,SincePerRelayMapare commons or move with Batch 1).reqCommand/account/purefilterXxx()functions (~9 files:FilterDraftsAndReportsFromKey,FilterBasicAccountInfoFromKeys,FilterBookmarksAndReportsFromKey,FilterFollowsAndMutesFromKey,FilterLastPostsFromKey,FilterAccountInfoAndListsFromKey,FilterNotificationsToPubkey,FilterCashuHistoryToPubkey) + the account EOSE managers that consume them →commons/relayClient/account.reqCommand/channel/filters + watchers (FilterChannelMetadata*,FilterLiveStreamUpdatesByAddress, watcher sub-assemblers) →commons/relayClient/channel.reqCommand/nwc/(FilterNWCPaymentsFromRequests, NWCPaymentFilterAssembler, NWCPaymentWatcherSubAssembler) →commons/relayClient/nip47WalletConnect.searchCommand/subassemblies/SearchPeopleByName,SearchPostsByText→commons/relayClient/search. Desktop'sSearchFilterFactory.kt:56literally comments "ported from Android SearchPostsByText".chatDelivery/,speedLogger/→ commonMain;diagnostics/→ jvmAndroid.TorCircuitHealthTracker→commons/relays/health.authCommand/model(9 of 11 files) → merge into the existingcommons/relayClientauth types (AuthApprovalPolicy/AuthApprovalRequestsare a thinner second implementation of the same concern — reconcile, don't copy).DataStoreRelayAuthPermissionStorestays as the Android actual.
Batch 2 — the ~145 ui/screen/**/subassemblies/ filter files
Pure fun filterXByY(relay, …): List<RelayBasedFilter> functions, 97%
Tier A, already importing commons.relayClient.subscriptions.ExplainedFilter.
Sole prerequisite: SincePerRelayMap (Batch 1). Desktop reimplements 34 of
them by hand in desktop/subscriptions/FilterBuilders.kt (742 lines).
Batch 3 — okhttp stack → commons/service/http (jvmAndroid)
16 of 20 files in service/okhttp/ move as-is: IHttpClientManager,
DualHttpClientManager(+ForRelays), OkHttpClientFactory(+ForRelays), all 8
interceptors, both event listeners, OnionLocationCache, OkHttpDebugLogging.
Plus model/privacyOptions/ (4 of 5: role-based client builders,
ProxiedSocketFactory). okhttp3 is an established commons jvmAndroid dep
(BlossomClient, lnurl, UrlPreview) with no shared client manager today.
EncryptionKeyCacheneedsandroid.util.LruCache→ KMP cache first.IsEmulatorstays (androidMain actual).OkHttpWebSocketis production-dead (only androidTest usages) and duplicates quartz'sBasicOkHttpWebSocket— migrate the tests, delete.- Closes a real desktop gap:
desktop/network/DesktopHttpClient.ktre-implements the dual direct/SOCKS client but is missing every interceptor (onion-location, Blossom auth, encrypted blobs).
Batch 4 — theme, layouts, feed shell (UI quick wins, no string work)
ui/theme/(Theme.kt 780 lines, Shape.kt, Type.kt, Color.kt) →commons/ui/theme. commons' theme package is only a fragment (no ColorScheme/Typography/Shapes); desktop re-declares the entire palette indesktop/platform/PlatformColorScheme.kt— two divergent Amethyst palettes ship today.ui/layouts/:NoteComposeLayout(374),RepostLayout,ScreenLayoutare 100% clean;LeftPictureLayout,ChatHeaderLayout,SlimListItemLayoutneed only content-description strings.DisappearingScaffold→ SPLIT (drop theAccountViewModelparam for a slot; its nested-scroll half is already in commons).ui/feeds/(12 of 14):FeedStates,RefresheableBox,WatchScrollToTop,RememberForeverStates,ChannelFeedContentState,WatchLifecycleAndUpdateModelhave zero blockers;FeedEmpty/Error/Loading/UserBlockedFeedneed 2-3 strings each. Desktop hand-rolls its own empty/error/loading states.ui/components/20 zero-blocker files: theClickable*family,M3ActionDialog,OutlinedThinPaddingTextField,SlidingCarousel,AudioWaveformReadOnly,LatexEquation,FileAttachmentCard,pdf/PdfFetcher, andtoasts/(6 of 9 — despite the name it's a Compose snackbar queue, not Android Toast).navigation/topbars/chrome (ShorterTopAppBar,TopBarWithBackButton,ActionTopBar,AmethystClickableIcon) →commons/ui/layouts.
Batch 5 — model + misc clean files
model/root, 14 files:HomeFeedType,VideoPostKind,HashtagIcon,ConcordInviteResult,NoteEditOverlays,PrivateChatroomReadState,RelayGroupContentRouting,MutedPublicChats,ParticipantListBuilder,LargeSoftCacheAddressExt,Dao,EventBroadcaster→commons/model;AccountMarmotActions,AccountRelayGroupActions→commons/actions.model/nip64Chess/ChessAction→commons/nip64Chess.model/nip03Timestamp(4 of 6: OTS settings, explorer endpoints, verification, resolver builder) →commons/model/nip03Timestamp.model/marmot/InMemoryMlsGroupStateStore→commons/marmot.model/nip47WalletConnect/NwcInfoCache→commons/model/nip47WalletConnect.ui/dal/AdditiveComplexFeedFilter(6-line abstract class, zero imports) →commons/ui/feeds;DefaultFeedOrderEventlikewise.- napplet clean half:
NappletRelayCleartext(pure NIP-04/44 → quartz orcommons/napplet/protocol),NappletLaunchRegistry,NappletNotificationStore,NappletIdentityWatch→commons/napplet(jvmAndroid). service/namecoin/NamecoinNameService→commons/service/namecoin. Desktop'sDesktopNamecoinNameService.ktsays "Same functionality as the Android NamecoinNameService… mirrors AppModules#buildNamecoinBackend exactly" and is the superset — merge into it, not alongside it.service/images/fetchers (7 of 11:Base64Fetcher,BlurHashFetcher,ThumbHashFetcher,ProfilePictureFetcher,BlossomFetcher,BlossomReadAuthFetcher,DeferredDeleteFileSystem) →commons/service/image(jvmAndroid). Desktop clones four of them file-for-file;commons/{blurhash,thumbhash,base64Image}actuals already exist to sit under them.service/resourceusage/13 of 18 files (UsageKeys,UsageSummary,ResourceUsageStore,ResourceUsageAccountant,ResourceUsageAlerts,RefCountedSession,MeteringNostrSigner,BatteryDrainSampler,ResourceUsageReportAssembler, time integrators,RelayUsageListener) →commons/service/resourceusage(onlySystemClock→TimeUtilsswaps).service/cashu/→commons/cashu(rest of the wallet is already there);CachedCashuParser/MeltProcessorneed the KMP LRU.- Small singles:
PodcastRemoteContent→commons/podcasts;BuzzInviteMinter→commons/actionsorcommons/buzz;WritingAssistant(pure interface) → commonMain;DetectedWorkout+WorkoutMerger(197 lines of merge logic) →commons/workouts;ConnectivityStatus+ConnectivityManagerflow plumbing → commonMain with the AndroidConnectivityFlowas actual; crashreports logic (UnexpectedCrashSaver,CrashReportCache,DevReportContact) → jvmAndroid (desktop currently has no crash reporting);ScheduledPostWorkGate→commons/scheduledposts(desktop duplicates the drain gating);PowJobStore/PowJobRestorer→commons/service/pow(desktop lacks PoW persistence entirely);service/uploads/clean half (ImageDownloader,MediaUploadResult,MediaMimeTypes,SuspendableConfirmation,blossom/*,hls/*builders,nip96/ServerInfoRetriever) →commons/service/upload(jvmAndroid). ui/tor/non-service half (8 files:TorSettings(+Flow),TorServiceStatus,TorBackend,ArtiGuardState,TorPreferencesPort,TorManager,TorDialogViewModel) → fold into existingcommons/tor.ui/screenroot strays:AccountState,UserFeedState,DebouncedPublisher,relays/common/RelaySuggestionState,embed/SelectionUiState.ui/screen/loggedIn/discover/is 67% Tier A — near-wholesale move of its datasource + subassemblies layers.
Delete-only (shims whose move already happened)
model/Note.kt, model/User.kt, model/HashtagIcon.kt,
model/torState/TorRelaySettings.kt, model/torState/TorRelayEvaluation.kt,
ui/dal/FeedFilters.kt, ui/dal/ChangesFlowFilter.kt, ui/feeds/FeedStates.kt
(partly), service/BundledUpdates.kt, service/relays/EOSE.kt (after
Batch 1), ui/screen/FeedViewModel.kt,
chats/privateDM/dal/ChatroomFeedViewModel.kt + ChatroomFeedFilter.kt,
threadview/dal/LevelFeedViewModel.kt, reqCommand/user/UserFinderShims.kt,
reqCommand/event/EventFinderShims.kt — 13+ typealias re-export files.
Rewrite importers to the commons FQNs and delete.
Also: adopt commons/util/toTimeAgo and delete
ui/note/TimeAgoFormatter.kt (377 lines on android.text.format.DateUtils
— the app and desktop currently render time-ago with two divergent
formatters).
MOVE-AFTER (blocked, with the named blocker)
| Area | Files | Blocker(s) | Target |
|---|---|---|---|
ui/screen/**/dal/ feed filters + thin VMs |
~114 | Account (106 of 109 block on it alone), LocalCache |
commons/ui/feeds + commons/viewmodels |
ui/screen/**/datasource/ assemblers |
~168 | AccountViewModel, TopFilter, app-side eoseManagers |
commons/relayClient |
model/nip51Lists/ state classes |
18 | ICacheProvider swap + ListBackupStore port |
commons/model/nip51Lists |
model/topNavFeeds/ |
37 | the IFeedTopNavFilter signature edit |
commons/model/topNavFeeds |
~18 one-file model/nipNN state pkgs (nip17Dms, nip65RelayList, nipB7Blossom, edits, localRelays, zap, buzz, algoFeeds, trustedAssertions, …) |
~25 | same two ports; four already have commons twins — reconcile-and-delete, don't copy (nip65RelayList, nipB7Blossom, nip30CustomEmojis, nip72Communities) |
matching commons/model/nipNN |
model/serverList, model/nip01UserMetadata, model/nip02FollowLists |
17 | LocalCache |
commons/model/…; replaces desktop's hand-rolled DesktopAccountRelays (250 lines) |
AntiSpamFilter, MediaAspectRatioCache, UrlCachedPreviewer, Nip11CachedRetriever |
5 | KMP LruCache expect (or quartz LargeCache) |
commons/model, commons/util |
relays/ list-editing VMs (17 *RelayListViewModel) |
~20 | 2-3 R.string toasts each |
commons/viewmodels; desktop hand-rolls all seven relay editors today |
wallet/WalletViewModel (750 lines, zero android imports) |
1 | 19 R.string refs + Amethyst singleton |
commons/viewmodels |
SearchBarViewModel (435) |
1 | Account + Route (make route-building a callback) |
commons/viewmodels (its SearchBarState is already there) |
chess/ChessViewModelNew |
3 | trivial — desktop's DesktopChessViewModelNew.kt is line-for-line the same class |
commons/nip64Chess |
ui/note/types/ kind renderers |
~89 | AccountViewModel threading (81 files) + strings; introduce a NoteRenderContext/callback bundle |
commons/ui/note (22 files there prove the pattern) |
ReactionsRow (2,602 lines) + note/buttons/ |
~10 | 30 strings, 1 stray Context import |
commons/ui/note — desktop has zero reactions/zaps UI today |
UsernameDisplay, NIP05VerificationDisplay, UserProfilePicture |
3 | strings; merge avatar stack into commons UserAvatar |
commons/ui/note |
RichTextViewer family |
~8 | strings; commons ui/richtext exists and desktop already uses it — reconcile the app's fork |
commons/ui |
notifications state (CardFeedContentState, CardFeedState, NotificationSummaryState, OpenPollsState) |
6 | Account; commons ui/notifications/CardFeedState is the other half of a half-done move |
commons/ui/notifications |
6 structurally-identical *MetadataViewModels (lists, followPacks, bookmarkgroups, emojipacks, interestSets, nip28 channel) |
6 | one shared error-enum extraction (8 R.string + Context each) |
commons/viewmodels |
uploads core (UploadOrchestrator 525, MediaCompressor, MetadataStripper) |
~8 | Uri/ContentResolver → stream core; Int string-res errors → typed enum; promote commons' jvmMain orchestrator to jvmAndroid |
commons/service/upload — today two independent orchestrators exist |
ZapPaymentHandler (580) / V4VPaymentHandler (295) |
2 | split zap-split math + NWC/LNURL sequencing (shareable) from Context/string error surface |
commons/model/nip57Zaps / commons/onchain |
playback policy leaves (SimultaneousPlaybackCalculator, VideoViewedPositionCache, AutoReplayLimiter, HlsLivenessCache, LowLatencyHlsStripper, websocket/Wss*) |
~7 | extract from the 71-file media3 package | commons/service/playback |
push logic (PushWrapDecryptor, RegisterAccounts) |
2 | none real — just entangled with FCM glue | commons/service/push |
napplet/NappletLiveSubscriptions, gateways/AccountIdentityReader, NappletManifestLookup |
3 | Account / inject cache |
commons/napplet |
ui/broadcast/ banners |
3 | strings + theme + AccountViewModel; logic already in commons/service/broadcast |
commons/service/broadcast/ui |
post composers (ShortNotePostViewModel 2,046, *NewMessageViewModel, HLS/music authoring) |
~20 | deep: uploads core + location + StringProvider + Amethyst singleton |
last wave |
STAY (correctly platform-native)
- Navigation shell & screens:
*Screen.kt,*TopBar.kt,New*Button.kt,INav/Route/RouteMaker/AppNavigation, drawer/bottom-bar,AccountScreen/AccountSessionManager/LoggedInPage,loggedOff/,settings/screens (~480 files import INav/Route — by design). - Process/DI roots:
Amethyst.kt,AppModules.kt,EncryptedStorage,LocalPreferences,DebugUtils,model/accountsCache,model/preferences/(the one genuinely-Android model package: DataStore/ Keystore actuals — but define their ports in commons). - Media & capture:
service/playback/(media3/MediaSession/PiP — desktop's player is a genuinely different implementation, not a port),ui/actions/uploadspickers/camera/voice,creators/capture files, Zoomable/PDF viewers,GifVideoView. - Android services: notifications (channels/FCM/TileService), calendar
(WorkManager), location (
LocationManager—LocationGeoHashcaches could move), tts, cast (SDK), nests foreground service, foreground/eventCache/ priority/logging (Choreographer/StrictMode instrumentation), workouts' Health Connect half. - napplet broker & sandbox (Messenger IPC, WebView, consent activities),
connectedApps/(the shareable signer/nip46 halves are already in commons; what remains is DataStore actuals + activities),favorites/registries (models already extracted tocommons/favorites). - Flavor sources
src/play/+src/fdroid/— they exist precisely to encode the Play-vs-FOSS split; only interfaces they implement may move. StringResourceCache,SafeImeInsets,NwcResponseMessages— the Android halves of i18n/IME bridges.
Desktop duplication catalog (the "should" evidence)
| Desktop file | Hand-rolled copy of |
|---|---|
cache/DesktopLocalCache.kt (1,173) |
model/LocalCache.kt (3,980) |
model/DesktopIAccount.kt (539) |
the ~14% of Account.kt a second front end needs |
subscriptions/FilterBuilders.kt (742) |
~34 ui/screen/**/subassemblies/ filter fns |
subscriptions/SearchFilterFactory.kt |
searchCommand/subassemblies/SearchPostsByText (self-documented port) |
subscriptions/{FeedSubscription,ProfileSubscription,FilterDMs,ChessSubscription}.kt |
reqCommand watchers / gift-wrap / profile filters |
feeds/DesktopFeedFilters.kt (404) + DesktopFeedViewModel.kt |
ui/screen/**/dal/ feed filters |
model/DesktopAccountRelays.kt (250) |
model/serverList/ + model/nip01UserMetadata/ relay states |
model/{DesktopHiddenUsersState,DesktopDmRelayState,BlossomServers}.kt |
nip51Lists/HiddenUsersState, nip17Dms/*, nipB7Blossom/* |
chess/DesktopChessViewModelNew.kt (203) |
chess/ChessViewModelNew.kt (215) — line-for-line |
service/namecoin/DesktopNamecoinNameService.kt |
service/namecoin/NamecoinNameService.kt (says so in its KDoc) |
service/images/Desktop{Base64,BlurHash,ThumbHash}Fetcher.kt |
service/images/ fetchers |
network/DesktopHttpClient.kt |
DualHttpClientManagerForRelays (minus all interceptors — a live behavior gap) |
platform/PlatformColorScheme.kt |
ui/theme/Theme.kt palette |
ui/relay/*Editor.kt (7 files) |
relays/*RelayListViewModel edit logic |
ui/notifications/NotificationGroup.kt |
notifications/ MultiSetCard grouping |
ui/thread/ (784) |
threadview dal + UI |
ui/search/ (1,495) + search/DesktopRelayUserSearchDelegate.kt |
SearchBarViewModel |
network/RelayConnectionManager.kt::RelayMetrics |
speedLogger/ telemetry subset |
| Desktop has no reactions/zaps row, no crash reporting, no PoW persistence | gaps that sharing closes for free |
Recommended sequence
- Wave 0 (mechanical, this branch's follow-ups): delete the 13+ typealias shims; land Batches 1-5 above (~350 files) — no refactoring required.
- Wave 1 (three small edits, huge fan-out): finish
EOSE.kt;IFeedTopNavFilter→ICacheProvider; add KMPLruCacheexpect. Unlocks subassemblies (145 — landed 2026-09-05, see the handoff below), topNavFeeds (23), caches (5). - Wave 2 (ports): extract
TopFilter+ListBackupStorefromAccountSettings; moveLocalCachebehind its 3 tiny expects. Unlocks nip51Lists, the nipNN state packages, serverList/userMetadata,ui/screen/**/dal/— and deletesDesktopLocalCache. - Wave 3 (strings): build the Compose-resources
stringRestwin, then migrate strings feature-by-feature with their composables (theme, layouts, feed shell first — they need no strings at all and can go in Wave 0). - Wave 4 (decompose the god-objects): grow
IAccountand migrateAccount's six concern groups (relay-set derivation, nip51 state, the 30-feed-type × 2 field explosion → aMap<FeedType, FollowListPair>, relay-auth policy, buzz state, the 218 action methods →commons/actions) until app-sideAccountis only composition wiring. ShrinkAccountViewModelthe same way. This is what unblocks the remaining ~400datasource/+ui/note/types/files. - Wave 5 (deep platform seams): uploads core (
Uri→stream + typed errors), zap payment handlers, playback policy extraction, post composers.
jvmAndroid audit: what the migration parked there, and what can promote to commonMain
Everything below landed in commons/src/jvmAndroid (or androidMain) during
Waves 0-1. For each file: the exact JVM-only API pinning it there, and whether
the repo already has a KMP replacement. Replacements referenced:
KmpLock/withLock (commons/util), TimeUtils.nowMillis() (quartz),
kotlin.concurrent.atomics (stdlib KMP, already used in quartz BLE),
quartz/utils/concurrent/ConcurrentMap + ConcurrentSet (expect/actual),
RandomInstance (quartz secure random), kotlin.time.TimeSource.Monotonic
(used in quartz RelayProber), okio (KMP), kotlinx-serialization +
commons/util/JsonTreeUtils, and the PlatformImage expect (android + jvm +
ios actuals in commons/blurhash).
Tier 1 — promotable to commonMain by moving the file (no code change)
Executed on this branch: the 7 auth files,
EncryptionKeyCacheandHttpClientEnvironmentnow live in commonMain (verified against JVM, iOS andverifyKmpPurity).NWCPaymentWatcherSubAssemblerturned out to useNWCPaymentQueryStatefrom its pinned same-package sibling — reclassified to Tier 2.
| File | JVM pin | Note |
|---|---|---|
relayClient/auth/ RelayAuthPermissionCache, RelayAuthPermissionLedger, RelayAuthSessionGrants, InMemoryRelayAuthPermissionStore, RelayAuthVenues, RelayAuthPurposeDeriver, RelayAuthFirstParty (7 files) |
none | Zero JVM imports; no deps on the two pinned siblings. Wave 0b parked the whole group conservatively. |
relayClient/nip47WalletConnect/NWCPaymentWatcherSubAssembler |
none directly — but uses NWCPaymentQueryState, declared same-package in the pinned assembler |
Reclassified to Tier 2: moves with NWCPaymentFilterAssembler. |
service/http/EncryptionKeyCache |
none | androidx.collection LruCache is commonMain-safe. |
service/http/HttpClientEnvironment |
none | Plain object; conceptually pairs with the factories but nothing pins it. |
Tier 2 — promotable with mechanical one-line swaps (replacement exists in-repo)
Executed on this branch, with a concurrency review per file rather than blind swaps. Locks that guarded nothing were removed instead of ported:
RelayAuthPromptBusis now lock-free (ConcurrentMap.getOrPut+ identity-check ownership), andNappletLaunchRegistrydropped all three@Synchronizedby replacing its JVM-only access-ordered LinkedHashMap withandroidx.collection.LruCache(internally synchronized, access-ordered cap — same semantics). Locks that protect real multi-field invariants stayed asKmpLockdeliberately:ChatDeliveryTracker(its hot OK path was already lock-free via volatile immutable maps; CAS-ing the three coordinated structures would copy maps per write — GC churn for zero contention win),NWCPaymentFilterAssembler(debounce set + job swapped atomically),NappletNotificationStore(per-coordinate ordered buckets),DeferredDeleteFileSystem(pending-set membership must decide deletion atomically).java.util.concurrentatomics/CHM became stdlibkotlin.concurrent.atomics+ quartzConcurrentMap(which grewputIfAbsent/remove(key,value)/clearfor the leader-follower caches). Extra pins found while executing: speedLogger usedkotlin.concurrent.timerand::class.java(the tick now runs on a cancellable coroutine scope — the old daemon timer outliveddestroy());OnionLocationCache,BlossomReadAuthTokenProvider,DmRelayDiagnosticsLogger,NappletNotificationStoreswapped toTimeUtils.nowMillis(). Bonus promote:service/upload/BlossomAuth(quartz-only imports). Two reclassified to Tier 4:NappletIdentityWatch(depends onNappletProtocolJson, pinned byjava.util.Base64) andNamecoinNameService(quartzElectrumXClientis jvmAndroid).
| File | JVM pin | KMP replacement |
|---|---|---|
relayClient/auth/RelayAuthPromptBus |
synchronized() |
KmpLock.withLock |
relayClient/auth/ListWithUniqueSetCache |
AtomicReference |
kotlin.concurrent.atomics.AtomicReference |
relayClient/chatDelivery/ChatDeliveryTracker |
@Volatile + synchronized() ×9 |
KmpLock + stdlib atomics |
relayClient/speedLogger/FrameStat, KindGroup |
AtomicInteger |
kotlin.concurrent.atomics.AtomicInt |
relayClient/speedLogger/RelaySpeedLogger |
none (blocked by the two above) | moves with them |
relayClient/diagnostics/DmRelayDiagnosticsLogger |
System.currentTimeMillis |
TimeUtils.nowMillis() |
relayClient/nip47WalletConnect/NWCPaymentFilterAssembler |
@Volatile + synchronized() |
KmpLock + atomics |
model/nip47WalletConnect/NwcInfoCache |
ConcurrentHashMap |
quartz ConcurrentMap |
marmot/InMemoryMlsGroupStateStore |
ConcurrentHashMap |
quartz ConcurrentMap |
napplet/NappletIdentityWatch |
ConcurrentHashMap |
quartz ConcurrentMap |
napplet/NappletLaunchRegistry |
SecureRandom, @Synchronized |
RandomInstance.bytes(), KmpLock |
napplet/NappletNotificationStore |
ConcurrentHashMap, AtomicLong, synchronized(), millis |
ConcurrentMap + atomics + KmpLock + TimeUtils |
service/namecoin/NamecoinNameService |
@Volatile ×1 |
atomics (verify the injected resolver ifaces are commonMain) |
service/http/BlossomReadAuthTokenProvider |
ConcurrentHashMap, millis |
ConcurrentMap + TimeUtils.nowMillis() |
service/http/OnionLocationCache |
ConcurrentHashMap, TimeUnit, millis |
ConcurrentMap + plain-ms TTL + TimeUtils |
service/image/DeferredDeleteFileSystem |
java.io.IOException, synchronized() |
already okio-based → okio.IOException + KmpLock |
Tier 3 — promotable with a small refactor
| File | JVM pin | Path |
|---|---|---|
relayClient/diagnostics/BootRelayDiagnostics |
kotlin.concurrent.thread, ConcurrentHashMap, atomics, millis |
swap thread → coroutine launch; rest is Tier-2 swaps |
actions/BuzzInviteMinter |
Jackson ObjectMapper, OkHttp call |
Jackson → kotlinx-serialization (JsonTreeUtils landed in commonMain); inject a suspend (url, body) -> String fetch or Ktor client |
podcasts/PodcastRemoteContent |
OkHttp call | inject a fetch function or Ktor client |
[androidMain] Base64Fetcher, BlurHashFetcher, ThumbHashFetcher |
android Bitmap bridge (toAndroidBitmap) |
coil3 core is KMP and PlatformImage has android/jvm/ios actuals — add an expect PlatformImage → coil3.Image bridge; deletes desktop's three clone fetchers |
Tier 4 — blocked by a dependency that must move first
| File | Blocker |
|---|---|
scheduledposts/ScheduledPostWorkGate |
ScheduledPostStore (pre-existing jvmAndroid: Jackson + java.io.File) — store needs kotlinx-serialization + okio first |
model/cache/LargeSoftCacheAddressExt |
LargeSoftCache (WeakReference, ConcurrentSkipListMap) — soft/weak-reference caching has no KMP equivalent; a native actual would change eviction semantics. Long-term expect/actual candidate. |
model/nip03Timestamp/BitcoinExplorerEndpoint, OtsSettings, TorAwareOkHttpOtsResolverBuilder |
quartz's own OTS explorer/calendar clients are OkHttp-only (quartz…nip03Timestamp.okhttp.*) — quartz needs a KMP (Ktor) OTS transport before these can follow |
Tier 5 — stays jvmAndroid by design (typed against OkHttp / java.net)
service/http/: IHttpClientManager, IRoleBasedHttpClientBuilder,
DualHttpClientManager(+ForRelays), OkHttpClientFactory(+ForRelays),
Empty/SingleRoleBasedHttpClientBuilder, ProxiedSocketFactory, and the
interceptor/listener set (BlossomReadAuth, EncryptedBlob,
LocalBlossomCacheRedirect, OnionLocation, OnionUrlRewrite,
DefaultContentType, Logging, DnsInvalidatingEventListener,
MediaCallEventListener, OkHttpDebugLogging), plus
service/image/BlossomReadAuthFetcher (coil-network-okhttp).
These are the OkHttp engine itself: okhttp3.Interceptor/EventListener
types, java.net.Proxy/Socket. The KMP path is a Ktor-client rewrite
(Ktor 3.5.2 is already in the catalog, server-side only today), but quartz's
relay websockets are equally OkHttp-bound on jvmAndroid, so an iOS transport
story has to land in quartz first; rewriting commons alone buys nothing.
Revisit when quartz grows a non-JVM socket/HTTP engine.
Score: of the 59 files parked, 11 move with zero code change, 17 with one-line in-repo swaps, 6 with small refactors, 5 wait on a dependency, and 20 are the OkHttp engine that should stay until quartz has a KMP transport.
Corrections to commons/ARCHITECTURE.md found during the sweep
- §2 is stale:
commons/servicealso holdspow/,georelay/,broadcast/; several service-ish concerns live as top-level packages (scheduledposts,cashu,podcasts,audio,connectedApps,favorites,napplet,browser) — theservice/vs top-level split deserves a stated rule. commons/model/nip02FollowList(singular) vs appmodel/nip02FollowLists(plural) — reconcile on the quartz name when merging.
Session handoff — state as of 2026-09-01 (PR #4025)
Everything below is the live state for whoever picks this up next; the sections above are the original audit and stay as reference.
What is DONE and pushed (branch claude/amethyst-commons-migration-hm8vgm)
- Waves 0 + 1 (~145 files moved to
:commons, 12 typealias shims deleted, importers rewritten repo-wide). See the batch tables above. - jvmAndroid promotion Tiers 1–3 (the 5-tier table above, annotated
per-tier): ~28 more files promoted to commonMain, incl. the diagnostics
stack (BootRelayDiagnostics, RelaySpeedLogger, DmRelayDiagnosticsLogger),
the image fetchers (Base64/BlurHash/ThumbHash via the new
CoilImageBridgeexpect/actual, three Desktop clones deleted), and the Tier-2 concurrency review (2 locks removed, 4 kept asKmpLockwith documented invariants; quartzConcurrentMapgainedputIfAbsent/remove(k,v)/clear). - Merged main twice; second merge brought the #4026 Shorts rename, and
commit
5cf47f6fdropped the 30 orphanedroute_video/new_shorttranslation entries (15 locales × 2 keys) that were failing:amethyst:lintFdroidBenchmarkon main and every branch. Main is still red until that cleanup lands there — cherry-picking5cf47f6ffixes it. - CI fully green on head
5cf47f6f; PR #4025 mergeable_state clean.
Audit findings (2026-09-01 review) — 5 of 6 FIXED on the branch
Findings 1, 2, 3, 5 and 6 below are fixed and covered where testable
(TopFilterSerialNameTest in commons commonTest pins the pre-move serial
names and legacy-JSON decoding on JVM and iOS). The one still open:
- Baseline profile stale (finding 4) —
baseline-prof.txthas ~251 rules naming pre-move classes; regenerating requires the:baselineprofilemacrobenchmark on a device, which this environment cannot run. Regenerate before or shortly after release.
Original findings, for reference:
- [ship-blocker]
TopFilterserial names changed — the move tocommons.model.topNavFeedschanged every subclass's kotlinx default serial name; persisted per-tab feed-filter prefs (written byJsonMapper.toJson, read viaparseTopFilterOrDefaultwhich swallows decode errors) silently reset for every user on upgrade. Fix: add@SerialName("com.vitorpamplona.amethyst.model.TopFilter.…")(old FQNs) to each subclass. HttpClientEnvironment.isEmulatorset too late —Amethyst.onCreatebuildsAppModules(which eagerly constructs both OkHttp factories and their dispatchers) before setting the flag; the emulator branch is dead. Set the flag beforeAppModules(this), or read it lazily.@Contextual AddressinTopFilterhas no iOS serializer — the nativeMainAddressactual is not@Serializableand JsonMapper registers no contextual serializer; serializing address-carrying filters throws on iOS. Annotate the native actual or register a serializer.- Baseline profile stale —
baseline-prof.txthas ~251 rules naming pre-move classes; regenerate via:baselineprofile(cold-start wins regress until then). NappletLaunchRegistryin commons breaches the documented napplet sandbox boundary (CLAUDE.md says the broker-side registry stays in:amethystso:nappletHostcannot import it). Move it back or update the boundary doc + add a guard.PlatformImage.toSkiaBitmap()duplicated verbatim inCoilImageBridge.jvm.ktand.ios.kt— hoist to a shared Skiko source set.
Batch 2 landed — subassemblies are in commons/relayClient (2026-09-05)
The audit's Batch 2 (the ui/screen/**/subassemblies/ filter functions) was
never executed by the earlier waves; it is done now on branch
claude/amethyst-commons-migration-ua8ma8. What moved and where:
- All 161 subassembly files →
commons/relayClient/<feature>/…, with thesubassembliespath segment dropped to match how Batch 1 landed (relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt). Mapping:searchCommand/subassemblies→relayClient/search,chats/publicChannels/datasource/subassemblies→relayClient/channel,…/relayGroup/datasource/subassemblies→relayClient/channel/relayGroup,discover/<nip>/subassemblies→relayClient/discover/<nip>, and<feature>/datasource/subassemblies[/<sub>]→relayClient/<feature>[/<sub>]. The misnamedshorts/…/FilterPollsByAllCommunities.ktbecameFilterShortsByAllCommunities.kton the way. - Pure companions the 161 needed, moved alongside:
video/datasource/FeedBasis.kt(the picture/video kind lists) →relayClient/video, andrelayGroup/datasource/RelayGroupFilterBuilders.kt(+ its test) →relayClient/channel/relayGroup. The two*_PAGE_LIMITconsts went torelayClient/nsites/relayClient/napplets. - The three query-state keys the sub-assemblers are typed on now live in commons
on
IAccount:ChannelQueryState(unchanged shape),SearchQueryState(carries the search / indexer / follow-plus-mine relay sets asStateFlows instead of reaching intoAccount), andVideoQueryState(carrieslistName,followsPerRelayandlastNoteCreatedAtWhenFullyLoadedflows instead ofAccount+AccountFeedContentStates). The app constructs them inSearchBarViewModel,UserSuggestionStateandVideoFilterAssemblerSubscription; the*FilterAssemblerclasses stayed in the app (they are now import-clean and can follow with the datasource batch). ICacheProvidergrew the seams the sixLocalCache-coupled files needed:consume(nip19: Entity)(hint + placeholder seeding for a search query),allRelayGroupChannels()andgetRelayGroupChannelsOnRelay(relay).DesktopLocalCacheimplements them (relay groups: empty, desktop has no NIP-29 cache yet).filterByAuthor/Event/Addressand the set-levelfilterRelayGroupsBy{Authors,MutedAuthors,Follows}+filterRelayGroupsDiscoverytakecache: ICacheProvideras their first parameter; the per-relayfilterRelayGroupsByAuthorslost itsLocalCachedefault forcachedChannels(the test always passed it explicitly).- The four tests moved to
commons/src/commonTestonkotlin.test(JUnit's message-first asserts were reordered).
Not done on purpose: the SubAssemblyHelper.kt / *Filter.kt dispatchers and
the *FilterAssembler / *Subscription classes in each datasource package —
they are the datasource batch (333 files, 102 of them Account/
AccountViewModel-coupled) and belong with Wave 4. Desktop's
subscriptions/FilterBuilders.kt (742 lines of hand-rolled copies of these
filters) is now deletable in favour of the commons functions — a desktop
follow-up, not part of this batch.
Top-nav feed datasource layer landed in commons/relayClient (2026-09-05)
The family that dispatches to the Batch-2 filters — 26 XFilterAssembler +
XSubAssembler + XQueryState triples, one per top-nav feed (polls,
pictures, longs, badges, nests, podcasts ×2, music ×2, discover, …) — was 26
copies of the same ~90 lines differing only in the makeXFilter dispatcher and
which FeedContentState floor bounds since. Moving the copies would have
relocated the duplication, so they collapsed onto three shared classes in
commons/relayClient/topNavFeeds/:
TopNavFeedQueryState(account: IAccount, listName, followsPerRelay, scope, feeds)— the one key type for all of them (open;DiscoveryQueryStateadds its seven named tabs,SoftwareAppsQueryStateadds the blocked-relay set).TopNavFeedSubAssembler<K>— the abstract EOSE manager carrying the shared job wiring (list-name watcher, per-relay follows sampled at 500 ms, feed floors combined and sampled at 5 s, plusonListChanged/extraInvalidatorshooks).SingleTopNavFeedSubAssembler(client, keys, ::makeXFilter, resetEoseOnListChange)is the concrete one every single-dispatcher feed uses; Discovery's three and SoftwareApps' one are subclasses.TopNavFeedFilterAssembler<K>— theComposeSubscriptionManagerthat owns the sub-assemblers. Each feature keeps a one-linecommons/relayClient/<feature>/XFilterAssembler.ktnaming its dispatcher, soRelaySubscriptionsCoordinatorand the*Subscriptioncomposables kept their types and names.
The pure makeXFilter dispatchers (SubAssemblyHelper.kt, XFilter.kt,
FilterBadges.kt, the seven discover/<nip>/SubAssemblyHelper.kt) moved next
to their filters. What stays in the app is exactly the Compose glue: each
XFilterAssemblerSubscription.kt now builds the key with
AccountViewModel.topNavFeedQueryState(listName, followsPerRelay, feed…)
(ui/screen/loggedIn/TopNavFeedQueryStates.kt), reading the Account /
AccountSettings / AccountFeedContentStates fields there.
Deliberate behaviour deltas, all small: every watcher now runs on the key's
(screen) scope — the floor watcher used to run on account.scope; the
per-relay-follows sampler is 500 ms everywhere (Discovery tab 2 and Video had
1000 ms); Video gained the list-name watcher the others always had, and its
inline when became makePictureAndVideoFilter. VideoQueryState from the
previous batch is gone in favour of the shared key.
Not in this batch — the datasource files with other key shapes:
RelayGroupsDiscovery* (reads LocalCache.live.newEventBundles and the
relay-group server lists), home/ (its filters are still app-side and
HomeOutboxEventsEoseManager is a different animal), and the non-top-nav
families (profile, thread, chatroom(s), hashtag, geohash, url, relay feed,
community, gitRepo, chess, one/my podcast, connectedApps, profile badges, app
recommendations, nest room, follow-pack feed, onchain zaps, NIP-66 relay
info). Many are import-clean and can follow mechanically; the Account-typed
ones wait on Wave 4.
Import-clean non-top-nav datasource families landed (2026-09-05)
The families whose keys are a User, an AddressableNote, a string, or an
IAccount-compatible account moved wholesale into commons/relayClient/:
chess, relay feed, NIP-66 relay info, url, geohash, the channel assembler,
communities, git repo, profile (all 14 files), one/my podcast, onchain zaps,
poll responses, plus the pure filterEventsInThreadForRoot /
filterMissingEventsForThread functions and FilterPostsByScopes.kt
(CommentKinds), which those filters and the home feed share. The URL filter
test came along onto kotlin.test. Three Subscription composables that never
touched AccountViewModel (community, repository, profile) moved too.
The only seam was LocalCache, read for relayHints and
checkGetOrCreateUser — both already on ICacheProvider — so the affected
assemblers take cache: ICacheProvider as their first constructor parameter,
thread it into their sub-assemblers, and the filter functions that need it
take it as their first parameter. RelaySubscriptionsCoordinator passes the
cache it already holds.
Still in the app, with the reason: hashtag/ (FilterHashtagLabels reads
Account.followsPerRelay, a per-outbox follow resolution IAccount lacks);
threadview/datasources/ assembler + sub-assemblers and apps/recommendations,
badges/profile, napplets/ConnectedApps* (each reads one relay-list state
off Account — followPlusAllMineWithSearch, outboxRelays /
defaultGlobalRelays, notificationRelays, homeRelays; the
SearchQueryState recipe of carrying the flow on the key applies, one small
change each); followPacks/feed (dispatches to the app-side home filters);
chats/* (DM plumbing: DmRelayLog, GeohashRelays,
launchChatFeedToggleObserver); nests/NestRoom*, relayGroup/*, concord/*
and home/ (Account + LocalCache live streams + AccountViewModel).
Single-relay-read families and the home feed landed (2026-09-05, after #4059)
The four families that read one relay-set state off Account now carry that
set as a StateFlow on their key, the way SearchQueryState does:
ProfileAppRecommendationsQueryState(outboxRelays, defaultGlobalRelays),
ProfileBadgesQueryState(notificationRelays),
ConnectedAppsQueryState(homeRelays) and ThreadQueryState(defaultRelays).
Thread also takes the cache (ThreadFilterAssembler(cache, client); its two
sub-assemblers run ThreadAssembler(cache)). All live under
commons/relayClient/{apps/recommendations,badges/profile,napplets,thread}.
The home feed moved whole: the FilterHomePosts* dispatchers under
commons/relayClient/home/<nip>/, and HomeOutboxEventsEoseManager is now a
TopNavFeedSubAssembler<HomeQueryState> whose key carries the new-threads and
replies floors plus enabledHomeFeedTypes; the Settings › Home toggle is an
extraInvalidators entry (still drop(1)), and the disabled-kinds stripping
is unchanged. Deltas as for the other feeds: watchers on the screen scope,
follows sampler 500 ms (was 1000). The commented-out alternative
sub-assemblers in HomeFilterAssembler were dropped.
What remains in ui/screen/**/datasource*/ (92 files) is the Compose glue —
one *Subscription.kt per feature, which builds the key from
AccountViewModel — plus the families that need Wave 4: hashtag/
(Account.followsPerRelay), followPacks/feed (Account.proxyRelayList,
blockedRelayList, cache), chats/*, nests/NestRoom*, relayGroup/*,
concord/*, and wallet's and polls/results' composables.
Desktop subscriptions/FilterBuilders.kt — investigated, not migrated (2026-09-05)
The audit listed this 742-line file as a hand-rolled copy of ~34 subassemblies that the Batch-2 move would make deletable. That premise is wrong, and the two sides do not share a model:
- Desktop builds plain quartz
Filters and broadcasts the same list to a whole relay set (RelayConnectionManager.subscribedoesrelays.associateWith { filters }; relay choice is "all connected/configured relays"). No per-relaysince, no EOSE bookkeeping, noSubPurpose. - Commons builds
List<RelayBasedFilter>— oneExplainedFilterper relay — from anIFeedTopNavPerRelayFilterSetthe outbox model derives from follow lists + each author's NIP-65 write relays, withsinceper relay from the EOSE managers, driven byComposeSubscriptionManager. - Both end in the same quartz call,
NostrClient.subscribe(subId, Map<relay, List<Filter>>); the mismatch is one layer up (who picks the relay per filter, wheresincelives).
What the file actually is: 27 functions, 6 with no callers at all
(nip04DmsToUser/FromUser, giftWrapsToUser, dmRelayList,
chessChallengesToUser, chessAllEvents); a 90-line buildFilter DSL used
only by its own 525-line test; most of the rest are one-line named-argument
wrappers over the Filter constructor with hard-coded kind integers
(listOf(0), 9735, 30023, 30064…). The genuine overlaps with commons are
the search kind groups (SearchFilterFactory says "ported from Android
SearchPostsByText"; commons now exports SearchPostsByTextKinds1..3), the
chess filters (commons nip64Chess/subscription/ChessFilterBuilder has
challengesFilter/userGamesFilter/userTaggedFilter), and
notificationsForUser, which already delegates to commons.
Options, for the maintainer to pick:
- Adapter, partial match — teach
SubscriptionConfig/RelayConnectionManagerto acceptList<RelayBasedFilter>(group by relay into the map) so desktop can call commons functions with a uniform set, e.g.GlobalTopNavPerRelayFilterSet(relays.associateWith { GlobalTopNavPerRelayFilter })or anAuthorsTopNavPerRelayFilterSetfrom the follow list. Reuses the shared filter shapes and deletes most ofFilterBuilders; keeps desktop's broadcast relay policy andsince = null. Prerequisite for (2). Recommended next step. - Adopt the outbox model, full match — desktop builds filter sets via
commons
OutboxLoaderState(needs the users' NIP-65 lists in its cache) and drives subscriptions through the commons EOSE + subscription managers. This is the Wave-2/DesktopLocalCachedirection; a project, not a cleanup. - No model change — inline the trivial wrappers as
Filter(...)with quartz kind constants, delete the dead functions and the test-only DSL, switch search/chess to the commons functions. Removes the file, gains no routing parity.
Next work, in recommended order (needs maintainer go-ahead per item)
- Tier 4 unlocks (small, mechanical):
NappletProtocolJsonjava.util.Base64→kotlin.io.encoding.Base64(freesNappletIdentityWatch);ScheduledPostStoreJackson+java.io.File→ kotlinx-serialization+okio (freesScheduledPostWorkGate).LargeSoftCachestays parked (needs a WeakReference expect/actual). - Wave 2: LocalCache move-group (~200 files behind it) — blocked on a
design decision:
CachePruner/CacheSearchcallAccount.isFollowing(...); eitherIAccountgrows it (DesktopIAccount must implement) or they take a narrower interface (recommended). - Wave 3: strings bridge — BUILT (2026-09-01). The pieces:
commons/ui/StringRes.kt(commonMain):stringRes(StringResource), formatted + plural variants,loadStringRes/loadPluralStringResfor non-composable scopes. Thin delegates over compose-resources — no extra cache (the library caches parsed locale files process-wide).- App-side
ui/StringResourceCache.ktgainedstringRes(StringResource)overloads delegating to the bridge, so ONEstringResimport serves mixed files: migrating a key is justR.string.x→Res.string.x(+ the twocommons.resourcesimports). No aliasing, no churn. tools/strings-migrate/migrate.py <keys…>: moves keys from app res to commons composeResources across all 57 locales byte-for-byte (Crowdin covers both trees with the same mapping — see crowdin.yml). It refuses keys with bare%s/%d(compose-resources needs positional%1$s).- Exemplar shipped:
profile_banner(the ui/layouts cluster's only string blocker) migrated + all 7 call-site files repointed. - Bulk migration executed (2026-09-01, commit
7c6a18ee): all 2,565 mechanically-safe keys (composable-only call sites, no XML refs, only%N$s/%N$dargs, no markup) moved to commons; 568 app files repointed. The app keeps 1,866 keys that are genuinely Android-bound: ctx-based call sites (adapt toloadStringResto free them),Int-typed id storage (maps/whens over resource ids),@string/XML references, and exotic format specifiers. - Remaining Wave-3 work: free the ctx-based keys by adapting call
sites, then move the now-unblocked composables. Note the layouts cluster ALSO needs theme
constants (
DividerThickness,Size55Modifier, …) hoisted from appui/theme/Shape.ktintocommons/ui/theme/Sizes.kt, pluspainterRes/TimeAgo/NewItemsBubbledecisions — the audit's "strings-only" tally under-counted transitive deps.
- Wave 4: Account/AccountViewModel decomposition — long-tail.
Environment notes for the next session (hard-won)
- Run gradle tests and pushes with
LC_ALL=C.UTF-8 LANG=C.UTF-8(a POSIX locale breaks an em-dash test-report filename). - One gradle invocation at a time; concurrent runs die on the project lock.
- Pre-push hook runs
:quartz:jvmTest :commons:jvmTest :nestsClient:jvmTest :quic:jvmTest :amethyst:testPlayDebugUnitTest :cli:test(not geode); a PreToolUse hook blocks pushes when spotlessApply reformatted files — commit first, then push. :commons:compileKotlinIosArm64is the cheap local gate for iOS breakage (CI's test-quartz-ios compiles commons for iosSimulatorArm64). If the Kotlin/Native toolchain corrupts (danglingliblto_plugin.sosymlink), delete the extracted dirs under/root/.konan/dependenciesand let gradle re-extract.- stdlib atomics have no
incrementAndFetch()here — useaddAndFetch(1);withLock {}can't assign outervals — restructure to lambda-return.