Audit: PullToRefreshBox on remaining list screens (iOS parity)

HomeScreen + AllTasksScreen + TasksScreen now support pull-to-refresh.
forceRefresh=true per CLAUDE.md mutation pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 17:50:27 -05:00
parent 95f7318ee6
commit d8569c7aed
3 changed files with 105 additions and 48 deletions

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -49,6 +50,7 @@ fun AllTasksScreen(
var showCompleteDialog by remember { mutableStateOf(false) }
var showNewTaskDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var isRefreshing by remember { mutableStateOf(false) }
// Track which column to scroll to (from push notification navigation)
var scrollToColumnIndex by remember { mutableStateOf<Int?>(null) }
@@ -58,6 +60,13 @@ fun AllTasksScreen(
residenceViewModel.loadMyResidences()
}
// Reset pull-to-refresh state once tasks finish loading
LaunchedEffect(tasksState) {
if (tasksState !is ApiResult.Loading) {
isRefreshing = false
}
}
// When tasks load and we have a pending navigation, find the column containing the task
LaunchedEffect(navigateToTaskId, tasksState) {
if (navigateToTaskId != null && tasksState is ApiResult.Success) {
@@ -206,62 +215,71 @@ fun AllTasksScreen(
}
}
} else {
DynamicTaskKanbanView(
columns = taskData.columns,
onCompleteTask = { task ->
if (onNavigateToCompleteTask != null) {
// Use full-screen navigation
val residenceName = (myResidencesState as? ApiResult.Success)
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
onNavigateToCompleteTask(task, residenceName)
} else {
// Fall back to dialog
selectedTask = task
showCompleteDialog = true
}
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadTasks(forceRefresh = true)
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
modifier = Modifier.fillMaxSize()
) {
DynamicTaskKanbanView(
columns = taskData.columns,
onCompleteTask = { task ->
if (onNavigateToCompleteTask != null) {
// Use full-screen navigation
val residenceName = (myResidencesState as? ApiResult.Success)
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
onNavigateToCompleteTask(task, residenceName)
} else {
// Fall back to dialog
selectedTask = task
showCompleteDialog = true
}
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
// viewModel.cancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onUncancelTask = { task ->
},
onUncancelTask = { task ->
// viewModel.uncancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onMarkInProgress = { task ->
viewModel.markInProgress(task.id) { success ->
if (success) {
viewModel.loadTasks()
},
onMarkInProgress = { task ->
viewModel.markInProgress(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
}
},
onArchiveTask = { task ->
viewModel.archiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
},
onArchiveTask = { task ->
viewModel.archiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
}
},
onUnarchiveTask = { task ->
viewModel.unarchiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
},
onUnarchiveTask = { task ->
viewModel.unarchiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
},
modifier = Modifier,
bottomPadding = bottomNavBarPadding,
scrollToColumnIndex = scrollToColumnIndex,
onScrollComplete = {
scrollToColumnIndex = null
onClearNavigateToTask()
}
},
modifier = Modifier,
bottomPadding = bottomNavBarPadding,
scrollToColumnIndex = scrollToColumnIndex,
onScrollComplete = {
scrollToColumnIndex = null
onClearNavigateToTask()
}
)
)
}
}
}
}

View File

@@ -2,9 +2,12 @@ package com.tt.honeyDue.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,6 +35,7 @@ fun HomeScreen(
) {
val summaryState by viewModel.myResidencesState.collectAsStateWithLifecycle()
val totalSummary by DataManager.totalSummary.collectAsStateWithLifecycle()
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.loadMyResidences()
@@ -39,6 +43,13 @@ fun HomeScreen(
taskViewModel.loadTasks()
}
// Reset refresh state once the underlying load completes
LaunchedEffect(summaryState) {
if (summaryState !is ApiResult.Loading) {
isRefreshing = false
}
}
// Handle errors for loading summary
summaryState.HandleErrors(
onRetry = { viewModel.loadMyResidences() },
@@ -65,10 +76,21 @@ fun HomeScreen(
}
) { paddingValues ->
WarmGradientBackground {
Column(
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadMyResidences(forceRefresh = true)
taskViewModel.loadTasks(forceRefresh = true)
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous)
) {
@@ -199,6 +221,7 @@ fun HomeScreen(
onClick = onNavigateToTasks
)
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -43,6 +44,7 @@ fun TasksScreen(
var selectedTask by remember { mutableStateOf<com.tt.honeyDue.models.TaskDetail?>(null) }
var showErrorDialog by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var isRefreshing by remember { mutableStateOf(false) }
// Show error dialog when tasks fail to load
LaunchedEffect(tasksState) {
@@ -50,6 +52,9 @@ fun TasksScreen(
errorMessage = com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)
showErrorDialog = true
}
if (tasksState !is ApiResult.Loading) {
isRefreshing = false
}
}
LaunchedEffect(Unit) {
@@ -138,12 +143,22 @@ fun TasksScreen(
}
}
} else {
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadTasks(forceRefresh = true)
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy,
bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy,
top = OrganicSpacing.cozy,
bottom = OrganicSpacing.cozy,
start = OrganicSpacing.cozy,
end = OrganicSpacing.cozy
),
@@ -261,6 +276,7 @@ fun TasksScreen(
}
}
}
}
}
}