TASK-069: rows keep their branch while the repository list reloads
Going between the two repository tabs sent every row back to "checking" and then to its branch chip, which changed each row's height and the lines of its text twice per move. The view model keeps the branch last found for each repository and starts a reloaded list with it, still checking for a newer one. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1abdfc4ca
commit
4dab3a4ba4
@ -185,6 +185,10 @@ class RepositoryListViewModel(
|
|||||||
val browserRequests: SharedFlow<String> = mutableBrowserRequests.asSharedFlow()
|
val browserRequests: SharedFlow<String> = mutableBrowserRequests.asSharedFlow()
|
||||||
private var operation: Job? = null
|
private var operation: Job? = null
|
||||||
private val rowBranchJobs = mutableMapOf<RepositoryCoordinates, Job>()
|
private val rowBranchJobs = mutableMapOf<RepositoryCoordinates, Job>()
|
||||||
|
// 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<RepositoryCoordinates, LastPushedBranchUiState>()
|
||||||
// Repositories whose star/unstar request is still on its way. A second tap on the same row is
|
// 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
|
// 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
|
// quick double tap sent a star and an unstar together, and whichever reached the server last
|
||||||
@ -475,6 +479,7 @@ class RepositoryListViewModel(
|
|||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
repositoryListCache.clear()
|
repositoryListCache.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
repositoryContent = null
|
repositoryContent = null
|
||||||
cachedOrganizations = null
|
cachedOrganizations = null
|
||||||
@ -496,6 +501,7 @@ class RepositoryListViewModel(
|
|||||||
if (failure is ApiFailure.AuthenticationExpired) {
|
if (failure is ApiFailure.AuthenticationExpired) {
|
||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
}
|
}
|
||||||
mutableState.value = RepositoryListUiState.Error(
|
mutableState.value = RepositoryListUiState.Error(
|
||||||
@ -527,6 +533,7 @@ class RepositoryListViewModel(
|
|||||||
if (failure is ApiFailure.AuthenticationExpired) {
|
if (failure is ApiFailure.AuthenticationExpired) {
|
||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
}
|
}
|
||||||
val error = failure.toRepositoryListError()
|
val error = failure.toRepositoryListError()
|
||||||
@ -563,7 +570,10 @@ class RepositoryListViewModel(
|
|||||||
// showing) -- FavoriteStar's own rendering already handles that mode without consulting
|
// showing) -- FavoriteStar's own rendering already handles that mode without consulting
|
||||||
// favoriteAction at all, so the cross-reference below only matters for ALL.
|
// favoriteAction at all, so the cross-reference below only matters for ALL.
|
||||||
val alreadyStarred = if (mode == RepositoryListMode.ALL) loadStarredCoordinatesOnce() else emptySet()
|
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) {
|
if (rows.isEmpty() && page == 1) {
|
||||||
repositoryContent = null
|
repositoryContent = null
|
||||||
mutableState.value = RepositoryListUiState.Empty(accountName, mode, organization)
|
mutableState.value = RepositoryListUiState.Empty(accountName, mode, organization)
|
||||||
@ -580,12 +590,13 @@ class RepositoryListViewModel(
|
|||||||
)
|
)
|
||||||
repositoryContent = content
|
repositoryContent = content
|
||||||
mutableState.value = content
|
mutableState.value = content
|
||||||
loadRowBranches(content)
|
loadRowBranches(content, all = true)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
if (failure is CancellationException) throw failure
|
if (failure is CancellationException) throw failure
|
||||||
if (failure is ApiFailure.AuthenticationExpired) {
|
if (failure is ApiFailure.AuthenticationExpired) {
|
||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
repositoryContent = null
|
repositoryContent = null
|
||||||
}
|
}
|
||||||
@ -731,9 +742,9 @@ class RepositoryListViewModel(
|
|||||||
return loaded
|
return loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadRowBranches(content: RepositoryListUiState.Content) {
|
private fun loadRowBranches(content: RepositoryListUiState.Content, all: Boolean = false) {
|
||||||
content.repositories
|
content.repositories
|
||||||
.filter { it.lastPushedBranch is LastPushedBranchUiState.Loading }
|
.filter { all || it.lastPushedBranch is LastPushedBranchUiState.Loading }
|
||||||
.forEach { launchRowBranchFetch(it.coordinates) }
|
.forEach { launchRowBranchFetch(it.coordinates) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -745,22 +756,21 @@ class RepositoryListViewModel(
|
|||||||
val latest = branches
|
val latest = branches
|
||||||
.filter { it.commitAtEpochMillis != null }
|
.filter { it.commitAtEpochMillis != null }
|
||||||
.maxByOrNull { requireNotNull(it.commitAtEpochMillis) }
|
.maxByOrNull { requireNotNull(it.commitAtEpochMillis) }
|
||||||
updateRow(coordinates) {
|
val found = latest?.let { branch ->
|
||||||
it.copy(
|
|
||||||
lastPushedBranch = latest?.let { branch ->
|
|
||||||
LastPushedBranchUiState.Loaded(branch.name.value)
|
LastPushedBranchUiState.Loaded(branch.name.value)
|
||||||
} ?: if (branches.isEmpty()) {
|
} ?: if (branches.isEmpty()) {
|
||||||
LastPushedBranchUiState.Empty
|
LastPushedBranchUiState.Empty
|
||||||
} else {
|
} else {
|
||||||
LastPushedBranchUiState.Failed
|
LastPushedBranchUiState.Failed
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if (found != LastPushedBranchUiState.Failed) knownRowBranches[coordinates] = found
|
||||||
|
updateRow(coordinates) { it.copy(lastPushedBranch = found) }
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
if (failure is CancellationException) throw failure
|
if (failure is CancellationException) throw failure
|
||||||
if (failure is ApiFailure.AuthenticationExpired) {
|
if (failure is ApiFailure.AuthenticationExpired) {
|
||||||
clearSessionOnAuthenticationExpired()
|
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) }
|
updateRow(coordinates) { it.copy(lastPushedBranch = LastPushedBranchUiState.Failed) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -771,6 +781,7 @@ class RepositoryListViewModel(
|
|||||||
cancelRowBranchJobs()
|
cancelRowBranchJobs()
|
||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
repositoryContent = null
|
repositoryContent = null
|
||||||
mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED)
|
mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED)
|
||||||
@ -830,6 +841,7 @@ class RepositoryListViewModel(
|
|||||||
if (failure is ApiFailure.AuthenticationExpired) {
|
if (failure is ApiFailure.AuthenticationExpired) {
|
||||||
tokenVault.clear()
|
tokenVault.clear()
|
||||||
activeAccountName = null
|
activeAccountName = null
|
||||||
|
knownRowBranches.clear()
|
||||||
hasAuthenticatedSession = false
|
hasAuthenticatedSession = false
|
||||||
repositoryContent = null
|
repositoryContent = null
|
||||||
mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED)
|
mutableState.value = RepositoryListUiState.Error(RepositoryListError.AUTHENTICATION_EXPIRED)
|
||||||
|
|||||||
@ -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;
|
- 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
|
a non-debuggable build of the same code measured about a third less. R8 is still on hold
|
||||||
under TASK-016.
|
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.
|
||||||
|
|||||||
@ -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
|
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
|
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.
|
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
|
## 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/MainActivity.kt`
|
||||||
- `app/src/main/java/com/ubisam/ubgitea/feature/home/HomeScreen.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/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/work/WorkScreen.kt`
|
||||||
- `app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt`
|
- `app/src/main/java/com/ubisam/ubgitea/feature/notifications/NotificationsScreen.kt`
|
||||||
- `app/src/main/java/com/ubisam/ubgitea/feature/workitems/WorkItemsScreen.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
|
## 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
|
- [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.
|
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
|
- [x] Moving between code tabs, or between pages of one screen, does not replay the screen
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user