Skip to content

fix: replace modal restart dialog with inline banner#711

Merged
torlando-tech merged 5 commits intomainfrom
fix/694-inline-restart-banner
Mar 25, 2026
Merged

fix: replace modal restart dialog with inline banner#711
torlando-tech merged 5 commits intomainfrom
fix/694-inline-restart-banner

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Extracts shared ServiceRestartBanner composable from the inline pattern already used in DiscoveredInterfacesScreen
  • Replaces blocking modal ServiceRestartDialog in SettingsScreen and IdentityScreen with the non-modal inline banner
  • Adds onServiceReady callback to InterfaceConfigManager.applyInterfaceChanges() so ViewModels clear isRestarting after Reticulum init (Step 9) rather than waiting for post-init bookkeeping (Steps 10-12: identity restore, manager restart)

The old modal dialog had a cancel button that was hidden for 15 seconds via delay(15_000) — the same duration as the init timeout — so it only appeared when the operation had already succeeded or failed. The inline banner lets users continue interacting with the screen while the service restarts.

Test plan

  • All existing InterfaceConfigManagerTest tests pass (17/17)
  • All existing SettingsViewModelTest, DebugViewModelEventDrivenTest, DiscoveredInterfacesViewModelTest pass
  • Trigger restart from Settings screen — inline banner appears near top of scroll area, disappears promptly after init
  • Trigger restart from Identity screen — inline banner appears below ServiceControlCard
  • Toggle discovery on DiscoveredInterfaces screen — inline banner still works as before
  • Screen remains scrollable and navigable during restart on all 3 screens

Closes #694

🤖 Generated with Claude Code

The ServiceRestartDialog was duplicated in SettingsScreen and
IdentityScreen as a blocking modal with a cancel button that only
appeared after 15s — the same duration as the init timeout, making
it useless. DiscoveredInterfacesScreen already had a better inline
banner pattern.

- Extract shared ServiceRestartBanner composable from the inline
  pattern in DiscoveredInterfacesScreen
- Replace modal dialogs in SettingsScreen and IdentityScreen with
  the inline banner
- Add onServiceReady callback to applyInterfaceChanges() so
  ViewModels clear isRestarting after Reticulum init (Step 9)
  rather than waiting for post-init bookkeeping (Steps 10-12)
- All 3 screens now use consistent non-blocking restart UX

Closes #694

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Mar 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR replaces the blocking ServiceRestartDialog (with its effectively-dead 15-second cancel button) with a non-modal inline ServiceRestartBanner composable across SettingsScreen, IdentityScreen, and DiscoveredInterfacesScreen. It also extracts the banner from duplicated inline code in DiscoveredInterfacesScreen into a shared component, and wires up an onServiceReady callback in InterfaceConfigManager.applyInterfaceChanges() so ViewModels can clear isRestarting right after Reticulum init (Step 9) rather than waiting for post-init bookkeeping.

Key changes:

  • New ServiceRestartBanner composable (extracted + localised string with proper Unicode ellipsis)
  • applyInterfaceChanges(onServiceReady: (() -> Unit)? = null) — callback fires after Step 9, before Steps 10–12
  • SettingsViewModel and DiscoveredInterfacesViewModel correctly handle early failures: they use .getOrThrow() so their outer catch blocks reset isRestarting = false when the restart fails before Step 9
  • DebugViewModel.restartService() does not call .getOrThrow() or otherwise check the Result. Because applyInterfaceChanges uses runCatching and never throws, a failure before Step 9 silently returns Result.failure, onServiceReady is never invoked, the catch block is never reached, and _isRestarting is permanently stuck at true — the IdentityScreen banner never dismisses. The fix is one line: check result.isFailure and reset the flag, mirroring the SettingsViewModel pattern.
  • cancelRestart() removed from both SettingsViewModel and DebugViewModel — correct, as the banner no longer has a cancel affordance
  • Tests updated throughout with mockApplyInterfaceChangesSuccess() helper and applyInterfaceChanges(any()) matchers

