Web Push Notifications
* Add support for Web Push Notifications when users receive a new Message or Friend Request on the main website. * Users opt in or out of this on their Notification Settings. They can also individually opt out of Message and Friend Request push notifications.
This commit is contained in:
parent
dbeb5060e4
commit
a314aab7ec
|
@ -142,6 +142,10 @@ const (
|
||||||
NotificationOptOutSubscriptions = "notif_optout_subscriptions"
|
NotificationOptOutSubscriptions = "notif_optout_subscriptions"
|
||||||
NotificationOptOutFriendRequestAccepted = "notif_optout_friend_request_accepted"
|
NotificationOptOutFriendRequestAccepted = "notif_optout_friend_request_accepted"
|
||||||
NotificationOptOutPrivateGrant = "notif_optout_private_grant"
|
NotificationOptOutPrivateGrant = "notif_optout_private_grant"
|
||||||
|
|
||||||
|
// Web Push Notifications
|
||||||
|
PushNotificationOptOutMessage = "notif_optout_push_messages"
|
||||||
|
PushNotificationOptOutFriends = "notif_optout_push_friends"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notification opt-outs (stored in ProfileField table)
|
// Notification opt-outs (stored in ProfileField table)
|
||||||
|
@ -155,3 +159,9 @@ var NotificationOptOutFields = []string{
|
||||||
NotificationOptOutFriendRequestAccepted,
|
NotificationOptOutFriendRequestAccepted,
|
||||||
NotificationOptOutPrivateGrant,
|
NotificationOptOutPrivateGrant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push Notification opt-outs (stored in ProfileField table)
|
||||||
|
var PushNotificationOptOutFields = []string{
|
||||||
|
PushNotificationOptOutMessage,
|
||||||
|
PushNotificationOptOutFriends,
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/encryption/coldstorage"
|
"code.nonshy.com/nonshy/website/pkg/encryption/coldstorage"
|
||||||
"code.nonshy.com/nonshy/website/pkg/encryption/keygen"
|
"code.nonshy.com/nonshy/website/pkg/encryption/keygen"
|
||||||
"code.nonshy.com/nonshy/website/pkg/log"
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ type Variable struct {
|
||||||
BareRTC BareRTC
|
BareRTC BareRTC
|
||||||
Maintenance Maintenance
|
Maintenance Maintenance
|
||||||
Encryption Encryption
|
Encryption Encryption
|
||||||
|
WebPush WebPush
|
||||||
Turnstile Turnstile
|
Turnstile Turnstile
|
||||||
UseXForwardedFor bool
|
UseXForwardedFor bool
|
||||||
}
|
}
|
||||||
|
@ -111,6 +113,19 @@ func LoadSettings() {
|
||||||
writeSettings = true
|
writeSettings = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the VAPID keys for Web Push Notification.
|
||||||
|
if len(Current.WebPush.VAPIDPublicKey) == 0 {
|
||||||
|
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Initializing VAPID keys for Web Push: %s", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Current.WebPush.VAPIDPrivateKey = privateKey
|
||||||
|
Current.WebPush.VAPIDPublicKey = publicKey
|
||||||
|
writeSettings = true
|
||||||
|
}
|
||||||
|
|
||||||
// Have we added new config fields? Save the settings.json.
|
// Have we added new config fields? Save the settings.json.
|
||||||
if Current.Version != currentVersion || writeSettings {
|
if Current.Version != currentVersion || writeSettings {
|
||||||
log.Warn("New options are available for your settings.json file. Your settings will be re-saved now.")
|
log.Warn("New options are available for your settings.json file. Your settings will be re-saved now.")
|
||||||
|
@ -181,6 +196,12 @@ type Encryption struct {
|
||||||
ColdStorageRSAPublicKey []byte
|
ColdStorageRSAPublicKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebPush settings.
|
||||||
|
type WebPush struct {
|
||||||
|
VAPIDPublicKey string
|
||||||
|
VAPIDPrivateKey string
|
||||||
|
}
|
||||||
|
|
||||||
// Turnstile (Cloudflare CAPTCHA) settings.
|
// Turnstile (Cloudflare CAPTCHA) settings.
|
||||||
type Turnstile struct {
|
type Turnstile struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
|
|
@ -276,6 +276,28 @@ func Settings() http.HandlerFunc {
|
||||||
session.Flash(w, r, "Unsubscribed from all comment threads!")
|
session.Flash(w, r, "Unsubscribed from all comment threads!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "push_notifications":
|
||||||
|
hashtag = "#notifications"
|
||||||
|
|
||||||
|
// Store their notification opt-outs.
|
||||||
|
for _, key := range config.PushNotificationOptOutFields {
|
||||||
|
var value = r.PostFormValue(key)
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
value = "true" // opt-out, store opt-out=true in the DB
|
||||||
|
} else if value == "true" {
|
||||||
|
value = "false" // the box remained checked, they don't opt-out, store opt-out=false in the DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save it.
|
||||||
|
user.SetProfileField(key, value)
|
||||||
|
}
|
||||||
|
session.Flash(w, r, "Notification preferences updated!")
|
||||||
|
|
||||||
|
// Save the user for new fields to be committed to DB.
|
||||||
|
if err := user.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
||||||
|
}
|
||||||
case "location":
|
case "location":
|
||||||
hashtag = "#location"
|
hashtag = "#location"
|
||||||
var (
|
var (
|
||||||
|
@ -472,6 +494,9 @@ func Settings() http.HandlerFunc {
|
||||||
// Count of subscribed comment threads.
|
// Count of subscribed comment threads.
|
||||||
vars["SubscriptionCount"] = models.CountSubscriptions(user)
|
vars["SubscriptionCount"] = models.CountSubscriptions(user)
|
||||||
|
|
||||||
|
// Count of push notification subscriptions.
|
||||||
|
vars["PushNotificationsCount"] = models.CountPushNotificationSubscriptions(user)
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/webpush"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddFriend controller to send a friend request.
|
// AddFriend controller to send a friend request.
|
||||||
|
@ -122,6 +123,21 @@ func AddFriend() http.HandlerFunc {
|
||||||
} else {
|
} else {
|
||||||
// Log the change.
|
// Log the change.
|
||||||
models.LogCreated(currentUser, "friends", user.ID, "Sent a friend request to "+user.Username+".")
|
models.LogCreated(currentUser, "friends", user.ID, "Sent a friend request to "+user.Username+".")
|
||||||
|
|
||||||
|
// Send a push notification to the recipient.
|
||||||
|
go func() {
|
||||||
|
// Opted out of this one?
|
||||||
|
if user.GetProfileField(config.PushNotificationOptOutFriends) == "true" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Try and send Web Push notification about new Friend Request to: %s", user.Username)
|
||||||
|
webpush.SendNotification(user, webpush.Payload{
|
||||||
|
Topic: "friend",
|
||||||
|
Title: "New Friend Request!",
|
||||||
|
Body: fmt.Sprintf("%s wants to be your friend on %s.", currentUser.Username, config.Title),
|
||||||
|
})
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
session.Flash(w, r, "Friend request sent!")
|
session.Flash(w, r, "Friend request sent!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/webpush"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compose a new chat coming from a user's profile page.
|
// Compose a new chat coming from a user's profile page.
|
||||||
|
@ -61,9 +64,25 @@ func Compose() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a push notification to the recipient.
|
||||||
|
go func() {
|
||||||
|
// Opted out of this one?
|
||||||
|
if user.GetProfileField(config.PushNotificationOptOutMessage) == "true" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Try and send Web Push notification about new Message to: %s", user.Username)
|
||||||
|
webpush.SendNotification(user, webpush.Payload{
|
||||||
|
Topic: "inbox",
|
||||||
|
Title: "New Message!",
|
||||||
|
Body: fmt.Sprintf("%s has left you a message on %s.", currentUser.Username, config.Title),
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
session.Flash(w, r, "Your message has been delivered!")
|
session.Flash(w, r, "Your message has been delivered!")
|
||||||
if from == "inbox" {
|
if from == "inbox" {
|
||||||
templates.Redirect(w, fmt.Sprintf("/messages/read/%d", m.ID))
|
templates.Redirect(w, fmt.Sprintf("/messages/read/%d", m.ID))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
templates.Redirect(w, "/messages")
|
templates.Redirect(w, "/messages")
|
||||||
return
|
return
|
||||||
|
|
|
@ -38,3 +38,12 @@ func Manifest() http.HandlerFunc {
|
||||||
http.ServeFile(w, r, config.StaticPath+"/manifest.json")
|
http.ServeFile(w, r, config.StaticPath+"/manifest.json")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service Worker for web push.
|
||||||
|
func ServiceWorker() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Content-Type", "application/javascript; charset=UTF-8")
|
||||||
|
w.Header().Add("Service-Worker-Allowed", "/")
|
||||||
|
http.ServeFile(w, r, config.StaticPath+"/js/service-worker.js")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ func DeleteUser(user *models.User) error {
|
||||||
{"User Notes", DeleteUserNotes},
|
{"User Notes", DeleteUserNotes},
|
||||||
{"Change Logs", DeleteChangeLogs},
|
{"Change Logs", DeleteChangeLogs},
|
||||||
{"IP Addresses", DeleteIPAddresses},
|
{"IP Addresses", DeleteIPAddresses},
|
||||||
|
{"Push Notifications", DeletePushNotifications},
|
||||||
}
|
}
|
||||||
for _, item := range todo {
|
for _, item := range todo {
|
||||||
if err := item.Fn(user.ID); err != nil {
|
if err := item.Fn(user.ID); err != nil {
|
||||||
|
@ -361,3 +362,13 @@ func DeleteIPAddresses(userID uint64) error {
|
||||||
).Delete(&models.IPAddress{})
|
).Delete(&models.IPAddress{})
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePushNotifications scrubs data for deleting a user.
|
||||||
|
func DeletePushNotifications(userID uint64) error {
|
||||||
|
log.Error("DeleteUser: DeletePushNotifications(%d)", userID)
|
||||||
|
result := models.DB.Where(
|
||||||
|
"user_id = ?",
|
||||||
|
userID,
|
||||||
|
).Delete(&models.PushNotification{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
|
@ -33,4 +33,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&TwoFactor{})
|
DB.AutoMigrate(&TwoFactor{})
|
||||||
DB.AutoMigrate(&ChangeLog{})
|
DB.AutoMigrate(&ChangeLog{})
|
||||||
DB.AutoMigrate(&IPAddress{})
|
DB.AutoMigrate(&IPAddress{})
|
||||||
|
DB.AutoMigrate(&PushNotification{})
|
||||||
}
|
}
|
||||||
|
|
85
pkg/models/notification_push.go
Normal file
85
pkg/models/notification_push.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PushNotification table for Web Push subscriptions.
|
||||||
|
type PushNotification struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
UserID uint64 `gorm:"index"`
|
||||||
|
Subscription string `gorm:"index"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterPushNotification stores a registration for the user.
|
||||||
|
func RegisterPushNotification(user *User, subscription string) (*PushNotification, error) {
|
||||||
|
// Check for an existing registration.
|
||||||
|
pn, err := GetPushNotificationFromSubscription(user, subscription)
|
||||||
|
if err == nil {
|
||||||
|
return pn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create it.
|
||||||
|
pn = &PushNotification{
|
||||||
|
UserID: user.ID,
|
||||||
|
Subscription: subscription,
|
||||||
|
}
|
||||||
|
result := DB.Create(pn)
|
||||||
|
return pn, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPushNotificationFromSubscription checks for an existing subscription.
|
||||||
|
func GetPushNotificationFromSubscription(user *User, subscription string) (*PushNotification, error) {
|
||||||
|
var (
|
||||||
|
pn *PushNotification
|
||||||
|
result = DB.Model(&PushNotification{}).Where(
|
||||||
|
"user_id = ? AND subscription = ?",
|
||||||
|
user.ID, subscription,
|
||||||
|
).First(&pn)
|
||||||
|
)
|
||||||
|
return pn, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountPushNotificationSubscriptions returns how many subscriptions the user has for push.
|
||||||
|
func CountPushNotificationSubscriptions(user *User) int64 {
|
||||||
|
var count int64
|
||||||
|
result := DB.Where(
|
||||||
|
"user_id = ?",
|
||||||
|
user.ID,
|
||||||
|
).Model(&PushNotification{}).Count(&count)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Error("CountPushNotificationSubscriptions(%d): %s", user.ID, result.Error)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPushNotificationSubscriptions returns all subscriptions for a user.
|
||||||
|
func GetPushNotificationSubscriptions(user *User) ([]*PushNotification, error) {
|
||||||
|
var (
|
||||||
|
pn = []*PushNotification{}
|
||||||
|
result = DB.Model(&PushNotification{}).Where("user_id = ?", user.ID).Scan(&pn)
|
||||||
|
)
|
||||||
|
return pn, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePushNotifications scrubs data for deleting a user.
|
||||||
|
func DeletePushNotificationSubscriptions(user *User) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"user_id = ?",
|
||||||
|
user.ID,
|
||||||
|
).Delete(&PushNotification{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePushNotification removes a single subscription from the database.
|
||||||
|
func DeletePushNotification(user *User, subscription string) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"user_id = ? AND subscription = ?",
|
||||||
|
user.ID, subscription,
|
||||||
|
).Delete(&PushNotification{})
|
||||||
|
return result.Error
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/controller/poll"
|
"code.nonshy.com/nonshy/website/pkg/controller/poll"
|
||||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||||
nst "code.nonshy.com/nonshy/website/pkg/templates"
|
nst "code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/webpush"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() http.Handler {
|
func New() http.Handler {
|
||||||
|
@ -30,6 +31,7 @@ func New() http.Handler {
|
||||||
mux.HandleFunc("/", index.Create())
|
mux.HandleFunc("/", index.Create())
|
||||||
mux.HandleFunc("GET /favicon.ico", index.Favicon())
|
mux.HandleFunc("GET /favicon.ico", index.Favicon())
|
||||||
mux.HandleFunc("GET /manifest.json", index.Manifest())
|
mux.HandleFunc("GET /manifest.json", index.Manifest())
|
||||||
|
mux.HandleFunc("GET /sw.js", index.ServiceWorker())
|
||||||
mux.HandleFunc("GET /about", index.StaticTemplate("about.html")())
|
mux.HandleFunc("GET /about", index.StaticTemplate("about.html")())
|
||||||
mux.HandleFunc("GET /features", index.StaticTemplate("features.html")())
|
mux.HandleFunc("GET /features", index.StaticTemplate("features.html")())
|
||||||
mux.HandleFunc("GET /faq", index.StaticTemplate("faq.html")())
|
mux.HandleFunc("GET /faq", index.StaticTemplate("faq.html")())
|
||||||
|
@ -111,6 +113,9 @@ func New() http.Handler {
|
||||||
mux.HandleFunc("GET /v1/version", api.Version())
|
mux.HandleFunc("GET /v1/version", api.Version())
|
||||||
mux.HandleFunc("GET /v1/users/me", api.LoginOK())
|
mux.HandleFunc("GET /v1/users/me", api.LoginOK())
|
||||||
mux.HandleFunc("POST /v1/users/check-username", api.UsernameCheck())
|
mux.HandleFunc("POST /v1/users/check-username", api.UsernameCheck())
|
||||||
|
mux.HandleFunc("GET /v1/web-push/vapid-public-key", webpush.VAPIDPublicKey)
|
||||||
|
mux.Handle("POST /v1/web-push/register", middleware.LoginRequired(webpush.Register()))
|
||||||
|
mux.Handle("GET /v1/web-push/unregister", middleware.LoginRequired(webpush.UnregisterAll()))
|
||||||
mux.Handle("POST /v1/likes", middleware.LoginRequired(api.Likes()))
|
mux.Handle("POST /v1/likes", middleware.LoginRequired(api.Likes()))
|
||||||
mux.Handle("GET /v1/likes/users", middleware.LoginRequired(api.WhoLikes()))
|
mux.Handle("GET /v1/likes/users", middleware.LoginRequired(api.WhoLikes()))
|
||||||
mux.Handle("POST /v1/notifications/read", middleware.LoginRequired(api.ReadNotification()))
|
mux.Handle("POST /v1/notifications/read", middleware.LoginRequired(api.ReadNotification()))
|
||||||
|
|
180
pkg/webpush/webpush.go
Normal file
180
pkg/webpush/webpush.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// Package webpush provides Web Push Notification functionality.
|
||||||
|
package webpush
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/controller/api"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
webpush "github.com/SherClockHolmes/webpush-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VAPIDPublicKey returns the site's public key as an endpoint.
|
||||||
|
func VAPIDPublicKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(config.Current.WebPush.VAPIDPublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterAll resets a user's stored push notification subscriptions.
|
||||||
|
func UnregisterAll() http.HandlerFunc {
|
||||||
|
var next = "/settings#notifications"
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "You must be logged in to do that!")
|
||||||
|
templates.Redirect(w, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.DeletePushNotificationSubscriptions(currentUser); err != nil {
|
||||||
|
session.FlashError(w, r, "Error removing your subscriptions: %s", err)
|
||||||
|
} else {
|
||||||
|
session.Flash(w, r, "Your push notification subscriptions have been reset!")
|
||||||
|
}
|
||||||
|
templates.Redirect(w, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register endpoint for push notification.
|
||||||
|
func Register() http.HandlerFunc {
|
||||||
|
type Request struct {
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
ExpirationTime float64 `json:"expirationTime"`
|
||||||
|
Keys struct {
|
||||||
|
Auth string `json:"auth"`
|
||||||
|
P256DH string `json:"p256dh"`
|
||||||
|
} `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
OK bool `json:"OK"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
api.SendJSON(w, http.StatusUnauthorized, Response{
|
||||||
|
Error: "You must be logged in to do that!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request payload.
|
||||||
|
var req Request
|
||||||
|
if err := api.ParseJSON(r, &req); err != nil {
|
||||||
|
api.SendJSON(w, http.StatusBadRequest, Response{
|
||||||
|
Error: fmt.Sprintf("Error with request payload: %s", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate it looked correct.
|
||||||
|
if req.Endpoint == "" || req.Keys.Auth == "" || req.Keys.P256DH == "" {
|
||||||
|
api.SendJSON(w, http.StatusBadRequest, Response{
|
||||||
|
Error: "Subscription fields were missing.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize and store it in the database.
|
||||||
|
buf, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
api.SendJSON(w, http.StatusInternalServerError, Response{
|
||||||
|
Error: "Couldn't reserialize your subscription!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = models.RegisterPushNotification(currentUser, string(buf))
|
||||||
|
if err != nil {
|
||||||
|
api.SendJSON(w, http.StatusInternalServerError, Response{
|
||||||
|
Error: "Couldn't create the registration in the database!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.SendJSON(w, http.StatusCreated, Response{
|
||||||
|
OK: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload sent in push notifications.
|
||||||
|
type Payload struct {
|
||||||
|
Topic string `json:"-"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification sends a push notification to a user, broadcast to all of their subscriptions.
|
||||||
|
func SendNotification(user *models.User, body Payload) error {
|
||||||
|
// Send to all of their subscriptions.
|
||||||
|
subs, err := models.GetPushNotificationSubscriptions(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range subs {
|
||||||
|
if err := SendRawNotification(user, payload, body.Topic, sub.Subscription); err != nil {
|
||||||
|
log.Error("SendNotification: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotificationToSubscription sends to a specific push subscriber.
|
||||||
|
func SendNotificationToSubscription(user *models.User, subscription string, body Payload) error {
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendRawNotification(user, payload, body.Topic, subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawNotification sends out a push message.
|
||||||
|
func SendRawNotification(user *models.User, message []byte, topic, subscription string) error {
|
||||||
|
// Decode the subscription.
|
||||||
|
var (
|
||||||
|
s = &webpush.Subscription{}
|
||||||
|
err = json.Unmarshal([]byte(subscription), s)
|
||||||
|
options = &webpush.Options{
|
||||||
|
Topic: topic,
|
||||||
|
Subscriber: user.Email,
|
||||||
|
VAPIDPublicKey: config.Current.WebPush.VAPIDPublicKey,
|
||||||
|
VAPIDPrivateKey: config.Current.WebPush.VAPIDPrivateKey,
|
||||||
|
TTL: 30,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := webpush.SendNotification(message, s, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("webpush.SendNotification: %s", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Handle error response codes.
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
log.Error("Got StatusCode %d when sending push notification; removing the subscription from DB", resp.StatusCode)
|
||||||
|
models.DeletePushNotification(user, subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
BIN
web/static/img/favicon-128.png
Normal file
BIN
web/static/img/favicon-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
BIN
web/static/img/site-settings.png
Normal file
BIN
web/static/img/site-settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
19
web/static/js/service-worker.js
Normal file
19
web/static/js/service-worker.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/* nonshy service worker, for web push notifications */
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
const payload = JSON.parse(event.data.text());
|
||||||
|
try {
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(payload.title, {
|
||||||
|
body: payload.body,
|
||||||
|
icon: "/static/img/favicon-192.png",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch(e) {
|
||||||
|
console.error("sw.showNotification:", e);
|
||||||
|
}
|
||||||
|
});
|
45
web/static/js/web-push.js
Normal file
45
web/static/js/web-push.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/* nonshy web push notification helper */
|
||||||
|
navigator.serviceWorker.register("/sw.js", {
|
||||||
|
scope: "/",
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Service Worker NOT registered:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
function PushNotificationSubscribe() {
|
||||||
|
navigator.serviceWorker.ready.then(async function(registration) {
|
||||||
|
return registration.pushManager.getSubscription().then(async function(subscription) {
|
||||||
|
// If a subscription was already found, return it.
|
||||||
|
if (subscription) {
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the server's public key.
|
||||||
|
const response = await fetch("/v1/web-push/vapid-public-key");
|
||||||
|
const vapidPublicKey = await response.text();
|
||||||
|
|
||||||
|
// Subscribe the user.
|
||||||
|
return registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: vapidPublicKey,
|
||||||
|
});
|
||||||
|
}).then(subscription => {
|
||||||
|
|
||||||
|
// Post it to the backend.
|
||||||
|
const serialized = JSON.stringify(subscription);
|
||||||
|
fetch("/v1/web-push/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: serialized
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has already given notification permission, (re)subscribe for push.
|
||||||
|
document.addEventListener("DOMContentLoaded", e => {
|
||||||
|
if (Notification.permission === "granted") {
|
||||||
|
PushNotificationSubscribe();
|
||||||
|
}
|
||||||
|
});
|
|
@ -930,188 +930,279 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notification Settings -->
|
<!-- Notification Settings -->
|
||||||
<div class="card mb-5" id="notifications">
|
<div id="notifications">
|
||||||
<header class="card-header has-background-link">
|
<div class="card mb-5">
|
||||||
<p class="card-header-title has-text-light">
|
<header class="card-header has-background-link">
|
||||||
<i class="fa fa-bell pr-2"></i>
|
<p class="card-header-title has-text-light">
|
||||||
Notification Settings
|
<i class="fa fa-bell pr-2"></i>
|
||||||
</p>
|
Web Push Notifications <span class="tag is-success ml-2">New!</span>
|
||||||
</header>
|
</p>
|
||||||
|
</header>
|
||||||
<div class="card-content">
|
|
||||||
<p class="block">
|
|
||||||
On this page you may opt-out of certain kinds of (on-site) notification messages.
|
|
||||||
{{PrettyTitle}} does not send you any e-mails or push notification -- these on-site
|
|
||||||
notifications only appear while you are visiting the website (on your
|
|
||||||
<a href="/me">home/user dashboard page</a>).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="POST" action="/settings">
|
|
||||||
{{InputCSRF}}
|
|
||||||
<input type="hidden" name="intent" value="notifications">
|
|
||||||
|
|
||||||
<h2 class="subtitle">New Photo Uploads</h2>
|
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
<p class="block">
|
<p class="block">
|
||||||
By default you will be notified when your friends upload a new picture to the site.
|
You may opt-in to receive Web Push Notifications for some of your important updates
|
||||||
Below, you may opt-out of new photo upload notifications.
|
from {{PrettyTitle}}, such as when you receive a new Direct Message on the main website,
|
||||||
|
even when you have closed your browser.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="field">
|
<p class="block">
|
||||||
<label class="label">Notify me when...</label>
|
<strong>Push Notification Permission:</strong>
|
||||||
<label class="checkbox">
|
<strong class="has-text-success" id="push-status-enabled" style="display: none">
|
||||||
<input type="checkbox"
|
<i class="fa fa-check mr-1"></i> Granted
|
||||||
name="notif_optout_friends_photos"
|
</strong>
|
||||||
value="true"
|
<strong class="has-text-danger" id="push-status-disabled" style="display: none">
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_friends_photos") "true"}}checked{{end}}>
|
<i class="fa fa-xmark mr-1"></i> Denied
|
||||||
My friends upload a new photo
|
</strong>
|
||||||
</label>
|
<strong class="has-text-warning" id="push-status-default" style="display: none">
|
||||||
<p class="help">
|
<i class="fa fa-xmark mr-1"></i> Not Granted
|
||||||
If unchecked, the following two notifications will not be sent either.
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
<!-- Button to Grant Permission or Test Notifications -->
|
||||||
|
<a href="#" id="grant-push-permission"
|
||||||
|
class="button is-small is-success">
|
||||||
|
Test Notifications
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Help error if the permission is denied -->
|
||||||
|
<span id="push-denied-help" style="display: none">
|
||||||
|
<i class="fa fa-info-circle has-text-warning mr-1"></i>
|
||||||
|
You had denied notification permission to this site. Please check in your web browser's
|
||||||
|
settings (or click in your address bar to the left of the website URL) to reset your
|
||||||
|
permission setting. Please <a href="/faq#troubleshoot-web-push">see this page</a> for
|
||||||
|
help in case you want to resolve this.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Have existing subscriptions? -->
|
||||||
|
{{if .PushNotificationsCount}}
|
||||||
|
<p class="block">
|
||||||
|
<strong>Sessions:</strong> you have enabled push notifications on {{.PushNotificationsCount}} web browser{{Pluralize64 .PushNotificationsCount}}. You may
|
||||||
|
<a href="/v1/web-push/unregister">click here</a> to reset your subscriptions. Devices that you actively use
|
||||||
|
(and had granted permission on before) may re-subscribe on your next visit.
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Specific Push Notification Opt-out Form -->
|
||||||
|
<form method="POST" action="/settings">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="intent" value="push_notifications">
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Send a Web Push Notification when...</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_push_messages"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_push_messages") "true"}}checked{{end}}>
|
||||||
|
I receive a Direct Message on the main website
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_push_friends"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_push_friends") "true"}}checked{{end}}>
|
||||||
|
I receive a new Friend Request
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<i class="fa fa-save mr-2"></i> Save Push Notification Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-5">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<i class="fa fa-bell pr-2"></i>
|
||||||
|
Notification Settings
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="block">
|
||||||
|
On this page you may opt-out of certain kinds of (on-site) notification messages.
|
||||||
|
{{PrettyTitle}} does not send you any e-mails or push notification -- these on-site
|
||||||
|
notifications only appear while you are visiting the website (on your
|
||||||
|
<a href="/me">home/user dashboard page</a>).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/settings">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="intent" value="notifications">
|
||||||
|
|
||||||
|
<h2 class="subtitle">New Photo Uploads</h2>
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
By default you will be notified when your friends upload a new picture to the site.
|
||||||
|
Below, you may opt-out of new photo upload notifications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox">
|
<label class="label">Notify me when...</label>
|
||||||
<input type="checkbox"
|
<label class="checkbox">
|
||||||
name="notif_optout_private_photos"
|
<input type="checkbox"
|
||||||
value="true"
|
name="notif_optout_friends_photos"
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_private_photos") "true"}}checked{{end}}>
|
value="true"
|
||||||
A friend who shared their private photos with me uploads a new private photo
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_friends_photos") "true"}}checked{{end}}>
|
||||||
</label>
|
My friends upload a new photo
|
||||||
</div>
|
</label>
|
||||||
|
<p class="help">
|
||||||
|
If unchecked, the following two notifications will not be sent either.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="notif_optout_explicit_photos"
|
name="notif_optout_private_photos"
|
||||||
value="true"
|
value="true"
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_explicit_photos") "true"}}checked{{end}}>
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_private_photos") "true"}}checked{{end}}>
|
||||||
Allow notifications for 'explicit' photo uploads by my friends
|
A friend who shared their private photos with me uploads a new private photo
|
||||||
</label>
|
</label>
|
||||||
<p class="help">
|
</div>
|
||||||
This will also depend on your <a href="/settings#prefs" target="_blank">opt-in to see explicit content</a> -- otherwise
|
|
||||||
notifications about explicit photo uploads will not be sent to you.
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_explicit_photos"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_explicit_photos") "true"}}checked{{end}}>
|
||||||
|
Allow notifications for 'explicit' photo uploads by my friends
|
||||||
|
</label>
|
||||||
|
<p class="help">
|
||||||
|
This will also depend on your <a href="/settings#prefs" target="_blank">opt-in to see explicit content</a> -- otherwise
|
||||||
|
notifications about explicit photo uploads will not be sent to you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="subtitle mt-5">Likes & Comments</h2>
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
By default you will be notified when somebody 'likes' or comments on your profile page
|
||||||
|
or photos. You may turn off those notifications with the options below.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="subtitle mt-5">Likes & Comments</h2>
|
<div class="field">
|
||||||
|
<label class="label">Notify me when...</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_likes"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_likes") "true"}}checked{{end}}>
|
||||||
|
Somebody 'likes' my profile page, photos, or comments
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="block">
|
<div class="field">
|
||||||
By default you will be notified when somebody 'likes' or comments on your profile page
|
<label class="checkbox">
|
||||||
or photos. You may turn off those notifications with the options below.
|
<input type="checkbox"
|
||||||
</p>
|
name="notif_optout_comments"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_comments") "true"}}checked{{end}}>
|
||||||
|
Somebody leaves a comment on one of my photos
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<h2 class="subtitle mt-5">Comment Thread Subscriptions</h2>
|
||||||
<label class="label">Notify me when...</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="notif_optout_likes"
|
|
||||||
value="true"
|
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_likes") "true"}}checked{{end}}>
|
|
||||||
Somebody 'likes' my profile page, photos, or comments
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<p class="block">
|
||||||
<label class="checkbox">
|
Comment threads and forum posts may be 'subscribed' to so that you can be notified about
|
||||||
<input type="checkbox"
|
comments left by other people after you. By default, you will subscribe to comment threads
|
||||||
name="notif_optout_comments"
|
after you leave your first comment.
|
||||||
value="true"
|
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_comments") "true"}}checked{{end}}>
|
|
||||||
Somebody leaves a comment on one of my photos
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="subtitle mt-5">Comment Thread Subscriptions</h2>
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
Comment threads and forum posts may be 'subscribed' to so that you can be notified about
|
|
||||||
comments left by other people after you. By default, you will subscribe to comment threads
|
|
||||||
after you leave your first comment.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
<strong>Note:</strong> you may unsubscribe from comment threads by using the link at the
|
|
||||||
top of its page (for example: at the top of a forum thread page or the top of the list of
|
|
||||||
comments on a photo page). You may also opt <em>in</em> to get notifications on a thread
|
|
||||||
that you didn't comment on by using the same link at the top of their pages.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
The options below can control the automatic opt-in for subscriptions when you leave a
|
|
||||||
comment on a new comment thread.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="notif_optout_subscriptions"
|
|
||||||
value="true"
|
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_subscriptions") "true"}}checked{{end}}>
|
|
||||||
Subscribe to notifications for future comments when I leave a comment on something
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Unsubscribe from Comment Threads</label>
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="unsubscribe_all_threads"
|
|
||||||
value="true">
|
|
||||||
Unsubscribe NOW from <strong>all ({{.SubscriptionCount}}) comment threads</strong> that I am currently following.
|
|
||||||
</label>
|
|
||||||
<p class="help">
|
|
||||||
You are currently subscribed to <strong>{{.SubscriptionCount}}</strong> comment thread{{Pluralize64 .SubscriptionCount}}.
|
|
||||||
You may immediately unsubscribe from all of these threads by checking this box and clicking "Save" below.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="subtitle mt-5">Miscellaneous</h2>
|
<p class="block">
|
||||||
|
<strong>Note:</strong> you may unsubscribe from comment threads by using the link at the
|
||||||
<div class="field">
|
top of its page (for example: at the top of a forum thread page or the top of the list of
|
||||||
<label class="label">Notify me when...</label>
|
comments on a photo page). You may also opt <em>in</em> to get notifications on a thread
|
||||||
<label class="checkbox">
|
that you didn't comment on by using the same link at the top of their pages.
|
||||||
<input type="checkbox"
|
|
||||||
name="notif_optout_friend_request_accepted"
|
|
||||||
value="true"
|
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_friend_request_accepted") "true"}}checked{{end}}>
|
|
||||||
Somebody approves my friendship request
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="notif_optout_private_grant"
|
|
||||||
value="true"
|
|
||||||
{{if ne (.CurrentUser.GetProfileField "notif_optout_private_grant") "true"}}checked{{end}}>
|
|
||||||
Somebody unlocks their private photos for me to see
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Read-only box for certification photo response -->
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox"
|
|
||||||
value="true"
|
|
||||||
checked
|
|
||||||
disabled>
|
|
||||||
My certification photo is approved or rejected
|
|
||||||
</label>
|
|
||||||
<p class="help">
|
|
||||||
This notification is important for your account status, is rarely sent out, and can
|
|
||||||
not be opted-out from.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<p class="block">
|
||||||
<button type="submit" class="button is-primary">
|
The options below can control the automatic opt-in for subscriptions when you leave a
|
||||||
<i class="fa fa-save mr-2"></i> Save Privacy Settings
|
comment on a new comment thread.
|
||||||
</button>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_subscriptions"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_subscriptions") "true"}}checked{{end}}>
|
||||||
|
Subscribe to notifications for future comments when I leave a comment on something
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Unsubscribe from Comment Threads</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="unsubscribe_all_threads"
|
||||||
|
value="true">
|
||||||
|
Unsubscribe NOW from <strong>all ({{.SubscriptionCount}}) comment threads</strong> that I am currently following.
|
||||||
|
</label>
|
||||||
|
<p class="help">
|
||||||
|
You are currently subscribed to <strong>{{.SubscriptionCount}}</strong> comment thread{{Pluralize64 .SubscriptionCount}}.
|
||||||
|
You may immediately unsubscribe from all of these threads by checking this box and clicking "Save" below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="subtitle mt-5">Miscellaneous</h2>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Notify me when...</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_friend_request_accepted"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_friend_request_accepted") "true"}}checked{{end}}>
|
||||||
|
Somebody approves my friendship request
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="notif_optout_private_grant"
|
||||||
|
value="true"
|
||||||
|
{{if ne (.CurrentUser.GetProfileField "notif_optout_private_grant") "true"}}checked{{end}}>
|
||||||
|
Somebody unlocks their private photos for me to see
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Read-only box for certification photo response -->
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
value="true"
|
||||||
|
checked
|
||||||
|
disabled>
|
||||||
|
My certification photo is approved or rejected
|
||||||
|
</label>
|
||||||
|
<p class="help">
|
||||||
|
This notification is important for your account status, is rarely sent out, and can
|
||||||
|
not be opted-out from.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<i class="fa fa-save mr-2"></i> Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1143,8 +1234,8 @@
|
||||||
<p>
|
<p>
|
||||||
Your Two-Factor is currently:
|
Your Two-Factor is currently:
|
||||||
{{if .TwoFactorEnabled}}
|
{{if .TwoFactorEnabled}}
|
||||||
<i class="fa fa-check mr-1 has-text-success-dark"></i>
|
<i class="fa fa-check mr-1 has-text-success"></i>
|
||||||
<strong class="has-text-success-dark">Enabled</strong>
|
<strong class="has-text-success">Enabled</strong>
|
||||||
{{else}}
|
{{else}}
|
||||||
<i class="fa fa-xmark mr-1 has-text-danger"></i>
|
<i class="fa fa-xmark mr-1 has-text-danger"></i>
|
||||||
<strong class="has-text-danger">Not Enabled</strong>
|
<strong class="has-text-danger">Not Enabled</strong>
|
||||||
|
@ -1462,6 +1553,59 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notifications tab scripts.
|
||||||
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
// Get useful controls from the tab.
|
||||||
|
const $pushStatusGranted = document.querySelector("#push-status-enabled"),
|
||||||
|
$pushStatusDenied = document.querySelector("#push-status-disabled"),
|
||||||
|
$pushStatusDefault = document.querySelector("#push-status-default"),
|
||||||
|
$pushEnableButton = document.querySelector("#grant-push-permission"),
|
||||||
|
$pushDeniedHelp = document.querySelector("#push-denied-help");
|
||||||
|
|
||||||
|
// Get the current permission status: default, granted, denied.
|
||||||
|
const showPermission = (permission) => {
|
||||||
|
$pushStatusGranted.style.display = "none";
|
||||||
|
$pushStatusDenied.style.display = "none";
|
||||||
|
$pushStatusDefault.style.display = "none";
|
||||||
|
switch (permission) {
|
||||||
|
case "granted":
|
||||||
|
$pushStatusGranted.style.display = "";
|
||||||
|
$pushEnableButton.innerHTML = "Test Push Notification";
|
||||||
|
break;
|
||||||
|
case "denied":
|
||||||
|
$pushEnableButton.style.display = "none";
|
||||||
|
$pushDeniedHelp.style.display = "";
|
||||||
|
$pushStatusDenied.style.display = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$pushStatusDefault.style.display = "";
|
||||||
|
$pushEnableButton.innerHTML = "Grant Push Notification Permission";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
showPermission(Notification.permission);
|
||||||
|
|
||||||
|
$pushEnableButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
Notification.requestPermission().then(permission => {
|
||||||
|
// Update the displayed permission status.
|
||||||
|
showPermission(Notification.permission);
|
||||||
|
|
||||||
|
// If granted, subscribe to push notifications now.
|
||||||
|
if (permission === "granted") {
|
||||||
|
// In static/js/web-push.js
|
||||||
|
PushNotificationSubscribe();
|
||||||
|
|
||||||
|
// Test the notification now.
|
||||||
|
const notification = new Notification(`Hello from ${document.location.hostname}!`, {
|
||||||
|
body: "This is an example notification from this site.",
|
||||||
|
icon: "/static/img/favicon-192.png",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Location tab scripts.
|
// Location tab scripts.
|
||||||
window.addEventListener("DOMContentLoaded", (event) => {
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
// Get useful controls from the tab.
|
// Get useful controls from the tab.
|
||||||
|
|
|
@ -387,6 +387,7 @@
|
||||||
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/htmx-1.9.12.min.js"></script>
|
<script type="text/javascript" src="/static/js/htmx-1.9.12.min.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/slim-forms.js?build={{.BuildHash}}"></script>
|
<script type="text/javascript" src="/static/js/slim-forms.js?build={{.BuildHash}}"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/web-push.js?build={{.BuildHash}}"></script>
|
||||||
{{template "scripts" .}}
|
{{template "scripts" .}}
|
||||||
|
|
||||||
<!-- Likes modal -->
|
<!-- Likes modal -->
|
||||||
|
|
|
@ -70,10 +70,19 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#chat-access">Who can access the chat rooms?</a></li>
|
<li><a href="#chat-access">Who can access the chat rooms?</a></li>
|
||||||
<li><a href="#chat-support">What are the technical requirements to use the chat room?</a></li>
|
<li><a href="#chat-support">What are the technical requirements to use the chat room?</a></li>
|
||||||
<li><a href="#webcam-support">I can't share or connect to other peoples' webcams</a></li>
|
<li><a href="#webcam-support">I am experiencing a problem with <strong>webcam sharing</strong></a></li>
|
||||||
<li><a href="#chat-more">Where can I learn more about the chat room?</a></li>
|
<li><a href="#chat-more">Where can I learn more about the chat room?</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#notification-faqs">Notification FAQs</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#notifications">Does nonshy send me notifications?</a></li>
|
||||||
|
<li><a href="#web-push">About <strong>Web Push Notifications</strong></a></li>
|
||||||
|
<li><a href="#cancel-web-push">How do I turn off Web Push Notifications?</a></li>
|
||||||
|
<li><a href="#troubleshoot-web-push">Troubleshooting Web Push Notifications</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#shy-faqs">Shy Account FAQs</a>
|
<a href="#shy-faqs">Shy Account FAQs</a>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -908,56 +917,75 @@
|
||||||
<h3 id="chat-support">What are the technical requirements to use the chat room?</h3>
|
<h3 id="chat-support">What are the technical requirements to use the chat room?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The chat room seems to work the best on the following combination of devices and
|
The chat room should generally work well on all major web browsers, operating systems
|
||||||
web browsers:
|
and device types. Recommended browsers include Mozilla Firefox, Google Chrome (or any
|
||||||
|
other Chromium-based browser of your choice, such as Microsoft Edge, Opera or Brave),
|
||||||
|
or Apple's Safari browser. Most Androids, iPads and iPhones should be able to use the
|
||||||
|
chat room successfully, including webcam support.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>Firefox</strong> and <strong>Chromium</strong>-based web browsers
|
|
||||||
on <strong>all desktop-like operating systems</strong> including Windows, Mac OS
|
|
||||||
and Linux. Chromium-based browsers include Google Chrome, Microsoft Edge, Opera,
|
|
||||||
Brave or others that are based on the open source Chromium browser.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
On <strong>Android</strong> devices, all <strong>Firefox</strong> and <strong>Chromium-based</strong>
|
|
||||||
browsers are generally working quite well.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The chat room and video sharing generally works well on the above devices. Below
|
<strong>When opening many webcams:</strong> the number of cameras you can watch at a
|
||||||
are some that are known to have issues with the chat room at this time:
|
time is mainly limited by your device's hardware specifications and your local network
|
||||||
|
bandwidth. The chat room doesn't enforce an arbitrary limit to the number of cameras,
|
||||||
|
so you can experiment and find out how many your device can support.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<p>
|
||||||
<li>
|
For some examples: a Macbook Air M3 laptop from 2024 is able to comfortably open more
|
||||||
<strong>Safari on Mac OS X</strong> usually works for the text chat portions
|
than 20 webcams at a time. However, a Dell XPS 13 laptop from 2018 (which had 16GB RAM,
|
||||||
(entering the room and chatting), but webcam sharing doesn't seem to work (either
|
an nVIDIA graphics card, etc.) was seen to only be able to open 10 or 15 cameras before
|
||||||
broadcasting or viewing others' cameras).
|
the laptop became very warm.
|
||||||
</li>
|
</p>
|
||||||
<li>
|
|
||||||
<strong>iPhone and iPad</strong> (all web browsers) have difficulty logging in to
|
|
||||||
the chat room at all. Chrome or Firefox do not work on iOS, either - because under
|
|
||||||
the hood all web browsers on iOS are just custom wrappers around Mobile Safari,
|
|
||||||
which doesn't like my chat room right now. So unfortunately, Apple mobile devices
|
|
||||||
can not use the chat room.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 id="webcam-support">I can't share or connect to other peoples' webcams</h3>
|
<h3 id="webcam-support">I am experiencing a problem with webcam sharing</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
First, verify that you're using a <a href="#chat-support">known supported device</a>
|
Please see the <strong><a href="/forum/thread/309">Webcam Troubleshooting</a></strong>
|
||||||
|
thread in the forums for some advice on things to try.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Verify that you're using a <a href="#chat-support">known supported device</a>
|
||||||
when accessing the chat room. Generally this means you're running a Firefox or
|
when accessing the chat room. Generally this means you're running a Firefox or
|
||||||
Chromium-based browser on a desktop computer, laptop, or Android device. Webcam
|
Chromium-based browser on a desktop computer, laptop, or Android device. The Apple
|
||||||
sharing does not work <strong>at all</strong> in Safari on Mac OS, and portable
|
Safari browser should also work from a Mac, iPhone or iPad computer.
|
||||||
Apple devices such as iPad and iPhone do not work with the chat room at all.
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The most common type of error message people encounter on chat looks like:
|
||||||
|
<strong>NotAllowedError: Permission denied.</strong> This error message usually has
|
||||||
|
one of three causes:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Your web browser has denied permission to chat.nonshy.com, and you should check in
|
||||||
|
your web browser's settings (Privacy & Security section) for Webcam and Microphone
|
||||||
|
and remove chat.nonshy.com from the list of sites. After doing so, restart your browser
|
||||||
|
and log onto the chat room, and be sure to click "Allow" when it asks for permission
|
||||||
|
when going on webcam.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Sometimes, your operating system itself is actually denying permission to your web
|
||||||
|
browser. You can check in your System Settings for App Permissions for your webcam
|
||||||
|
and microphone, and ensure that your web browser has this permission on your device.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Sometimes this error comes up when your webcam is already in use by another
|
||||||
|
application (such as Skype or Discord). Ensure that there is no other app currently
|
||||||
|
using your webcam before you go on camera in the chat room.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The linked Webcam Troubleshooting thread above has instructions for common web browsers
|
||||||
|
and operating systems on where to check for permission errors.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you are on a supported device, check out the following information about how
|
If you are on a supported device, check out the following information about how
|
||||||
webcam sharing works:
|
webcam sharing works in general:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -1017,15 +1045,6 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
|
||||||
If all else fails, it's unfortunate but at least the text chat functions should work at least.
|
|
||||||
Some big video apps like Zoom or Jitsi Meet tend to work better because, if you can't establish
|
|
||||||
a peer-to-peer connection, they will fall back on using a proxy server to transmit your video
|
|
||||||
through to the other party. The {{PrettyTitle}} chat room does not have such a proxy server --
|
|
||||||
because the bandwidth can get expensive to carry video across! Only peer-to-peer video sharing
|
|
||||||
is supported at this time.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 id="chat-more">Where can I learn more about the chat room?</h3>
|
<h3 id="chat-more">Where can I learn more about the chat room?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -1033,6 +1052,85 @@
|
||||||
a tour of the chat room interface and some additional information about how to use the chat room.
|
a tour of the chat room interface and some additional information about how to use the chat room.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h1 id="notification-faqs">Notification FAQs</h1>
|
||||||
|
|
||||||
|
<h3 id="notifications">Does nonshy send me notifications?</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Most {{PrettyTitle}} notifications are "on-site only" by default, meaning you need to log onto
|
||||||
|
the website to see them. We send <em>very</em> few e-mails from this website, ever: only for
|
||||||
|
your new account verification e-mail, certification photo approval, and when you forgot your password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The on-site notifications include things like when your friends upload a new picture, or when somebody
|
||||||
|
comments on or likes something you posted. You can manage your {{PrettyTitle}} notifications on your
|
||||||
|
<a href="/settings#notifications">Notification Settings</a> page, to opt in or out of any of these.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
We have optional <a href="#web-push">Web Push Notifications</a> that you may enable so you can
|
||||||
|
know when somebody has left you a message or a friend request on the website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="web-push">About Web Push Notifications</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you would like to enable timely notifications when you receive a new Message or Friend Request
|
||||||
|
on the website, you may enable <a href="/settings#notifications">Web Push Notifications</a> in your
|
||||||
|
settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Currently, only a small subset of the site notifications can be sent via push notification: when you
|
||||||
|
get a new Direct Message or Friend Request. You may opt either of those out, in case you only care to
|
||||||
|
be notified about messages but not friend requests.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
All your other notifications (likes, comments, etc.) are still "on-site only" so you will need to log in
|
||||||
|
and check your <a href="/me">Dashboard Page</a> to catch up on those.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="cancel-web-push">How do I turn off Web Push Notifications?</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The easiest way to turn these off is to revoke your Notifications permission given to the nonshy website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
On Firefox, Chrome and Chrome-like web browsers: in your URL address bar there should be a button to the
|
||||||
|
left of address which you can click on and see permissions for the website, where the "Notification" permission
|
||||||
|
can be easily revoked.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<img src="/static/img/site-settings.png" alt="A screenshot of the Google Chrome settings drop-down near the address bar." style="padding: 2px; border: 1px solid #666; background-color: #aaa">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
On other web browsers (or on mobile) you may need to go into your browser's settings. Under a section for
|
||||||
|
"Websites" (maybe under "Privacy & Security"), find the {{PrettyTitle}} website or the Notifications permission
|
||||||
|
and you can change your setting there.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="troubleshoot-web-push">Troubleshooting Web Push Notifications</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When you first enable Web Push Notifications, your web browser should have prompted you for permission. In case
|
||||||
|
you have clicked "Deny" or "Never Allow," your web browser remembers your decision and the website is not allowed
|
||||||
|
to ask again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When this happens, the <a href="/settings#notifications">Notification Settings</a> page will say that you have "Denied"
|
||||||
|
notification permission.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In case you want to undo this: you will need to change or reset your Notification permission for this website. Please
|
||||||
|
see the <a href="#cancel-web-push">previous answer</a> for places to look. After you have reset your Notification permission,
|
||||||
|
refresh the <a href="/settings#notifications">Notification Settings</a> page and try enabling notifications again.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1 id="shy-faqs"><i class="fa fa-ghost"></i> Shy Account FAQs</h1>
|
<h1 id="shy-faqs"><i class="fa fa-ghost"></i> Shy Account FAQs</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -207,6 +207,13 @@
|
||||||
other column to read the conversation here.
|
other column to read the conversation here.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Push Notifications banner -->
|
||||||
|
<div class="notification is-success is-light" id="push-notification-tip" style="display: none">
|
||||||
|
<i class="fa fa-gift mr-1"></i>
|
||||||
|
<strong>New Feature:</strong> You can now enable <a href="/settings#notifications">Web Push Notifications</a>
|
||||||
|
so you can be notified that a new message was received on {{PrettyTitle}}, even if your web browser is not running.
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-info-circle has-text-success"></i>
|
<i class="fa fa-info-circle has-text-success"></i>
|
||||||
<strong class="has-text-success">Pro Tip:</strong>
|
<strong class="has-text-success">Pro Tip:</strong>
|
||||||
|
@ -335,3 +342,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{define "scripts"}}
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Push Notification tip.
|
||||||
|
document.addEventListener("DOMContentLoaded", (e) => {
|
||||||
|
const permission = Notification.permission;
|
||||||
|
if (permission !== "granted") {
|
||||||
|
document.querySelector("#push-notification-tip").style.display = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
Loading…
Reference in New Issue
Block a user