Online users badge in the Chat link on nav bar

This commit is contained in:
Noah Petherbridge 2023-06-07 21:59:15 -07:00
parent 50d05f92f1
commit 78abee6e9e
8 changed files with 189 additions and 25 deletions

View File

@ -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/models/backfill" "code.nonshy.com/nonshy/website/pkg/models/backfill"
"code.nonshy.com/nonshy/website/pkg/redis" "code.nonshy.com/nonshy/website/pkg/redis"
"code.nonshy.com/nonshy/website/pkg/worker"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
@ -75,6 +76,9 @@ func main() {
Port: c.Int("port"), Port: c.Int("port"),
} }
// Kick off background worker threads.
go worker.WatchBareRTC()
return app.Run() return app.Run()
}, },
}, },

View File

@ -64,6 +64,9 @@ const (
// How frequently to refresh LastLoginAt since sessions are long-lived. // How frequently to refresh LastLoginAt since sessions are long-lived.
LastLoginAtCooldown = 8 * time.Hour LastLoginAtCooldown = 8 * time.Hour
// Chat room status refresh interval.
ChatStatusRefreshInterval = 30 * time.Second
) )
var ( var (

View File

@ -11,6 +11,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/photo" "code.nonshy.com/nonshy/website/pkg/photo"
"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/worker"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
@ -106,6 +107,9 @@ func Landing() http.HandlerFunc {
var vars = map[string]interface{}{ var vars = map[string]interface{}{
"ChatAPI": strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics", "ChatAPI": strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics",
"IsShyUser": isShy, "IsShyUser": isShy,
// Pre-populate the "who's online" widget from backend cache data
"ChatStatistics": worker.GetChatStatistics(),
} }
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)

View File

@ -8,6 +8,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/log" "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/worker"
) )
// MergeVars mixes in globally available template variables. The http.Request is optional. // MergeVars mixes in globally available template variables. The http.Request is optional.
@ -37,6 +38,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
m["NavFriendRequests"] = 0 // Friend requests m["NavFriendRequests"] = 0 // Friend requests
m["NavUnreadNotifications"] = 0 // general notifications m["NavUnreadNotifications"] = 0 // general notifications
m["NavTotalNotifications"] = 0 // Total of above m["NavTotalNotifications"] = 0 // Total of above
m["NavChatStatistics"] = worker.GetChatStatistics()
// Admin notification counts for nav bar. // Admin notification counts for nav bar.
m["NavCertificationPhotos"] = 0 // Cert. photos needing approval m["NavCertificationPhotos"] = 0 // Cert. photos needing approval

92
pkg/worker/barertc.go Normal file
View File

@ -0,0 +1,92 @@
package worker
import (
"encoding/json"
"io/ioutil"
"net/http"
"sync"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
)
// ChatStatistics is the json result of the BareRTC /api/statistics endpoint.
type ChatStatistics struct {
UserCount int
Usernames []string
}
// GetChatStatistics returns the latest (cached) chat statistics.
func GetChatStatistics() ChatStatistics {
chatStatisticsMu.RLock()
defer chatStatisticsMu.RUnlock()
if cachedChatStatistics != nil {
return *cachedChatStatistics
}
return ChatStatistics{
Usernames: []string{},
}
}
var (
cachedChatStatistics *ChatStatistics
chatStatisticsMu sync.RWMutex
)
// WatchBareRTC is a worker goroutine that caches the current online chatters in the chat room.
func WatchBareRTC() {
if config.Current.BareRTC.JWTSecret == "" || config.Current.BareRTC.URL == "" {
log.Error("Worker (WatchBareRTC): chat room is not configured, will not watch chat room status")
return
}
// Check it immediately.
DoCheckBareRTC()
// And on an interval forever.
ticker := time.NewTicker(config.ChatStatusRefreshInterval)
for range ticker.C {
DoCheckBareRTC()
}
}
// DoCheckBareRTC invokes the attempt to refresh data from the chat server about who's online.
func DoCheckBareRTC() {
log.Info("Refresh BareRTC")
req, err := http.NewRequest(http.MethodGet, config.Current.BareRTC.URL+"/api/statistics", nil)
// Lock the cached statistics.
chatStatisticsMu.Lock()
defer chatStatisticsMu.Unlock()
if err != nil {
log.Error("WatchBareRTC: couldn't make request: %s", err)
cachedChatStatistics = nil
return
}
client := http.Client{
Timeout: 30 * time.Second,
}
res, err := client.Do(req)
if err != nil {
log.Error("WatchBareRTC: request error: %s", err)
cachedChatStatistics = nil
return
}
if res.StatusCode == http.StatusOK {
var cs ChatStatistics
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
if err = json.Unmarshal(body, &cs); err != nil {
log.Error("WatchBareRTC: json decode error: %s", err)
return
}
cachedChatStatistics = &cs
}
}

View File

