From 36bf8cca2fb5dabc2cffca513d5dd2a9090b844e Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 27 May 2026 17:46:26 +0100 Subject: [PATCH] appview/notifications: add ability to mark read/unread for a single notif Signed-off-by: oppiliappan --- appview/db/notifications.go | 43 +++++++++++++++++++ appview/notifications/notifications.go | 32 +++++++++++++- appview/pages/pages.go | 8 +--- .../notifications/fragments/item.html | 31 +++++++++++-- 4 files changed, 102 insertions(+), 12 deletions(-) diff --git a/appview/db/notifications.go b/appview/db/notifications.go index baf3f9f6..83059249 100644 --- a/appview/db/notifications.go +++ b/appview/db/notifications.go @@ -113,6 +113,20 @@ func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Fi return notifications, nil } +func GetNotificationWithEntity(e Execer, notificationID int64, userDID string) (*models.NotificationWithEntity, error) { + results, err := GetNotificationsWithEntities(e, pagination.Page{Limit: 1, Offset: 0}, + orm.FilterEq("n.id", notificationID), + orm.FilterEq("n.recipient_did", userDID), + ) + if err != nil { + return nil, err + } + if len(results) == 0 { + return nil, fmt.Errorf("notification not found") + } + return results[0], nil +} + // GetNotificationsWithEntities retrieves notifications with their related entities func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) { var conditions []string @@ -317,6 +331,35 @@ func MarkNotificationRead(e Execer, notificationID int64, userDID string) error return nil } +func MarkNotificationUnread(e Execer, notificationID int64, userDID string) error { + idFilter := orm.FilterEq("id", notificationID) + recipientFilter := orm.FilterEq("recipient_did", userDID) + + query := fmt.Sprintf(` + UPDATE notifications + SET read = 0 + WHERE %s AND %s + `, idFilter.Condition(), recipientFilter.Condition()) + + args := append(idFilter.Arg(), recipientFilter.Arg()...) + + result, err := e.Exec(query, args...) + if err != nil { + return fmt.Errorf("failed to mark notification as unread: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("notification not found or access denied") + } + + return nil +} + func MarkAllNotificationsRead(e Execer, userDID string) error { recipientFilter := orm.FilterEq("recipient_did", userDID) readFilter := orm.FilterEq("read", 0) diff --git a/appview/notifications/notifications.go b/appview/notifications/notifications.go index 7ea04482..261f9f60 100644 --- a/appview/notifications/notifications.go +++ b/appview/notifications/notifications.go @@ -41,6 +41,7 @@ func (n *Notifications) Router(mw *middleware.Middleware) http.Handler { r.With(middleware.Paginate).Get("/", n.notificationsPage) r.Get("/preview", n.previewHandler) r.Post("/{id}/read", n.markRead) + r.Post("/{id}/unread", n.markUnread) r.Post("/read-all", n.markAllRead) r.Delete("/{id}", n.deleteNotification) }) @@ -224,6 +225,15 @@ func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { } func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) { + n.toggleRead(w, r, true) +} + +func (n *Notifications) markUnread(w http.ResponseWriter, r *http.Request) { + n.toggleRead(w, r, false) +} + +func (n *Notifications) toggleRead(w http.ResponseWriter, r *http.Request, read bool) { + l := n.logger.With("handler", "toggleRead") userDid := n.oauth.GetDid(r) idStr := chi.URLParam(r, "id") @@ -233,9 +243,27 @@ func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) { return } - err = db.MarkNotificationRead(n.db, notificationID, userDid) + if read { + err = db.MarkNotificationRead(n.db, notificationID, userDid) + } else { + err = db.MarkNotificationUnread(n.db, notificationID, userDid) + } if err != nil { - http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError) + http.Error(w, "Failed to update notification", http.StatusInternalServerError) + return + } + + // if called via HTMX (has HX-Request header), return the updated item fragment + if r.Header.Get("HX-Request") == "true" { + notif, err := db.GetNotificationWithEntity(n.db, notificationID, userDid) + if err != nil { + l.Error("failed to fetch notification after toggle", "err", err) + http.Error(w, "Failed to fetch notification", http.StatusInternalServerError) + return + } + if err := n.pages.NotificationItem(w, notif); err != nil { + l.Error("failed to render notification item", "err", err) + } return } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 2edcc664..fab2c2e8 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -487,12 +487,8 @@ func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { return p.execute("notifications/list", w, params) } -type NotificationItemParams struct { - Notification *models.Notification -} - -func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { - return p.executePlain("notifications/fragments/item", w, params) +func (p *Pages) NotificationItem(w io.Writer, notif *models.NotificationWithEntity) error { + return p.executePlain("notifications/fragments/item", w, notif) } type NotificationCountParams struct { diff --git a/appview/pages/templates/notifications/fragments/item.html b/appview/pages/templates/notifications/fragments/item.html index 085491e9..b3799778 100644 --- a/appview/pages/templates/notifications/fragments/item.html +++ b/appview/pages/templates/notifications/fragments/item.html @@ -1,7 +1,7 @@ {{define "notifications/fragments/item"}} + onclick="if(!event.target.closest('button'))navigator.sendBeacon('/notifications/{{ .ID }}/read')" + class="block no-underline hover:no-underline group">
- {{/* row 1, col 3: time */}} - {{ template "repo/fragments/shortTime" .Created }} + {{/* row 1, col 3: timestamp normally, button on hover */}} +
+ {{ template "repo/fragments/shortTime" .Created }} + {{ if .Read }} + + {{ else }} + + {{ end }} +
{{/* row 2, col 1: #N */}} {{ template "notificationNumber" . }} {{/* row 2, col 2: title */}} -- 2.51.2