Notification Filters
This commit is contained in:
parent
dd24aa1987
commit
28111585ef
|
@ -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.
|
||||||
|
|
|
@ -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...,
|
||||||
|
|
92
pkg/models/notification_filters.go
Normal file
92
pkg/models/notification_filters.go
Normal 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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user