fix(notification): read the inbox by default and stream watch as NDJSON

notification commands required a repository unless --mine was given.
They now act on the account's inbox (D17); -R, TEA_REPO or the new
--this-repo limit them to one repository, and --mine is a hidden no-op.

notification list is a table with ID, TYPE, REPOSITORY, SUBJECT ("#17
Improve greeting", bold when unread), STATE and UPDATED, a "Showing N of
M" line and an empty message on a terminal; piped rows add the status
and the URL. The ID links to the subject. JSON gains the subject number.

notification view shows the subject, its state and the latest comment,
then marks an unread notification as read. mark-read, mark-unread, pin
and unpin report each change ("Pinned notification 12
(alice/project#17)"), and --all confirms on a terminal and reports the
count. notification watch --json printed one array when it exited; it
now prints each change as one JSON object per line, prints a line per
change on a terminal, and runs until Ctrl-C there instead of stopping
after a minute.

Status and type filters complete and validate their values, notification
IDs complete from the inbox, and every command has examples.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:50:46 +02:00
1 parent 014dba8512
commit 85034c524f
6 files changed
+569 -200

No files matched your search

+69 -27
View File
@@ -7,6 +7,10 @@ import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
@@ -18,13 +22,17 @@ import (
type Notification struct {
ID int64 `json:"id"`
Repository string `json:"repository"`
Title string `json:"title"`
URL string `json:"url"`
Type string `json:"type"`
State string `json:"state"`
Unread bool `json:"unread"`
Pinned bool `json:"pinned"`
UpdatedAt string `json:"updated_at"`
// Number is the issue or pull request number of the subject, when it is one.
Number int64 `json:"number,omitempty"`
Title string `json:"title"`
URL string `json:"url"`
Type string `json:"type"`
State string `json:"state"`
Unread bool `json:"unread"`
Pinned bool `json:"pinned"`
UpdatedAt string `json:"updated_at"`
// latestComment is the API URL of the latest comment on the subject.
latestComment string
}
func notificationFrom(thread *gitea.NotificationThread) Notification {
@@ -32,15 +40,32 @@ func notificationFrom(thread *gitea.NotificationThread) Notification {
if thread.Repository != nil {
result.Repository = thread.Repository.FullName
}
if thread.Subject != nil {
result.Title = thread.Subject.Title
result.URL = thread.Subject.HTMLURL
result.Type = string(thread.Subject.Type)
result.State = string(thread.Subject.State)
if subject := thread.Subject; subject != nil {
result.Title = subject.Title
result.URL = subject.HTMLURL
result.Type = string(subject.Type)
result.State = string(subject.State)
result.latestComment = subject.LatestCommentURL
if subject.Type == gitea.NotificationSubjectIssue || subject.Type == gitea.NotificationSubjectPull {
result.Number = lastPathNumber(subject.URL)
}
}
return result
}
// lastPathNumber returns the number that ends the path of a URL, such as 17 of …/issues/17, or 0.
func lastPathNumber(raw string) int64 {
u, err := url.Parse(raw)
if err != nil {
return 0
}
number, err := strconv.ParseInt(path.Base(u.Path), 10, 64)
if err != nil {
return 0
}
return number
}
func notificationStatuses(statuses []string) []gitea.NotificationStatus {
result := make([]gitea.NotificationStatus, 0, len(statuses))
for _, status := range statuses {
@@ -76,22 +101,26 @@ type Notifications struct {
Repository string
}
// List returns a non-nil page of notifications.
func (s Notifications) List(ctx context.Context, opts NotificationOptions) ([]Notification, error) {
threads, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.NotificationThread, *gitea.Response, error) {
if s.Repository == "" {
return c.Notifications.List(ctx, opts.sdk())
}
owner, name, err := splitRepository(s.Repository)
// List returns a non-nil page of notifications and what the server reported about the rest.
func (s Notifications) List(ctx context.Context, opts NotificationOptions) (Page[Notification], error) {
return listPage(opts.Limit, opts.Page, func(limit, page int) ([]Notification, http.Header, error) {
window := opts
window.Limit, window.Page = limit, page
threads, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.NotificationThread, *gitea.Response, error) {
if s.Repository == "" {
return c.Notifications.List(ctx, window.sdk())
}
owner, name, err := splitRepository(s.Repository)
if err != nil {
return nil, nil, err
}
return c.Notifications.ListByRepo(ctx, owner, name, window.sdk())
})
if err != nil {
return nil, nil, err
}
return c.Notifications.ListByRepo(ctx, owner, name, opts.sdk())
return convertAll(threads, notificationFrom), headerOf(resp), nil
})
if err != nil {
return nil, err
}
return convertAll(threads, notificationFrom), nil
}
// View retrieves an account-owned notification thread.
@@ -100,7 +129,7 @@ func (s Notifications) View(ctx context.Context, id int64) (Notification, error)
return c.Notifications.GetByID(ctx, id)
})
if err != nil {
return Notification{}, err
return Notification{}, Classify(err, "notification", strconv.FormatInt(id, 10), "")
}
item := notificationFrom(thread)
if s.Repository != "" && !strings.EqualFold(item.Repository, s.Repository) {
@@ -109,6 +138,19 @@ func (s Notifications) View(ctx context.Context, id int64) (Notification, error)
return item, nil
}
// LatestComment returns the latest comment on the subject of a notification, or nil without one.
func (s Notifications) LatestComment(ctx context.Context, item Notification) (*Comment, error) {
id := lastPathNumber(item.latestComment)
if id == 0 || item.Repository == "" {
return nil, nil
}
comment, err := (Comments{Client: s.Client, Repository: item.Repository}).View(ctx, id)
if err != nil {
return nil, err
}
return &comment, nil
}
// Mark changes the selected notification status.
func (s Notifications) Mark(ctx context.Context, id int64, status string) (Notification, error) {
if s.Repository != "" {
@@ -151,13 +193,13 @@ func (s Notifications) Watch(ctx context.Context, opts NotificationOptions, inte
}
seen := map[int64]Notification{}
for {
items, err := s.List(ctx, opts)
listed, err := s.List(ctx, opts)
if err != nil {
return err
}
changes := []Notification{}
next := map[int64]Notification{}
for _, item := range items {
for _, item := range listed.Items {
next[item.ID] = item
if previous, exists := seen[item.ID]; !exists || previous != item {
changes = append(changes, item)