Display names and WIP multiple camera support
This commit is contained in:
parent
2c2d140e57
commit
219413ae6d
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"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.
|
||||
var duplicate bool
|
||||
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(),
|
||||
)
|
||||
}
|
||||
msg.Username = s.UniqueUsername(msg.Username)
|
||||
|
||||
// Use their username.
|
||||
sub.Username = msg.Username
|
||||
|
|
|
@ -15,6 +15,7 @@ type Claims struct {
|
|||
IsAdmin bool `json:"op"`
|
||||
Avatar string `json:"img"`
|
||||
ProfileURL string `json:"url"`
|
||||
Nick string `json:"nick"`
|
||||
|
||||
// Standard claims. Notes:
|
||||
// subject = username
|
||||
|
|
|
@ -69,6 +69,7 @@ const (
|
|||
// WhoList is a member entry in the chat room.
|
||||
type WhoList struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
VideoActive bool `json:"videoActive,omitempty"`
|
||||
VideoMutual bool `json:"videoMutual,omitempty"`
|
||||
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
||||
|
|
|
@ -259,6 +259,31 @@ func (s *Server) IterSubscribers(isLocked ...bool) []*Subscriber {
|
|||
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.
|
||||
func (s *Server) Broadcast(msg Message) {
|
||||
if len(msg.Message) < 1024 {
|
||||
|
@ -358,6 +383,7 @@ func (s *Server) SendWhoList() {
|
|||
who.Operator = user.JWTClaims.IsAdmin
|
||||
who.Avatar = user.JWTClaims.Avatar
|
||||
who.ProfileURL = user.JWTClaims.ProfileURL
|
||||
who.Nickname = user.JWTClaims.Nick
|
||||
}
|
||||
users = append(users, who)
|
||||
}
|
||||
|
|
|
@ -107,6 +107,12 @@ const app = Vue.createApp({
|
|||
[ "x3", "3x larger videos" ],
|
||||
[ "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.
|
||||
|
@ -865,6 +871,17 @@ const app = Vue.createApp({
|
|||
}
|
||||
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() {
|
||||
// Validate we're in a DM currently.
|
||||
if (this.channel.indexOf("@") !== 0) return;
|
||||
|
@ -946,12 +963,47 @@ const app = Vue.createApp({
|
|||
|
||||
// Tell backend the camera is ready.
|
||||
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 => {
|
||||
this.ChatClient(`Webcam error: ${err}`);
|
||||
}).finally(() => {
|
||||
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.
|
||||
openVideo(user, force) {
|
||||
|
|
|
@ -114,8 +114,102 @@
|
|||
</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-label is-normal">
|
||||
<label class="label">DM chat</label>
|
||||
|
@ -199,6 +293,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
|
@ -595,12 +690,12 @@
|
|||
</em>
|
||||
</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-left">
|
||||
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
|
||||
:class="{'cursor-default': !profileURLForUsername(msg.username)}">
|
||||
<figure class="image is-24x24">
|
||||
<figure class="image is-48x48">
|
||||
<img v-if="msg.isChatServer"
|
||||
src="/static/img/server.png">
|
||||
<img v-else-if="msg.isChatClient"
|
||||
|
@ -612,50 +707,81 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<div class="columns is-mobile pb-0">
|
||||
<div class="column is-narrow pb-0">
|
||||
<label class="label"
|
||||
:class="{'has-text-success is-dark': msg.isChatServer,
|
||||
'has-text-warning is-dark': msg.isAdmin,
|
||||
'has-text-danger': msg.isChatClient}">
|
||||
[[msg.username]]
|
||||
|
||||
<!-- User nickname/display name -->
|
||||
[[nicknameForUsername(msg.username)]]
|
||||
</label>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<small class="has-text-grey" :title="msg.at">[[prettyDate(msg.at)]]</small>
|
||||
</div>
|
||||
<div class="column is-narrow pr-0"
|
||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
|
||||
|
||||
<!-- 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 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 -->
|
||||
<button type="button"
|
||||
class="button is-grey is-outlined is-small px-2"
|
||||
@click="openDMs({username: msg.username})">
|
||||
<i class="fa fa-message mr-1"></i>
|
||||
DM
|
||||
<i class="fa fa-message"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow"
|
||||
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username)">
|
||||
<div class="column is-narrow pl-1 pt-0"
|
||||
v-if="!(msg.username === username)">
|
||||
<!-- Mute button -->
|
||||
<button type="button"
|
||||
class="button is-grey is-outlined is-small px-2"
|
||||
@click="muteUser(msg.username)"
|
||||
title="Mute user">
|
||||
<i class="fa fa-comment-slash mr-1"
|
||||
<i class="fa fa-comment-slash"
|
||||
:class="{'has-text-success': isMutedUser(msg.username),
|
||||
'has-text-danger': !isMutedUser(msg.username)}"></i>
|
||||
[[isMutedUser(msg.username) ? 'Unmute' : 'Mute']]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="msg.action === 'presence'">
|
||||
<em>[[msg.message]]</em>
|
||||
<!-- Message box -->
|
||||
<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 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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user