Skip to content

refactor(messenger): move RestrictedMembers and Report view models onto BaseViewModel - #1973

Draft
bmc08gt wants to merge 3 commits into
code/cashfrom
refactor/messenger-moderation-base-vm
Draft

bmc08gt wants to merge 3 commits into
code/cashfrom
refactor/messenger-moderation-base-vm

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Moves RestrictedMembersViewModel and ReportViewModel onto BaseViewModel<State, Event>, so both screens have one State, one sealed Event type and a reducer (updateStateForEvent in the companion). This is the first area of the roll-in. The audit below covers every Android view model that doesn't follow that contract yet.

What changed

  • RestrictedMembersViewModel: Load, Unsilence and Unban are Events, handled in one eventFlow collector so a second tap on a row still sees the first one's pending entry. The row-lifting rules that were lambdas passed to run() are now the Unsilenced and Unbanned reducer branches. The repeat-open guard loadedFor is now State.loadedFor, so neither view model keeps a mutable field outside State.
  • ReportViewModel: State is the existing LoadingSuccessState. Submit is an Event; Sending, Sent and SendFailed move the button. Sending is dispatched before the send launches, so a second press is still dropped. confirmed stays a buffered Channel: it fires from a bottom bar onDismiss that can outlive the composition, and eventFlow has no buffer.
  • Both take DispatcherProvider and pass Default to BaseViewModel, so tests run the event collectors on the test dispatcher.
  • The screens dispatch events and read stateFlow. Existing VM tests now read stateFlow; new reducer tests: RestrictedMembersStateTest, ReportStateTest.

Audit

Rule for every migration: no mutable fields outside State. Guards, cursors and once-only flags (loadedFor, prefilled, seededEntry, started) go into State too; what stays outside is limited to collaborators, Compose-owned holders like TextFieldState, PagingData, and one-shot effect channels.

Verdicts: migrate fits State + sealed Event + reducer without behaviour change; partial folds the extra flows into State but leaves something out; leave has a reason. Paths are under apps/flipcash/. "common" means the State types have no Android imports and could move to commonMain.

Plain ViewModel(): messenger (features/messenger/.../internal/)

VM Lines Holds instead Tests Verdict
RestrictedMembersViewModel 142 one _state + loadedFor RestrictedMembersViewModelTest migrated here (common)
ReportViewModel 133 _state: LoadingSuccessState + confirmed channel ReportViewModelTest migrated here (common)
invites/ManageInvitesViewModel 154 _state, effects channel, chatId var ManageInvitesViewModelTest migrate (common)
invites/InviteLinkViewModel 247 _state(link), effects channel, chatId/specific/job vars InviteLinkViewModelTest migrate; handOverCard(): Boolean stays a function (common once InviteLinkState moves)
invites/InviteHostSheetViewModel 210 _state, effects channel, pending: Job InviteHostSheetViewModelTest migrate (common)
GroupInviteViewModel 212 _state, invited channel GroupInviteViewModelTest migrate (common)
InviteLandingViewModel 248 _state, effects channel, secret/token/started vars InviteLandingViewModelTest migrate; secret/token go into State with a redacting toString, since State is logged
request/RequestMoneyViewModel 335 _state (incl. paging cursor), effects, amountDelegate, derived stateIns RequestMoneyAnalyticsTest migrate; back(): Boolean stays a function (State common)
currencysend/CurrencySendViewModel 345 _state, mint MSF, _bill/_done/effects channels, amountDelegate CurrencySendInsufficientFundsTest migrate; channels and mint (keys maxAmount) stay (common)
request/RequestDetailsViewModel 274 _state, _now ticker, derived request/unavailable/members, payFlow RequestDetailsInsufficientFundsTest, RequestDetailsAnalyticsTest partial: payFlow state belongs to RequestPayFlow
request/ChatRequestsViewModel 174 _now, _expanded, _deck, cycled, chatId MSFs; held/missing cache passthroughs; settle-hold vars ChatRequestsViewModelTest partial: leave the requests cache and settle-hold; first-frame deck publish must be re-checked
MemberModerationViewModel 192 _restrictions, _rosterFailed MSFs, rosters cache passthrough MemberRestrictionsTest, MemberModerationAnalyticsTest, MemberRosterCheckTest partial: fold the two MSFs, leave rosters and the suspend roster calls; shared by five screens
SharePickerViewModel 193 _chats, _selected, _done, _limitHits MSFs; two TextFieldState; images: List<Uri> none for the VM partial: TextFieldState/Uri stay out; needs a VM test first
invites/InviteHostViewModel 36 canInvite passthrough + suspend approveOn none leave: no state of its own
invites/InviteLinkActionsViewModel 21 one sync lookup none leave: stateless
request/RequestPayViewModel 83 adapter over RequestPayFlow none leave: RequestPayFlow owns the state
MuteChatViewModel 109 sync seed + per-chat cold flow MuteChatAnalyticsTest, MuteChatViewerStateSeedTest leave: moving it changes the seed-before-first-frame behaviour the seed test guards
SilenceUserViewModel 57 fire-and-forget silence() SilenceUserViewModelTest leave: no state
screens/boost/ReactionBoostAmountViewModel 68 amountDelegate + one nullable _split ReactionBoostAmountViewModelTest leave: one nullable value

