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>
7.4 KiB
7.4 KiB
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-typesquery 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).
- Read-only
- Excluded:
- Marking a notification read, unread, or pinned — Gitea exposes this as a real, separate write endpoint; per
AGENTS.mdand 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).
- Marking a notification read, unread, or pinned — Gitea exposes this as a real, separate write endpoint; per
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.xmlapp/src/main/java/com/ubisam/ubgitea/MainActivity.kt(route + wiring only)app/src/main/java/com/ubisam/ubgitea/ui/navigation/(additive: aNotificationsroute 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 commit4fe3bf1, in worktree.claude/worktrees/task-015-notifications. - Domain:
domain/notifications/NotificationModels.kt,NotificationsRepository.kt(read-only, singlelistNotificationsmethod). - Data:
data/notifications/GiteaNotificationsApi.kt—GET /api/v1/notifications, paginated viapage/limit; unread-only is the endpoint's own default, "전체" requests both by repeatingstatus-types=unreadandstatus-types=read. Subject type/state fall back toOTHER/UNKNOWNinstead 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 isnull, not guessed, when unparseable (e.g. commit/repository subjects).- Known limitation, recorded here rather than silently assumed: Gitea's public API docs describe
status-typesonly 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.
- Known limitation, recorded here rather than silently assumed: Gitea's public API docs describe
- Feature:
feature/notifications/NotificationsViewModel.kt,NotificationsScreen.kt. SealedNotificationsUiState(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'sRoute.WorkItems) — a disclosed scope reduction: it opens the issue/PR list for that repository, not the specific item's detail view, sinceRoute.WorkItemscarries 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.Errordid not carry which filter (읽지 않음/전체) was active when the failure happened, soretry()after a failure while viewing "전체" would have silently reset to "읽지 않음". Fixed by addingunreadOnlytoError, covered byNotificationsViewModelTest'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), anotificationsViewModelinstance inMainActivity.kt, and an entry point ("알림" button) inRepositoryListScreen.kt's top-level repository header, next to refresh/logout. - Strings:
app/src/main/res/values/strings_notifications.xml, all prefixednotifications_. - 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) andNotificationsViewModelTest.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:
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../gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks 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.