diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt index 1e7430b..4e1cd09 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt @@ -185,6 +185,10 @@ class RepositoryListViewModel( val browserRequests: SharedFlow = mutableBrowserRequests.asSharedFlow() private var operation: Job? = null private val rowBranchJobs = mutableMapOf() + // The branch each repository was last found to have been pushed to. A list that is loaded + // again (a tab change, a refresh, a page turn) shows this while it checks, where it used to + // show "checking" on every row: the rows changed height and their text changed line, twice. + private val knownRowBranches = mutableMapOf() // Repositories whose star/unstar request is still on its way. A second tap on the same row is // ignored until the first request settles: the tap flips the star at once, so without this a // quick double tap sent a star and an unstar together, and whichever reached the server last @@ -475,6 +479,7 @@ class RepositoryListViewModel( tokenVault.clear() repositoryListCache.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false repositoryContent = null cachedOrganizations = null @@ -496,6 +501,7 @@ class RepositoryListViewModel( if (failure is ApiFailure.AuthenticationExpired) { tokenVault.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false } mutableState.value = RepositoryListUiState.Error( @@ -527,6 +533,7 @@ class RepositoryListViewModel( if (failure is ApiFailure.AuthenticationExpired) { tokenVault.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false } val error = failure.toRepositoryListError() @@ -563,7 +570,10 @@ class RepositoryListViewModel( // showing) -- FavoriteStar's own rendering already handles that mode without consulting // favoriteAction at all, so the cross-reference below only matters for ALL. val alreadyStarred = if (mode == RepositoryListMode.ALL) loadStarredCoordinatesOnce() else emptySet() - val rows = pageItems.map { it.toUiModel(alreadyStarred = it.coordinates in alreadyStarred) } + val rows = pageItems.map { summary -> + val row = summary.toUiModel(alreadyStarred = summary.coordinates in alreadyStarred) + knownRowBranches[row.coordinates]?.let { row.copy(lastPushedBranch = it) } ?: row + } if (rows.isEmpty() && page == 1) { repositoryContent = null mutableState.value = RepositoryListUiState.Empty(accountName, mode, organization) @@ -580,12 +590,13 @@ class RepositoryListViewModel( ) repositoryContent = content mutableState.value = content - loadRowBranches(content) + loadRowBranches(content, all = true) } catch (failure: Throwable) { if (failure is CancellationException) throw failure if (failure is ApiFailure.AuthenticationExpired) { tokenVault.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false repositoryContent = null } @@ -731,9 +742,9 @@ class RepositoryListViewModel( return loaded } - private fun loadRowBranches(content: RepositoryListUiState.Content) { + private fun loadRowBranches(content: RepositoryListUiState.Content, all: Boolean = false) { content.repositories - .filter { it.lastPushedBranch is LastPushedBranchUiState.Loading } + .filter { all || it.lastPushedBranch is LastPushedBranchUiState.Loading } .forEach { launchRowBranchFetch(it.coordinates) } } @@ -745,22 +756,21 @@ class RepositoryListViewModel( val latest = branches .filter { it.commitAtEpochMillis != null } .maxByOrNull { requireNotNull(it.commitAtEpochMillis) } - updateRow(coordinates) { - it.copy( - lastPushedBranch = latest?.let { branch -> - LastPushedBranchUiState.Loaded(branch.name.value) - } ?: if (branches.isEmpty()) { - LastPushedBranchUiState.Empty - } else { - LastPushedBranchUiState.Failed - }, - ) + val found = latest?.let { branch -> + LastPushedBranchUiState.Loaded(branch.name.value) + } ?: if (branches.isEmpty()) { + LastPushedBranchUiState.Empty + } else { + LastPushedBranchUiState.Failed } + if (found != LastPushedBranchUiState.Failed) knownRowBranches[coordinates] = found + updateRow(coordinates) { it.copy(lastPushedBranch = found) } } catch (failure: Throwable) { if (failure is CancellationException) throw failure if (failure is ApiFailure.AuthenticationExpired) { clearSessionOnAuthenticationExpired() - } else { + } else if (coordinates !in knownRowBranches) { + // A check that fails leaves a branch already known where it is. updateRow(coordinates) { it.copy(lastPushedBranch = LastPushedBranchUiState.Failed) } } } @@ -771,6 +781,7 @@ class RepositoryListViewModel( cancelRowBranchJobs() tokenVault.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false repositoryContent = null mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED) @@ -830,6 +841,7 @@ class RepositoryListViewModel( if (failure is ApiFailure.AuthenticationExpired) { tokenVault.clear() activeAccountName = null + knownRowBranches.clear() hasAuthenticatedSession = false repositoryContent = null mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED) diff --git a/docs/development-log.md b/docs/development-log.md index 5156c9f..50aac18 100644 --- a/docs/development-log.md +++ b/docs/development-log.md @@ -1708,3 +1708,21 @@ This is the append-only implementation and handoff record shared by Codex and Cl - Caution: what remains (about 25 to 50 ms on the first frame) is in good part the debug build; a non-debuggable build of the same code measured about a third less. R8 is still on hold under TASK-016. + +## 2026-10-12 (continued) — TASK-069: rows keep their lines between repository tabs + +- Request: the product owner turned the screen back on for the measurement left open, and + reported that going between 전체 저장소 and 즐겨찾기 kept changing the lines of text. +- Measured, the branches beside the list: opening starts with a frame of 30–42 ms and slides for + the full 390 ms, and the rows are built in one 52–64 ms frame after the slide has stopped; + Back starts with 13–17 ms and slides without a slow frame (it was a cut with 69–105 ms). +- What was found: a capture taken 120 ms into the slide showed every row saying + "마지막 push 브랜치 확인 중…". Each load of the list began every row at that text and + replaced it with the branch chip when the check returned; the chip is taller and, with a long + branch name, pushes the date to a line of its own, so each row changed height twice per move. +- What changed: `RepositoryListViewModel` keeps the branch last found for each repository and a + list loaded again starts its rows with it, still checking every row for a newer one. A check + that fails leaves a known branch in place. The record is dropped with the session. +- Measured after: the same capture shows the chips in place during the slide; the repository + tab change starts with 35–53 ms and has no frame over 25 ms after it. +- Checks and results: the five required checks pass. diff --git a/tasks/069-in-app-motion.md b/tasks/069-in-app-motion.md index d1c8622..ce3ad0c 100644 --- a/tasks/069-in-app-motion.md +++ b/tasks/069-in-app-motion.md @@ -39,6 +39,10 @@ The app moves where it used to jump, and what moves is the launcher icon's tea c 120 Hz screen. A move now starts without every screen being drawn again; the two repository lists stay built side by side; a repository's branches open beside the list and not in place of it; nothing that loops is drawn while it is off screen. +14. Reported by the product owner (2026-10-12): going between 전체 저장소 and 즐겨찾기 changed + the lines of text in the rows. Each row keeps the branch it was last found to have been + pushed to while the list is loaded again, where it went back to "확인 중" and then to the + branch, changing the row's height twice. ## Scope @@ -53,6 +57,7 @@ The app moves where it used to jump, and what moves is the launcher icon's tea c - `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt` - `app/src/main/java/com/ubisam/ubgitea/feature/home/HomeScreen.kt` - `app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListScreen.kt` +- `app/src/main/java/com/ubisam/ubgitea/feature/repositories/RepositoryListViewModel.kt` - `app/src/main/java/com/ubisam/ubgitea/feature/work/WorkScreen.kt` - `app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt` - `app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.kt` @@ -63,7 +68,7 @@ The app moves where it used to jump, and what moves is the launcher icon's tea c ## Acceptance criteria -- [x] Each of the thirteen items above is in the code and the app builds. +- [x] Each of the fourteen items above is in the code and the app builds. - [x] The splash covers the app without delaying it: sign-in, loading and the OAuth callback run underneath, and taps do not pass through the cover. - [x] Moving between code tabs, or between pages of one screen, does not replay the screen