Video Status Bitflags + Improvements
* Consolidate all the Video flags (active, nsfw, mutual, mutualOpen) into a bitmask flag (single integer) * New video flag for when the source has muted their video, to show a crossed out grey mic on their camera for other chatters * Bugfixes around syncing the mute state for self and other videos when videos are opened, closed and opened again * Profile pictures on the DMs list
This commit is contained in:
parent
3c1ad4ec6d
commit
2445d45d3f
24
Protocol.md
24
Protocol.md
|
@ -30,6 +30,22 @@ The video stream can be interrupted and closed via various methods:
|
|||
* Also, a `who` update that says a person's videoActive went false will instruct all clients who had the video open, to close it (in case the PeerConnection closure didn't already do this).
|
||||
* If a user exits the room, e.g. exited their browser abruptly without gracefully closing PeerConnections, any client who had their video open will close it immediately.
|
||||
|
||||
# Video Flags
|
||||
|
||||
The various video settings sent on Who List updates are now consolidated
|
||||
to a bit flag field:
|
||||
|
||||
```javascript
|
||||
VideoFlag: {
|
||||
Active: 1 << 0, // or 00000001 in binary
|
||||
NSFW: 1 << 1, // or 00000010
|
||||
Muted: 1 << 2, // or 00000100, etc.
|
||||
IsTalking: 1 << 3,
|
||||
MutualRequired: 1 << 4,
|
||||
MutualOpen: 1 << 5,
|
||||
}
|
||||
```
|
||||
|
||||
# WebSocket Message Actions
|
||||
|
||||
Every message has an "action" and may have other fields depending on the action type.
|
||||
|
@ -201,8 +217,7 @@ The client sends "me" messages to send their webcam broadcast status and NSFW fl
|
|||
// Client Me
|
||||
{
|
||||
"action": "me",
|
||||
"videoActive": true,
|
||||
"nsfw": false
|
||||
"video": 1,
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -213,7 +228,7 @@ The server may also push "me" messages to the user: for example if there is a co
|
|||
{
|
||||
"action": "me",
|
||||
"username": "soandso 12345",
|
||||
"videoActive": true
|
||||
"video": 1,
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -233,8 +248,7 @@ The `who` action sends the Who Is Online list to all connected chatters.
|
|||
"op": false, // operator status
|
||||
"avatar": "/picture/soandso.png",
|
||||
"profileURL": "/u/soandso",
|
||||
"videoActive": true,
|
||||
"nsfW": false
|
||||
"video": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -49,8 +49,8 @@ func (s *Server) Statistics() http.HandlerFunc {
|
|||
unique[sub.Username] = struct{}{}
|
||||
|
||||
// Count cameras by color.
|
||||
if sub.VideoActive {
|
||||
if sub.VideoNSFW {
|
||||
if sub.VideoStatus&VideoFlagActive == VideoFlagActive {
|
||||
if sub.VideoStatus&VideoFlagNSFW == VideoFlagNSFW {
|
||||
result.Cameras.Red++
|
||||
} else {
|
||||
result.Cameras.Blue++
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package barertc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
@ -42,7 +45,7 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
|||
sub.ChatServer("/nsfw: username not found: %s", username)
|
||||
} else {
|
||||
other.ChatServer("Your camera has been marked as NSFW by %s", sub.Username)
|
||||
other.VideoNSFW = true
|
||||
other.VideoStatus |= VideoFlagNSFW
|
||||
other.SendMe()
|
||||
s.SendWhoList()
|
||||
sub.ChatServer("%s has their camera marked as NSFW", username)
|
||||
|
@ -52,11 +55,23 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
|||
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
|
||||
"* `/kick <username>` to kick from chat\n" +
|
||||
"* `/nsfw <username>` to mark their camera NSFW\n" +
|
||||
"* `/shutdown` to gracefully shut down (reboot) the chat server\n" +
|
||||
"* `/kickall` to kick EVERYBODY off and force them to log back in\n" +
|
||||
"* `/help` to show this message\n\n" +
|
||||
"Note: shell-style quoting is supported, if a username has a space in it, quote the whole username, e.g.: `/kick \"username 2\"`",
|
||||
))
|
||||
return true
|
||||
case "/shutdown":
|
||||
s.Broadcast(Message{
|
||||
Action: ActionError,
|
||||
Username: "ChatServer",
|
||||
Message: "The chat server is going down for a reboot NOW!",
|
||||
})
|
||||
os.Exit(1)
|
||||
case "/kickall":
|
||||
s.KickAllCommand()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Not handled.
|
||||
|
@ -87,6 +102,48 @@ func (s *Server) KickCommand(words []string, sub *Subscriber) {
|
|||
}
|
||||
}
|
||||
|
||||
// KickAllCommand kicks everybody out of the room.
|
||||
func (s *Server) KickAllCommand() {
|
||||
|
||||
// If we have JWT enabled and a landing page, link users to it.
|
||||
if config.Current.JWT.Enabled && config.Current.JWT.LandingPageURL != "" {
|
||||
s.Broadcast(Message{
|
||||
Action: ActionError,
|
||||
Username: "ChatServer",
|
||||
Message: fmt.Sprintf(
|
||||
"<strong>Notice:</strong> The chat operator has requested that you log back in to the chat room. "+
|
||||
"Probably, this is because a new feature was launched that needs you to reload the page. "+
|
||||
"You may refresh the tab or <a href=\"%s\">click here</a> to re-enter the room.",
|
||||
config.Current.JWT.LandingPageURL,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
s.Broadcast(Message{
|
||||
Action: ActionError,
|
||||
Username: "ChatServer",
|
||||
Message: "<strong>Notice:</strong> The chat operator has kicked everybody from the room. Usually, this " +
|
||||
"may mean a new feature of the chat has been launched and you need to reload the page for it " +
|
||||
"to function correctly.",
|
||||
})
|
||||
}
|
||||
|
||||
// Kick everyone off.
|
||||
s.Broadcast(Message{
|
||||
Action: ActionKick,
|
||||
})
|
||||
|
||||
// Disconnect everybody.
|
||||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
for _, sub := range s.IterSubscribers(true) {
|
||||
if !sub.authenticated {
|
||||
continue
|
||||
}
|
||||
|
||||
s.DeleteSubscriber(sub)
|
||||
}
|
||||
}
|
||||
|
||||
// BanCommand handles the `/ban` operator command.
|
||||
func (s *Server) BanCommand(words []string, sub *Subscriber) {
|
||||
if len(words) == 1 {
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
// Version of the config format - when new fields are added, it will attempt
|
||||
// to write the settings.toml to disk so new defaults populate.
|
||||
var currentVersion = 3
|
||||
var currentVersion = 4
|
||||
|
||||
// Config for your BareRTC app.
|
||||
type Config struct {
|
||||
|
@ -22,6 +22,7 @@ type Config struct {
|
|||
Enabled bool
|
||||
Strict bool
|
||||
SecretKey string
|
||||
LandingPageURL string
|
||||
}
|
||||
|
||||
Title string
|
||||
|
|
|
@ -252,7 +252,7 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
|||
|
||||
// OnMe handles current user state updates.
|
||||
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||
if msg.VideoActive {
|
||||
if msg.VideoStatus&VideoFlagActive == VideoFlagActive {
|
||||
log.Debug("User %s turns on their video feed", sub.Username)
|
||||
}
|
||||
|
||||
|
@ -278,10 +278,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
|||
msg.ChatStatus = "away"
|
||||
}
|
||||
|
||||
sub.VideoActive = msg.VideoActive
|
||||
sub.VideoMutual = msg.VideoMutual
|
||||
sub.VideoMutualOpen = msg.VideoMutualOpen
|
||||
sub.VideoNSFW = msg.NSFW
|
||||
sub.VideoStatus = msg.VideoStatus
|
||||
sub.ChatStatus = msg.ChatStatus
|
||||
|
||||
// Sync the WhoList to everybody.
|
||||
|
|
|
@ -23,11 +23,8 @@ type Message struct {
|
|||
WhoList []WhoList `json:"whoList,omitempty"`
|
||||
|
||||
// Sent on `me` actions along with Username
|
||||
VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status
|
||||
VideoMutual bool `json:"videoMutual,omitempty"` // user wants mutual viewers
|
||||
VideoMutualOpen bool `json:"videoMutualOpen,omitempty"`
|
||||
VideoStatus int `json:"video,omitempty"` // user video flags
|
||||
ChatStatus string `json:"status,omitempty"` // online vs. away
|
||||
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
|
||||
|
||||
// Message ID to support takebacks/local deletions
|
||||
MessageID int `json:"msgID,omitempty"`
|
||||
|
@ -77,14 +74,22 @@ const (
|
|||
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"`
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Video int `json:"video"`
|
||||
|
||||
// JWT auth extra settings.
|
||||
Operator bool `json:"op"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
ProfileURL string `json:"profileURL,omitempty"`
|
||||
}
|
||||
|
||||
// VideoFlags to convey the state and setting of users' cameras concisely.
|
||||
// Also see the VideoFlag object in BareRTC.js for front-end sync.
|
||||
const (
|
||||
VideoFlagActive int = 1 << iota // user's camera is enabled/broadcasting
|
||||
VideoFlagNSFW // viewer's camera is marked as NSFW
|
||||
VideoFlagMuted // user source microphone is muted
|
||||
VideoFlagIsTalking // broadcaster seems to be talking
|
||||
VideoFlagMutualRequired // video wants viewers to share their camera too
|
||||
VideoFlagMutualOpen // viewer wants to auto-open viewers' cameras
|
||||
)
|
||||
|
|
|
@ -40,9 +40,16 @@ func IndexPage() http.HandlerFunc {
|
|||
|
||||
// Are we enforcing strict JWT authentication?
|
||||
if config.Current.JWT.Enabled && config.Current.JWT.Strict && !authOK {
|
||||
// Do we have a landing page to redirect to?
|
||||
if config.Current.JWT.LandingPageURL != "" {
|
||||
w.Header().Add("Location", config.Current.JWT.LandingPageURL)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(
|
||||
fmt.Sprintf("Authentication denied. Please go back and try again."),
|
||||
"Authentication denied. Please go back and try again.",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,11 +23,8 @@ type Subscriber struct {
|
|||
// User properties
|
||||
ID int // ID assigned by server
|
||||
Username string
|
||||
VideoActive bool
|
||||
VideoMutual bool
|
||||
VideoMutualOpen bool
|
||||
VideoNSFW bool
|
||||
ChatStatus string
|
||||
VideoStatus int
|
||||
JWTClaims *jwt.Claims
|
||||
authenticated bool // has passed the login step
|
||||
conn *websocket.Conn
|
||||
|
@ -130,8 +127,7 @@ func (sub *Subscriber) SendMe() {
|
|||
sub.SendJSON(Message{
|
||||
Action: ActionMe,
|
||||
Username: sub.Username,
|
||||
VideoActive: sub.VideoActive,
|
||||
NSFW: sub.VideoNSFW,
|
||||
VideoStatus: sub.VideoStatus,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -386,16 +382,12 @@ func (s *Server) SendWhoList() {
|
|||
who := WhoList{
|
||||
Username: user.Username,
|
||||
Status: user.ChatStatus,
|
||||
VideoActive: user.VideoActive,
|
||||
VideoMutual: user.VideoMutual,
|
||||
VideoMutualOpen: user.VideoMutualOpen,
|
||||
NSFW: user.VideoNSFW,
|
||||
Video: user.VideoStatus,
|
||||
}
|
||||
|
||||
// If this person had booted us, force their camera to "off"
|
||||
if user.Boots(sub.Username) || user.Mutes(sub.Username) {
|
||||
who.VideoActive = false
|
||||
who.NSFW = false
|
||||
who.Video = 0
|
||||
}
|
||||
|
||||
if user.JWTClaims != nil {
|
||||
|
|
|
@ -127,6 +127,16 @@ const app = Vue.createApp({
|
|||
audioDeviceID: null,
|
||||
},
|
||||
|
||||
// Video flag constants (sync with values in messages.go)
|
||||
VideoFlag: {
|
||||
Active: 1 << 0,
|
||||
NSFW: 1 << 1,
|
||||
Muted: 1 << 2,
|
||||
IsTalking: 1 << 3,
|
||||
MutualRequired: 1 << 4,
|
||||
MutualOpen: 1 << 5,
|
||||
},
|
||||
|
||||
// WebRTC sessions with other users.
|
||||
WebRTC: {
|
||||
// Streams per username.
|
||||
|
@ -315,6 +325,16 @@ const app = Vue.createApp({
|
|||
// Returns if the current user has operator rights
|
||||
return this.jwt.claims.op;
|
||||
},
|
||||
myVideoFlag() {
|
||||
// Compute the current user's video status flags.
|
||||
let status = 0;
|
||||
if (this.webcam.active) status |= this.VideoFlag.Active;
|
||||
if (this.webcam.muted) status |= this.VideoFlag.Muted;
|
||||
if (this.webcam.nsfw) status |= this.VideoFlag.NSFW;
|
||||
if (this.webcam.mutual) status |= this.VideoFlag.MutualRequired;
|
||||
if (this.webcam.mutualOpen) status |= this.VideoFlag.MutualOpen;
|
||||
return status;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Load user prefs from localStorage, called on startup
|
||||
|
@ -375,11 +395,8 @@ const app = Vue.createApp({
|
|||
sendMe() {
|
||||
this.ws.conn.send(JSON.stringify({
|
||||
action: "me",
|
||||
videoActive: this.webcam.active,
|
||||
videoMutual: this.webcam.mutual,
|
||||
videoMutualOpen: this.webcam.mutualOpen,
|
||||
video: this.myVideoFlag,
|
||||
status: this.status,
|
||||
nsfw: this.webcam.nsfw,
|
||||
}));
|
||||
},
|
||||
onMe(msg) {
|
||||
|
@ -391,11 +408,11 @@ const app = Vue.createApp({
|
|||
}
|
||||
|
||||
// The server can set our webcam NSFW flag.
|
||||
if (this.webcam.nsfw != msg.nsfw) {
|
||||
this.webcam.nsfw = msg.nsfw;
|
||||
let myNSFW = this.webcam.nsfw;
|
||||
let theirNSFW = (msg.video & this.VideoFlag.NSFW) > 0;
|
||||
if (myNSFW != theirNSFW) {
|
||||
this.webcam.nsfw = theirNSFW;
|
||||
}
|
||||
|
||||
// this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
||||
},
|
||||
|
||||
// WhoList updates.
|
||||
|
@ -412,14 +429,14 @@ const app = Vue.createApp({
|
|||
for (let row of this.whoList) {
|
||||
this.whoMap[row.username] = row;
|
||||
if (this.WebRTC.streams[row.username] != undefined &&
|
||||
row.videoActive !== true) {
|
||||
!(row.video & this.VideoFlag.Active)) {
|
||||
this.closeVideo(row.username, "offerer");
|
||||
}
|
||||
}
|
||||
|
||||
// Has the back-end server forgotten we are on video? This can
|
||||
// happen if we disconnect/reconnect while we were streaming.
|
||||
if (this.webcam.active && !this.whoMap[this.username]?.videoActive) {
|
||||
if (this.webcam.active && !(this.whoMap[this.username]?.video & this.VideoFlag.Active)) {
|
||||
this.sendMe();
|
||||
}
|
||||
},
|
||||
|
@ -473,18 +490,10 @@ const app = Vue.createApp({
|
|||
},
|
||||
onOpen(msg) {
|
||||
// Response for the opener to begin WebRTC connection.
|
||||
const secret = msg.openSecret;
|
||||
// console.log("OPEN: connect to %s with secret %s", msg.username, secret);
|
||||
// this.ChatClient(`onOpen called for ${msg.username}.`);
|
||||
|
||||
this.startWebRTC(msg.username, true);
|
||||
},
|
||||
onRing(msg) {
|
||||
// Message for the receiver to begin WebRTC connection.
|
||||
const secret = msg.openSecret;
|
||||
// console.log("RING: connection from %s with secret %s", msg.username, secret);
|
||||
this.ChatServer(`${msg.username} has opened your camera.`);
|
||||
|
||||
this.startWebRTC(msg.username, false);
|
||||
},
|
||||
onUserExited(msg) {
|
||||
|
@ -768,7 +777,7 @@ const app = Vue.createApp({
|
|||
|
||||
// If we are the offerer, and this member wants to auto-open our camera
|
||||
// then add our own stream to the connection.
|
||||
if (isOfferer && this.whoMap[username].videoMutualOpen && this.webcam.active) {
|
||||
if (isOfferer && (this.whoMap[username].video & this.VideoFlag.MutualOpen) && this.webcam.active) {
|
||||
let stream = this.webcam.stream;
|
||||
stream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, stream)
|
||||
|
@ -1113,7 +1122,7 @@ const app = Vue.createApp({
|
|||
|
||||
// Is the target user NSFW? Go thru the modal.
|
||||
let dontShowAgain = localStorage["skip-nsfw-modal"] == "true";
|
||||
if (user.nsfw && !dontShowAgain && !force) {
|
||||
if ((user.video & this.VideoFlag.NSFW) && !dontShowAgain && !force) {
|
||||
this.nsfwModalView.user = user;
|
||||
this.nsfwModalView.visible = true;
|
||||
return;
|
||||
|
@ -1130,7 +1139,7 @@ const app = Vue.createApp({
|
|||
}
|
||||
|
||||
// If this user requests mutual viewership...
|
||||
if (user.videoMutual && !this.webcam.active) {
|
||||
if ((user.video & this.VideoFlag.MutualRequired) && !this.webcam.active) {
|
||||
this.ChatClient(
|
||||
`<strong>${user.username}</strong> has requested that you should share your own camera too before opening theirs.`
|
||||
);
|
||||
|
@ -1155,6 +1164,8 @@ const app = Vue.createApp({
|
|||
if (name === "offerer") {
|
||||
// We are closing another user's video stream.
|
||||
delete (this.WebRTC.streams[username]);
|
||||
delete (this.WebRTC.muted[username]);
|
||||
delete (this.WebRTC.poppedOut[username]);
|
||||
if (this.WebRTC.pc[username] != undefined && this.WebRTC.pc[username].offerer != undefined) {
|
||||
this.WebRTC.pc[username].offerer.close();
|
||||
delete (this.WebRTC.pc[username]);
|
||||
|
@ -1183,6 +1194,8 @@ const app = Vue.createApp({
|
|||
this.WebRTC.pc[username].answerer.close();
|
||||
}
|
||||
delete (this.WebRTC.pc[username]);
|
||||
delete (this.WebRTC.muted[username]);
|
||||
delete (this.WebRTC.poppedOut[username]);
|
||||
}
|
||||
|
||||
// Inform backend we have closed it.
|
||||
|
@ -1193,16 +1206,33 @@ const app = Vue.createApp({
|
|||
// and then we turn ours off: we should unfollow the ones with mutual video.
|
||||
for (let row of this.whoList) {
|
||||
let username = row.username;
|
||||
if (row.videoMutual && this.WebRTC.pc[username] != undefined) {
|
||||
if ((row.video & this.VideoFlag.MutualRequired) && this.WebRTC.pc[username] != undefined) {
|
||||
this.closeVideo(username);
|
||||
}
|
||||
}
|
||||
},
|
||||
webcamIconClass(user) {
|
||||
// Return the icon to show on a video button.
|
||||
// - Usually a video icon
|
||||
// - May be a crossed-out video if isVideoNotAllowed
|
||||
// - Or an eyeball for cameras already opened
|
||||
if (user.username === this.username && this.webcam.active) {
|
||||
return 'fa-eye'; // user sees their own self camera always
|
||||
}
|
||||
|
||||
// Already opened?
|
||||
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.streams[user.username] != undefined) {
|
||||
return 'fa-eye';
|
||||
}
|
||||
|
||||
if (this.isVideoNotAllowed(user)) return 'fa-video-slash';
|
||||
return 'fa-video';
|
||||
},
|
||||
isVideoNotAllowed(user) {
|
||||
// Returns whether the video button to open a user's cam will be not allowed (crossed out)
|
||||
|
||||
// Mutual video sharing is required on this camera, and ours is not active
|
||||
if (user.videoActive && user.videoMutual && !this.webcam.active) {
|
||||
if ((user.video & this.VideoFlag.Active) && (user.video & this.VideoFlag.MutualRequired) && !this.webcam.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1263,6 +1293,7 @@ const app = Vue.createApp({
|
|||
this.webcam.elem.srcObject = null;
|
||||
this.webcam.stream = null;
|
||||
this.webcam.active = false;
|
||||
this.webcam.muted = false;
|
||||
this.whoTab = "online";
|
||||
|
||||
// Close all WebRTC sessions.
|
||||
|
@ -1283,6 +1314,16 @@ const app = Vue.createApp({
|
|||
this.webcam.stream.getAudioTracks().forEach(track => {
|
||||
track.enabled = !this.webcam.muted;
|
||||
});
|
||||
|
||||
// Communicate our local mute to others.
|
||||
this.sendMe();
|
||||
},
|
||||
isSourceMuted(username) {
|
||||
// See if the webcam broadcaster muted their mic at the source
|
||||
if (this.whoMap[username] != undefined && this.whoMap[username].video & this.VideoFlag.Muted) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isMuted(username) {
|
||||
return this.WebRTC.muted[username] === true;
|
||||
|
|
|
@ -544,12 +544,26 @@
|
|||
<a :href="'#'+c.channel"
|
||||
@click.prevent="setChannel(c.channel)"
|
||||
:class="{'is-active': c.channel == channel}">
|
||||
|
||||
<div class="columns is-mobile">
|
||||
<!-- Avatar URL if available (copied from Who List) -->
|
||||
<div class="column is-narrow pr-0" style="position: relative">
|
||||
<img v-if="avatarForUsername(normalizeUsername(c.channel))" :src="avatarForUsername(normalizeUsername(c.channel))"
|
||||
width="24" height="24" alt="">
|
||||
<img v-else src="/static/img/shy.png"
|
||||
width="24" height="24">
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
[[c.name]]
|
||||
|
||||
<span v-if="hasUnread(c.channel)"
|
||||
class="tag is-danger">
|
||||
[[hasUnread(c.channel)]]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -621,6 +635,8 @@
|
|||
</video>
|
||||
|
||||
<div class="caption">
|
||||
<i class="fa fa-microphone-slash mr-1 has-text-grey"
|
||||
v-if="webcam.muted"></i>
|
||||
[[username]]
|
||||
</div>
|
||||
|
||||
|
@ -659,6 +675,8 @@
|
|||
autoplay>
|
||||
</video>
|
||||
<div class="caption">
|
||||
<i class="fa fa-microphone-slash mr-1 has-text-grey"
|
||||
v-if="isSourceMuted(username)"></i>
|
||||
[[username]]
|
||||
</div>
|
||||
<div class="close">
|
||||
|
@ -1022,19 +1040,19 @@
|
|||
|
||||
<!-- Video button -->
|
||||
<button type="button" class="button is-small px-2 py-1"
|
||||
:disabled="!u.videoActive"
|
||||
:disabled="!(u.video & VideoFlag.Active)"
|
||||
:class="{
|
||||
'is-danger is-outlined': u.videoActive && u.nsfw,
|
||||
'is-info is-outlined': u.videoActive && !u.nsfw,
|
||||
'is-danger is-outlined': (u.video & VideoFlag.Active) && (u.video & VideoFlag.NSFW),
|
||||
'is-info is-outlined': (u.video & VideoFlag.Active) && !(u.video & VideoFlag.NSFW),
|
||||
'cursor-notallowed': isVideoNotAllowed(u),
|
||||
}"
|
||||
:title="`Open video stream` +
|
||||
(u.videoActive && u.videoMutual ? '; mutual video sharing required' : '') +
|
||||
(u.videoActive && u.videoMutualOpen ? '; will auto-open your video' : '')"
|
||||
(u.video & VideoFlag.MutualRequired ? '; mutual video sharing required' : '') +
|
||||
(u.video & VideoFlag.MutualOpen ? '; will auto-open your video' : '')"
|
||||
|
||||
@click="openVideo(u)">
|
||||
<i class="fa"
|
||||
:class="isVideoNotAllowed(u) ? 'fa-video-slash' : 'fa-video'"></i>
|
||||
:class="webcamIconClass(u)"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user