Confidence Score: 3/5

  • Mostly safe to merge, but DebugViewModel has a concrete isRestarting leak that would leave the IdentityScreen banner stuck on any early-failure restart.
  • 12 of 13 changed files are clean and well-tested. The single bug in DebugViewModel.restartService() is a one-line fix — the result of applyInterfaceChanges is discarded so the failure path never clears _isRestarting. This breaks the primary user path for IdentityScreen restarts (banner never dismisses on failure), warranting a 3 rather than a 4.
  • app/src/main/java/com/lxmf/messenger/viewmodel/DebugViewModel.kt — restartService() does not handle Result.failure from applyInterfaceChanges, leaving _isRestarting stuck on early failures.

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/viewmodel/DebugViewModel.kt Moves isRestarting clear to onServiceReady callback, but the result of applyInterfaceChanges is not checked — early failures (before Step 9) leave _isRestarting stuck at true indefinitely since the outer catch block never fires for Result.failure.
app/src/main/java/com/lxmf/messenger/service/InterfaceConfigManager.kt Adds optional onServiceReady callback, invoked after Step 9 (Reticulum init) but before post-init bookkeeping. Clean, well-documented addition; placement inside runCatching is correct.
app/src/main/java/com/lxmf/messenger/ui/components/ServiceRestartBanner.kt New shared composable extracted from the pre-existing inline pattern in DiscoveredInterfacesScreen. Clean implementation with localised string.
app/src/main/java/com/lxmf/messenger/viewmodel/SettingsViewModel.kt Uses .getOrThrow() correctly so the catch block resets isRestarting on failure; onServiceReady handles the happy path. Removes cancelRestart() cleanly.
app/src/main/java/com/lxmf/messenger/viewmodel/DiscoveredInterfacesViewModel.kt Correctly moves isRestarting clear to onServiceReady; failure path still sets isRestarting = false via onFailure + state update.
app/src/main/java/com/lxmf/messenger/ui/screens/SettingsScreen.kt Replaces blocking ServiceRestartDialog with inline ServiceRestartBanner near top of scroll area. Clean removal of modal and cancelRestart wiring.
app/src/main/java/com/lxmf/messenger/ui/screens/IdentityScreen.kt Replaces modal ServiceRestartDialog with inline ServiceRestartBanner below ServiceControlCard. UI change is correct; the liveness bug is in the underlying DebugViewModel.

Sequence Diagram

sequenceDiagram
    participant VM as ViewModel
    participant ICM as InterfaceConfigManager
    participant UI as Screen (Banner)

    VM->>UI: isRestarting = true → banner appears
    VM->>ICM: applyInterfaceChanges(onServiceReady)
    ICM-->>ICM: Steps 1–8 (shutdown, restart, bind…)
    ICM-->>ICM: Step 9: Reticulum init ✓
    ICM-->>VM: onServiceReady() callback
    VM->>UI: isRestarting = false → banner disappears
    ICM-->>ICM: Steps 10–12 (identity restore, manager restart)
    ICM-->>VM: Result.success(Unit)

    note over VM,ICM: On early failure (Steps 1–9 fail)
    ICM-->>VM: Result.failure (no throw) [SettingsVM/.getOrThrow() re-throws → catch resets flag ✓]
    note over VM: DebugViewModel: result discarded,<br/>catch never fires → isRestarting stuck true ✗
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/DebugViewModel.kt
Line: 581-587

Comment:
**`isRestarting` stuck `true` on early-failure path**

`applyInterfaceChanges` wraps all failures in `runCatching` and returns `Result.failure` — it never throws. This means the outer `catch` block on line 585 only fires for exceptions raised by other code in the `try` block (practically nothing), not for a failed restart.

- **Happy path**: Step 9 succeeds → `onServiceReady` fires → `_isRestarting.value = false`- **Late failure** (Step 10–12 fail): `onServiceReady` already fired → `_isRestarting` already `false`- **Early failure** (Steps 1–9 fail, e.g. timeout, service shutdown error): `onServiceReady` is **never invoked**, `applyInterfaceChanges` returns `Result.failure` silently, `catch` block never runs → `_isRestarting` stays `true` forever ✗

In contrast, `SettingsViewModel.restartService()` avoids this by calling `.getOrThrow()` which re-throws the failure, allowing its `catch` block to reset the flag.

Fix: either ignore-and-reset, or mirror the `SettingsViewModel` pattern:

```suggestion
                    interfaceConfigManager.applyInterfaceChanges(
                        onServiceReady = { _isRestarting.value = false },
                    ).also { result ->
                        if (result.isFailure) _isRestarting.value = false
                    }
                    Log.i(TAG, "Service restart completed successfully")
                } catch (e: Exception) {
                    Log.e(TAG, "Error restarting service", e)
                    _isRestarting.value = false
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "fix: update test to match unicode ellips..." | Re-trigger Greptile

torlando-tech and others added 4 commits March 24, 2026 22:54
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
No UI callers remain after replacing the modal dialog with
the inline banner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the pattern used by OfflineModeBanner. Uses \u2026 ellipsis
consistent with offline_banner_reconnecting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The string resource uses \u2026 (…) instead of three dots (...).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@torlando-tech torlando-tech merged commit 1a8e00a into main Mar 25, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fails with "Initialization timeout" and "Restart Reticulum" dialog is weirdly broken

1 participant