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
|
||||
// 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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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}`;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user