User status and idle detection

This commit is contained in:
Noah 2023-03-27 21:13:04 -07:00
parent 75fbed4a4d
commit 3560e63096
6 changed files with 78 additions and 5 deletions

View File

@ -240,6 +240,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
sub.VideoActive = msg.VideoActive sub.VideoActive = msg.VideoActive
sub.VideoNSFW = msg.NSFW sub.VideoNSFW = msg.NSFW
sub.ChatStatus = msg.ChatStatus
// Sync the WhoList to everybody. // Sync the WhoList to everybody.
s.SendWhoList() s.SendWhoList()

View File

@ -21,6 +21,7 @@ type Message struct {
// Sent on `me` actions along with Username // Sent on `me` actions along with Username
VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status
ChatStatus string `json:"status,omitempty"` // online vs. away
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
// Sent on `open` actions along with the (other) Username. // Sent on `open` actions along with the (other) Username.
@ -68,6 +69,7 @@ type WhoList struct {
Username string `json:"username"` Username string `json:"username"`
VideoActive bool `json:"videoActive,omitempty"` VideoActive bool `json:"videoActive,omitempty"`
NSFW bool `json:"nsfw,omitempty"` NSFW bool `json:"nsfw,omitempty"`
Status string `json:"status"`
// JWT auth extra settings. // JWT auth extra settings.
Operator bool `json:"op"` Operator bool `json:"op"`

View File

@ -24,6 +24,7 @@ type Subscriber struct {
Username string Username string
VideoActive bool VideoActive bool
VideoNSFW bool VideoNSFW bool
ChatStatus string
JWTClaims *jwt.Claims JWTClaims *jwt.Claims
authenticated bool // has passed the login step authenticated bool // has passed the login step
conn *websocket.Conn conn *websocket.Conn
@ -165,6 +166,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
}, },
booted: make(map[string]struct{}), booted: make(map[string]struct{}),
muted: make(map[string]struct{}), muted: make(map[string]struct{}),
ChatStatus: "online",
} }
s.AddSubscriber(sub) s.AddSubscriber(sub)
@ -317,6 +319,7 @@ func (s *Server) SendWhoList() {
for _, user := range subscribers { for _, user := range subscribers {
who := WhoList{ who := WhoList{
Username: user.Username, Username: user.Username,
Status: user.ChatStatus,
VideoActive: user.VideoActive, VideoActive: user.VideoActive,
NSFW: user.VideoNSFW, NSFW: user.VideoNSFW,
} }

View File

@ -243,3 +243,10 @@ body {
.youtube-embed { .youtube-embed {
max-width: 100%; max-width: 100%;
} }
/* The Away icon that overlays profile pics */
.status-away-icon {
position: absolute;
top: 14px;
left: 16px;
}

View File

@ -65,6 +65,11 @@ const app = Vue.createApp({
autoLogin: false, // e.g. from JWT auth autoLogin: false, // e.g. from JWT auth
message: "", message: "",
typingNotifDebounce: null, typingNotifDebounce: null,
status: "online", // away/idle status
// Idle detection variables
idleTimeout: null,
idleThreshold: 60, // number of seconds you must be idle
// WebSocket connection. // WebSocket connection.
ws: { ws: {
@ -166,6 +171,7 @@ const app = Vue.createApp({
mounted() { mounted() {
this.setupSounds(); this.setupSounds();
this.setupConfig(); // localSettings persisted settings this.setupConfig(); // localSettings persisted settings
this.setupIdleDetection();
this.webcam.elem = document.querySelector("#localVideo"); this.webcam.elem = document.querySelector("#localVideo");
this.historyScrollbox = document.querySelector("#chatHistory"); this.historyScrollbox = document.querySelector("#chatHistory");
@ -229,6 +235,10 @@ const app = Vue.createApp({
// Store the setting persistently. // Store the setting persistently.
localStorage.fontSizeClass = this.fontSizeClass; localStorage.fontSizeClass = this.fontSizeClass;
}, },
status() {
// Send presence updates to the server.
this.sendMe();
}
}, },
computed: { computed: {
chatHistory() { chatHistory() {
@ -321,6 +331,7 @@ const app = Vue.createApp({
this.ws.conn.send(JSON.stringify({ this.ws.conn.send(JSON.stringify({
action: "me", action: "me",
videoActive: this.webcam.active, videoActive: this.webcam.active,
status: this.status,
nsfw: this.webcam.nsfw, nsfw: this.webcam.nsfw,
})); }));
}, },
@ -1281,6 +1292,7 @@ const app = Vue.createApp({
localStorage[`sound:${event}`] = this.config.sounds.settings[event]; localStorage[`sound:${event}`] = this.config.sounds.settings[event];
}, },
// Make all links in chat open in new windows
makeLinksExternal() { makeLinksExternal() {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
let $history = document.querySelector("#chatHistory"); let $history = document.querySelector("#chatHistory");
@ -1293,6 +1305,34 @@ const app = Vue.createApp({
}); });
}) })
}, },
/*
* Idle Detection methods
*/
setupIdleDetection() {
window.addEventListener("keypress", this.deidle);
window.addEventListener("mousemove", this.deidle);
},
// Common "de-idle" event handler
deidle(e) {
if (this.status === "idle") {
this.status = "online";
}
if (this.idleTimeout !== null) {
clearTimeout(this.idleTimeout);
}
this.idleTimeout = setTimeout(this.goIdle, 1000 * this.idleThreshold);
},
goIdle() {
// only if we aren't already set on away
if (this.status === "online") {
this.status = "idle";
}
}
} }
}); });

View File

@ -687,6 +687,21 @@
</header> </header>
<div class="card-content p-2"> <div class="card-content p-2">
<div class="columns is-mobile mb-0">
<div class="column is-narrow">
Status:
</div>
<div class="column">
<div class="select is-small is-fullwidth">
<select v-model="status">
<option value="online">☀️ Active</option>
<option value="away">🕒 Away</option>
<option value="idle" v-show="status==='idle'">🕒 Idle</option>
</select>
</div>
</div>
</div>
<div class="tabs has-text-small"> <div class="tabs has-text-small">
<ul> <ul>
<li :class="{'is-active': whoTab==='online'}"> <li :class="{'is-active': whoTab==='online'}">
@ -710,12 +725,17 @@
<li v-for="(u, i) in whoList" v-bind:key="i"> <li v-for="(u, i) in whoList" v-bind:key="i">
<div class="columns is-mobile"> <div class="columns is-mobile">
<!-- Avatar URL if available --> <!-- Avatar URL if available -->
<div class="column is-narrow pr-0"> <div class="column is-narrow pr-0" style="position: relative">
<img v-if="u.avatar" :src="avatarURL(u)" <img v-if="u.avatar" :src="avatarURL(u)"
width="24" height="24" width="24" height="24"
:alt="'Avatar image for ' + u.username"> :alt="'Avatar image for ' + u.username">
<img v-else src="/static/img/shy.png" <img v-else src="/static/img/shy.png"
width="24" height="24"> width="24" height="24">
<!-- Away symbol -->
<div v-if="u.status !== 'online'" class="status-away-icon" :title="'Status: '+u.status">
<i class="fa fa-clock has-text-light"></i>
</div>
</div> </div>
<div class="column pr-0" <div class="column pr-0"
:class="{'pl-1': u.avatar}"> :class="{'pl-1': u.avatar}">