Status Message overhaul

master
Noah 2023-12-30 14:50:52 -08:00
parent ebf5b3f47e
commit 27380ec558
4 changed files with 322 additions and 58 deletions

View File

@ -122,6 +122,14 @@ img {
bottom: 4px;
}
/* User status indicator in the lower left corner of DMs */
.user-status-dm-field {
position: absolute;
z-index: 38; /* below auto-scroll checkbox */
left: 12px;
bottom: 4px;
}
/* Footer row: message entry box */
.chat-container > .chat-footer {
grid-column: 1 / 4;

View File

@ -16,6 +16,7 @@ import ProfileModal from './components/ProfileModal.vue';
import ChatClient from './lib/ChatClient';
import LocalStorage from './lib/LocalStorage';
import VideoFlag from './lib/VideoFlag';
import StatusMessage from './lib/StatusMessage';
import { SoundEffects, DefaultSounds } from './lib/sounds';
import { isAppleWebkit } from './lib/browsers';
@ -121,6 +122,7 @@ export default {
messageBox: null, // HTML element for message entry box
typingNotifDebounce: null,
status: "online", // away/idle status
StatusMessage: StatusMessage,
// Emoji picker visible for messages
showEmojiPicker: false,
@ -317,6 +319,15 @@ export default {
this.setupIdleDetection();
this.setupDropZone(); // file upload drag/drop
// Configure the StatusMessage controller.
StatusMessage.nsfw = this.config.permitNSFW;
StatusMessage.currentStatus = () => {
return this.status;
};
StatusMessage.isAdmin = () => {
return this.isOp;
};
this.webcam.elem = document.querySelector("#localVideo");
this.historyScrollbox = document.querySelector("#chatHistory");
@ -545,6 +556,27 @@ export default {
// Is the current channel a DM?
return this.channel.indexOf("@") === 0;
},
chatPartnerStatusMessage() {
// In a DM thread, returns your chat partner's status message.
if (!this.isDM) {
return null;
}
let username = this.normalizeUsername(this.channel),
user = this.whoMap[username];
if (user == undefined || this.isUserOffline(username)) {
return this.StatusMessage.offline();
}
return this.StatusMessage.getStatus(user.status);
},
isChatPartnerAway() {
// In a DM thread, returns if your chat partner's status is anything
// other than "online".
if (!this.isDM) return false;
let status = this.chatPartnerStatusMessage;
return status === null || status.name !== "online";
},
canUploadFile() {
// Public channels: OK
if (!this.channel.indexOf('@') === 0) {
@ -3948,6 +3980,15 @@ export default {
'p-1 pb-5': messageStyle.indexOf('compact') === 0
}">
<!-- Show your chat partner's status message in DMs -->
<div class="user-status-dm-field tag is-info" v-if="isChatPartnerAway">
<strong class="mr-2 has-text-light">Status:</strong>
<span v-if="chatPartnerStatusMessage">
{{ chatPartnerStatusMessage.emoji }} {{ chatPartnerStatusMessage.label }}
</span>
<em v-else>undefined</em>
</div>
<div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
<input type="checkbox" v-model="autoscroll" :value="true">
@ -4016,8 +4057,7 @@ export default {
<div v-if="isDM && isMutedUser(channel)" class="has-text-danger">
<i class="fa fa-comment-slash"></i>
<strong>{{ channel }}</strong> is currently <strong>muted</strong> so you have not been seeing their
recent
chat messages or DMs.
recent chat messages or DMs.
<a href="#" v-on:click.prevent="muteUser(channel)">Unmute them?</a>
</div>
@ -4121,25 +4161,15 @@ export default {
<div class="column">
<div class="select is-small is-fullwidth">
<select v-model="status">
<optgroup label="Status">
<option value="online"> Active</option>
<option value="away">🕒 Away</option>
<option value="brb"> Be right back</option>
<option value="lunch">🍴 Out to lunch</option>
<option value="call">📞 On the phone</option>
<option value="busy">💼 Working</option>
<option value="book">📖 Studying</option>
<option value="gaming">🎮 Gaming</option>
<option value="idle" v-show="status === 'idle'">🕒 Idle</option>
<option value="hidden" v-if="jwt.claims != undefined && jwt.claims.op">🕵 Hidden
<optgroup v-for="group in StatusMessage.iterSelectOptGroups()"
v-bind:key="group.category"
:label="group.category">
<option v-for="item in StatusMessage.iterSelectOptions(group.category)"
v-bind:key="item.name"
:value="item.name">
{{ item.emoji }} {{ item.label }}
</option>
</optgroup>
<optgroup label="Mood">
<option value="chatty">🗨 Chatty and sociable</option>
<option value="introverted">🥄 Introverted and quiet</option>
<option value="horny" v-if="config.permitNSFW">🔥 Horny</option>
<option value="exhibitionist" v-if="config.permitNSFW">👀 Watch me</option>
</optgroup>
</select>
</div>
</div>
@ -4185,23 +4215,48 @@ export default {
<!-- Who Is Online -->
<ul class="menu-list" v-if="whoTab === 'online'">
<li v-for="(u, i) in sortedWhoList" v-bind:key="i">
<WhoListRow :user="u" :username="username" :website-url="config.website"
:is-dnd="isUsernameDND(u.username)" :is-muted="isMutedUser(u.username)"
:is-booted="isBooted(u.username)" :is-op="isOp" :is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)" :vip-config="config.VIP" @send-dm="openDMs"
@mute-user="muteUser" @open-video="openVideo" @open-profile="showProfileModal"></WhoListRow>
<WhoListRow
:user="u"
:username="username"
:website-url="config.website"
:is-dnd="isUsernameDND(u.username)"
:is-muted="isMutedUser(u.username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)"
:vip-config="config.VIP"
:status-message="StatusMessage"
@send-dm="openDMs"
@mute-user="muteUser"
@open-video="openVideo"
@open-profile="showProfileModal">
</WhoListRow>
</li>
</ul>
<!-- Watching My Webcam -->
<ul class="menu-list" v-if="whoTab === 'watching'">
<li v-for="(u, i) in sortedWatchingList" v-bind:key="username">
<WhoListRow :is-watching-tab="true" :user="u" :username="username" :website-url="config.website"
:is-dnd="isUsernameDND(username)" :is-muted="isMutedUser(username)"
:is-booted="isBooted(u.username)" :is-op="isOp" :is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)" :vip-config="config.VIP" @send-dm="openDMs"
@mute-user="muteUser" @open-video="openVideo" @boot-user="bootUser"
@open-profile="showProfileModal"></WhoListRow>
<WhoListRow
:is-watching-tab="true"
:user="u"
:username="username"
:website-url="config.website"
:is-dnd="isUsernameDND(username)"
:is-muted="isMutedUser(username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"
:video-icon-class="webcamIconClass(u)"
:vip-config="config.VIP"
:status-message="StatusMessage"
@send-dm="openDMs"
@mute-user="muteUser"
@open-video="openVideo"
@boot-user="bootUser"
@open-profile="showProfileModal">
</WhoListRow>
</li>
</ul>

View File

@ -14,6 +14,7 @@ export default {
isVideoNotAllowed: Boolean, // whether opening this camera is not allowed
videoIconClass: String, // CSS class for the open video icon
isWatchingTab: Boolean, // is the "Watching" tab (replace video button w/ boot)
statusMessage: Object, // StatusMessage controller
},
data() {
return {
@ -101,6 +102,19 @@ export default {
hasReactions() {
return this.reactions != undefined && Object.keys(this.reactions).length > 0;
},
// Status icons
hasStatusIcon() {
return this.user.status !== 'online' && this.statusMessage != undefined;
},
statusIconClass() {
let status = this.statusMessage.getStatus(this.user.status);
return status.icon;
},
statusLabel() {
let status = this.statusMessage.getStatus(this.user.status);
return `${status.emoji} ${status.label}`;
},
},
methods: {
openProfile() {
@ -157,34 +171,9 @@ export default {
<img v-else src="/static/img/shy.png" width="24" height="24">
<!-- Away symbol -->
<div v-if="user.status !== 'online'" class="status-away-icon">
<i v-if="user.status === 'away'" class="fa fa-clock has-text-light"
title="Status: Away"></i>
<i v-else-if="user.status === 'lunch'" class="fa fa-utensils has-text-light"
title="Status: Out to lunch"></i>
<i v-else-if="user.status === 'call'" class="fa fa-phone-volume has-text-light"
title="Status: On the phone"></i>
<i v-else-if="user.status === 'brb'" class="fa fa-stopwatch-20 has-text-light"
title="Status: Be right back"></i>
<i v-else-if="user.status === 'busy'" class="fa fa-briefcase has-text-light"
title="Status: Working"></i>
<i v-else-if="user.status === 'book'" class="fa fa-book has-text-light"
title="Status: Studying"></i>
<i v-else-if="user.status === 'gaming'"
class="fa fa-gamepad who-status-wide-icon-2 has-text-light"
title="Status: Gaming"></i>
<i v-else-if="user.status === 'idle'" class="fa-regular fa-moon has-text-light"
title="Status: Idle"></i>
<i v-else-if="user.status === 'horny'" class="fa fa-fire has-text-light"
title="Status: Horny"></i>
<i v-else-if="user.status === 'chatty'" class="fa fa-comment has-text-light"
title="Status: Chatty and sociable"></i>
<i v-else-if="user.status === 'introverted'" class="fa fa-spoon has-text-light"
title="Status: Introverted and quiet"></i>
<i v-else-if="user.status === 'exhibitionist'"
class="fa-regular fa-eye who-status-wide-icon-1 has-text-light"
title="Status: Watch me"></i>
<i v-else class="fa fa-clock has-text-light" :title="'Status: ' + user.status"></i>
<div v-if="hasStatusIcon" class="status-away-icon">
<i :class="statusIconClass" class="has-text-light"
:title="'Status: ' + statusLabel"></i>
</div>
</a>
</div>

212
src/lib/StatusMessage.js Normal file
View File

@ -0,0 +1,212 @@
// Available status options.
const StatusOptions = [
{
category: "Status",
options: [
{
name: "online",
label: "Active",
emoji: "☀️",
icon: "fa fa-clock"
},
{
name: "away",
label: "Away",
emoji: "🕒",
icon: "fa fa-clock"
},
{
name: "brb",
label: "Be right back",
emoji: "⏰",
icon: "fa fa-stopwatch-20"
},
{
name: "afk",
label: "Away from keyboard",
emoji: "⌨️",
icon: "fa fa-keyboard who-status-wide-icon-1"
},
{
name: "lunch",
label: "Out to lunch",
emoji: "🍴",
icon: "fa fa-utensils"
},
{
name: "call",
label: "On the phone",
emoji: "📞",
icon: "fa fa-phone-volume"
},
{
name: "busy",
label: "Working",
emoji: "💼",
icon: "fa fa-briefcase"
},
{
name: "book",
label: "Studying",
emoji: "📖",
icon: "fa fa-book"
},
{
name: "gaming",
label: "Gaming",
emoji: "🎮",
icon: "fa fa-gamepad who-status-wide-icon-2"
},
{
name: "movie",
label: "Watching a movie",
emoji: "🎞️",
icon: "fa fa-film"
},
{
name: "travel",
label: "Traveling",
emoji: "✈️",
icon: "fa fa-plane"
},
// Hidden/special statuses
{
name: "idle",
label: "Idle",
emoji: "🕒",
icon: "fa-regular fa-moon",
hidden: true
},
{
name: "hidden",
label: "Hidden",
emoji: "🕵️",
icon: "",
adminOnly: true
},
],
},
{
category: "Mood",
options: [
{
name: "chatty",
label: "Chatty and sociable",
emoji: "🗨️",
icon: "fa fa-comment"
},
{
name: "introverted",
label: "Introverted and quiet",
emoji: "🥄",
icon: "fa fa-spoon"
},
// If NSFW enabled
{
name: "horny",
label: "Horny",
emoji: "🔥",
icon: "fa fa-fire",
nsfw: true,
},
{
name: "exhibitionist",
label: "Watch me",
emoji: "👀",
icon: "fa-regular fa-eye who-status-wide-icon-1",
nsfw: true,
}
]
}
];
// Flatten the set of all status options.
const StatusFlattened = (function() {
let result = [];
for (let category of StatusOptions) {
for (let option of category.options) {
result.push(option);
}
}
return result;
})();
// Hash map lookup of status by name.
const StatusByName = (function() {
let result = {};
for (let item of StatusFlattened) {
result[item.name] = item;
}
return result;
})();
// An API surface layer of functions.
class StatusMessageController {
// The caller configures:
// - nsfw (bool): the BareRTC PermitNSFW setting, which controls some status options.
// - isAdmin (func): return a boolean if the current user is operator.
// - currentStatus (func): return the name of the user's current status.
constructor() {
this.nsfw = false;
this.isAdmin = function() { return false };
this.currentStatus = function() { return StatusFlattened[0] };
}
// Iterate the category <optgroup> for the Status dropdown menu.
iterSelectOptGroups() {
return StatusOptions;
}
// Iterate the <option> for a category of statuses.
iterSelectOptions(category) {
let current = this.currentStatus(),
isAdmin = this.isAdmin();
for (let group of StatusOptions) {
if (group.category === category) {
// Return the filtered options.
let result = group.options.filter(option => {
if ((option.hidden && current !== option.name) ||
(option.adminOnly && !isAdmin) ||
(option.nsfw && !this.nsfw)) {
return false;
}
return true;
});
return result;
}
}
return [];
}
// Get details on a status message.
getStatus(name) {
if (StatusByName[name] != undefined) {
return StatusByName[name];
}
// Return a dummy status object.
return {
name: name,
label: name,
icon: "fa fa-clock",
emoji: "🕒"
};
}
// Offline status.
offline() {
return {
name: "offline",
label: "Offline",
icon: "fa fa-house-circle-xmark",
emoji: "🌜",
}
}
}
const StatusMessage = new StatusMessageController();
export default StatusMessage;