fix(mailbox): show spinner during initial folder fetch instead of empty state (#167)
selectFolder() kicked off its background syncFolder() fetch fire-and-forget with no loading flag, so opening a per-account folder with no cached messages yet flashed "No messages to display" for the whole IMAP fetch instead of a spinner. Adds isSyncingFolder, a StateFlow set for the duration of that sync (mirroring isRefreshing) and cleared via try/finally regardless of outcome. A private latestFolderSelection token guards the clear so a stale sync from a folder no longer selected can't hide the spinner for whichever folder is actually selected now. MailboxScreen's non-paged empty branch now holds NoMessagesState back while isSyncingFolder is true, showing a CircularProgressIndicator instead — mirroring how the unified-inbox paged branch already gates on loadState.refresh. Closes #149 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit was merged in pull request #167.
This commit is contained in:
co-authored by
Claude Opus 4.8
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
parent
26d4295c30
commit
a48d68c139
@@ -120,6 +120,7 @@ fun MailboxScreen(
|
||||
val searchActive by viewModel.searchActive.collectAsStateWithLifecycle()
|
||||
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
|
||||
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
|
||||
val isSyncingFolder by viewModel.isSyncingFolder.collectAsStateWithLifecycle()
|
||||
val error by viewModel.error.collectAsStateWithLifecycle()
|
||||
val selectedIds by viewModel.selectedIds.collectAsStateWithLifecycle()
|
||||
val pendingConfirm by viewModel.pendingConfirm.collectAsStateWithLifecycle()
|
||||
@@ -330,8 +331,16 @@ fun MailboxScreen(
|
||||
}
|
||||
} else if (messages.isEmpty()) {
|
||||
item {
|
||||
// Hold the empty state back while the folder's initial background
|
||||
// sync is still in flight, so opening an uncached folder doesn't
|
||||
// flash "No messages to display" before the fetch has a chance to
|
||||
// populate anything (issue #149).
|
||||
if (searchActive && searchQuery.isNotBlank()) {
|
||||
NoResultsState(Modifier.fillParentMaxSize())
|
||||
} else if (isSyncingFolder) {
|
||||
Box(Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
NoMessagesState(Modifier.fillParentMaxSize())
|
||||
}
|
||||
|
||||
@@ -186,6 +186,22 @@ class MailboxViewModel @Inject constructor(
|
||||
private val _isRefreshing = MutableStateFlow(false)
|
||||
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
|
||||
|
||||
/**
|
||||
* True while [selectFolder]'s background sync for the currently selected folder is in flight,
|
||||
* so the screen can hold the empty state back until the initial fetch either populates the
|
||||
* cache or confirms the folder really is empty (issue #149).
|
||||
*/
|
||||
private val _isSyncingFolder = MutableStateFlow(false)
|
||||
val isSyncingFolder: StateFlow<Boolean> = _isSyncingFolder.asStateFlow()
|
||||
|
||||
/**
|
||||
* (accountId, folder) of the most recent [selectFolder] call. Used only so a completing sync
|
||||
* can tell whether it's stale — superseded by a newer folder selection — before clearing
|
||||
* [_isSyncingFolder], so rapid folder switching can't let a stale sync hide the spinner for
|
||||
* whichever folder is actually selected now.
|
||||
*/
|
||||
private var latestFolderSelection: Pair<String, String>? = null
|
||||
|
||||
private val _error = MutableStateFlow<String?>(null)
|
||||
val error: StateFlow<String?> = _error.asStateFlow()
|
||||
|
||||
@@ -365,7 +381,18 @@ class MailboxViewModel @Inject constructor(
|
||||
_selectedAccountId.value = accountId
|
||||
explicitDrawerAccountId.value = accountId
|
||||
_selectedFolder.value = folderFullName
|
||||
viewModelScope.launch { mailSyncer.syncFolder(accountId, folderFullName) }
|
||||
val key = accountId to folderFullName
|
||||
latestFolderSelection = key
|
||||
_isSyncingFolder.value = true
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
mailSyncer.syncFolder(accountId, folderFullName)
|
||||
} finally {
|
||||
// Only clear if this is still the most recent selection — a stale sync superseded
|
||||
// by another selectFolder() must not clobber the newer one's in-flight spinner.
|
||||
if (latestFolderSelection == key) _isSyncingFolder.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns to the unified inbox across all accounts. */
|
||||
|
||||
Reference in New Issue
Block a user