Signed-in users can view notifications (unread-only or all), paginated, and jump from an issue/PR notification to that repository's work items. No mark-read/unread/pinned write endpoint is exposed, matching the existing single-write-scope posture from ADR-004. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
7.4 KiB
Markdown
75 lines
7.4 KiB
Markdown
# TASK-015: Read-only notifications
|
|
|
|
Status: implemented, pending Codex review
|
|
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
|
|
|
|
- [x] Notification list is paginated and read-only; no mark-read/unread/pinned endpoint is reachable from any new code.
|
|
- [x] Unread state is shown with a text label, never color alone.
|
|
- [x] 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.
|
|
- [x] Loading, empty, offline, authentication-expired, and server-error states are localized and accessible.
|
|
- [x] Tests cover subject-type/state mapping, unread/all toggle request construction, target-number parsing (success and failure), and error states.
|
|
- [x] Resource names use the `notifications_` prefix.
|
|
|
|
## Required checks
|
|
|
|
```text
|
|
./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 yet — pending Codex review.
|