Display names and WIP multiple camera support

ipad-testing
Noah 2023-04-18 22:18:12 -07:00
parent 2c2d140e57
commit 219413ae6d
6 changed files with 228 additions and 37 deletions

View File

@ -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

View File

@ -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

View File

@ -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"`

View File

@ -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)
}

View File

@ -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) {

View File

@ -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>
<!-- 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 class="column is-narrow pr-0"
v-if="!(msg.isChatServer || msg.isChatClient || msg.username === username || isDM)">
</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']]
'has-text-danger': !isMutedUser(msg.username)}"></i>
</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>