Timestamps, Sound Effects & Love
This commit is contained in:
parent
4810d95a65
commit
9487595e04
5
Makefile
5
Makefile
|
@ -1,2 +1,7 @@
|
|||
.PHONY: run
|
||||
run:
|
||||
go run cmd/BareRTC/main.go -debug
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -o BareRTC cmd/BareRTC/main.go
|
|
@ -33,5 +33,7 @@ func main() {
|
|||
|
||||
app := barertc.NewServer()
|
||||
app.Setup()
|
||||
|
||||
log.Info("Listening at %s", address)
|
||||
panic(app.ListenAndServe(address))
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
Title string
|
||||
Branding string
|
||||
WebsiteURL string
|
||||
|
||||
PublicChannels []Channel
|
||||
|
|
|
@ -112,6 +112,7 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
Message: markdown,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Is this a DM?
|
||||
|
@ -119,7 +120,9 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
// Echo the message only to both parties.
|
||||
s.SendTo(sub.Username, message)
|
||||
message.Channel = "@" + sub.Username
|
||||
s.SendTo(msg.Channel, message)
|
||||
if err := s.SendTo(msg.Channel, message); err != nil {
|
||||
sub.ChatServer("Your message could not be delivered: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package barertc
|
||||
|
||||
import "time"
|
||||
|
||||
type Message struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp time.Time `json:"at,omitempty"`
|
||||
|
||||
// JWT token for `login` actions.
|
||||
JWTToken string `json:"jwt,omitempty"`
|
||||
|
|
|
@ -239,23 +239,26 @@ func (s *Server) Broadcast(msg Message) {
|
|||
continue
|
||||
}
|
||||
|
||||
sub.SendJSON(Message{
|
||||
Action: msg.Action,
|
||||
Channel: msg.Channel,
|
||||
Username: msg.Username,
|
||||
Message: msg.Message,
|
||||
})
|
||||
sub.SendJSON(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// SendTo sends a message to a given username.
|
||||
func (s *Server) SendTo(username string, msg Message) {
|
||||
func (s *Server) SendTo(username string, msg Message) error {
|
||||
log.Debug("SendTo(%s): %+v", username, msg)
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
|
||||
// If no timestamp, add it.
|
||||
if msg.Timestamp.IsZero() {
|
||||
msg.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, sub := range s.IterSubscribers(true) {
|
||||
if sub.Username == username {
|
||||
found = true
|
||||
sub.SendJSON(Message{
|
||||
Action: msg.Action,
|
||||
Channel: msg.Channel,
|
||||
|
@ -264,6 +267,11 @@ func (s *Server) SendTo(username string, msg Message) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("%s is not online", username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendWhoList broadcasts the connected members to everybody in the room.
|
||||
|
@ -294,6 +302,7 @@ func (s *Server) SendWhoList() {
|
|||
sub.SendJSON(Message{
|
||||
Action: ActionWhoList,
|
||||
WhoList: users,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,13 @@ const app = Vue.createApp({
|
|||
config: {
|
||||
channels: PublicChannels,
|
||||
website: WebsiteURL,
|
||||
sounds: {
|
||||
available: SoundEffects,
|
||||
settings: DefaultSounds,
|
||||
ready: false,
|
||||
audioContext: null,
|
||||
audioTracks: {},
|
||||
}
|
||||
},
|
||||
|
||||
// User JWT settings if available.
|
||||
|
@ -96,9 +103,15 @@ const app = Vue.createApp({
|
|||
loginModal: {
|
||||
visible: false,
|
||||
},
|
||||
|
||||
settingsModal: {
|
||||
visible: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setupSounds();
|
||||
|
||||
this.webcam.elem = document.querySelector("#localVideo");
|
||||
this.historyScrollbox = document.querySelector("#chatHistory");
|
||||
|
||||
|
@ -280,10 +293,20 @@ const app = Vue.createApp({
|
|||
|
||||
// Handle messages sent in chat.
|
||||
onMessage(msg) {
|
||||
// Play sound effects if this is not the active channel.
|
||||
if (msg.channel.indexOf("@") === 0) {
|
||||
if (msg.channel !== this.channel) {
|
||||
this.playSound("DM");
|
||||
}
|
||||
} else if (msg.channel !== this.channel) {
|
||||
this.playSound("Chat");
|
||||
}
|
||||
|
||||
this.pushHistory({
|
||||
channel: msg.channel,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
at: msg.at,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -293,6 +316,9 @@ const app = Vue.createApp({
|
|||
if (msg.message.indexOf("has exited the room!") > -1) {
|
||||
// Clean up data about this user.
|
||||
this.onUserExited(msg);
|
||||
this.playSound("Leave");
|
||||
} else {
|
||||
this.playSound("Enter");
|
||||
}
|
||||
|
||||
// Push it to the history of all public channels.
|
||||
|
@ -302,6 +328,19 @@ const app = Vue.createApp({
|
|||
action: msg.action,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
at: msg.at,
|
||||
});
|
||||
}
|
||||
|
||||
// Push also to any DM channels for this user.
|
||||
let channel = "@" + msg.username;
|
||||
if (this.channels[channel] != undefined) {
|
||||
this.pushHistory({
|
||||
channel: channel,
|
||||
action: msg.action,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
at: msg.at,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -320,8 +359,8 @@ const app = Vue.createApp({
|
|||
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||
|
||||
if (ev.code !== 1001) {
|
||||
this.ChatClient("Reconnecting in 1s");
|
||||
setTimeout(this.dial, 1000);
|
||||
this.ChatClient("Reconnecting in 5s");
|
||||
setTimeout(this.dial, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -345,6 +384,13 @@ const app = Vue.createApp({
|
|||
}
|
||||
|
||||
let msg = JSON.parse(ev.data);
|
||||
try {
|
||||
// Cast timestamp to date.
|
||||
msg.at = new Date(msg.at);
|
||||
} catch(e) {
|
||||
console.error("Parsing timestamp '%s' on msg: %s", msg.at, e);
|
||||
}
|
||||
|
||||
switch (msg.action) {
|
||||
case "who":
|
||||
console.log("Got the Who List: %s", msg);
|
||||
|
@ -384,7 +430,12 @@ const app = Vue.createApp({
|
|||
username: msg.username || 'Internal Server Error',
|
||||
message: msg.message,
|
||||
isChatServer: true,
|
||||
at: new Date(),
|
||||
});
|
||||
break;
|
||||
case "ping":
|
||||
console.debug("Received ping from server");
|
||||
break;
|
||||
default:
|
||||
console.error("Unexpected action: %s", JSON.stringify(msg));
|
||||
}
|
||||
|
@ -574,6 +625,14 @@ const app = Vue.createApp({
|
|||
* Front-end web app concerns.
|
||||
*/
|
||||
|
||||
// Settings modal.
|
||||
showSettings() {
|
||||
this.settingsModal.visible = true;
|
||||
},
|
||||
hideSettings() {
|
||||
this.settingsModal.visible = false;
|
||||
},
|
||||
|
||||
// Set active chat room.
|
||||
setChannel(channel) {
|
||||
this.channel = typeof(channel) === "string" ? channel : channel.ID;
|
||||
|
@ -803,7 +862,7 @@ const app = Vue.createApp({
|
|||
};
|
||||
}
|
||||
},
|
||||
pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient }) {
|
||||
pushHistory({ channel, username, message, action = "message", at, isChatServer, isChatClient }) {
|
||||
// Default channel = your current channel.
|
||||
if (!channel) {
|
||||
channel = this.channel;
|
||||
|
@ -818,6 +877,7 @@ const app = Vue.createApp({
|
|||
action: action,
|
||||
username: username,
|
||||
message: message,
|
||||
at: at || new Date(),
|
||||
isChatServer,
|
||||
isChatClient,
|
||||
});
|
||||
|
@ -894,6 +954,77 @@ const app = Vue.createApp({
|
|||
isChatClient: true,
|
||||
});
|
||||
},
|
||||
|
||||
// Format a datetime nicely for chat timestamp.
|
||||
prettyDate(date) {
|
||||
let hours = date.getHours(),
|
||||
minutes = String(date.getMinutes()).padStart(2, '0'),
|
||||
seconds = String(date.getSeconds()).padStart(2, '0'),
|
||||
ampm = hours >= 11 ? "pm" : "am";
|
||||
|
||||
return `${(hours%12)+1}:${minutes}:${seconds} ${ampm}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sound effect concerns.
|
||||
*/
|
||||
|
||||
setupSounds() {
|
||||
if (AudioContext) {
|
||||
this.config.sounds.audioContext = new AudioContext();
|
||||
} else {
|
||||
this.config.sounds.audioContext = window.AudioContext || window.webkitAudioContext;
|
||||
}
|
||||
if (!this.config.sounds.audioContext) {
|
||||
console.error("Couldn't set up AudioContext! No sound effects will be supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("AudioContext:", this.config.sounds.audioContext);
|
||||
|
||||
// Create <audio> elements for all the sounds.
|
||||
for (let effect of this.config.sounds.available) {
|
||||
if (!effect.filename) continue; // 'Quiet' has no audio
|
||||
|
||||
let elem = document.createElement("audio");
|
||||
elem.autoplay = false;
|
||||
elem.src = `/static/sfx/${effect.filename}`;
|
||||
document.body.appendChild(elem);
|
||||
|
||||
let track = this.config.sounds.audioContext.createMediaElementSource(elem);
|
||||
track.connect(this.config.sounds.audioContext.destination);
|
||||
this.config.sounds.audioTracks[effect.name] = elem;
|
||||
|
||||
console.warn(effect.name, this.config.sounds.audioTracks[effect.name]);
|
||||
}
|
||||
|
||||
// Apply the user's saved preferences if any.
|
||||
for (let setting of Object.keys(this.config.sounds.settings)) {
|
||||
if (localStorage[`sound:${setting}`] != undefined) {
|
||||
this.config.sounds.settings[setting] = localStorage[`sound:${setting}`];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
playSound(event) {
|
||||
let filename = this.config.sounds.settings[event];
|
||||
console.error("Play sound:", event, filename, JSON.stringify(this.config.sounds));
|
||||
// Do we have an audio track?
|
||||
console.log(this.config.sounds.audioTracks[filename]);
|
||||
if (this.config.sounds.audioTracks[filename] != undefined) {
|
||||
let track = this.config.sounds.audioTracks[filename];
|
||||
console.log("Track:", track);
|
||||
track.play();
|
||||
console.log("Playing %s", filename);
|
||||
}
|
||||
},
|
||||
|
||||
setSoundPref(event) {
|
||||
this.playSound(event);
|
||||
|
||||
// Store the user's setting in localStorage.
|
||||
localStorage[`sound:${event}`] = this.config.sounds.settings[event];
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
|
35
web/static/js/sounds.js
Normal file
35
web/static/js/sounds.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Available sound effects.
|
||||
const SoundEffects = [
|
||||
{
|
||||
name: "Quiet",
|
||||
filename: null
|
||||
},
|
||||
{
|
||||
name: "Trill",
|
||||
filename: "beep-6-96243.mp3"
|
||||
},
|
||||
{
|
||||
name: "Beep",
|
||||
filename: "beep-sound-8333.mp3"
|
||||
},
|
||||
{
|
||||
name: "Bird",
|
||||
filename: "bird-3-f-89236.mp3"
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
filename: "ping-82822.mp3"
|
||||
},
|
||||
{
|
||||
name: "Sonar",
|
||||
filename: "sonar-ping-95840.mp3"
|
||||
}
|
||||
];
|
||||
|
||||
// Defaults
|
||||
var DefaultSounds = {
|
||||
Chat: "Quiet",
|
||||
DM: "Trill",
|
||||
Enter: "Quiet",
|
||||
Leave: "Quiet",
|
||||
};
|
BIN
web/static/sfx/beep-6-96243.mp3
Normal file
BIN
web/static/sfx/beep-6-96243.mp3
Normal file
Binary file not shown.
BIN
web/static/sfx/beep-sound-8333.mp3
Normal file
BIN
web/static/sfx/beep-sound-8333.mp3
Normal file
Binary file not shown.
BIN
web/static/sfx/bird-3-f-89236.mp3
Normal file
BIN
web/static/sfx/bird-3-f-89236.mp3
Normal file
Binary file not shown.
1
web/static/sfx/credits.txt
Normal file
1
web/static/sfx/credits.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Free sounds from https://pixabay.com/sound-effects
|
BIN
web/static/sfx/ping-82822.mp3
Normal file
BIN
web/static/sfx/ping-82822.mp3
Normal file
Binary file not shown.
BIN
web/static/sfx/sonar-ping-95840.mp3
Normal file
BIN
web/static/sfx/sonar-ping-95840.mp3
Normal file
Binary file not shown.
|
@ -8,10 +8,11 @@
|
|||
<link rel="stylesheet" type="text/css" href="/static/css/bulma-prefers-dark.css">
|
||||
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
||||
<title>BareRTC</title>
|
||||
<title>{{.Config.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="BareRTC-App">
|
||||
|
||||
<!-- Sign In modal -->
|
||||
<div class="modal" :class="{'is-active': loginModal.visible}">
|
||||
<div class="modal-background"></div>
|
||||
|
@ -45,13 +46,122 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings modal -->
|
||||
<div class="modal" :class="{'is-active': settingsModal.visible}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-info">
|
||||
<p class="card-header-title has-text-light">Chat Settings</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<h3 class="subtitle">Sounds</h3>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">DM chat</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.DM" @change="setSoundPref('DM')">
|
||||
<option v-for="s in config.sounds.available"
|
||||
v-bind:key="s.name"
|
||||
:value="s.name">
|
||||
[[s.name]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Public chat</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.Chat" @change="setSoundPref('Chat')">
|
||||
<option v-for="s in config.sounds.available"
|
||||
v-bind:key="s.name"
|
||||
:value="s.name">
|
||||
[[s.name]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Room enter</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.Enter" @change="setSoundPref('Enter')">
|
||||
<option v-for="s in config.sounds.available"
|
||||
v-bind:key="s.name"
|
||||
:value="s.name">
|
||||
[[s.name]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Room leave</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="config.sounds.settings.Leave" @change="setSoundPref('Leave')">
|
||||
<option v-for="s in config.sounds.available"
|
||||
v-bind:key="s.name"
|
||||
:value="s.name">
|
||||
[[s.name]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button type="button" class="button is-primary"
|
||||
@click="hideSettings()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
|
||||
<!-- Top header panel -->
|
||||
<header class="chat-header">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<strong class="is-6">{{AsHTML .Config.Title}}</strong>
|
||||
<strong class="is-6">{{AsHTML .Config.Branding}}</strong>
|
||||
</div>
|
||||
<div class="column">
|
||||
<!-- Stop/Start video buttons -->
|
||||
|
@ -98,6 +208,12 @@
|
|||
</div>
|
||||
<div class="column is-narrow">
|
||||
<a href="/about" target="_blank" class="button is-small is-link">About</a>
|
||||
<button type="button"
|
||||
class="button is-small is-grey"
|
||||
@click="showSettings()"
|
||||
title="Chat Settings">
|
||||
<i class="fa fa-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -285,7 +401,7 @@
|
|||
</div>
|
||||
<div class="media-content">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column">
|
||||
<div class="column is-narrow">
|
||||
<label class="label"
|
||||
:class="{'has-text-success is-dark': msg.isChatServer,
|
||||
'has-text-warning is-dark': msg.isAdmin,
|
||||
|
@ -293,10 +409,13 @@
|
|||
[[msg.username]]
|
||||
</label>
|
||||
</div>
|
||||
<div class="column is-narrow"
|
||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username)">
|
||||
<div class="column is-narrow">
|
||||
<small class="has-text-grey" :title="msg.at">[[prettyDate(msg.at)]]</small>
|
||||
</div>
|
||||
<div class="column"
|
||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
|
||||
<button type="button"
|
||||
class="button is-dark is-outlined is-small"
|
||||
class="button is-grey is-outlined is-small px-2"
|
||||
@click="openDMs({username: msg.username})">
|
||||
<i class="fa fa-message"></i>
|
||||
</button>
|
||||
|
@ -420,6 +539,7 @@ const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
|||
</script>
|
||||
|
||||
<script src="/static/js/vue-3.2.45.js"></script>
|
||||
<script src="/static/js/sounds.js?{{.CacheHash}}"></script>
|
||||
<script src="/static/js/BareRTC.js?{{.CacheHash}}"></script>
|
||||
|
||||
</body>
|
||||
|
|
Loading…
Reference in New Issue
Block a user