aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.scss8
-rw-r--r--src/components/attachment/attachment.js38
-rw-r--r--src/components/attachment/attachment.vue9
-rw-r--r--src/components/post_status_form/post_status_form.vue49
-rw-r--r--src/components/settings/settings.js22
-rw-r--r--src/components/settings/settings.vue118
-rw-r--r--src/components/status/status.vue59
-rw-r--r--src/components/timeline/timeline.js5
-rw-r--r--src/i18n/messages.js10
-rw-r--r--src/main.js4
-rw-r--r--src/modules/config.js4
11 files changed, 252 insertions, 74 deletions
diff --git a/src/App.scss b/src/App.scss
index 43786e9d..3b8b3224 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -142,6 +142,14 @@ input, textarea, .select {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}
+ &:disabled,
+ {
+ &,
+ & + label,
+ & + label::before {
+ opacity: .5;
+ }
+ }
+ label::before {
display: inline-block;
content: '✔';
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index d9bc4477..cc19714d 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -13,9 +13,10 @@ const Attachment = {
return {
nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw,
+ loopVideo: this.$store.state.config.loopVideo,
showHidden: false,
loading: false,
- img: document.createElement('img')
+ img: this.type === 'image' && document.createElement('img')
}
},
components: {
@@ -45,14 +46,35 @@ const Attachment = {
}
},
toggleHidden () {
- if (this.img.onload) {
- this.img.onload()
+ if (this.img) {
+ if (this.img.onload) {
+ this.img.onload()
+ } else {
+ this.loading = true
+ this.img.src = this.attachment.url
+ this.img.onload = () => {
+ this.loading = false
+ this.showHidden = !this.showHidden
+ }
+ }
} else {
- this.loading = true
- this.img.src = this.attachment.url
- this.img.onload = () => {
- this.loading = false
- this.showHidden = !this.showHidden
+ this.showHidden = !this.showHidden
+ }
+ },
+ onVideoDataLoad (e) {
+ if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') {
+ // non-zero if video has audio track
+ if (e.srcElement.webkitAudioDecodedByteCount > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.mozHasAudio !== 'undefined') {
+ // true if video has audio track
+ if (e.srcElement.mozHasAudio) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.audioTracks !== 'undefined') {
+ if (e.srcElement.audioTracks.length > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
}
}
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index c48fb16b..d01c8566 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -2,7 +2,7 @@
<div v-if="size==='hide'">
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
</div>
- <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth}" v-show="!isEmpty">
+ <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty">
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/>
</a>
@@ -14,7 +14,7 @@
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a>
- <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video>
+ <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@@ -38,7 +38,6 @@
.attachments {
display: flex;
flex-wrap: wrap;
- margin-right: -0.7em;
.attachment.media-upload-container {
flex: 0 0 auto;
@@ -50,6 +49,10 @@
margin-right: 0.5em;
}
+ .nsfw-placeholder {
+ cursor: pointer;
+ }
+
.small-attachment {
&.image, &.video {
max-width: 35%;
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 3749271d..2b84758b 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -65,12 +65,14 @@
<i class="icon-cancel" @click="clearError"></i>
</div>
<div class="attachments">
- <div class="media-upload-container attachment" v-for="file in newStatus.files">
+ <div class="media-upload-wrapper" v-for="file in newStatus.files">
<i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
- <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
- <video v-if="type(file) === 'video'" :src="file.image" controls></video>
- <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
- <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ <div class="media-upload-container attachment">
+ <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
+ <video v-if="type(file) === 'video'" :src="file.image" controls></video>
+ <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
+ <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ </div>
</div>
</div>
</form>
@@ -120,14 +122,49 @@
text-align: center;
}
+ .media-upload-wrapper {
+ flex: 0 0 auto;
+ max-width: 100%;
+ min-width: 50px;
+ margin-right: .2em;
+ margin-bottom: .5em;
+
+ .icon-cancel {
+ display: inline-block;
+ position: static;
+ margin: 0;
+ padding-bottom: 0;
+ margin-left: $fallback--attachmentRadius;
+ margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
+ background-color: $fallback--btn;
+ background-color: var(--btn, $fallback--btn);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+
.attachments {
padding: 0 0.5em;
.attachment {
+ margin: 0;
position: relative;
+ flex: 0 0 auto;
border: 1px solid $fallback--border;
border: 1px solid var(--border, $fallback--border);
- margin: 0.5em 0.8em 0.2em 0;
+ text-align: center;
+
+ audio {
+ min-width: 300px;
+ flex: 1 0 auto;
+ }
+
+ a {
+ display: block;
+ text-align: left;
+ line-height: 1.2;
+ padding: .5em;
+ }
}
i {
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index a26111d6..169b9080 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -1,3 +1,4 @@
+/* eslint-env browser */
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import { filter, trim } from 'lodash'
@@ -7,11 +8,21 @@ const settings = {
hideAttachmentsLocal: this.$store.state.config.hideAttachments,
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
hideNsfwLocal: this.$store.state.config.hideNsfw,
+ loopVideoLocal: this.$store.state.config.loopVideo,
+ loopVideoSilentOnlyLocal: this.$store.state.config.loopVideoSilentOnly,
muteWordsString: this.$store.state.config.muteWords.join('\n'),
autoLoadLocal: this.$store.state.config.autoLoad,
streamingLocal: this.$store.state.config.streaming,
+ pauseOnUnfocusedLocal: this.$store.state.config.pauseOnUnfocused,
hoverPreviewLocal: this.$store.state.config.hoverPreview,
- stopGifs: this.$store.state.config.stopGifs
+ stopGifs: this.$store.state.config.stopGifs,
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
@@ -32,12 +43,21 @@ const settings = {
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
+ loopVideoLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideo', value })
+ },
+ loopVideoSilentOnlyLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value })
+ },
autoLoadLocal (value) {
this.$store.dispatch('setOption', { name: 'autoLoad', value })
},
streamingLocal (value) {
this.$store.dispatch('setOption', { name: 'streaming', value })
},
+ pauseOnUnfocusedLocal (value) {
+ this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value })
+ },
hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
},
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 6245e758..d6aec2ff 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -1,53 +1,77 @@
<template>
- <div class="settings panel panel-default">
- <div class="panel-heading">
- {{$t('settings.settings')}}
+<div class="settings panel panel-default">
+ <div class="panel-heading">
+ {{$t('settings.settings')}}
+ </div>
+ <div class="panel-body">
+ <div class="setting-item">
+ <h2>{{$t('settings.theme')}}</h2>
+ <style-switcher></style-switcher>
</div>
- <div class="panel-body">
- <div class="setting-item">
- <h2>{{$t('settings.theme')}}</h2>
- <style-switcher></style-switcher>
- </div>
- <div class="setting-item">
- <h2>{{$t('settings.filtering')}}</h2>
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
- </div>
- <div class="setting-item">
- <h2>{{$t('settings.attachments')}}</h2>
- <ul class="setting-list">
- <li>
- <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
- <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
- </li>
- <li>
- <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
- <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
- </li>
- <li>
- <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
- <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
- </li>
- <li>
- <input type="checkbox" id="autoload" v-model="autoLoadLocal">
- <label for="autoload">{{$t('settings.autoload')}}</label>
- </li>
- <li>
- <input type="checkbox" id="streaming" v-model="streamingLocal">
- <label for="streaming">{{$t('settings.streaming')}}</label>
- </li>
+ <div class="setting-item">
+ <h2>{{$t('settings.filtering')}}</h2>
+ <p>{{$t('settings.filtering_explanation')}}</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('nav.timeline')}}</h2>
+ <ul class="setting-list">
+ <li>
+ <input type="checkbox" id="streaming" v-model="streamingLocal">
+ <label for="streaming">{{$t('settings.streaming')}}</label>
+ <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
<li>
- <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
- <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
+ <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
+ <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label>
</li>
+ </ul>
+ </li>
+ <li>
+ <input type="checkbox" id="autoload" v-model="autoLoadLocal">
+ <label for="autoload">{{$t('settings.autoload')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
+ <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('settings.attachments')}}</h2>
+ <ul class="setting-list">
+ <li>
+ <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
+ <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
+ <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
+ <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="stopGifs" v-model="stopGifs">
+ <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="loopVideo" v-model="loopVideoLocal">
+ <label for="loopVideo">{{$t('settings.loop_video')}}</label>
+ <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
<li>
- <input type="checkbox" id="stopGifs" v-model="stopGifs">
- <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
+ <input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal">
+ <label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label>
+ <div v-if="!loopSilentAvailable" class="unavailable">
+ <i class="icon-globe"/>! {{$t('settings.limited_availability')}}
+ </div>
</li>
- </ul>
- </div>
+ </ul>
+ </li>
+ </ul>
</div>
</div>
+</div>
</template>
<script src="./settings.js">
@@ -67,6 +91,12 @@
height: 100px;
}
+ .unavailable,
+ .unavailable i {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+
.old-avatar {
width: 128px;
border-radius: $fallback--avatarRadius;
@@ -89,8 +119,12 @@
}
.setting-list {
list-style-type: none;
+ padding-left: 2em;
li {
margin-bottom: 0.5em;
}
+ .suboptions {
+ margin-top: 0.3em
+ }
}
</style>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e2fb5d36..b8993b0d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -11,8 +11,8 @@
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
- <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
- <a v-else :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
+ <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
+ <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
<i class='fa icon-retweet retweeted'></i>
{{$t('timeline.repeated')}}
</div>
@@ -57,8 +57,10 @@
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
- <span v-if="status.visibility"><i :class="visibilityIcon(status.visibility)"></i> </span>
- <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext"></i></a>
+ <div class="visibility-icon" v-if="status.visibility">
+ <i :class="visibilityIcon(status.visibility)"></i>
+ </div>
+ <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext-alt"></i></a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a>
</template>
@@ -141,6 +143,7 @@
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
+
.status {
flex: 1;
border: 0;
@@ -155,6 +158,7 @@
text-align: center;
border-width: 1px;
border-style: solid;
+
i {
font-size: 2em;
}
@@ -196,6 +200,7 @@
.media-heading {
flex-wrap: nowrap;
+ line-height: 18px;
}
.media-heading-left {
@@ -218,12 +223,22 @@
flex: 1 0;
display: flex;
flex-wrap: wrap;
- align-content: center;
+ align-items: baseline;
+
+ .user-name {
+ margin-right: .45em;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
}
+
.links {
display: flex;
- padding-top: 1px;
- margin-left: 0.2em;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
@@ -247,19 +262,25 @@
}
.media-heading-right {
+ display: inline-flex;
flex-shrink: 0;
- display: flex;
flex-wrap: nowrap;
- max-height: 1.5em;
- margin-left: 0.25em;
+ margin-left: .25em;
+ align-self: baseline;
+
.timeago {
margin-right: 0.2em;
font-size: 12px;
- padding-top: 1px;
+ align-self: last baseline;
}
- i {
+
+ > * {
margin-left: 0.2em;
}
+ a:hover i {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
+ }
}
a {
@@ -318,6 +339,7 @@
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
margin: 0;
+
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
@@ -333,9 +355,22 @@
display: flex;
align-content: center;
flex-wrap: wrap;
+
+ .user-name {
+ font-weight: bold;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
+
i {
padding: 0 0.2em;
}
+
a {
max-width: 100%;
overflow: hidden;
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 5c179567..a651f619 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -133,7 +133,10 @@ const Timeline = {
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
- if (window.pageYOffset < 15 && !this.paused && !this.unfocused) {
+ if (window.pageYOffset < 15 &&
+ !this.paused &&
+ !(this.unfocused && this.$store.state.config.pauseOnUnfocused)
+ ) {
this.showNewStatuses()
} else {
this.paused = true
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 38e58c4c..99382364 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -314,6 +314,9 @@ const en = {
stop_gifs: 'Play-on-hover GIFs',
autoload: 'Enable automatic loading when scrolled to the bottom',
streaming: 'Enable automatic streaming of new posts when scrolled to the top',
+ pause_on_unfocused: 'Pause streaming when tab is not focused',
+ loop_video: 'Loop videos',
+ loop_video_silent_only: 'Loop only videos without sound (i.e. Mastodon\'s "gifs")',
reply_link_preview: 'Enable reply-link preview on mouse hover',
follow_import: 'Follow import',
import_followers_from_a_csv_file: 'Import follows from a csv file',
@@ -333,6 +336,7 @@ const en = {
changed_password: 'Password changed successfully!',
change_password_error: 'There was an issue changing your password.',
lock_account_description: 'Restrict your account to approved followers only',
+ limited_availability: 'Unavailable in your browser',
default_vis: 'Default visibility scope',
profile_tab: 'Profile',
security_tab: 'Security',
@@ -1609,6 +1613,9 @@ const ru = {
nsfw_clickthrough: 'Включить скрытие NSFW вложений',
autoload: 'Включить автоматическую загрузку при прокрутке вниз',
streaming: 'Включить автоматическую загрузку новых сообщений при прокрутке вверх',
+ pause_on_unfocused: 'Приостановить загрузку когда вкладка не в фокусе',
+ loop_video: 'Зациливать видео',
+ loop_video_silent_only: 'Зацикливать только беззвучные видео (т.е. "гифки" с Mastodon)',
reply_link_preview: 'Включить предварительный просмотр ответа при наведении мыши',
follow_import: 'Импортировать читаемых',
import_followers_from_a_csv_file: 'Импортировать читаемых из файла .csv',
@@ -1626,7 +1633,8 @@ const ru = {
new_password: 'Новый пароль',
confirm_new_password: 'Подтверждение нового пароля',
changed_password: 'Пароль изменён успешно.',
- change_password_error: 'Произошла ошибка при попытке изменить пароль.'
+ change_password_error: 'Произошла ошибка при попытке изменить пароль.',
+ limited_availability: 'Не доступно в вашем браузере'
},
notifications: {
notifications: 'Уведомления',
diff --git a/src/main.js b/src/main.js
index cb53edd3..df271ce3 100644
--- a/src/main.js
+++ b/src/main.js
@@ -54,6 +54,10 @@ const persistedStateOptions = {
'config.muteWords',
'config.customTheme',
'config.highlight',
+ 'config.loopVideo',
+ 'config.loopVideoSilentOnly',
+ 'config.pauseOnUnfocused',
+ 'config.stopGifs',
'users.lastLoginName'
]
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 2b50655b..fe31ab01 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -7,9 +7,13 @@ const defaultState = {
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
+ loopVideo: true,
+ loopVideoSilentOnly: true,
autoLoad: true,
streaming: false,
hoverPreview: true,
+ pauseOnUnfocused: true,
+ stopGifs: false,
muteWords: [],
highlight: {}
}