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

View File

@ -41,13 +41,13 @@ const (
NotificationAlsoPosted NotificationType = "also_posted" // forum replies NotificationAlsoPosted NotificationType = "also_posted" // forum replies
NotificationCertRejected NotificationType = "cert_rejected" NotificationCertRejected NotificationType = "cert_rejected"
NotificationCertApproved NotificationType = "cert_approved" NotificationCertApproved NotificationType = "cert_approved"
NotificationPrivatePhoto NotificationType = "private_photo" NotificationPrivatePhoto NotificationType = "private_photo" // private photo grants
NotificationNewPhoto NotificationType = "new_photo" NotificationNewPhoto NotificationType = "new_photo"
NotificationInnerCircle NotificationType = "inner_circle" NotificationInnerCircle NotificationType = "inner_circle"
NotificationCustom NotificationType = "custom" // custom message pushed NotificationCustom NotificationType = "custom" // custom message pushed
) )
// CreateNotification // CreateNotification inserts a new notification into the database.
func CreateNotification(n *Notification) error { func CreateNotification(n *Notification) error {
// Insert via raw SQL query, reasoning: // Insert via raw SQL query, reasoning:
// the AboutUser relationship has gorm do way too much work: // 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. // 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 ( var (
ns = []*Notification{} ns = []*Notification{}
blockedUserIDs = BlockedUserIDs(user) 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( query := (&Notification{}).Preload().Where(
strings.Join(where, " AND "), strings.Join(where, " AND "),
placeholders..., 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 => { (document.querySelectorAll(".card.nonshy-collapsible-mobile") || []).forEach(node => {
const header = node.querySelector(".card-header"), const header = node.querySelector(".card-header"),
body = node.querySelector(".card-content"), 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. // Icon classes.
const iconExpanded = "fa-angle-up", const iconExpanded = "fa-angle-up",
iconContracted = "fa-angle-down"; iconContracted = "fa-angle-down";
// If we are already on mobile, hide the body now. // If we are already on mobile, hide the body now.
if (screen.width <= 768) { if (screen.width <= 768 || always) {
body.style.display = "none"; body.style.display = "none";
if (icon !== null) { if (icon !== null) {
icon.classList.remove(iconExpanded); icon.classList.remove(iconExpanded);

View File

@ -321,12 +321,140 @@
<hr> <hr>
</div> </div>
<p class="block"> <!-- Filters -->
<a href="/settings#notifications"> <div class="block">
<i class="fa fa-gear mr-1"></i> <form action="{{.Request.URL.Path}}" method="GET">
Manage notification settings
</a> <div class="card nonshy-collapsible-mobile nonshy-collapsible-always mb-5">
</p> <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"> <table class="table is-striped is-fullwidth is-hoverable">
<tbody> <tbody>