diff --git a/appview/db/notifications.go b/appview/db/notifications.go --- a/appview/db/notifications.go +++ b/appview/db/notifications.go @@ -113,6 +113,20 @@ 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 @@ -303,6 +317,35 @@ result, err := e.Exec(query, args...) if err != nil { return fmt.Errorf("failed to mark notification as read: %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 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() diff --git a/appview/notifications/notifications.go b/appview/notifications/notifications.go --- a/appview/notifications/notifications.go +++ b/appview/notifications/notifications.go @@ -41,6 +41,7 @@ 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) 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 @@ 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 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -487,12 +487,8 @@ 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 --- 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 */}}