Display names and WIP multiple camera support
This commit is contained in:
parent
2c2d140e57
commit
219413ae6d
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.kirsle.net/apps/barertc/pkg/config"
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
|
@ -54,21 +53,7 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the username is unique, or rename it.
|
// Ensure the username is unique, or rename it.
|
||||||
var duplicate bool
|
msg.Username = s.UniqueUsername(msg.Username)
|
||||||
for _, other := range s.IterSubscribers() {
|
|
||||||
if other.ID != sub.ID && other.Username == msg.Username {
|
|
||||||
duplicate = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if duplicate {
|
|
||||||
// Give them one that is unique.
|
|
||||||
msg.Username = fmt.Sprintf("%s %d",
|
|
||||||
msg.Username,
|
|
||||||
time.Now().Nanosecond(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use their username.
|
// Use their username.
|
||||||
sub.Username = msg.Username
|
sub.Username = msg.Username
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Claims struct {
|
||||||
IsAdmin bool `json:"op"`
|
IsAdmin bool `json:"op"`
|
||||||
Avatar string `json:"img"`
|
Avatar string `json:"img"`
|
||||||
ProfileURL string `json:"url"`
|
ProfileURL string `json:"url"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
|
||||||
// Standard claims. Notes:
|
// Standard claims. Notes:
|
||||||
// subject = username
|
// subject = username
|
||||||
|
|
|
@ -69,6 +69,7 @@ const (
|
||||||
// WhoList is a member entry in the chat room.
|
// WhoList is a member entry in the chat room.
|
||||||
type WhoList struct {
|
type WhoList struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
Nickname string `json:"nickname,omitempty"`
|
||||||
VideoActive bool `json:"videoActive,omitempty"`
|
VideoActive bool `json:"videoActive,omitempty"`
|
||||||
VideoMutual bool `json:"videoMutual,omitempty"`
|
VideoMutual bool `json:"videoMutual,omitempty"`
|
||||||
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
||||||
|
|
|
@ -259,6 +259,31 @@ func (s *Server) IterSubscribers(isLocked ...bool) []*Subscriber {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UniqueUsername ensures a username will be unique or renames it.
|
||||||
|
func (s *Server) UniqueUsername(username string) string {
|
||||||
|
var (
|
||||||
|
subs = s.IterSubscribers()
|
||||||
|
usernames = map[string]interface{}{}
|
||||||
|
origUsername = username
|
||||||
|
counter = 2
|
||||||
|
)
|
||||||
|
for _, sub := range subs {
|
||||||
|
usernames[sub.Username] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check until unique.
|
||||||
|
for {
|
||||||
|
if _, ok := usernames[username]; ok {
|
||||||
|
username = fmt.Sprintf("%s %d", origUsername, counter)
|
||||||
|
counter++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast a message to the chat room.
|
// Broadcast a message to the chat room.
|
||||||
func (s *Server) Broadcast(msg Message) {
|
func (s *Server) Broadcast(msg Message) {
|
||||||
if len(msg.Message) < 1024 {
|
if len(msg.Message) < 1024 {
|
||||||
|
@ -358,6 +383,7 @@ func (s *Server) SendWhoList() {
|
||||||
who.Operator = user.JWTClaims.IsAdmin
|
who.Operator = user.JWTClaims.IsAdmin
|
||||||
who.Avatar = user.JWTClaims.Avatar
|
who.Avatar = user.JWTClaims.Avatar
|
||||||
who.ProfileURL = user.JWTClaims.ProfileURL
|
who.ProfileURL = user.JWTClaims.ProfileURL
|
||||||
|
who.Nickname = user.JWTClaims.Nick
|
||||||
}
|
}
|
||||||
users = append(users, who)
|
users = append(users, who)
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,12 @@ const app = Vue.createApp({
|
||||||
[ "x3", "3x larger videos" ],
|
[ "x3", "3x larger videos" ],
|
||||||
[ "x4", "4x larger videos (not recommended)" ],
|
[ "x4", "4x larger videos (not recommended)" ],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Available cameras and microphones for the Settings modal.
|
||||||
|
videoDevices: [],
|
||||||
|
videoDeviceID: null,
|
||||||
|
audioDevices: [],
|
||||||
|
audioDeviceID: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
// WebRTC sessions with other users.
|
// WebRTC sessions with other users.
|
||||||
|
@ -865,6 +871,17 @@ const app = Vue.createApp({
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
nicknameForUsername(username) {
|
||||||
|
if (!username) return;
|
||||||
|
username = username.replace(/^@/, "");
|
||||||
|
if (this.whoMap[username] != undefined && this.whoMap[username].profileURL) {
|
||||||
|
let nick = this.whoMap[username].nickname;
|
||||||
|
if (nick) {
|
||||||
|
return nick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return username;
|
||||||
|
},
|
||||||
leaveDM() {
|
leaveDM() {
|
||||||
// Validate we're in a DM currently.
|
// Validate we're in a DM currently.
|
||||||
if (this.channel.indexOf("@") !== 0) return;
|
if (this.channel.indexOf("@") !== 0) return;
|
||||||
|
@ -946,12 +963,47 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// Tell backend the camera is ready.
|
// Tell backend the camera is ready.
|
||||||
this.sendMe();
|
this.sendMe();
|
||||||
|
|
||||||
|
// Record the selected device IDs.
|
||||||
|
this.webcam.videoDeviceID = stream.getVideoTracks()[0].getSettings().deviceId;
|
||||||
|
this.webcam.audioDeviceID = stream.getAudioTracks()[0].getSettings().deviceId;
|
||||||
|
console.log("device IDs:", this.webcam.videoDeviceID, this.webcam.audioDeviceID);
|
||||||
|
|
||||||
|
// Collect video and audio devices to let the user change them in their settings.
|
||||||
|
this.getDevices();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.ChatClient(`Webcam error: ${err}`);
|
this.ChatClient(`Webcam error: ${err}`);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.webcam.busy = false;
|
this.webcam.busy = false;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getDevices() {
|
||||||
|
// Collect video and audio devices.
|
||||||
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||||
|
console.log("enumerateDevices() not supported.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(devices => {
|
||||||
|
this.webcam.videoDevices = [];
|
||||||
|
this.webcam.audioDevices = [];
|
||||||
|
devices.forEach(device => {
|
||||||
|
if (device.kind === 'videoinput') {
|
||||||
|
this.webcam.videoDevices.push({
|
||||||
|
id: device.deviceId,
|
||||||
|
label: device.label,
|
||||||
|
});
|
||||||
|
} else if (device.kind === 'audioinput') {
|
||||||
|
this.webcam.audioDevices.push({
|
||||||
|
id: device.deviceId,
|
||||||
|
label: device.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
this.ChatClient(`Error listing your cameras and microphones: ${err.name}: ${err.message}`);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// Begin connecting to someone else's webcam.
|
// Begin connecting to someone else's webcam.
|
||||||
openVideo(user, force) {
|
openVideo(user, force) {
|
||||||
|
|
|
@ -114,8 +114,102 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="subtitle">Sounds</h3>
|
<!-- Under construction
|
||||||
|
<div class="columns is-mobile" v-if="webcam.videoDevices.length > 0 || webcam.audioDevices.length > 0">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<label class="label">Video source</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="webcam.videoDeviceID">
|
||||||
|
<option v-for="(d, i) in webcam.videoDevices"
|
||||||
|
:value="d.id">
|
||||||
|
[[ d.label || `Camera ${i}` ]]
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<label class="label">Audio source</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select v-model="webcam.audioDeviceID">
|
||||||
|
<option v-for="(d, i) in webcam.audioDevices"
|
||||||
|
:value="d.id">
|
||||||
|
[[ d.label || `Microphone ${i}` ]]
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<h3 class="subtitle mb-2">Sounds</h3>
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-2 pr-1">
|
||||||
|
<label class="label">DM chat</label>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<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 class="column is-2 pr-1">
|
||||||
|
<label class="label">Public chat</label>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<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 class="columns is-mobile">
|
||||||
|
<div class="column is-2 pr-1">
|
||||||
|
<label class="label">Room enter</label>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<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 class="column is-2 pr-1">
|
||||||
|
<label class="label">Room leave</label>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<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 class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label class="label">DM chat</label>
|
<label class="label">DM chat</label>
|
||||||
|
@ -199,6 +293,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
|
@ -595,12 +690,12 @@
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(msg, i) in chatHistory" v-bind:key="i" class="mb-2">
|
<div v-for="(msg, i) in chatHistory" v-bind:key="i" class="box mb-2 px-4 pt-3 pb-1">
|
||||||
<div class="media mb-0">
|
<div class="media mb-0">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
|
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
|
||||||
:class="{'cursor-default': !profileURLForUsername(msg.username)}">
|
:class="{'cursor-default': !profileURLForUsername(msg.username)}">
|
||||||
<figure class="image is-24x24">
|
<figure class="image is-48x48">
|
||||||
<img v-if="msg.isChatServer"
|
<img v-if="msg.isChatServer"
|
||||||
src="/static/img/server.png">
|
src="/static/img/server.png">
|
||||||
<img v-else-if="msg.isChatClient"
|
<img v-else-if="msg.isChatClient"
|
||||||
|
@ -612,50 +707,81 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile pb-0">
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow pb-0">
|
||||||
<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,
|
||||||
'has-text-danger': msg.isChatClient}">
|
'has-text-danger': msg.isChatClient}">
|
||||||
[[msg.username]]
|
|
||||||
|
<!-- User nickname/display name -->
|
||||||
|
[[nicknameForUsername(msg.username)]]
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
</div>
|
||||||
<small class="has-text-grey" :title="msg.at">[[prettyDate(msg.at)]]</small>
|
|
||||||
|
<!-- User @username below it which may link to a profile URL if JWT -->
|
||||||
|
<div class="columns is-mobile pt-0" v-if="(msg.isChatClient || msg.isChatServer)">
|
||||||
|
<div class="column is-narrow pt-0">
|
||||||
|
<small v-if="!(msg.isChatClient || msg.isChatServer)">
|
||||||
|
<a v-if="profileURLForUsername(msg.username)"
|
||||||
|
:href="profileURLForUsername(msg.username)"
|
||||||
|
target="_blank"
|
||||||
|
class="has-text-grey">
|
||||||
|
@[[msg.username]]
|
||||||
|
</a>
|
||||||
|
<span v-else class="has-text-grey">@[[msg.username]]</span>
|
||||||
|
</small>
|
||||||
|
<small v-else class="has-text-grey">internal</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow pr-0"
|
</div>
|
||||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
|
<div v-else class="columns is-mobile pt-0">
|
||||||
|
<div class="column is-narrow pt-0">
|
||||||
|
<small v-if="!(msg.isChatClient || msg.isChatServer)">
|
||||||
|
<a v-if="profileURLForUsername(msg.username)"
|
||||||
|
:href="profileURLForUsername(msg.username)"
|
||||||
|
target="_blank"
|
||||||
|
class="has-text-grey">
|
||||||
|
@[[msg.username]]
|
||||||
|
</a>
|
||||||
|
<span v-else class="has-text-grey">@[[msg.username]]</span>
|
||||||
|
</small>
|
||||||
|
<small v-else class="has-text-grey">internal</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-narrow px-1 pt-0"
|
||||||
|
v-if="!(msg.username === username || isDM)">
|
||||||
<!-- DMs button -->
|
<!-- DMs button -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="button is-grey is-outlined is-small px-2"
|
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 mr-1"></i>
|
<i class="fa fa-message"></i>
|
||||||
DM
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow"
|
<div class="column is-narrow pl-1 pt-0"
|
||||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username)">
|
v-if="!(msg.username === username)">
|
||||||
<!-- Mute button -->
|
<!-- Mute button -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="button is-grey is-outlined is-small px-2"
|
class="button is-grey is-outlined is-small px-2"
|
||||||
@click="muteUser(msg.username)"
|
@click="muteUser(msg.username)"
|
||||||
title="Mute user">
|
title="Mute user">
|
||||||
<i class="fa fa-comment-slash mr-1"
|
<i class="fa fa-comment-slash"
|
||||||
:class="{'has-text-success': isMutedUser(msg.username),
|
:class="{'has-text-success': isMutedUser(msg.username),
|
||||||
'has-text-danger': !isMutedUser(msg.username)}"></i>
|
'has-text-danger': !isMutedUser(msg.username)}"></i>
|
||||||
[[isMutedUser(msg.username) ? 'Unmute' : 'Mute']]
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="msg.action === 'presence'">
|
<!-- Message box -->
|
||||||
<em>[[msg.message]]</em>
|
<div class="content pl-5 py-3 mb-0">
|
||||||
|
<em v-if="msg.action === 'presence'">[[msg.message]]</em>
|
||||||
|
<div v-else v-html="msg.message"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="content">
|
|
||||||
<div v-html="msg.message"></div>
|
<div class="has-text-right">
|
||||||
|
<small class="has-text-grey is-size-7" :title="msg.at">[[prettyDate(msg.at)]]</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user