@@ -94,12 +94,15 @@ var RepoSpecContextKey = &contextKey{"RepoSpec"}
// The associated value will be of type string.
var BaseURIContextKey = &contextKey{"BaseURI"}
// Options for configuring changes app.
type Options struct {
Notifications notifications.Service // If not nil, changes containing unread notifications are highlighted.
DisableReactions bool // Disable all support for displaying and toggling reactions.
// Notifications, if not nil, is used to highlight changes containing
// unread notifications, and to mark changes that are viewed as read.
Notifications notifications.Service
DisableReactions bool // Disable all support for displaying and toggling reactions.
HeadPre, HeadPost template.HTML
BodyPre string // An html/template definition of "body-pre" template.
// BodyTop provides components to include on top of <body> of page rendered for req. It can be nil.
@@ -214,11 +217,13 @@ func (h *handler) ChangesHandler(w http.ResponseWriter, req *http.Request) error
}
var es []component.ChangeEntry
for _, i := range is {
es = append(es, component.ChangeEntry{Change: i, BaseURI: state.BaseURI})
}
es = state.augmentUnread(req.Context(), es, h.cs, h.Notifications)
if h.Notifications != nil {
es = state.augmentUnread(req.Context(), es, h.Notifications, h.cs)
}
state.Changes = component.Changes{
ChangesNav: component.ChangesNav{
OpenCount: openCount,
ClosedCount: closedCount,
Path: state.BaseURI + state.ReqPath,
@@ -255,42 +260,40 @@ func stateFilter(query url.Values) (change.StateFilter, error) {
default:
return "", fmt.Errorf("unsupported state filter value: %q", selectedTabName)
}
}
func (s state) augmentUnread(ctx context.Context, es []component.ChangeEntry, service change.Service, notificationsService notifications.Service) []component.ChangeEntry {
if notificationsService == nil {
return es
}
tt, ok := service.(interface {
func (s state) augmentUnread(ctx context.Context, es []component.ChangeEntry, notificationService notifications.Service, changeService change.Service) []component.ChangeEntry {
tt, ok := changeService.(interface {
ThreadType(repo string) string
})
if !ok {
log.Println("augmentUnread: change service doesn't implement ThreadType")
return es
}
threadType := tt.ThreadType(s.RepoSpec)
if s.CurrentUser.ID == 0 {
// Unauthenticated user cannot have any unread changes.
return es
}
// TODO: Consider starting to do this in background in parallel with is.List.
ns, err := notificationsService.List(ctx, notifications.ListOptions{
ns, err := notificationService.List(ctx, notifications.ListOptions{
Repo: ¬ifications.RepoSpec{URI: s.RepoSpec},
All: false,
})
if err != nil {
log.Println("augmentUnread: failed to notifications.List:", err)
return es
}
unreadThreads := make(map[uint64]struct{}) // Set of unread thread IDs.
for _, n := range ns {
// n.RepoSpec == s.RepoSpec is guaranteed because we filtered in notifications.ListOptions,
// so we only need to check that n.ThreadType matches.
if n.ThreadType != tt.ThreadType(s.RepoSpec) {
if n.ThreadType != threadType {
continue
}
unreadThreads[n.ThreadID] = struct{}{}
}
@@ -362,10 +365,16 @@ func (h *handler) ChangeHandler(w http.ResponseWriter, req *http.Request, change
}
ts, err := h.cs.ListTimeline(req.Context(), state.RepoSpec, state.ChangeID, nil)
if err != nil {
return fmt.Errorf("changes.ListTimeline: %v", err)
}
if h.Notifications != nil {
err := state.markRead(req.Context(), h.Notifications, h.cs)
if err != nil {
log.Println("ChangeHandler: failed to markRead:", err)
}
}
var timeline []timelineItem
for _, item := range ts {
timeline = append(timeline, timelineItem{item})
}
sort.Sort(byCreatedAtID(timeline))
@@ -381,10 +390,29 @@ func (h *handler) ChangeHandler(w http.ResponseWriter, req *http.Request, change
return fmt.Errorf("t.ExecuteTemplate: %v", err)
}
return nil
}
func (s state) markRead(ctx context.Context, notificationService notifications.Service, changeService change.Service) error {
tt, ok := changeService.(interface {
ThreadType(repo string) string
})
if !ok {
log.Println("markRead: change service doesn't implement ThreadType")
return nil
}
threadType := tt.ThreadType(s.RepoSpec)
if s.CurrentUser.ID == 0 {
// Unauthenticated user cannot mark anything as read.
return nil
}
err := notificationService.MarkRead(ctx, notifications.RepoSpec{URI: s.RepoSpec}, threadType, s.ChangeID)
return err
}
func (h *handler) ChangeCommitsHandler(w http.ResponseWriter, req *http.Request, changeID uint64) error {
if req.Method != http.MethodGet {
return httperror.Method{Allowed: []string{http.MethodGet}}
}
state, err := h.state(req, changeID)