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"],
|
||||
["x4", "4x larger chat room text"],
|
||||
],
|
||||
messageStyleSettings: [
|
||||
["cards", "Card style (default)"],
|
||||
["compact", "Compact style (with display names)"],
|
||||
["compact2", "Compact style (usernames only)"],
|
||||
],
|
||||
imageDisplaySettings: [
|
||||
["show", "Always show images in chat"],
|
||||
["collapse", "Collapse images in chat, clicking to expand (default)"],
|
||||
|
@ -229,6 +234,7 @@ export default {
|
|||
historyScrollbox: null,
|
||||
autoscroll: true, // scroll to bottom on new messages
|
||||
fontSizeClass: "", // font size magnification
|
||||
messageStyle: "cards", // message display style
|
||||
imageDisplaySetting: "collapse", // image show/hide setting
|
||||
scrollback: 1000, // scrollback buffer (messages to keep per channel)
|
||||
DMs: {},
|
||||
|
@ -361,6 +367,9 @@ export default {
|
|||
// Store the setting persistently.
|
||||
LocalStorage.set('fontSizeClass', this.fontSizeClass);
|
||||
},
|
||||
messageStyle() {
|
||||
LocalStorage.set('messageStyle', this.messageStyle);
|
||||
},
|
||||
imageDisplaySetting() {
|
||||
LocalStorage.set('imageDisplaySetting', this.imageDisplaySetting);
|
||||
},
|
||||
|
@ -675,6 +684,10 @@ export default {
|
|||
this.fontSizeClass = settings.fontSizeClass;
|
||||
}
|
||||
|
||||
if (settings.messageStyle != undefined) {
|
||||
this.messageStyle = settings.messageStyle;
|
||||
}
|
||||
|
||||
if (settings.videoScale != undefined) {
|
||||
this.webcam.videoScale = settings.videoScale;
|
||||
}
|
||||
|
@ -2896,6 +2909,25 @@ export default {
|
|||
</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-label is-normal">
|
||||
<label class="label">Images</label>
|
||||
|
@ -3599,7 +3631,9 @@ export default {
|
|||
</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">
|
||||
<label class="checkbox is-size-6" title="Automatically scroll when new chat messages come in.">
|
||||
|
@ -3643,6 +3677,11 @@ export default {
|
|||
</figure>
|
||||
</div>
|
||||
<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>
|
||||
<span v-if="isUserOffline(msg.username)" class="ml-1">(offline)</span>
|
||||
<small v-else class="ml-1">(@{{ msg.username }})</small>
|
||||
|
@ -3656,6 +3695,7 @@ export default {
|
|||
<MessageBox
|
||||
v-else
|
||||
:message="msg"
|
||||
:appearance="messageStyle"
|
||||
:position="i"
|
||||
:user="getUser(msg.username)"
|
||||
:is-offline="isUserOffline(msg.username)"
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'vue3-emoji-picker/css';
|
|||
export default {
|
||||
props: {
|
||||
message: Object, // chat Message object
|
||||
appearance: String, // message style appearance (cards, compact, etc.)
|
||||
user: Object, // User object of the Message author
|
||||
isOffline: Boolean, // user is not currently online
|
||||
username: String, // current username logged in
|
||||
|
@ -26,6 +27,9 @@ export default {
|
|||
// Emoji picker visible
|
||||
showEmojiPicker: false,
|
||||
|
||||
// Message menu (compact displays)
|
||||
menuVisible: false,
|
||||
|
||||
// Favorite emojis
|
||||
customEmojiGroups: {
|
||||
frequently_used: [
|
||||
|
@ -72,6 +76,11 @@ export default {
|
|||
hasReactions() {
|
||||
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: {
|
||||
openProfile() {
|
||||
|
@ -154,12 +163,20 @@ export default {
|
|||
let hour = hours % 12 || 12;
|
||||
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>
|
||||
|
||||
<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-left">
|
||||
<a :href="profileURL"
|
||||
|
@ -314,6 +331,7 @@ export default {
|
|||
<!-- 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)">
|
||||
|
@ -323,6 +341,148 @@ export default {
|
|||
</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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
const keys = {
|
||||
'fontSizeClass': String, // Text magnification
|
||||
'videoScale': String, // Video magnification (CSS classnames)
|
||||
'messageStyle': String, // Message display style (cards, compact, etc.)
|
||||
'imageDisplaySetting': String, // Show/hide/expand image preference
|
||||
'scrollback': Number, // Scrollback buffer (int)
|
||||
'preferredDeviceNames': Object, // Webcam/mic device names (object, keys video,audio)
|
||||
|
@ -32,20 +33,11 @@ class UserSettings {
|
|||
// found in localStorage on page load.
|
||||
for (let key of Object.keys(keys)) {
|
||||
if (localStorage[key] != undefined) {
|
||||
switch (keys[key]) {
|
||||
case String:
|
||||
this[key] = localStorage[key];
|
||||
case Number:
|
||||
this[key] = parseInt(localStorage[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]);
|
||||
}
|
||||
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