diff --git a/pkg/handlers.go b/pkg/handlers.go index 423a0ee..7dee948 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -240,6 +240,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) { sub.VideoActive = msg.VideoActive sub.VideoNSFW = msg.NSFW + sub.ChatStatus = msg.ChatStatus // Sync the WhoList to everybody. s.SendWhoList() diff --git a/pkg/messages.go b/pkg/messages.go index 612ba56..98fc1b5 100644 --- a/pkg/messages.go +++ b/pkg/messages.go @@ -20,8 +20,9 @@ type Message struct { WhoList []WhoList `json:"whoList,omitempty"` // Sent on `me` actions along with Username - VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status - NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW + 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 // Sent on `open` actions along with the (other) Username. OpenSecret string `json:"openSecret,omitempty"` @@ -68,6 +69,7 @@ type WhoList struct { Username string `json:"username"` VideoActive bool `json:"videoActive,omitempty"` NSFW bool `json:"nsfw,omitempty"` + Status string `json:"status"` // JWT auth extra settings. Operator bool `json:"op"` diff --git a/pkg/websocket.go b/pkg/websocket.go index 010db17..c229e2c 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -24,6 +24,7 @@ type Subscriber struct { Username string VideoActive bool VideoNSFW bool + ChatStatus string JWTClaims *jwt.Claims authenticated bool // has passed the login step conn *websocket.Conn @@ -163,8 +164,9 @@ func (s *Server) WebSocket() http.HandlerFunc { closeSlow: func() { c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") }, - booted: make(map[string]struct{}), - muted: make(map[string]struct{}), + booted: make(map[string]struct{}), + muted: make(map[string]struct{}), + ChatStatus: "online", } s.AddSubscriber(sub) @@ -317,6 +319,7 @@ func (s *Server) SendWhoList() { for _, user := range subscribers { who := WhoList{ Username: user.Username, + Status: user.ChatStatus, VideoActive: user.VideoActive, NSFW: user.VideoNSFW, } diff --git a/web/static/css/chat.css b/web/static/css/chat.css index a62acd4..77ba40b 100644 --- a/web/static/css/chat.css +++ b/web/static/css/chat.css @@ -242,4 +242,11 @@ body { /* YouTube embeds */ .youtube-embed { max-width: 100%; +} + +/* The Away icon that overlays profile pics */ +.status-away-icon { + position: absolute; + top: 14px; + left: 16px; } \ No newline at end of file diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js index dae3353..597633b 100644 --- a/web/static/js/BareRTC.js +++ b/web/static/js/BareRTC.js @@ -65,6 +65,11 @@ const app = Vue.createApp({ autoLogin: false, // e.g. from JWT auth message: "", typingNotifDebounce: null, + status: "online", // away/idle status + + // Idle detection variables + idleTimeout: null, + idleThreshold: 60, // number of seconds you must be idle // WebSocket connection. ws: { @@ -166,6 +171,7 @@ const app = Vue.createApp({ mounted() { this.setupSounds(); this.setupConfig(); // localSettings persisted settings + this.setupIdleDetection(); this.webcam.elem = document.querySelector("#localVideo"); this.historyScrollbox = document.querySelector("#chatHistory"); @@ -229,6 +235,10 @@ const app = Vue.createApp({ // Store the setting persistently. localStorage.fontSizeClass = this.fontSizeClass; }, + status() { + // Send presence updates to the server. + this.sendMe(); + } }, computed: { chatHistory() { @@ -321,6 +331,7 @@ const app = Vue.createApp({ this.ws.conn.send(JSON.stringify({ action: "me", videoActive: this.webcam.active, + status: this.status, nsfw: this.webcam.nsfw, })); }, @@ -1281,6 +1292,7 @@ const app = Vue.createApp({ localStorage[`sound:${event}`] = this.config.sounds.settings[event]; }, + // Make all links in chat open in new windows makeLinksExternal() { window.requestAnimationFrame(() => { 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"; + } + } } }); diff --git a/web/templates/chat.html b/web/templates/chat.html index a09168d..f7174cc 100644 --- a/web/templates/chat.html +++ b/web/templates/chat.html @@ -687,6 +687,21 @@
+
+
+ Status: +
+
+
+ +
+
+
+