No task waits on a Codex review; the redesign tasks are confirmed on the product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric reauthentication. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
8.5 KiB
Markdown
127 lines
8.5 KiB
Markdown
# TASK-034: Star count doesn't increment, repeat tap doesn't un-star, then feels laggy
|
|
|
|
Status: implemented; all required checks pass; live-installed, awaiting the human product owner's confirmation
|
|
Owner: Claude
|
|
Reviewer: Codex
|
|
|
|
## Outcome
|
|
|
|
Three rounds of live feedback on the same feature, following TASK-033's OAuth scope fix. First: the
|
|
human product owner confirmed starring itself now works ("별표 눌러서 즐겨찾기는 되는데") but the
|
|
displayed star count didn't go up ("카운팅은 안올라가네") — `toggleFavorite`'s ALL-list success
|
|
branch updated only `favoriteAction` (to `Starred`) and never touched `starsCount`. Fixed by
|
|
incrementing `starsCount` in the same state update. Second, immediately after: with a naive
|
|
repeat-tap guard in place, tapping an already-starred row no longer un-starred it either
|
|
("이제 올라가는데 다시 눌렀을때 취소가 안되네") — the first fix's guard made a repeat tap a dead
|
|
no-op instead of a real toggle. Replaced that guard with an actual toggle: a repeat tap on a
|
|
row this session knows is starred now calls `unstar()` and decrements the count back down. Third:
|
|
with the toggle working correctly, the human product owner reported the response still felt slow
|
|
("좀 늦게 카운팅이 변하는 느낌") — every state/count update was applied only *after* the full
|
|
network round trip to this app's home-NAS-hosted Gitea completed, so a tap visibly did nothing for
|
|
however long that took. Made the star/count update optimistically, on tap, before the network call
|
|
starts; the update is only rolled back if the request actually turns out to have failed.
|
|
|
|
## Scope
|
|
|
|
- Included: `feature/repositories/RepositoryListViewModel.kt` (`toggleFavorite` only), matching
|
|
test additions in `RepositoryListViewModelTest.kt`.
|
|
- Excluded: No change to the FAVORITES-list unstar path (removing the row entirely already made
|
|
its count irrelevant, no bug there). No change to how the initial list load populates
|
|
`starsCount` (that already came from Gitea's own response, unaffected). No attempt to fetch the
|
|
server's authoritative post-star/unstar count instead of adjusting it client-side (see Design
|
|
notes).
|
|
|
|
## Design notes
|
|
|
|
- **Optimistic ±1, not a re-fetch**: Gitea's star/unstar endpoints (`PUT`/`DELETE`, 204 No Content)
|
|
don't return the new count, so getting the authoritative number back would mean a second network
|
|
call purely to update one number — not worth it for a value the app already knows how to derive
|
|
from something it just confirmed itself (the action succeeded, so the count is exactly one
|
|
higher or lower than what was last displayed). `starsCount` is floored at 0 via
|
|
`coerceAtLeast(0)` as a defensive guard, not because normal use can reach it.
|
|
- **A real toggle, not a guard**: the first pass at this fix added an early-return guard that made
|
|
a repeat tap on an already-`Starred` row do nothing — that stopped the double-count bug but also,
|
|
as immediately reported, made the star impossible to undo from the ALL list without switching to
|
|
the FAVORITES tab. The actual fix branches on the row's own `favoriteAction`: not-yet-starred-this-
|
|
session taps call `star()` (`+1`, → `Starred`), already-starred-this-session taps call
|
|
`unstar()` (`-1`, → `Idle`) — this both fixes the double-count and restores the obviously-expected
|
|
behavior of tapping a filled star to unfill it.
|
|
- **The underlying "can't tell if already starred on load" limitation is untouched**: this toggle
|
|
only tracks what happened *within this session's own interaction* with a row, same as
|
|
`favoriteAction` always has (TASK-018/TASK-030). A repository already starred before the app
|
|
session started still shows outlined/untapped until acted on directly.
|
|
- **Optimistic on tap, not after the network call, for the ALL list specifically**: `updateRow(...)`
|
|
for the star/unstar case now runs synchronously in `toggleFavorite`, before
|
|
`viewModelScope.launch { ... }` even starts, instead of inside the coroutine after
|
|
`favoritesApi.star()/unstar()` returns. This app's Gitea host is a home NAS (the same root cause
|
|
category as TASK-031's timeout fix), so waiting for a full round trip before moving the star or
|
|
count at all made every tap feel delayed rather than instant. The catch block now explicitly
|
|
rolls the optimistic change back (`Failed` state, count restored to what it was before the tap)
|
|
if the request turns out to have actually failed, rather than only ever moving the display forward
|
|
on success as the first two rounds of this fix did. The FAVORITES-list unstar (which removes the
|
|
whole row) deliberately stays non-optimistic — reverting a *removed* row is a more jarring failure
|
|
case than reverting a color/count, and wasn't part of what was reported as feeling slow.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Starring a repository from the ALL list increments its displayed star count by exactly 1.
|
|
- [x] Tapping an already-`Starred` row again un-stars it: calls `unstar()`, decrements the count by
|
|
1, and returns the row to `Idle` (outlined, tappable to star again).
|
|
- [x] The count never displays below 0.
|
|
- [x] The star icon and count update immediately on tap, not after the network call resolves; a
|
|
failed request rolls the display back to its pre-tap state instead of leaving it stuck at the
|
|
optimistic value.
|
|
- [x] Unstarring from the FAVORITES list is unaffected (row removal, no count to update, still
|
|
waits for confirmation as before).
|
|
- [x] Build, unit tests, and lint pass.
|
|
- [ ] Live confirmation from the human product owner that starring/un-starring now feels instant —
|
|
installed on the device but not yet re-tested in this exact session.
|
|
|
|
## Required checks
|
|
|
|
```text
|
|
./gradlew compileDebugKotlin compileDebugUnitTestKotlin
|
|
./gradlew testDebugUnitTest
|
|
./gradlew assembleDebug lintDebug
|
|
```
|
|
|
|
## Evidence
|
|
|
|
- `RepositoryListViewModel.kt`: `toggleFavorite`'s ALL-list `favoriteAction`/`starsCount` update
|
|
now happens synchronously before the coroutine launches (optimistic), with the network call
|
|
itself reduced to just `favoritesApi.star()/unstar()` inside `viewModelScope.launch`; the catch
|
|
block now rolls the optimistic change back on failure (restoring the pre-tap count, marking
|
|
`Failed`) instead of only ever advancing the display on success.
|
|
- `RepositoryListViewModelTest.kt`: `repo()` test helper gained a `starsCount` parameter; tests
|
|
cover starring incrementing the count by 1, a repeat tap un-starring (calls both `star()` and
|
|
`unstar()`, decrements the count, returns to `Idle`), a full star→unstar round trip landing back
|
|
at the original count, the state/count updating immediately on tap *before* the queued coroutine
|
|
has run on the `StandardTestDispatcher` (the optimism itself), and a failed star/unstar each
|
|
rolling the count back to its pre-tap value.
|
|
- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin`: BUILD SUCCESSFUL.
|
|
- `./gradlew testDebugUnitTest`: BUILD SUCCESSFUL, all unit tests passing.
|
|
- `./gradlew assembleDebug lintDebug`: BUILD SUCCESSFUL, 0 lint errors.
|
|
- Installed on the human product owner's device over wireless `adb` (`adb install -r`, streamed
|
|
install succeeded) three times across this task's three rounds; not yet re-tested live against
|
|
the optimistic-update behavior in this exact session.
|
|
|
|
## Known limitations
|
|
|
|
- Still optimistic, not authoritative: if Gitea's real count differs from this app's local ±1 for
|
|
any reason (e.g. another client starred/unstarred the same repo around the same time), the
|
|
displayed number stays off until the next full list reload. Considered an acceptable tradeoff
|
|
given the alternative (a second network call per action) for a rarely-contended personal count.
|
|
- The underlying "can't tell if already starred on initial load" limitation (TASK-018/TASK-030)
|
|
is unchanged — a repository starred before the current app session still shows outlined until
|
|
acted on directly in this session, same as before this task.
|
|
- A failed *un*star attempt (network error mid-toggle) still collapses to the same flat `Failed`
|
|
state as a failed star attempt, which renders as outlined/not-starred on the ALL list even though
|
|
the repository may still actually be starred server-side — `FavoriteActionUiState.Failed` carries
|
|
no memory of which action was being attempted. Accepted as a minor, retryable rendering gap
|
|
rather than adding state to track it, since a retry from that visual state is harmless either way
|
|
(an unwanted second `star()` on an already-starred repo is a no-op server-side).
|
|
|
|
## Review findings
|
|
|
|
None yet — no Codex review pending (Codex retired, 2026-09-29).
|