@ -87,3 +87,43 @@ abbr {
.nonshy-hidden { .nonshy-hidden {
display: none; display: none;
} }
/***
* Mobile navbar notification count badge no.
*/
/* mobile view: just superset text */
.nonshy-navbar-notification-count {
font-size: xx-small;
padding-bottom: 12px;
margin-right: -4px;
margin-left: -4px;
}
/* desktop view: colored badge similar to bulma `tag is-warning ml-1`*/
.nonshy-navbar-notification-tag {
align-items: center;
border-radius: 4px;
display: inline-flex;
font-size: xx-small;
height: 1em;
justify-content: center;
vertical-align: top;
line-height: 1.5;
padding: .75em;
white-space: nowrap;
margin-bottom: auto;
}
.nonshy-navbar-notification-tag.is-warning {
background-color: #ffd324;
color: rgba(0, 0, 0, 0.7);
}
.nonshy-navbar-notification-tag.is-info {
background-color: #0f81cc;
color: #fff;
}
.nonshy-navbar-notification-tag.is-danger {
background-color: #ff0537;
color: #fff;
}

View File

@ -52,6 +52,7 @@
<a class="navbar-item" href="/chat"> <a class="navbar-item" href="/chat">
<span class="icon"><i class="fa fa-message"></i></span> <span class="icon"><i class="fa fa-message"></i></span>
<span>Chat</span> <span>Chat</span>
{{if .NavChatStatistics.UserCount}}<span class="nonshy-navbar-notification-tag is-info ml-1">{{.NavChatStatistics.UserCount}}</span>{{end}}
</a> </a>
<a class="navbar-item" href="/forum"> <a class="navbar-item" href="/forum">
@ -67,13 +68,13 @@
<a class="navbar-item" href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}"> <a class="navbar-item" href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
<span class="icon"><i class="fa fa-user-group"></i></span> <span class="icon"><i class="fa fa-user-group"></i></span>
<span>Friends</span> <span>Friends</span>
{{if .NavFriendRequests}}<span class="tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}} {{if .NavFriendRequests}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}}
</a> </a>
<a class="navbar-item" href="/messages"> <a class="navbar-item" href="/messages">
<span class="icon"><i class="fa fa-envelope"></i></span> <span class="icon"><i class="fa fa-envelope"></i></span>
<span>Messages</span> <span>Messages</span>
{{if .NavUnreadMessages}}<span class="tag is-warning ml-1">{{.NavUnreadMessages}}</span>{{end}} {{if .NavUnreadMessages}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadMessages}}</span>{{end}}
</a> </a>
{{end}} {{end}}
@ -134,8 +135,8 @@
</div> </div>
<div class="column"> <div class="column">
{{.CurrentUser.Username}} {{.CurrentUser.Username}}
{{if .NavUnreadNotifications}}<span class="tag is-warning ml-1">{{.NavUnreadNotifications}}</span>{{end}} {{if .NavUnreadNotifications}}<span class="nonshy-navbar-notification-tag is-warning ml-1">{{.NavUnreadNotifications}}</span>{{end}}
{{if .NavAdminNotifications}}<span class="tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}} {{if .NavAdminNotifications}}<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
</div> </div>
</div> </div>
</a> </a>
@ -145,7 +146,7 @@
<span class="icon"><i class="fa fa-home-user"></i></span> <span class="icon"><i class="fa fa-home-user"></i></span>
<span>Dashboard</span> <span>Dashboard</span>
{{if .NavUnreadNotifications}} {{if .NavUnreadNotifications}}
<span class="tag is-warning ml-1"> <span class="nonshy-navbar-notification-tag is-warning ml-1">
<span class="icon"><i class="fa fa-bell"></i></span> <span class="icon"><i class="fa fa-bell"></i></span>
<span>{{.NavUnreadNotifications}}</span> <span>{{.NavUnreadNotifications}}</span>
</span> </span>
@ -181,7 +182,7 @@
<a class="navbar-item has-text-danger" href="/admin"> <a class="navbar-item has-text-danger" href="/admin">
<span class="icon"><i class="fa fa-gavel"></i></span> <span class="icon"><i class="fa fa-gavel"></i></span>
<span>Admin</span> <span>Admin</span>
{{if .NavAdminNotifications}}<span class="tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}} {{if .NavAdminNotifications}}<span class="nonshy-navbar-notification-tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
</a> </a>
{{end}} {{end}}
{{if .SessionImpersonated}} {{if .SessionImpersonated}}
@ -216,9 +217,12 @@
{{if .LoggedIn}} {{if .LoggedIn}}
<div class="mobile nonshy-mobile-notification"> <div class="mobile nonshy-mobile-notification">
{{if .CurrentUser.Certified}} {{if .CurrentUser.Certified}}
<a class="tag is-grey py-4" <a class="tag {{if gt .NavChatStatistics.UserCount 0}}is-info{{else}}is-grey{{end}} py-4"
href="/chat"> href="/chat">
<span class="icon"><i class="fa fa-message"></i></span> <span class="icon"><i class="fa fa-message"></i></span>
{{if gt .NavChatStatistics.UserCount 0}}
<small class="nonshy-navbar-notification-count">{{.NavChatStatistics.UserCount}}</small>
{{end}}
</a> </a>
<a class="tag is-grey py-4" <a class="tag is-grey py-4"
@ -236,7 +240,7 @@
href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}"> href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
<span class="icon"><i class="fa fa-user-group"></i></span> <span class="icon"><i class="fa fa-user-group"></i></span>
{{if gt .NavFriendRequests 0}} {{if gt .NavFriendRequests 0}}
<small>{{.NavFriendRequests}}</small> <small class="nonshy-navbar-notification-count">{{.NavFriendRequests}}</small>
{{end}} {{end}}
</a> </a>
@ -244,21 +248,21 @@
<a class="tag {{if gt .NavUnreadMessages 0}}is-warning{{else}}is-grey{{end}} py-4" href="/messages"> <a class="tag {{if gt .NavUnreadMessages 0}}is-warning{{else}}is-grey{{end}} py-4" href="/messages">
<span class="icon"><i class="fa fa-envelope"></i></span> <span class="icon"><i class="fa fa-envelope"></i></span>
{{if gt .NavUnreadMessages 0}} {{if gt .NavUnreadMessages 0}}
<small>{{.NavUnreadMessages}}</small> <small class="nonshy-navbar-notification-count">{{.NavUnreadMessages}}</small>
{{end}} {{end}}
</a> </a>
{{if gt .NavUnreadNotifications 0}} {{if gt .NavUnreadNotifications 0}}
<a class="tag is-warning py-4" href="/me#notifications"> <a class="tag is-warning py-4" href="/me#notifications">
<span class="icon"><i class="fa fa-bell"></i></span> <span class="icon"><i class="fa fa-bell"></i></span>
<small>{{.NavUnreadNotifications}}</small> <small class="nonshy-navbar-notification-count">{{.NavUnreadNotifications}}</small>
</a> </a>
{{end}} {{end}}
{{if gt .NavAdminNotifications 0}} {{if gt .NavAdminNotifications 0}}
<a class="tag is-danger py-4" href="/admin"> <a class="tag is-danger py-4" href="/admin">
<span class="icon"><i class="fa fa-gavel"></i></span> <span class="icon"><i class="fa fa-gavel"></i></span>
<small>{{.NavAdminNotifications}}</small> <small class="nonshy-navbar-notification-count">{{.NavAdminNotifications}}</small>
</a> </a>
{{end}} {{end}}
</div> </div>

View File

@ -128,13 +128,41 @@
</div> </div>
<script> <script>
function showWhoBanner(chatStatistics) {
const $banner = document.querySelector("#chatStatsBanner"),
$whoLink = document.querySelector("#whoLink"),
$whoList = document.querySelector("#whoList"),
$usersOnline = document.querySelector("#usersOnline");
$banner.style.display = "block";
console.log(chatStatistics);
let people = chatStatistics.UserCount === 1 ? 'person' : 'people';
let isAre = chatStatistics.UserCount === 1 ? 'is' : 'are';
$usersOnline.innerHTML = `There ${isAre} currently <strong>${chatStatistics.UserCount}</strong> ${people}</span> in the chat room`;
$whoList.innerHTML = chatStatistics.Usernames.join(", ");
// Show the "Who?" link if there's anybody.
if (chatStatistics.UserCount > 0) {
$usersOnline.innerHTML += ":";
$whoLink.style.visibility = "visible";
} else {
$usersOnline.innerHTML += ".";
}
}
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
let url = "{{.ChatAPI}}", let url = "{{.ChatAPI}}",
ChatStatistics = {{.ChatStatistics}},
$banner = document.querySelector("#chatStatsBanner"), $banner = document.querySelector("#chatStatsBanner"),
$whoLink = document.querySelector("#whoLink"), $whoLink = document.querySelector("#whoLink"),
$whoList = document.querySelector("#whoList"), $whoList = document.querySelector("#whoList"),
$usersOnline = document.querySelector("#usersOnline"); $usersOnline = document.querySelector("#usersOnline");
// If we already know people are online, show the banner immediately while we refresh from live data
if (ChatStatistics.UserCount > 0) {
showWhoBanner(ChatStatistics);
}
$whoLink.addEventListener("click", (e) => { $whoLink.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
$whoLink.style.display = "none"; $whoLink.style.display = "none";
@ -145,20 +173,7 @@ window.addEventListener("DOMContentLoaded", () => {
mode: 'cors', mode: 'cors',
cache: 'no-cache', cache: 'no-cache',
}).then(resp => resp.json()).then(result => { }).then(resp => resp.json()).then(result => {
$banner.style.display = "block"; showWhoBanner(result);
console.log(result);
let people = result.UserCount === 1 ? 'person' : 'people';
let isAre = result.UserCount === 1 ? 'is' : 'are';
$usersOnline.innerHTML = `There ${isAre} currently <strong>${result.UserCount}</strong> ${people}</span> in the chat room`;
$whoList.innerHTML = result.Usernames.join(", ");
// Show the "Who?" link if there's anybody.
if (result.UserCount > 0) {
$usersOnline.innerHTML += ":";
$whoLink.style.visibility = "visible";
} else {
$usersOnline.innerHTML += ".";
}
}).catch(console.error); }).catch(console.error);
}); });
</script> </script>