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:
parent
f7b9e026a0
commit
b966f85ecc
54
pkg/api.go
Normal file
54
pkg/api.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user