Status Message overhaul
This commit is contained in:
parent
ebf5b3f47e
commit
27380ec558
|
@ -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;
|
||||
|
|
115
src/App.vue
115
src/App.vue
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
212
src/lib/StatusMessage.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user