Compact-style message display options
This commit is contained in:
parent
15b291826e
commit
b5d0885c23
42
src/App.vue
42
src/App.vue
|
@ -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)"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user