Spit and polish

* Track the window focus/blur events. Leaving the tab while in a channel
  now means you may still hear sound effects in that channel.
* Add a CORS JSON API /v1/statistics to get details from the server
  about who is online. The CORSHosts whitelist in the settings.toml
  limits domain access to the endpoint.
This commit is contained in:
Noah 2023-02-09 23:03:06 -08:00
parent f7b9e026a0
commit b966f85ecc
6 changed files with 144 additions and 19 deletions

54
pkg/api.go Normal file
View File

@ -0,0 +1,54 @@
package barertc
import (
"encoding/json"
"net/http"
"git.kirsle.net/apps/barertc/pkg/config"
)
// Statistics (/api/statistics) returns info about the users currently logged onto the chat,
// for your website to call via CORS. The URL to your site needs to be in the CORSHosts array
// of your settings.toml.
func (s *Server) Statistics() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Handle the CORS header from your trusted domains.
if origin := r.Header.Get("Origin"); origin != "" {
var found bool
for _, allowed := range config.Current.CORSHosts {
if allowed == origin {
found = true
}
}
if found {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
}
var result = struct {
UserCount int
Usernames []string
}{
Usernames: []string{},
}
// Count all users + collect unique usernames.
var unique = map[string]struct{}{}
for _, sub := range s.IterSubscribers() {
if sub.authenticated {
result.UserCount++
if _, ok := unique[sub.Username]; ok {
continue
}
result.Usernames = append(result.Usernames, sub.Username)
unique[sub.Username] = struct{}{}
}
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(result)
})
}

View File

