mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
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:
6 files changed
+569
-200
No files matched your search
@@ -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)
|
||||
|
||||
@@ -105,17 +105,20 @@ func TestNotificationListSendsFilters(t *testing.T) {
|
||||
require.Empty(t, query.Get("before"))
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
}))
|
||||
items, err := (Notifications{Client: client}).List(context.Background(), NotificationOptions{Limit: 20, Page: 2, Types: []string{"Issue", "Pull"}, Since: &since})
|
||||
page, err := (Notifications{Client: client}).List(context.Background(), NotificationOptions{Limit: 20, Page: 2, Types: []string{"Issue", "Pull"}, Since: &since})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []Notification{}, items)
|
||||
require.Equal(t, []Notification{}, page.Items)
|
||||
}
|
||||
|
||||
func TestNotificationFromMapsServerFields(t *testing.T) {
|
||||
thread := decode[gitea.NotificationThread](t, `{"id":5,"repository":{"full_name":"alice/project"},"unread":true,"pinned":true,
|
||||
"updated_at":"2026-10-02T12:00:00+02:00","subject":{"title":"Fix","html_url":"https://gitea.test/alice/project/pulls/1","type":"Pull","state":"merged"}}`)
|
||||
"updated_at":"2026-10-02T12:00:00+02:00","subject":{"title":"Fix","html_url":"https://gitea.test/alice/project/pulls/1","type":"Pull","state":"merged",
|
||||
"url":"https://gitea.test/api/v1/repos/alice/project/issues/1","latest_comment_url":"https://gitea.test/api/v1/repos/alice/project/issues/comments/9"}}`)
|
||||
require.Equal(t, Notification{
|
||||
ID: 5, Repository: "alice/project", Title: "Fix", URL: "https://gitea.test/alice/project/pulls/1", Type: "Pull", State: "merged",
|
||||
Unread: true, Pinned: true, UpdatedAt: "2026-10-02T12:00:00+02:00",
|
||||
ID: 5, Repository: "alice/project", Number: 1, Title: "Fix", URL: "https://gitea.test/alice/project/pulls/1", Type: "Pull", State: "merged",
|
||||
Unread: true, Pinned: true, UpdatedAt: "2026-10-02T12:00:00+02:00", latestComment: "https://gitea.test/api/v1/repos/alice/project/issues/comments/9",
|
||||
}, notificationFrom(thread))
|
||||
commit := decode[gitea.NotificationThread](t, `{"id":7,"subject":{"type":"Commit","url":"https://gitea.test/api/v1/repos/alice/project/commits/123"}}`)
|
||||
require.Zero(t, notificationFrom(commit).Number)
|
||||
require.Equal(t, Notification{ID: 6}, notificationFrom(decode[gitea.NotificationThread](t, `{"id":6,"repository":null,"subject":null}`)))
|
||||
}
|
||||
Reference in new issue
Block a user