User status and idle detection
This commit is contained in:
parent
75fbed4a4d
commit
3560e63096
|
@ -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()
|
||||||
|
|
|
@ -20,8 +20,9 @@ type Message struct {
|
||||||
WhoList []WhoList `json:"whoList,omitempty"`
|
WhoList []WhoList `json:"whoList,omitempty"`
|
||||||
|
|
||||||
// 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
|
||||||
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
|
ChatStatus string `json:"status,omitempty"` // online vs. away
|
||||||
|
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.
|
||||||
OpenSecret string `json:"openSecret,omitempty"`
|
OpenSecret string `json:"openSecret,omitempty"`
|
||||||
|
@ -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"`
|
||||||
|
|
|
@ -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
|
||||||
|
@ -163,8 +164,9 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
||||||
closeSlow: func() {
|
closeSlow: func() {
|
||||||
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
|
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
|
||||||
},
|
},
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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}">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user