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.
ipad-testing
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
// to write the settings.toml to disk so new defaults populate.
var currentVersion = 1
var currentVersion = 2
// Config for your BareRTC app.
type Config struct {
@ -27,6 +27,7 @@ type Config struct {
Title string
Branding string
WebsiteURL string
CORSHosts []string
UseXForwardedFor bool
@ -59,6 +60,9 @@ func DefaultConfig() Config {
Title: "BareRTC",
Branding: "BareRTC",
WebsiteURL: "https://www.example.com",
CORSHosts: []string{
"https://www.example.com",
},
PublicChannels: []Channel{
{
ID: "lobby",

View File

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

View File

@ -155,6 +155,26 @@ body {
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 {
width: 100%;
height: 100%;

View File

@ -12,7 +12,9 @@ const app = Vue.createApp({
delimiters: ['[[', ']]'],
data() {
return {
busy: false,
// busy: false, // TODO: not used
windowFocused: true, // browser tab is active
windowFocusedAt: new Date(),
// Website configuration provided by chat.html template.
config: {
@ -37,6 +39,7 @@ const app = Vue.createApp({
channel: "lobby",
username: "", //"test",
message: "",
typingNotifDebounce: null,
// WebSocket connection.
ws: {
@ -58,6 +61,17 @@ const app = Vue.createApp({
// Who all is watching me? map of users.
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.
@ -87,7 +101,7 @@ const app = Vue.createApp({
historyScrollbox: null,
DMs: {},
// Responsive CSS for mobile.
// Responsive CSS controls for mobile.
responsive: {
leftDrawerOpen: false,
rightDrawerOpen: false,
@ -122,11 +136,23 @@ const app = Vue.createApp({
$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", () => {
// Reset CSS overrides for responsive display on any window size change.
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) {
this.initHistory(channel.ID);
}
@ -222,6 +248,10 @@ const app = Vue.createApp({
this.message = "";
},
sendTypingNotification() {
// TODO
},
// Sync the current user state (such as video broadcasting status) to
// the backend, which will reload everybody's Who List.
sendMe() {
@ -293,12 +323,12 @@ const app = Vue.createApp({
// Handle messages sent in chat.
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 !== this.channel) {
if (msg.channel !== this.channel || !this.windowFocused) {
this.playSound("DM");
}
} else if (msg.channel !== this.channel) {
} else if (msg.channel !== this.channel || !this.windowFocused) {
this.playSound("Chat");
}
@ -306,7 +336,6 @@ const app = Vue.createApp({
channel: msg.channel,
username: msg.username,
message: msg.message,
at: msg.at,
});
},
@ -328,7 +357,6 @@ const app = Vue.createApp({
action: msg.action,
username: msg.username,
message: msg.message,
at: msg.at,
});
}
@ -340,7 +368,6 @@ const app = Vue.createApp({
action: msg.action,
username: msg.username,
message: msg.message,
at: msg.at,
});
}
},
@ -428,7 +455,6 @@ const app = Vue.createApp({
username: msg.username || 'Internal Server Error',
message: msg.message,
isChatServer: true,
at: new Date(),
});
break;
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.
if (!channel) {
channel = this.channel;
@ -876,7 +902,7 @@ const app = Vue.createApp({
action: action,
username: username,
message: message,
at: at || new Date(),
at: new Date(),
isChatServer,
isChatClient,
});
@ -961,7 +987,8 @@ const app = Vue.createApp({
seconds = String(date.getSeconds()).padStart(2, '0'),
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>
<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>
<div class="field is-horizontal">
@ -328,7 +349,7 @@
</div>
</div>
</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-->
<!-- My video -->
@ -448,12 +469,10 @@
<form @submit.prevent="sendMessage()">
<input type="text" class="input"
v-model="message"
placeholder="Message">
placeholder="Message"
@keydown="sendTypingNotification()">
</form>
</div>
<div class="column is-narrow">
</div>
</div>
</div>