Chat Setting Menu + Various Tweaks

* In place of the Help and Settings buttons, add a hamburger menu
  dropdown and place the links under there.
* Also in the dropdown is Close All Cameras and Mute All Cameras (if you
  have any cams open; the links are hidden if not)
* Also in the dropdown add a Logout button that just links to a new
  /logout route in order to unload the page and align with some users'
  expectations (not knowing closing out of the chat page was enough to
  log out of the room before)
* Bring back "(offline)" indicators when a user is no longer in the
  room.
This commit is contained in:
Noah 2023-09-08 18:46:36 -07:00
parent 52dd53240e
commit cbfbcd768f
5 changed files with 132 additions and 15 deletions

View File

@ -125,3 +125,16 @@ func AboutPage() http.HandlerFunc {
tmpl.ExecuteTemplate(w, "index", values) tmpl.ExecuteTemplate(w, "index", values)
}) })
} }
// LogoutPage returns the HTML template for the logout page.
func LogoutPage() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Load the template, TODO: once on server startup.
tmpl := template.New("index")
tmpl, err := tmpl.ParseFiles("web/templates/logout.html")
if err != nil {
panic(err.Error())
}
tmpl.ExecuteTemplate(w, "index", nil)
})
}

View File

@ -32,6 +32,7 @@ func (s *Server) Setup() error {
mux.Handle("/", IndexPage()) mux.Handle("/", IndexPage())
mux.Handle("/about", AboutPage()) mux.Handle("/about", AboutPage())
mux.Handle("/logout", LogoutPage())
mux.Handle("/ws", s.WebSocket()) mux.Handle("/ws", s.WebSocket())
mux.Handle("/api/statistics", s.Statistics()) mux.Handle("/api/statistics", s.Statistics())
mux.Handle("/api/blocklist", s.BlockList()) mux.Handle("/api/blocklist", s.BlockList())

View File

@ -116,6 +116,7 @@ export default {
whoTab: 'online', whoTab: 'online',
whoSort: 'a-z', whoSort: 'a-z',
whoMap: {}, // map username to wholist entry whoMap: {}, // map username to wholist entry
whoOnline: {}, // map users actually online right now
muted: {}, // muted usernames for client side state muted: {}, // muted usernames for client side state
// Misc. user preferences (TODO: move all of them here) // Misc. user preferences (TODO: move all of them here)
@ -512,6 +513,10 @@ export default {
if (this.webcam.vipOnly && this.isVIP) status |= this.VideoFlag.VipOnly; if (this.webcam.vipOnly && this.isVIP) status |= this.VideoFlag.VipOnly;
return status; return status;
}, },
numVideosOpen() {
// Return the count of other peoples videos we have open.
return Object.keys(this.WebRTC.streams).length;
},
sortedWhoList() { sortedWhoList() {
let result = [...this.whoList]; let result = [...this.whoList];
@ -871,6 +876,7 @@ export default {
// WhoList updates. // WhoList updates.
onWho(msg) { onWho(msg) {
this.whoList = msg.whoList; this.whoList = msg.whoList;
this.whoOnline = {};
if (this.whoList == undefined) { if (this.whoList == undefined) {
this.whoList = []; this.whoList = [];
@ -880,6 +886,7 @@ export default {
// off camera, close our side of the connection. // off camera, close our side of the connection.
for (let row of this.whoList) { for (let row of this.whoList) {
this.whoMap[row.username] = row; this.whoMap[row.username] = row;
this.whoOnline[row.username] = true;
if (this.WebRTC.streams[row.username] != undefined && if (this.WebRTC.streams[row.username] != undefined &&
!(row.video & this.VideoFlag.Active)) { !(row.video & this.VideoFlag.Active)) {
this.closeVideo(row.username, "offerer"); this.closeVideo(row.username, "offerer");
@ -1510,9 +1517,6 @@ export default {
} }
return ""; return "";
}, },
isUsernameOnline(username) {
return this.whoMap[username] != undefined;
},
getUser(username) { getUser(username) {
// Return the full User object from the Who List, or a dummy placeholder if not online. // Return the full User object from the Who List, or a dummy placeholder if not online.
if (this.whoMap[username] != undefined) { if (this.whoMap[username] != undefined) {
@ -1523,6 +1527,10 @@ export default {
username: username, username: username,
}; };
}, },
isUserOffline(username) {
// Return if the username is not presently online in the chat.
return this.whoOnline[username] !== true;
},
avatarForUsername(username) { avatarForUsername(username) {
if (this.whoMap[username] != undefined && this.whoMap[username].avatar) { if (this.whoMap[username] != undefined && this.whoMap[username].avatar) {
return this.avatarURL(this.whoMap[username]); return this.avatarURL(this.whoMap[username]);
@ -1999,6 +2007,32 @@ export default {
// Inform backend we have closed it. // Inform backend we have closed it.
this.sendWatch(username, false); this.sendWatch(username, false);
}, },
closeOpenVideos() {
// Close all videos open of other users.
for (let username of Object.keys(this.WebRTC.streams)) {
this.closeVideo(username, "offerer");
}
},
muteAllVideos() {
// Mute the mic of all open videos.
let count = 0;
for (let username of Object.keys(this.WebRTC.streams)) {
if (this.WebRTC.muted[username]) continue;
// Find the <video> tag to mute it.
this.WebRTC.muted[username] = true;
let $ref = document.getElementById(`videofeed-${username}`);
if ($ref) {
$ref.muted = this.WebRTC.muted[username];
}
count++;
}
if (count > 0) {
this.ChatClient(`You have muted the audio on ${count} video${count === 1 ? '' : 's'}.`);
}
},
unMutualVideo() { unMutualVideo() {
// If we had our camera on to watch a video of someone who wants mutual cameras, // If we had our camera on to watch a video of someone who wants mutual cameras,
// and then we turn ours off: we should unfollow the ones with mutual video. // and then we turn ours off: we should unfollow the ones with mutual video.
@ -3218,15 +3252,46 @@ export default {
<i class="fa fa-fire mr-1" :class="{ 'has-text-danger': !webcam.nsfw }"></i> Explicit <i class="fa fa-fire mr-1" :class="{ 'has-text-danger': !webcam.nsfw }"></i> Explicit
</button> </button>
</div> </div>
<div class="column is-narrow pl-1"> <div class="column dropdown is-right is-narrow pl-1"
<a href="/about" target="_blank" class="button is-small is-link px-2"> onclick="this.classList.toggle('is-active')">
<i class="fa fa-info-circle"></i> <div class="dropdown-trigger">
</a> <button type="button" class="button is-small is-link px-2"
<button type="button" class="button is-small is-light ml-1 px-2" @click="showSettings()" aria-haspopup="true"
title="Chat Settings"> aria-controls="chat-settings-menu">
<i class="fa fa-gear"></i> <span>
<i class="fa fa-bars"></i>
</span>
</button> </button>
</div> </div>
<div class="dropdown-menu mr-3" id="chat-settings-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" @click.prevent="showSettings()">
<i class="fa fa-gear mr-1"></i> Chat Settings
</a>
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0"
@click.prevent="closeOpenVideos()">
<i class="fa fa-video-slash mr-1"></i> Close all cameras
</a>
<a href="#" class="dropdown-item" v-if="numVideosOpen > 0"
@click.prevent="muteAllVideos()">
<i class="fa fa-microphone-slash mr-1"></i> Mute all cameras
</a>
<hr class="dropdown-divider">
<a href="/about" target="_blank" class="dropdown-item">
<i class="fa fa-info-circle mr-1"></i> About
</a>
<a href="/logout" class="dropdown-item">
<i class="fa fa-arrow-right-from-bracket mr-1"></i> Log out
</a>
</div>
</div>
</div>
</div> </div>
</header> </header>
@ -3279,9 +3344,12 @@ export default {
</div> </div>
<div class="column"> <div class="column">
<del v-if="isUserOffline(c.name)">
{{ c.name }} {{ c.name }}
</del>
<span v-else>{{ c.name }}</span>
<span v-if="hasUnread(c.channel)" class="tag is-danger"> <span v-if="hasUnread(c.channel)" class="tag is-danger ml-1">
{{ hasUnread(c.channel) }} {{ hasUnread(c.channel) }}
</span> </span>
</div> </div>
@ -3486,8 +3554,8 @@ export default {
</div> </div>
<div class="column"> <div class="column">
<strong>{{ nicknameForUsername(msg.username) }}</strong> <strong>{{ nicknameForUsername(msg.username) }}</strong>
<small v-if="isUsernameOnline(msg.username)" <span v-if="isUserOffline(msg.username)" class="ml-1">(offline)</span>
class="ml-1">(@{{ msg.username }})</small> <small v-else class="ml-1">(@{{ msg.username }})</small>
{{ msg.message }} {{ msg.message }}
</div> </div>
</div> </div>
@ -3500,6 +3568,7 @@ export default {
:message="msg" :message="msg"
:position="i" :position="i"
:user="getUser(msg.username)" :user="getUser(msg.username)"
:is-offline="isUserOffline(msg.username)"
:username="username" :username="username"
:website-url="config.website" :website-url="config.website"
:is-dnd="isUsernameDND(msg.username)" :is-dnd="isUsernameDND(msg.username)"

View File

@ -3,6 +3,7 @@ export default {
props: { props: {
message: Object, // chat Message object message: Object, // chat Message object
user: Object, // User object of the Message author user: Object, // User object of the Message author
isOffline: Boolean, // user is not currently online
username: String, // current username logged in username: String, // current username logged in
websiteUrl: String, // Base URL to website (for profile/avatar URLs) websiteUrl: String, // Base URL to website (for profile/avatar URLs)
isDnd: Boolean, // user is not accepting DMs isDnd: Boolean, // user is not accepting DMs
@ -151,6 +152,9 @@ export default {
<!-- User nickname/display name --> <!-- User nickname/display name -->
{{ nickname }} {{ nickname }}
<!-- Offline now? -->
<span v-if="isOffline">(offline)</span>
</strong> </strong>
</div> </div>
<div class="column has-text-right pb-0"> <div class="column has-text-right pb-0">

30
web/templates/logout.html Normal file
View File

@ -0,0 +1,30 @@
{{define "index"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/bulma-prefers-dark.css">
<title>Logged Out</title>
</head>
<body>
<div class="container is-fullhd">
<div class="content my-5">
<h1>Logged Out</h1>
<p>
You are now logged out of the chat room. You may now close this page.
</p>
</div>
<script>
setTimeout(() => {
window.close();
}, 5000);
</script>
</body>
</html>
{{end}}