Compact-style message display options

This commit is contained in:
Noah 2023-09-30 14:53:43 -07:00
parent 15b291826e
commit b5d0885c23
3 changed files with 208 additions and 16 deletions

View File

@ -80,6 +80,11 @@ export default {
["x3", "3x larger chat room text"], ["x3", "3x larger chat room text"],
["x4", "4x larger chat room text"], ["x4", "4x larger chat room text"],
], ],
messageStyleSettings: [
["cards", "Card style (default)"],
["compact", "Compact style (with display names)"],
["compact2", "Compact style (usernames only)"],
],
imageDisplaySettings: [ imageDisplaySettings: [
["show", "Always show images in chat"], ["show", "Always show images in chat"],
["collapse", "Collapse images in chat, clicking to expand (default)"], ["collapse", "Collapse images in chat, clicking to expand (default)"],
@ -229,6 +234,7 @@ export default {
historyScrollbox: null, historyScrollbox: null,
autoscroll: true, // scroll to bottom on new messages autoscroll: true, // scroll to bottom on new messages
fontSizeClass: "", // font size magnification fontSizeClass: "", // font size magnification
messageStyle: "cards", // message display style
imageDisplaySetting: "collapse", // image show/hide setting imageDisplaySetting: "collapse", // image show/hide setting
scrollback: 1000, // scrollback buffer (messages to keep per channel) scrollback: 1000, // scrollback buffer (messages to keep per channel)
DMs: {}, DMs: {},
@ -361,6 +367,9 @@ export default {
// Store the setting persistently. // Store the setting persistently.
LocalStorage.set('fontSizeClass', this.fontSizeClass); LocalStorage.set('fontSizeClass', this.fontSizeClass);
}, },
messageStyle() {
LocalStorage.set('messageStyle', this.messageStyle);
},
imageDisplaySetting() { imageDisplaySetting() {
LocalStorage.set('imageDisplaySetting', this.imageDisplaySetting); LocalStorage.set('imageDisplaySetting', this.imageDisplaySetting);
}, },
@ -675,6 +684,10 @@ export default {
this.fontSizeClass = settings.fontSizeClass; this.fontSizeClass = settings.fontSizeClass;
} }
if (settings.messageStyle != undefined) {
this.messageStyle = settings.messageStyle;
}
if (settings.videoScale != undefined) { if (settings.videoScale != undefined) {
this.webcam.videoScale = settings.videoScale; this.webcam.videoScale = settings.videoScale;
} }
@ -2896,6 +2909,25 @@ export default {
</div> </div>
</div> </div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Text style</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-fullwidth">
<select v-model="messageStyle">
<option v-for="s in config.messageStyleSettings" v-bind:key="s[0]" :value="s[0]">
{{ s[1] }}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label is-normal"> <div class="field-label is-normal">
<label class="label">Images</label> <label class="label">Images</label>
@ -3599,7 +3631,9 @@ export default {
</div> --> </div> -->
</div> </div>
<div class="card-content" id="chatHistory" :class="{ 'has-background-dm': isDM }"> <div class="card-content" id="chatHistory"
:class="{ 'has-background-dm': isDM,
'p-1': messageStyle.indexOf('compact') === 0 }">
<div class="autoscroll-field tag"> <div class="autoscroll-field tag">
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in."> <label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
@ -3643,6 +3677,11 @@ export default {
</figure> </figure>
</div> </div>
<div class="column"> <div class="column">
<!-- Timestamp on the right -->
<span class="float-right is-size-7" :title="msg.at">
{{ prettyDate(msg.at) }}
</span>
<strong>{{ nicknameForUsername(msg.username) }}</strong> <strong>{{ nicknameForUsername(msg.username) }}</strong>
<span v-if="isUserOffline(msg.username)" class="ml-1">(offline)</span> <span v-if="isUserOffline(msg.username)" class="ml-1">(offline)</span>
<small v-else class="ml-1">(@{{ msg.username }})</small> <small v-else class="ml-1">(@{{ msg.username }})</small>
@ -3656,6 +3695,7 @@ export default {
<MessageBox <MessageBox
v-else v-else
:message="msg" :message="msg"
:appearance="messageStyle"
:position="i" :position="i"
:user="getUser(msg.username)" :user="getUser(msg.username)"
:is-offline="isUserOffline(msg.username)" :is-offline="isUserOffline(msg.username)"

View File

@ -5,6 +5,7 @@ import 'vue3-emoji-picker/css';
export default { export default {
props: { props: {
message: Object, // chat Message object message: Object, // chat Message object
appearance: String, // message style appearance (cards, compact, etc.)
user: Object, // User object of the Message author user: Object, // User object of the Message author
isOffline: Boolean, // user is not currently online isOffline: Boolean, // user is not currently online
username: String, // current username logged in username: String, // current username logged in
@ -26,6 +27,9 @@ export default {
// Emoji picker visible // Emoji picker visible
showEmojiPicker: false, showEmojiPicker: false,
// Message menu (compact displays)
menuVisible: false,
// Favorite emojis // Favorite emojis
customEmojiGroups: { customEmojiGroups: {
frequently_used: [ frequently_used: [
@ -72,6 +76,11 @@ export default {
hasReactions() { hasReactions() {
return this.reactions != undefined && Object.keys(this.reactions).length > 0; return this.reactions != undefined && Object.keys(this.reactions).length > 0;
}, },
// Compactify a message (remove paragraph breaks added by Markdown renderer)
compactMessage() {
return this.message.message.replace(/<\/p>\s*<p>/g, "<br><br>").replace(/<\/?p>/g, "");
}
}, },
methods: { methods: {
openProfile() { openProfile() {
@ -154,12 +163,20 @@ export default {
let hour = hours % 12 || 12; let hour = hours % 12 || 12;
return `${(hour)}:${minutes} ${ampm}`; return `${(hour)}:${minutes} ${ampm}`;
}, },
prettyDateCompact(date) {
if (date == undefined) return '';
let hour = date.getHours(),
minutes = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minutes}`;
},
} }
} }
</script> </script>
<template> <template>
<div class="box mb-2 px-4 pt-3 pb-1 position-relative"> <!-- Card Style (default) -->
<div v-if="appearance === 'cards' || !appearance" class="box mb-2 px-4 pt-3 pb-1 position-relative">
<div class="media mb-0"> <div class="media mb-0">
<div class="media-left"> <div class="media-left">
<a :href="profileURL" <a :href="profileURL"
@ -314,6 +331,7 @@ export default {
<!-- Reactions so far? --> <!-- Reactions so far? -->
<div v-if="hasReactions" class="mt-1"> <div v-if="hasReactions" class="mt-1">
<span v-for="(users, emoji) in reactions" <span v-for="(users, emoji) in reactions"
v-bind:key="emoji"
class="tag is-secondary mr-1 cursor-pointer" class="tag is-secondary mr-1 cursor-pointer"
:class="{ 'is-success is-light': iReacted(msg, emoji), 'is-secondary': !iReacted(msg, emoji) }" :class="{ 'is-success is-light': iReacted(msg, emoji), 'is-secondary': !iReacted(msg, emoji) }"
:title="emoji + ' by: ' + users.join(', ')" @click="sendReact(emoji)"> :title="emoji + ' by: ' + users.join(', ')" @click="sendReact(emoji)">
@ -323,6 +341,148 @@ export default {
</div> </div>
</div> </div>
<!-- Compact styles (with or without usernames) -->
<div v-else-if="appearance.indexOf('compact') === 0" class="columns is-mobile">
<!-- Timestamp -->
<div class="column is-narrow pr-0">
<small class="has-text-grey is-size-7" :title="message.at">{{ prettyDateCompact(message.at) }}</small>
</div>
<!-- Avatar icon -->
<div class="column is-narrow px-1">
<a :href="profileURL"
@click.prevent="openProfile()"
:class="{ 'cursor-default': !profileURL }" class="p-0">
<img v-if="avatarURL" :src="avatarURL" width="16" height="16" alt="">
<img v-else src="/static/img/shy.png" width="16" height="16">
</a>
</div>
<!-- Name/username/message -->
<div class="column px-1">
[<a :href="profileURL"
@click.prevent="openProfile()"
:class="{ 'cursor-default': !profileURL }">
<!-- Display name? -->
<strong v-if="(message.isChatServer || message.isChatClient || message.isAdmin)
|| (appearance === 'compact' && nickname !== message.username)"
class="has-text-dark"
:class="{
'has-text-success is-dark': message.isChatServer,
'has-text-warning is-dark': message.isAdmin,
'has-text-danger': message.isChatClient
}">
{{ nickname }}
</strong>
<small class="has-text-grey"
:class="{'ml-1': appearance === 'compact' && nickname !== message.username}"
v-if="!(message.isChatServer || message.isChatClient || message.isAdmin)"
>@{{ message.username }}</small>
</a>]
<span v-html="compactMessage"></span>
<!-- Reactions so far? -->
<div v-if="hasReactions" class="mt-1">
<span v-for="(users, emoji) in reactions"
v-bind:key="emoji"
class="tag is-secondary mr-1 cursor-pointer"
:class="{ 'is-success is-light': iReacted(msg, emoji), 'is-secondary': !iReacted(msg, emoji) }"
:title="emoji + ' by: ' + users.join(', ')" @click="sendReact(emoji)">
{{ emoji }} <small class="ml-1">{{ users.length }}</small>
</span>
</div>
</div>
<!-- Emoji/Menu button -->
<div v-if="message.msgID && !noButtons" class="column is-narrow pl-1">
<div class="columns is-mobile is-gapless mb-0">
<!-- More buttons menu (DM, mute, report, etc.) -->
<div class="column dropdown is-right" :class="{ 'is-up': position >= 2, 'is-active': menuVisible }"
@click="menuVisible=!menuVisible">
<div class="dropdown-trigger">
<button type="button" class="button is-small px-2 mr-1" aria-haspopup="true"
:aria-controls="`msg-menu-${message.msgID}`">
<small>
<i class="fa fa-ellipsis-vertical"></i>
</small>
</button>
</div>
<div class="dropdown-menu" :id="`msg-menu-${message.msgID}`" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" v-if="message.username !== username"
@click.prevent="openDMs()">
<i class="fa fa-comment mr-1"></i> Direct Message
</a>
<a href="#" class="dropdown-item" v-if="!(message.username === username)"
@click.prevent="muteUser()">
<i class="fa fa-comment-slash mr-1" :class="{
'has-text-success': isMuted,
'has-text-danger': !isMuted
}"></i>
<span v-if="isMuted">Unmute user</span>
<span v-else>Mute user</span>
</a>
<a href="#" class="dropdown-item" v-if="message.username === username || isOp"
@click.prevent="takeback()">
<i class="fa fa-rotate-left has-text-danger mr-1"></i>
Take back
</a>
<a href="#" class="dropdown-item" v-if="message.username !== username"
@click.prevent="removeMessage()">
<i class="fa fa-trash mr-1"></i>
Hide message
</a>
<!-- Report button -->
<a href="#" class="dropdown-item" v-if="reportEnabled && message.username !== username"
@click.prevent="reportMessage()">
<i class="fa fa-flag mr-1"
:class="{'has-text-danger': !message.reported}"></i>
<span v-if="message.reported">Reported</span>
<span v-else>Report</span>
</a>
</div>
</div>
</div>
<!-- Emoji reactions -->
<div class="column dropdown is-right" :class="{ 'is-up': position >= 2, 'is-active': showEmojiPicker }"
@click="showEmojiPicker=true">
<div class="dropdown-trigger">
<button type="button" class="button is-small px-2" aria-haspopup="true"
:aria-controls="`react-menu-${message.msgID}`"
@click="hideEmojiPicker()">
<small>
<i class="fa fa-heart has-text-grey"></i>
</small>
</button>
</div>
<div class="dropdown-menu" :id="`react-menu-${message.msgID}`" role="menu">
<div class="dropdown-content p-0">
<!-- Emoji reactions menu -->
<EmojiPicker
v-if="showEmojiPicker"
:native="true"
:display-recent="true"
:disable-skin-tones="true"
:additional-groups="customEmojiGroups"
:group-names="{ frequently_used: 'Frequently Used' }"
theme="auto"
@select="onSelectEmoji"></EmojiPicker>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@ -2,6 +2,7 @@
const keys = { const keys = {
'fontSizeClass': String, // Text magnification 'fontSizeClass': String, // Text magnification
'videoScale': String, // Video magnification (CSS classnames) 'videoScale': String, // Video magnification (CSS classnames)
'messageStyle': String, // Message display style (cards, compact, etc.)
'imageDisplaySetting': String, // Show/hide/expand image preference 'imageDisplaySetting': String, // Show/hide/expand image preference
'scrollback': Number, // Scrollback buffer (int) 'scrollback': Number, // Scrollback buffer (int)
'preferredDeviceNames': Object, // Webcam/mic device names (object, keys video,audio) 'preferredDeviceNames': Object, // Webcam/mic device names (object, keys video,audio)
@ -32,20 +33,11 @@ class UserSettings {
// found in localStorage on page load. // found in localStorage on page load.
for (let key of Object.keys(keys)) { for (let key of Object.keys(keys)) {
if (localStorage[key] != undefined) { if (localStorage[key] != undefined) {
switch (keys[key]) { try {
case String: this[key] = JSON.parse(localStorage[key]);
this[key] = localStorage[key]; } catch(e) {
case Number: console.error(`LocalStorage: parsing key ${key}: ${e}`);
this[key] = parseInt(localStorage[key]); delete(this[key]);
case Boolean:
this[key] = localStorage[key] === "true";
case Object:
try {
this[key] = JSON.parse(localStorage[key]);
} catch(e) {
console.error(`LocalStorage: parsing key ${key}: ${e}`);
delete(this[key]);
}
} }
} }
} }