Plain ViewModel(): other modules

VM Lines Holds instead Tests Verdict
features/login SeedInputViewModel 174 public mutable uiFlow + _navigationEvents SharedFlow none migrate (State common)
features/tokens CommunityChatsViewModel 45 single _state + loaded guard none migrate
features/menu ProfileCardViewModel 94 Compose mutableStateOf card; AssistedInject ProfileCardViewModelTest migrate
shared/accesskey BaseAccessKeyViewModel (+ BackupKeyScreenViewModel, LoginAccessKeyViewModel) 374 + 42 + 93 public mutable uiFlow with two android.graphics.Bitmaps LoginAccessKeyViewModelTest (doesn't read uiFlow) partial → migrate once the Canvas rendering is extracted and the Bitmaps leave State; suspend Result actions stay functions
features/search SearchViewModel 187 TextFieldState query, _chip, derived recents/results, private caches SearchViewModelAnalyticsTest partial: State(chip, recents, results); query stays outside
app HomeViewModel 142 _requireBiometrics, derived canSwitchAccounts, callback-based login handling none partial, low value: activity-scoped, most of it is side effects
features/menu GroupCardViewModel 93 constant vals from the route; AssistedInject none leave: nothing changes
features/myaccount LinkDesktopViewModel 20 owns a controller none leave
features/myaccount TrustedWebsitesViewModel 20 StateFlow passthrough + remove none leave: passthrough
features/search SearchPreloadViewModel 17 warm-up in init none leave
features/tipping NewChatViewModel 36 one derived flow none leave
features/lab LabsScreenViewModel 31 derived isStaff, raw betaOverride flow none leave
shared/transaction-history ActivityHistoryViewModel 26 PagingData passthrough + suspend route lookup none leave: paging source
NavEntrySavedStateHandleViewModel 65 SavedStateHandle holder none leave: navigation infrastructure, not a screen

BaseViewModel subclasses with state outside the reducer

VM Extra flows Tests Verdict
messenger ChatViewModel 7 many owned by refactor/chat-vm-scopes; not touched here
user-profile MinimumTipEntryViewModel entryChanged, style MinimumTipEntryStateTest migrate: isLastStep and entryChanged into State, style derived from stateFlow
messenger EditGroupBalanceRequirementViewModel canSave, a constant style flow EditGroupBalanceRequirementViewModelTest partial: canSave and seededEntry into State
tipping RequestsViewModel fetched metadata cache RequestsProjectionTest leave: a cache feeding the combine that already builds State

The four Hilt*ViewModel wrappers over commonMain view models already conform.

No open PR or pushed branch changes any file in this PR. The messenger branches that touched nearby files (feat/chat-media-save-stack-edit, fix/request-pay-no-balance, feat/chat-send-back, feat/standard-cash-bubbles, perf/parallel-async, feat/group-send-per-person) have all merged.

Load, Unsilence and Unban become Events handled in one eventFlow
collector, so a second tap on a row still sees the first one's pending
entry. Every state change now goes through updateStateForEvent; the
row-lifting rules move from lambdas passed to run() into the
Unsilenced and Unbanned reducer branches.

The screen dispatches the events instead of calling load, unsilence
and unban, and reads stateFlow instead of state.
Submit becomes an Event; Sending, Sent and SendFailed carry the button
state through updateStateForEvent. Sending is dispatched before the
send launches, so a second Submit still sees a non-idle button and is
dropped, as before.

confirmed stays a buffered Channel: it fires from a bottom bar
onDismiss that can outlive the collecting composition, and eventFlow
has no buffer.
@bmc08gt bmc08gt self-assigned this Nov 6, 2026
@github-actions github-actions Bot added the type: refactor Code restructuring, no behavior change label Nov 6, 2026
loadedFor was the last private var on the view model. It is now
State.loadedFor, set by LoadStarted(chatId) and cleared by LoadFailed,
so the repeat-open guard reads stateFlow and the reducer owns every
change to it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant