Online users badge in the Chat link on nav bar
This commit is contained in:
parent
50d05f92f1
commit
78abee6e9e
|
@ -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()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
92
pkg/worker/barertc.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,4 +86,44 @@ abbr {
|
||||||
/* Hide an element */
|
/* Hide an element */
|
||||||
.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;
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user