Ubisam-MobileGit/tasks/015-notifications.md
choiminyeong f6ac20c8ed TASK-063: task records say what is actually open
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>
2026-09-29 20:25:49 +09:00

7.4 KiB

TASK-015: Read-only notifications

Status: implemented; no review pending (Codex retired, 2026-09-29) Owner: Claude (human product owner directed Claude to own all subsequent tasks, 2026-08-26) Reviewer: Codex

Outcome

A signed-in user can see their Gitea notifications and open the related repository, issue, or pull request. Never marks a notification read, pinned, or unread — that is a write operation explicitly excluded from this task.

Scope

  • Included:
    • Read-only GET /notifications (GET /notifications/threads/{id} was considered but the list endpoint already carries everything this UI needs, so no per-notification detail call was added).
    • A toggle between "안읽음" (unread only, the endpoint's own default) and "전체" (unread + read, requested via repeated status-types query values).
    • Tapping a notification whose subject resolves to a parseable issue/PR number navigates to that work item (TASK-008); one that resolves to a commit or repository, or whose target number cannot be parsed from its URL, does not offer navigation rather than guessing or crashing.
    • Non-color unread indication (a text label, not just a dot/color).
  • Excluded:
    • Marking a notification read, unread, or pinned — Gitea exposes this as a real, separate write endpoint; per AGENTS.md and the existing "no write beyond the one approved star/unstar endpoint" posture, this requires its own human-approved task, not implicit inclusion here.
    • Push notifications, background sync, or a badge/count outside the notifications screen itself.
    • Navigating to a commit-type or repository-type notification's target screen (only issue/PR navigation exists in this app so far).

Writable paths

  • app/src/main/java/com/ubisam/ubgitea/domain/notifications/
  • app/src/main/java/com/ubisam/ubgitea/data/notifications/
  • app/src/test/java/com/ubisam/ubgitea/domain/notifications/
  • app/src/test/java/com/ubisam/ubgitea/data/notifications/
  • app/src/main/java/com/ubisam/ubgitea/feature/notifications/
  • app/src/test/java/com/ubisam/ubgitea/feature/notifications/
  • app/src/main/res/values/strings_notifications.xml
  • app/src/main/java/com/ubisam/ubgitea/MainActivity.kt (route + wiring only)
  • app/src/main/java/com/ubisam/ubgitea/ui/navigation/ (additive: a Notifications route case)
  • tasks/015-notifications.md

Acceptance criteria

  • Notification list is paginated and read-only; no mark-read/unread/pinned endpoint is reachable from any new code.
  • Unread state is shown with a text label, never color alone.
  • A notification whose subject cannot be resolved to a navigable target (parse failure, commit/repository type) still renders without crashing and without offering a broken navigation action.
  • Loading, empty, offline, authentication-expired, and server-error states are localized and accessible.
  • Tests cover subject-type/state mapping, unread/all toggle request construction, target-number parsing (success and failure), and error states.
  • Resource names use the notifications_ prefix.

Required checks

./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug

Evidence

  • Branch claude/task-015-notifications, based on the TASK-014 commit 4fe3bf1, in worktree .claude/worktrees/task-015-notifications.
  • Domain: domain/notifications/NotificationModels.kt, NotificationsRepository.kt (read-only, single listNotifications method).
  • Data: data/notifications/GiteaNotificationsApi.kt — GET /api/v1/notifications, paginated via page/limit; unread-only is the endpoint's own default, "전체" requests both by repeating status-types=unread and status-types=read. Subject type/state fall back to OTHER/UNKNOWN instead of failing closed on an unrecognized value (matches the "future Gitea types shouldn't crash the app" posture from TASK-008/009). Subject/issue/PR number is parsed from the trailing path segment of the subject URL and is null, not guessed, when unparseable (e.g. commit/repository subjects).
    • Known limitation, recorded here rather than silently assumed: Gitea's public API docs describe status-types only on the per-repo mark-read endpoint (unread/read/pinned); applying it to the notification-list endpoint to request both unread and read together was not independently verified against a live Gitea instance. If Codex's review or manual testing shows it behaves differently, the fallback is to fetch unread and read as two separate calls and merge.
  • Feature: feature/notifications/NotificationsViewModel.kt, NotificationsScreen.kt. Sealed NotificationsUiState (Idle/Loading/Content/Empty/Error), startOperation-style single-job cancellation, unread-state shown via a [읽지 않음] text label (never color alone). Tapping a notification whose subject is an issue or PR with a resolvable number navigates to that repository's Work Items list (TASK-008/013's Route.WorkItems) — a disclosed scope reduction: it opens the issue/PR list for that repository, not the specific item's detail view, since Route.WorkItems carries no item number and deep-linking to a specific issue/PR detail was not part of TASK-013's route shape. Commit/repository-type and unparseable-number notifications render as non-clickable rows instead of a broken or guessed navigation action.
  • Self-review fix (same class of bug as the TASK-007 history-retry issue): the first draft's NotificationsUiState.Error did not carry which filter (읽지 않음/전체) was active when the failure happened, so retry() after a failure while viewing "전체" would have silently reset to "읽지 않음". Fixed by adding unreadOnly to Error, covered by NotificationsViewModelTest's "a failure while viewing all retries with all rather than resetting to unread-only" test.
  • Navigation: added Route.Notifications (Route.kt, RouteStackSaver.kt, TYPE_NOTIFICATIONS), a notificationsViewModel instance in MainActivity.kt, and an entry point ("알림" button) in RepositoryListScreen.kt's top-level repository header, next to refresh/logout.
  • Strings: app/src/main/res/values/strings_notifications.xml, all prefixed notifications_.
  • Tests added: GiteaNotificationsApiTest.kt (8 cases — mapping, null-number fallback on unparseable subject URL, unknown type/state fallback, unreadOnly query-param construction, missing-token auth-expired, malformed-field invalid-response, 403 permission-denied, no-next-page-under-page-size) and NotificationsViewModelTest.kt (6 cases — default unread-only open, showAll + next/previous pagination, empty-first-page, listing-failure + retry-same-filter, empty-vs-content boundary, and the unreadOnly-preserving-retry regression test above).
  • Checks run from the worktree root:
    ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks
    
    Result: BUILD SUCCESSFUL, all unit tests passing (188 total across the module), no lint findings beyond the pre-existing native-symbol-strip notice unrelated to this change.
  • git diff --cached --check: clean. Sensitive-pattern scan (client_secret|password|api[_-]?key|secret|token=... against the staged diff): no matches.
  • Not verified on a physical device or emulator in this session (no device attached to this environment) — recorded as a known limitation, same as prior UI-only tasks this session; the acceptance criteria above were verified through unit tests and Compose source review only.

Review findings

None recorded. No review is pending: Codex was retired from the project (2026-09-29).