Notification Filters

main
Noah Petherbridge 2024-02-28 20:49:16 -08:00
parent dd24aa1987
commit 28111585ef
5 changed files with 243 additions and 12 deletions

View File

@ -43,6 +43,9 @@ func Dashboard() http.HandlerFunc {
return
}
// Parse notification filters.
nf := models.NewNotificationFilterFromForm(r)
// Get our notifications.
pager := &models.Pagination{
Page: 1,
@ -50,7 +53,7 @@ func Dashboard() http.HandlerFunc {
Sort: "created_at desc",
}
pager.ParsePage(r)
notifs, err := models.PaginateNotifications(currentUser, pager)
notifs, err := models.PaginateNotifications(currentUser, nf, pager)
if err != nil {
session.FlashError(w, r, "Couldn't get your notifications: %s", err)
}
@ -86,6 +89,7 @@ func Dashboard() http.HandlerFunc {
var vars = map[string]interface{}{
"Notifications": notifs,
"NotifMap": notifMap,
"Filters": nf,
"Pager": pager,
// Show a warning to 'restricted' profiles who are especially private.

View File

@ -41,13 +41,13 @@ const (
NotificationAlsoPosted NotificationType = "also_posted" // forum replies
NotificationCertRejected NotificationType = "cert_rejected"
NotificationCertApproved NotificationType = "cert_approved"
NotificationPrivatePhoto NotificationType = "private_photo"
NotificationPrivatePhoto NotificationType = "private_photo" // private photo grants
NotificationNewPhoto NotificationType = "new_photo"
NotificationInnerCircle NotificationType = "inner_circle"
NotificationCustom NotificationType = "custom" // custom message pushed
)
// CreateNotification
// CreateNotification inserts a new notification into the database.
func CreateNotification(n *Notification) error {
// Insert via raw SQL query, reasoning:
// the AboutUser relationship has gorm do way too much work:
@ -204,7 +204,7 @@ func CountUnreadNotifications(user *User) (int64, error) {
}
// PaginateNotifications returns the user's notifications.
func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, error) {
func PaginateNotifications(user *User, filters NotificationFilter, pager *Pagination) ([]*Notification, error) {
var (
ns = []*Notification{}
blockedUserIDs = BlockedUserIDs(user)
@ -232,6 +232,12 @@ func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, erro
)
`)
// Mix in notification type filters?
if w, ph, ok := filters.Query(); ok {
where = append(where, w)
placeholders = append(placeholders, ph)
}
query := (&Notification{}).Preload().Where(
strings.Join(where, " AND "),
placeholders...,

View File

@ -0,0 +1,92 @@
package models
import (
"net/http"
)
// NotificationFilter handles users filtering their notification list by category. It is populated
// from front-end checkboxes and translates to SQL filters for PaginateNotifications.
type NotificationFilter struct {
Likes bool `json:"likes"` // form field name
Comments bool `json:"comments"`
NewPhotos bool `json:"photos"`
AlsoCommented bool `json:"replies"` // also_comment and also_posted
PrivatePhoto bool `json:"private"`
Misc bool `json:"misc"` // friendship_approved, cert_approved, cert_rejected, inner_circle, custom
}
var defaultNotificationFilter = NotificationFilter{
Likes: true,
Comments: true,
NewPhotos: true,
AlsoCommented: true,
PrivatePhoto: true,
Misc: true,
}
// NewNotificationFilterFromForm creates a NotificationFilter struct parsed from an HTTP form.
func NewNotificationFilterFromForm(r *http.Request) NotificationFilter {
// Are these boxes checked in a frontend post?
var (
nf = NotificationFilter{
Likes: r.FormValue("likes") == "true",
Comments: r.FormValue("comments") == "true",
NewPhotos: r.FormValue("photos") == "true",
AlsoCommented: r.FormValue("replies") == "true",
PrivatePhoto: r.FormValue("private") == "true",
Misc: r.FormValue("misc") == "true",
}
)
// Default view or when no checkboxes were sent, all are true.
if nf.IsZero() {
return defaultNotificationFilter
}
return nf
}
// IsZero checks for an empty filter.
func (nf NotificationFilter) IsZero() bool {
return nf == NotificationFilter{}
}
// IsAll checks if all filters are checked.
func (nf NotificationFilter) IsAll() bool {
return nf == defaultNotificationFilter
}
// Query returns the SQL "WHERE" clause that applies the filters to the Notifications query.
//
// If no filters should be added, ok returns false.
func (nf NotificationFilter) Query() (where string, placeholders []interface{}, ok bool) {
if nf.IsAll() {
return "", nil, false
}
var (
// Notification types to include.
types = []interface{}{}
)
// Translate
if nf.Likes {
types = append(types, NotificationLike)
}
if nf.Comments {
types = append(types, NotificationComment)
}
if nf.NewPhotos {
types = append(types, NotificationNewPhoto)
}
if nf.AlsoCommented {
types = append(types, NotificationAlsoCommented, NotificationAlsoPosted)
}
if nf.PrivatePhoto {
types = append(types, NotificationPrivatePhoto)
}
if nf.Misc {
types = append(types, NotificationFriendApproved, NotificationCertApproved, NotificationCertRejected, NotificationCustom)
}
return "type IN ?", types, true
}

View File

@ -97,14 +97,15 @@ document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll(".card.nonshy-collapsible-mobile") || []).forEach(node => {
const header = node.querySelector(".card-header"),
body = node.querySelector(".card-content"),
icon = header.querySelector("button.card-header-icon > .icon > i");
icon = header.querySelector("button.card-header-icon > .icon > i"),
always = node.classList.contains("nonshy-collapsible-always");
// Icon classes.
const iconExpanded = "fa-angle-up",
iconContracted = "fa-angle-down";
// If we are already on mobile, hide the body now.
if (screen.width <= 768) {
if (screen.width <= 768 || always) {
body.style.display = "none";
if (icon !== null) {
icon.classList.remove(iconExpanded);

View File

@ -321,12 +321,140 @@
<hr>
</div>
<p class="block">
<a href="/settings#notifications">
<i class="fa fa-gear mr-1"></i>
Manage notification settings
</a>
</p>
<!-- Filters -->
<div class="block">
<form action="{{.Request.URL.Path}}" method="GET">
<div class="card nonshy-collapsible-mobile nonshy-collapsible-always mb-5">
<header class="card-header has-background-link-light">
<p class="card-header-title">
<i class="fa fa-list mr-2"></i> Notification Types
</p>
<button class="card-header-icon" type="button">
<span class="icon">
<i class="fa fa-angle-up"></i>
</span>
</button>
</header>
<div class="card-content">
<p class="block">
<a href="/settings#notifications">
<i class="fa fa-gear mr-1"></i>
Manage notification settings
</a>
</p>
<div class="columns is-multiline mb-0">
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="likes"
value="true"
{{if .Filters.Likes}}checked{{end}}
>
Likes
<p class="help">
on your photos, profile or comments
</p>
</label>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="comments"
value="true"
{{if .Filters.Comments}}checked{{end}}
>
Comments
<p class="help">
on your photos
</p>
</label>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="photos"
value="true"
{{if .Filters.NewPhotos}}checked{{end}}
>
New Photos
<p class="help">
of your friends
</p>
</label>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="replies"
value="true"
{{if .Filters.AlsoCommented}}checked{{end}}
>
Replies
<p class="help">
on comment threads you follow
</p>
</label>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="private"
value="true"
{{if .Filters.PrivatePhoto}}checked{{end}}
>
Private photos
<p class="help">
unlock notifications
</p>
</label>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="misc"
value="true"
{{if .Filters.Misc}}checked{{end}}
>
Miscellaneous
<p class="help">
new friends, certification photos, etc.
</p>
</label>
</div>
</div>
</div>
<div class="block has-text-centered">
<a href="{{.Request.URL.Path}}" class="button">
Reset
</a>
<button type="submit" class="button is-success">
Apply Filters
</button>
</div>
</div>
</div>
<table class="table is-striped is-fullwidth is-hoverable">
<tbody>