Timestamps, Sound Effects & Love
This commit is contained in:
parent
4810d95a65
commit
9487595e04
5
Makefile
5
Makefile
|
@ -1,2 +1,7 @@
|
||||||
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
go run cmd/BareRTC/main.go -debug
|
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 := barertc.NewServer()
|
||||||
app.Setup()
|
app.Setup()
|
||||||
|
|
||||||
|
log.Info("Listening at %s", address)
|
||||||
panic(app.ListenAndServe(address))
|
panic(app.ListenAndServe(address))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
|
Branding string
|
||||||
WebsiteURL string
|
WebsiteURL string
|
||||||
|
|
||||||
PublicChannels []Channel
|
PublicChannels []Channel
|
||||||
|
|
|
@ -108,10 +108,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
|
|
||||||
// Message to be echoed to the channel.
|
// Message to be echoed to the channel.
|
||||||
var message = Message{
|
var message = Message{
|
||||||
Action: ActionMessage,
|
Action: ActionMessage,
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: markdown,
|
Message: markdown,
|
||||||
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this a DM?
|
// Is this a DM?
|
||||||
|
@ -119,7 +120,9 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
// Echo the message only to both parties.
|
// Echo the message only to both parties.
|
||||||
s.SendTo(sub.Username, message)
|
s.SendTo(sub.Username, message)
|
||||||
message.Channel = "@" + sub.Username
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package barertc
|
package barertc
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
|
Timestamp time.Time `json:"at,omitempty"`
|
||||||
|
|
||||||
// JWT token for `login` actions.
|
// JWT token for `login` actions.
|
||||||
JWTToken string `json:"jwt,omitempty"`
|
JWTToken string `json:"jwt,omitempty"`
|
||||||
|
|
|
@ -239,23 +239,26 @@ func (s *Server) Broadcast(msg Message) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(msg)
|
||||||
Action: msg.Action,
|
|
||||||
Channel: msg.Channel,
|
|
||||||
Username: msg.Username,
|
|
||||||
Message: msg.Message,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTo sends a message to a given username.
|
// 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)
|
log.Debug("SendTo(%s): %+v", username, msg)
|
||||||
username = strings.TrimPrefix(username, "@")
|
username = strings.TrimPrefix(username, "@")
|
||||||
s.subscribersMu.RLock()
|
s.subscribersMu.RLock()
|
||||||
defer s.subscribersMu.RUnlock()
|
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) {
|
for _, sub := range s.IterSubscribers(true) {
|
||||||
if sub.Username == username {
|
if sub.Username == username {
|
||||||
|
found = true
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
Action: msg.Action,
|
Action: msg.Action,
|
||||||
Channel: msg.Channel,
|
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.
|
// SendWhoList broadcasts the connected members to everybody in the room.
|
||||||
|
@ -292,8 +300,9 @@ func (s *Server) SendWhoList() {
|
||||||
|
|
||||||
for _, sub := range subscribers {
|
for _, sub := range subscribers {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
Action: ActionWhoList,
|
Action: ActionWhoList,
|
||||||
WhoList: users,
|
WhoList: users,
|
||||||
|
Timestamp: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,13 @@ const app = Vue.createApp({
|
||||||
config: {
|
config: {
|
||||||
channels: PublicChannels,
|
channels: PublicChannels,
|
||||||
website: WebsiteURL,
|
website: WebsiteURL,
|
||||||
|
sounds: {
|
||||||
|
available: SoundEffects,
|
||||||
|
settings: DefaultSounds,
|
||||||
|
ready: false,
|
||||||
|
audioContext: null,
|
||||||
|
audioTracks: {},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// User JWT settings if available.
|
// User JWT settings if available.
|
||||||
|
@ -96,9 +103,15 @@ const app = Vue.createApp({
|
||||||
loginModal: {
|
loginModal: {
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
settingsModal: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.setupSounds();
|
||||||
|
|
||||||
this.webcam.elem = document.querySelector("#localVideo");
|
this.webcam.elem = document.querySelector("#localVideo");
|
||||||
this.historyScrollbox = document.querySelector("#chatHistory");
|
this.historyScrollbox = document.querySelector("#chatHistory");
|
||||||
|
|
||||||
|
@ -280,10 +293,20 @@ 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.
|
||||||
|
if (msg.channel.indexOf("@") === 0) {
|
||||||
|
if (msg.channel !== this.channel) {
|
||||||
|
this.playSound("DM");
|
||||||
|
}
|
||||||
|
} else if (msg.channel !== this.channel) {
|
||||||
|
this.playSound("Chat");
|
||||||
|
}
|
||||||
|
|
||||||
this.pushHistory({
|
this.pushHistory({
|
||||||
channel: msg.channel,
|
channel: msg.channel,
|
||||||
username: msg.username,
|
username: msg.username,
|
||||||
message: msg.message,
|
message: msg.message,
|
||||||
|
at: msg.at,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -293,6 +316,9 @@ const app = Vue.createApp({
|
||||||
if (msg.message.indexOf("has exited the room!") > -1) {
|
if (msg.message.indexOf("has exited the room!") > -1) {
|
||||||
// Clean up data about this user.
|
// Clean up data about this user.
|
||||||
this.onUserExited(msg);
|
this.onUserExited(msg);
|
||||||
|
this.playSound("Leave");
|
||||||
|
} else {
|
||||||
|
this.playSound("Enter");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push it to the history of all public channels.
|
// Push it to the history of all public channels.
|
||||||
|
@ -302,6 +328,19 @@ 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`);
|
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||||
|
|
||||||
if (ev.code !== 1001) {
|
if (ev.code !== 1001) {
|
||||||
this.ChatClient("Reconnecting in 1s");
|
this.ChatClient("Reconnecting in 5s");
|
||||||
setTimeout(this.dial, 1000);
|
setTimeout(this.dial, 5000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -345,6 +384,13 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = JSON.parse(ev.data);
|
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) {
|
switch (msg.action) {
|
||||||
case "who":
|
case "who":
|
||||||
console.log("Got the Who List: %s", msg);
|
console.log("Got the Who List: %s", msg);
|
||||||
|
@ -384,7 +430,12 @@ 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;
|
||||||
|
case "ping":
|
||||||
|
console.debug("Received ping from server");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error("Unexpected action: %s", JSON.stringify(msg));
|
console.error("Unexpected action: %s", JSON.stringify(msg));
|
||||||
}
|
}
|
||||||
|
@ -574,6 +625,14 @@ const app = Vue.createApp({
|
||||||
* Front-end web app concerns.
|
* Front-end web app concerns.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Settings modal.
|
||||||
|
showSettings() {
|
||||||
|
this.settingsModal.visible = true;
|
||||||
|
},
|
||||||
|
hideSettings() {
|
||||||
|
this.settingsModal.visible = false;
|
||||||
|
},
|
||||||
|
|
||||||
// Set active chat room.
|
// Set active chat room.
|
||||||
setChannel(channel) {
|
setChannel(channel) {
|
||||||
this.channel = typeof(channel) === "string" ? channel : channel.ID;
|
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.
|
// Default channel = your current channel.
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
channel = this.channel;
|
channel = this.channel;
|
||||||
|
@ -818,6 +877,7 @@ const app = Vue.createApp({
|
||||||
action: action,
|
action: action,
|
||||||
username: username,
|
username: username,
|
||||||
message: message,
|
message: message,
|
||||||
|
at: at || new Date(),
|
||||||
isChatServer,
|
isChatServer,
|
||||||
isChatClient,
|
isChatClient,
|
||||||
});
|
});
|
||||||
|
@ -894,6 +954,77 @@ const app = Vue.createApp({
|
||||||
isChatClient: true,
|
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" 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" href="/static/fontawesome-free-6.1.2-web/css/all.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
<link rel="stylesheet" type="text/css" href="/static/css/chat.css?{{.CacheHash}}">
|
||||||
<title>BareRTC</title>
|
<title>{{.Config.Title}}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="BareRTC-App">
|
<div id="BareRTC-App">
|
||||||
|
|
||||||
<!-- Sign In modal -->
|
<!-- Sign In modal -->
|
||||||
<div class="modal" :class="{'is-active': loginModal.visible}">
|
<div class="modal" :class="{'is-active': loginModal.visible}">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
|
@ -45,13 +46,122 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="chat-container">
|
||||||
|
|
||||||
<!-- Top header panel -->
|
<!-- Top header panel -->
|
||||||
<header class="chat-header">
|
<header class="chat-header">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<strong class="is-6">{{AsHTML .Config.Title}}</strong>
|
<strong class="is-6">{{AsHTML .Config.Branding}}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<!-- Stop/Start video buttons -->
|
<!-- Stop/Start video buttons -->
|
||||||
|
@ -98,6 +208,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="/about" target="_blank" class="button is-small is-link">About</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -285,7 +401,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column">
|
<div class="column is-narrow">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
:class="{'has-text-success is-dark': msg.isChatServer,
|
:class="{'has-text-success is-dark': msg.isChatServer,
|
||||||
'has-text-warning is-dark': msg.isAdmin,
|
'has-text-warning is-dark': msg.isAdmin,
|
||||||
|
@ -293,10 +409,13 @@
|
||||||
[[msg.username]]
|
[[msg.username]]
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow"
|
<div class="column is-narrow">
|
||||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username)">
|
<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"
|
<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})">
|
@click="openDMs({username: msg.username})">
|
||||||
<i class="fa fa-message"></i>
|
<i class="fa fa-message"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -420,6 +539,7 @@ const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/js/vue-3.2.45.js"></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>
|
<script src="/static/js/BareRTC.js?{{.CacheHash}}"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user