@ -12,7 +12,7 @@ import (
// Version of the config format - when new fields are added, it will attempt // Version of the config format - when new fields are added, it will attempt
// to write the settings.toml to disk so new defaults populate. // to write the settings.toml to disk so new defaults populate.
var currentVersion = 1 var currentVersion = 2
// Config for your BareRTC app. // Config for your BareRTC app.
type Config struct { type Config struct {
@ -27,6 +27,7 @@ type Config struct {
Title string Title string
Branding string Branding string
WebsiteURL string WebsiteURL string
CORSHosts []string
UseXForwardedFor bool UseXForwardedFor bool
@ -59,6 +60,9 @@ func DefaultConfig() Config {
Title: "BareRTC", Title: "BareRTC",
Branding: "BareRTC", Branding: "BareRTC",
WebsiteURL: "https://www.example.com", WebsiteURL: "https://www.example.com",
CORSHosts: []string{
"https://www.example.com",
},
PublicChannels: []Channel{ PublicChannels: []Channel{
{ {
ID: "lobby", ID: "lobby",

View File

@ -33,6 +33,7 @@ func (s *Server) Setup() error {
mux.Handle("/", IndexPage()) mux.Handle("/", IndexPage())
mux.Handle("/about", AboutPage()) mux.Handle("/about", AboutPage())
mux.Handle("/ws", s.WebSocket()) mux.Handle("/ws", s.WebSocket())
mux.Handle("/api/statistics", s.Statistics())
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
s.mux = mux s.mux = mux

View File

@ -155,6 +155,26 @@ body {
margin: 5px; margin: 5px;
} }
.video-feeds.x1 > .feed {
width: 252px;
height: 168px;
}
.video-feeds.x2 > .feed {
width: 336px;
height: 224px;
}
.video-feeds.x3 > .feed {
width: 504px;
height: 336px;
}
.video-feeds.x4 > .feed {
width: 672px;
height: 448px;
}
.video-feeds > .feed > video { .video-feeds > .feed > video {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -12,7 +12,9 @@ const app = Vue.createApp({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
data() { data() {
return { return {
busy: false, // busy: false, // TODO: not used
windowFocused: true, // browser tab is active
windowFocusedAt: new Date(),
// Website configuration provided by chat.html template. // Website configuration provided by chat.html template.
config: { config: {
@ -37,6 +39,7 @@ const app = Vue.createApp({
channel: "lobby", channel: "lobby",
username: "", //"test", username: "", //"test",
message: "", message: "",
typingNotifDebounce: null,
// WebSocket connection. // WebSocket connection.
ws: { ws: {
@ -58,6 +61,17 @@ const app = Vue.createApp({
// Who all is watching me? map of users. // Who all is watching me? map of users.
watching: {}, watching: {},
// Scaling setting for the videos drawer, so the user can
// embiggen the webcam sizes so a suitable size.
videoScale: "",
videoScaleOptions: [
[ "", "Default size" ],
[ "x1", "50% larger videos" ],
[ "x2", "2x larger videos" ],
[ "x3", "3x larger videos" ],
[ "x4", "4x larger videos (not recommended)" ],
],
}, },
// WebRTC sessions with other users. // WebRTC sessions with other users.
@ -87,7 +101,7 @@ const app = Vue.createApp({
historyScrollbox: null, historyScrollbox: null,
DMs: {}, DMs: {},
// Responsive CSS for mobile. // Responsive CSS controls for mobile.
responsive: { responsive: {
leftDrawerOpen: false, leftDrawerOpen: false,
rightDrawerOpen: false, rightDrawerOpen: false,
@ -122,11 +136,23 @@ const app = Vue.createApp({
$right: document.querySelector(".right-column"), $right: document.querySelector(".right-column"),
}; };
// Reset CSS overrides for responsive display on any window size change. In effect,
// making the chat panel the current screen again on phone rotation.
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
// Reset CSS overrides for responsive display on any window size change.
this.resetResponsiveCSS(); this.resetResponsiveCSS();
}); });
// Listen for window focus/unfocus events. Being on a different browser tab, for
// sound effect alert purposes, counts as not being "in" that chat channel when
// a message comes in.
window.addEventListener("focus", () => {
this.windowFocused = true;
this.windowFocusedAt = new Date();
});
window.addEventListener("blur", () => {
this.windowFocused = false;
})
for (let channel of this.config.channels) { for (let channel of this.config.channels) {
this.initHistory(channel.ID); this.initHistory(channel.ID);
} }
@ -222,6 +248,10 @@ const app = Vue.createApp({
this.message = ""; this.message = "";
}, },
sendTypingNotification() {
// TODO
},
// Sync the current user state (such as video broadcasting status) to // Sync the current user state (such as video broadcasting status) to
// the backend, which will reload everybody's Who List. // the backend, which will reload everybody's Who List.
sendMe() { sendMe() {
@ -293,12 +323,12 @@ const app = Vue.createApp({
// Handle messages sent in chat. // Handle messages sent in chat.
onMessage(msg) { onMessage(msg) {
// Play sound effects if this is not the active channel. // Play sound effects if this is not the active channel or the window is not focused.
if (msg.channel.indexOf("@") === 0) { if (msg.channel.indexOf("@") === 0) {
if (msg.channel !== this.channel) { if (msg.channel !== this.channel || !this.windowFocused) {
this.playSound("DM"); this.playSound("DM");
} }
} else if (msg.channel !== this.channel) { } else if (msg.channel !== this.channel || !this.windowFocused) {
this.playSound("Chat"); this.playSound("Chat");
} }
@ -306,7 +336,6 @@ const app = Vue.createApp({
channel: msg.channel, channel: msg.channel,
username: msg.username, username: msg.username,
message: msg.message, message: msg.message,
at: msg.at,
}); });
}, },
@ -328,7 +357,6 @@ const app = Vue.createApp({
action: msg.action, action: msg.action,
username: msg.username, username: msg.username,
message: msg.message, message: msg.message,
at: msg.at,
}); });
} }
@ -340,7 +368,6 @@ const app = Vue.createApp({
action: msg.action, action: msg.action,
username: msg.username, username: msg.username,
message: msg.message, message: msg.message,
at: msg.at,
}); });
} }
}, },
@ -428,7 +455,6 @@ const app = Vue.createApp({
username: msg.username || 'Internal Server Error', username: msg.username || 'Internal Server Error',
message: msg.message, message: msg.message,
isChatServer: true, isChatServer: true,
at: new Date(),
}); });
break; break;
case "ping": case "ping":
@ -861,7 +887,7 @@ const app = Vue.createApp({
}; };
} }
}, },
pushHistory({ channel, username, message, action = "message", at, isChatServer, isChatClient }) { pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient }) {
// Default channel = your current channel. // Default channel = your current channel.
if (!channel) { if (!channel) {
channel = this.channel; channel = this.channel;
@ -876,7 +902,7 @@ const app = Vue.createApp({
action: action, action: action,
username: username, username: username,
message: message, message: message,
at: at || new Date(), at: new Date(),
isChatServer, isChatServer,
isChatClient, isChatClient,
}); });
@ -961,7 +987,8 @@ const app = Vue.createApp({
seconds = String(date.getSeconds()).padStart(2, '0'), seconds = String(date.getSeconds()).padStart(2, '0'),
ampm = hours >= 11 ? "pm" : "am"; ampm = hours >= 11 ? "pm" : "am";
return `${(hours%12)+1}:${minutes}:${seconds} ${ampm}`; let hour = hours%12 || 12;
return `${(hour)}:${minutes}:${seconds} ${ampm}`;
}, },
/** /**

View File

@ -56,6 +56,27 @@
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Video size</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-fullwidth">
<select v-model="webcam.videoScale">
<option v-for="s in webcam.videoScaleOptions"
v-bind:key="s[0]"
:value="s[0]">
[[ s[1] ]]
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<h3 class="subtitle">Sounds</h3> <h3 class="subtitle">Sounds</h3>
<div class="field is-horizontal"> <div class="field is-horizontal">
@ -328,7 +349,7 @@
</div> </div>
</div> </div>
</header> </header>
<div class="video-feeds" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0"> <div class="video-feeds" :class="webcam.videoScale" v-show="webcam.active || Object.keys(WebRTC.streams).length > 0">
<!-- Video Feeds--> <!-- Video Feeds-->
<!-- My video --> <!-- My video -->
@ -448,12 +469,10 @@
<form @submit.prevent="sendMessage()"> <form @submit.prevent="sendMessage()">
<input type="text" class="input" <input type="text" class="input"
v-model="message" v-model="message"
placeholder="Message"> placeholder="Message"
@keydown="sendTypingNotification()">
</form> </form>
</div> </div>
<div class="column is-narrow">
</div>
</div> </div>
</div> </div>