diff options
33 files changed, 275 insertions, 858 deletions
diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index 87085a28..8f41e2fb 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const BasicUserCard = { @@ -13,7 +14,8 @@ const BasicUserCard = { }, components: { UserCard, - UserAvatar + UserAvatar, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index c53f6a9c..53deb1df 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -25,17 +25,11 @@ :title="user.name" class="basic-user-card-user-name" > - <!-- eslint-disable vue/no-v-html --> - <span - v-if="user.name_html" + <RichContent class="basic-user-card-user-name-value" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <span - v-else - class="basic-user-card-user-name-value" - >{{ user.name }}</span> </div> <div> <router-link diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index eec116db..65c62baa 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -41,11 +41,11 @@ const MentionLink = { }, computed: { user () { - return this.url && this.$store.getters.findUserByUrl(this.url) + return this.url && this.$store && this.$store.getters.findUserByUrl(this.url) }, isYou () { // FIXME why user !== currentUser??? - return this.user && this.user.screen_name === this.currentUser.screen_name + return this.user && this.user.id === this.currentUser.id }, userName () { return this.user && this.userNameFullUi.split('@')[0] @@ -65,9 +65,6 @@ const MentionLink = { highlightClass () { if (this.highlight) return highlightClass(this.user) }, - oldStyle () { - return !this.mergedConfig.mentionsNewStyle - }, style () { if (this.highlight) { const { @@ -83,8 +80,7 @@ const MentionLink = { return [ { '-you': this.isYou, - '-highlighted': this.highlight, - '-oldStyle': this.oldStyle + '-highlighted': this.highlight }, this.highlightType ] diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss index 5f5da98f..ec2689f8 100644 --- a/src/components/mention_link/mention_link.scss +++ b/src/components/mention_link/mention_link.scss @@ -10,10 +10,6 @@ border-radius: 2px; } - .original { - margin-right: 0.25em; - } - .full { position: absolute; display: inline-block; @@ -41,8 +37,6 @@ } .new { - margin-right: 0.25em; - &.-you { & .shortName, & .full { @@ -61,41 +55,6 @@ margin: 0; } - &:not(.-oldStyle) { - .short { - padding-left: 0.25em; - padding-right: 0; - padding-top: 0; - padding-bottom: 0; - line-height: 1.5; - font-size: inherit; - - .at { - color: var(--faint); - opacity: 0.8; - padding-right: 0.25em; - vertical-align: -20%; - } - } - - .you { - padding-right: 0.25em; - } - - .userName { - display: inline-block; - color: var(--link); - line-height: inherit; - margin-left: 0; - padding-left: 0.125em; - padding-right: 0.25em; - padding-top: 0; - padding-bottom: 0; - border-top-right-radius: var(--btnRadius); - border-bottom-right-radius: var(--btnRadius); - } - } - &.-striped { & .userName, & .full { diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index 514b7475..625eb727 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -18,8 +18,7 @@ :class="classnames" > <button - class="short" - :class="[{ '-sublime': !highlight }, oldStyle ? 'button-unstyled' : 'button-default']" + class="short button-unstyled" @click.prevent="onClick" > <!-- eslint-disable vue/no-v-html --> diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js index e52045ec..a4a0c724 100644 --- a/src/components/mentions_line/mentions_line.js +++ b/src/components/mentions_line/mentions_line.js @@ -1,6 +1,8 @@ import MentionLink from 'src/components/mention_link/mention_link.vue' import { mapGetters } from 'vuex' +export const MENTIONS_LIMIT = 5 + const MentionsLine = { name: 'MentionsLine', props: { @@ -14,31 +16,15 @@ const MentionsLine = { MentionLink }, computed: { - oldStyle () { - return !this.mergedConfig.mentionsNewStyle - }, - limit () { - return 6 - }, mentionsComputed () { - return this.mentions.slice(0, this.limit) + return this.mentions.slice(0, MENTIONS_LIMIT) }, extraMentions () { - return this.mentions.slice(this.limit) + return this.mentions.slice(MENTIONS_LIMIT) }, manyMentions () { return this.extraMentions.length > 0 }, - buttonClasses () { - return [ - this.oldStyle - ? 'button-unstyled' - : 'button-default -sublime', - this.oldStyle - ? '-oldStyle' - : '-newStyle' - ] - }, ...mapGetters(['mergedConfig']) }, methods: { diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss index 90d1e0a4..222940c8 100644 --- a/src/components/mentions_line/mentions_line.scss +++ b/src/components/mentions_line/mentions_line.scss @@ -1,17 +1,10 @@ .MentionsLine { .showMoreLess { white-space: normal; + color: var(--link); + } - &.-newStyle { - line-height: 1.5; - font-size: inherit; - display: inline-block; - padding-top: 0; - padding-bottom: 0; - } - - &.-oldStyle { - color: var(--link); - } + .mention-link:not(:last-child) { + margin-right: 0.25em; } } diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue index f4b3abb9..f375e3b0 100644 --- a/src/components/mentions_line/mentions_line.vue +++ b/src/components/mentions_line/mentions_line.vue @@ -25,15 +25,13 @@ /> </span><button v-if="!expanded" - class="showMoreLess" - :class="buttonClasses" + class="button-unstyled showMoreLess" @click="toggleShowMore" > {{ $t('status.plus_more', { number: extraMentions.length }) }} </button><button v-if="expanded" - class="showMoreLess" - :class="buttonClasses" + class="button-unstyled showMoreLess" @click="toggleShowMore" > {{ $t('general.show_less') }} diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 4aa9affd..398bb7a9 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -4,6 +4,7 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -44,7 +45,8 @@ const Notification = { UserAvatar, UserCard, Timeago, - Status + Status, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index f5905560..ec291547 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -2,6 +2,8 @@ // TODO Copypaste from Status, should unify it somehow .Notification { + --emoji-size: 14px; + &.-muted { padding: 0.25em 0.6em; height: 1.2em; diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 396ae0e1..eb02bed1 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -51,12 +51,14 @@ <span class="notification-details"> <div class="name-and-action"> <!-- eslint-disable vue/no-v-html --> - <bdi - v-if="!!notification.from_profile.name_html" - class="username" - :title="'@'+notification.from_profile.screen_name_ui" - v-html="notification.from_profile.name_html" - /> + <bdi v-if="!!notification.from_profile.name_html"> + <RichContent + class="username" + :title="'@'+notification.from_profile.screen_name_ui" + :html="notification.from_profile.name_html" + :emoji="notification.from_profile.emoji" + /> + </bdi> <!-- eslint-enable vue/no-v-html --> <span v-else diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 2bb627a8..77b3c438 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -148,13 +148,6 @@ max-width: 100%; text-overflow: ellipsis; white-space: nowrap; - - img { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .timeago { diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index 98db5582..a69b7886 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -1,10 +1,14 @@ -import Timeago from '../timeago/timeago.vue' +import Timeago from 'components/timeago/timeago.vue' +import RichContent from 'components/rich_content/rich_content.jsx' import { forEach, map } from 'lodash' export default { name: 'Poll', - props: ['basePoll'], - components: { Timeago }, + props: ['basePoll', 'emoji'], + components: { + Timeago, + RichContent + }, data () { return { loading: false, diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 187d1829..63b44e4f 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -17,8 +17,11 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> - <!-- eslint-disable-next-line vue/no-v-html --> - <span v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </div> <div class="result-fill" @@ -42,8 +45,11 @@ :value="index" > <label class="option-vote"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </label> </div> </div> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index cd73f2e5..1353541f 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -4,8 +4,7 @@ import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_con import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js' import StillImage from 'src/components/still-image/still-image.vue' -import MentionLink from 'src/components/mention_link/mention_link.vue' -import MentionsLine from 'src/components/mentions_line/mentions_line.vue' +import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue' import './rich_content.scss' @@ -13,12 +12,11 @@ import './rich_content.scss' * RichContent, The Über-powered component for rendering Post HTML. * * This takes post HTML and does multiple things to it: - * - Converts mention links to <MentionLink>-s - * - Removes mentions from beginning and end (hellthread style only) + * - Groups all mentions into <MentionsLine>, this affects all mentions regardles + * of where they are (beginning/middle/end), even single mentions are converted + * to a <MentionsLine> containing single <MentionLink>. * - Replaces emoji shortcodes with <StillImage>'d images. * - * Stuff like removing mentions from beginning and end is done so that they could - * be either replaced by collapsible <MentionsLine> or moved to separate place. * There are two problems with this component's architecture: * 1. Parsing HTML and rendering are inseparable. Attempts to separate the two * proven to be a massive overcomplication due to amount of things done here. @@ -56,25 +54,22 @@ export default Vue.component('RichContent', { required: false, type: Boolean, default: false - }, - hideMentions: { - required: false, - type: Boolean, - default: false } }, // NEVER EVER TOUCH DATA INSIDE RENDER render (h) { // Pre-process HTML - const { newHtml: html, lastMentions } = preProcessPerLine(this.html, this.greentext, this.handleLinks) - const firstMentions = [] // Mentions that appear in the beginning of post body + const { newHtml: html } = preProcessPerLine(this.html, this.greentext) + let currentMentions = null // Current chain of mentions, we group all mentions together + const lastTags = [] // Tags that appear at the end of post body const writtenMentions = [] // All mentions that appear in post body + const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine) + // to collapse too many mentions in a row const writtenTags = [] // All tags that appear in post body // unique index for vue "tag" property let mentionIndex = 0 let tagsIndex = 0 - let firstMentionReplaced = false const renderImage = (tag) => { return <StillImage @@ -98,41 +93,35 @@ export default Vue.component('RichContent', { const renderMention = (attrs, children) => { const linkData = getLinkData(attrs, children, mentionIndex++) linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url) - if (!linkData.notifying) { - encounteredText = true - } writtenMentions.push(linkData) - if (!encounteredText) { - firstMentions.push(linkData) - if (!firstMentionReplaced && !this.hideMentions) { - firstMentionReplaced = true - return <MentionsLine mentions={ firstMentions } /> - } else { - return '' - } + if (currentMentions === null) { + currentMentions = [] + } + currentMentions.push(linkData) + if (currentMentions.length > MENTIONS_LIMIT) { + invisibleMentions.push(linkData) + } + if (currentMentions.length === 1) { + return <MentionsLine mentions={ currentMentions } /> } else { - return <MentionLink - url={attrs.href} - content={flattenDeep(children).join('')} - /> + return '' } } - // We stop treating mentions as "first" ones when we encounter - // non-whitespace text - let encounteredText = false // Processor to use with html_tree_converter const processItem = (item, index, array, what) => { // Handle text nodes - just add emoji if (typeof item === 'string') { const emptyText = item.trim() === '' - if (emptyText) { - return encounteredText ? item : item.trim() + if (item.includes('\n')) { + currentMentions = null } - if (!encounteredText) { - item = item.trimStart() - encounteredText = true + if (emptyText) { + // don't include spaces when processing mentions - we'll include them + // in MentionsLine + return currentMentions !== null ? item.trim() : item } + currentMentions = null if (item.includes(':')) { item = ['', processTextForEmoji( item, @@ -156,28 +145,25 @@ export default Vue.component('RichContent', { const Tag = getTagName(opener) const attrs = getAttrs(opener) switch (Tag) { - case 'span': // Replace last mentions class with mentionsline - if (attrs['class'] && attrs['class'].includes('lastMentions')) { - if (firstMentions.length > 1 && lastMentions.length > 1) { - break - } else { - return !this.hideMentions ? <MentionsLine mentions={lastMentions} /> : '' - } - } else { - break - } + case 'br': + currentMentions = null + break case 'img': // replace images with StillImage return renderImage(opener) case 'a': // replace mentions with MentionLink if (!this.handleLinks) break if (attrs['class'] && attrs['class'].includes('mention')) { // Handling mentions here - return renderMention(attrs, children, encounteredText) + return renderMention(attrs, children) } else { // Everything else will be handled in reverse pass - encounteredText = true + currentMentions = null return item // We'll handle it later } + case 'span': + if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { + return ['', children.map(processItem), ''] + } } if (children !== undefined) { @@ -246,11 +232,10 @@ export default Vue.component('RichContent', { </span> const event = { - firstMentions, - lastMentions, lastTags, writtenMentions, - writtenTags + writtenTags, + invisibleMentions } // DO NOT MOVE TO UPDATE. BAD IDEA. @@ -261,44 +246,46 @@ export default Vue.component('RichContent', { }) const getLinkData = (attrs, children, index) => { + const stripTags = (item) => { + if (typeof item === 'string') { + return item + } else { + return item[1].map(stripTags).join('') + } + } + const textContent = children.map(stripTags).join('') return { index, url: attrs.href, hashtag: attrs['data-tag'], - content: flattenDeep(children).join('') + content: flattenDeep(children).join(''), + textContent } } /** Pre-processing HTML * - * Currently this does two things: + * Currently this does one thing: * - add green/cyantexting - * - wrap and mark last line containing only mentions as ".lastMentionsLine" for - * more compact hellthreads. * * @param {String} html - raw HTML to process * @param {Boolean} greentext - whether to enable greentexting or not - * @param {Boolean} handleLinks - whether to handle links or not */ -export const preProcessPerLine = (html, greentext, handleLinks) => { - const lastMentions = [] +export const preProcessPerLine = (html, greentext) => { const greentextHandle = new Set(['p', 'div']) - let nonEmptyIndex = -1 const lines = convertHtmlToLines(html) - const linesNum = lines.filter(c => c.text).length const newHtml = lines.reverse().map((item, index, array) => { // Going over each line in reverse to detect last mentions, // keeping non-text stuff as-is if (!item.text) return item const string = item.text - nonEmptyIndex += 1 // Greentext stuff if ( // Only if greentext is engaged greentext && - // Only handle p's and divs. Don't want to affect blocquotes, code etc + // Only handle p's and divs. Don't want to affect blockquotes, code etc item.level.every(l => greentextHandle.has(l)) && // Only if line begins with '>' or '<' (string.includes('>') || string.includes('<')) @@ -313,80 +300,8 @@ export const preProcessPerLine = (html, greentext, handleLinks) => { } } - // Converting that line part into tree - const tree = convertHtmlToTree(string) - - // If line has loose text, i.e. text outside a mention or a tag - // we won't touch mentions. - let hasLooseText = false - let mentionsNum = 0 - const process = (item) => { - if (Array.isArray(item)) { - const [opener, children, closer] = item - const tag = getTagName(opener) - // If we have a link we probably have mentions - if (tag === 'a') { - if (!handleLinks) return [opener, children, closer] - const attrs = getAttrs(opener) - if (attrs['class'] && attrs['class'].includes('mention')) { - // Got mentions - mentionsNum++ - return [opener, children, closer] - } else { - // Not a mention? Means we have loose text or whatever - hasLooseText = true - return [opener, children, closer] - } - } else if (tag === 'span' || tag === 'p') { - // For span and p we need to go deeper - return [opener, [...children].map(process), closer] - } else { - // Everything else equals to a loose text - hasLooseText = true - return [opener, children, closer] - } - } - - if (typeof item === 'string') { - if (item.trim() !== '') { - // only meaningful strings are loose text - hasLooseText = true - } - return item - } - } - - // We now processed our tree, now we need to mark line as lastMentions - const result = [...tree].map(process) - - if ( - handleLinks && // Do we handle links at all? - mentionsNum > 1 && // Does it have more than one mention? - !hasLooseText && // Don't do anything if it has something besides mentions - nonEmptyIndex === 0 && // Only check last (first since list is reversed) line - nonEmptyIndex !== linesNum - 1 // Don't do anything if there's only one line - ) { - let mentionIndex = 0 - const process = (item) => { - if (Array.isArray(item)) { - const [opener, children] = item - const tag = getTagName(opener) - if (tag === 'a') { - const attrs = getAttrs(opener) - lastMentions.push(getLinkData(attrs, children, mentionIndex++)) - } else if (children) { - children.forEach(process) - } - } - } - result.forEach(process) - // we DO need mentions here so that we conditionally remove them if don't - // have first mentions - return ['<span class="lastMentions">', flattenDeep(result).join(''), '</span>'].join('') - } else { - return flattenDeep(result).join('') - } + return string }).reverse().join('') - return { newHtml, lastMentions } + return { newHtml } } diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index 12cb9776..db08ef1e 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -49,6 +49,7 @@ } .emoji { + display: inline-block; width: var(--emoji-size, 32px); height: var(--emoji-size, 32px); } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 71780e00..d3e71b31 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -42,16 +42,6 @@ </BooleanSetting> </li> <li> - <BooleanSetting path="mentionsOwnLine"> - {{ $t('settings.mentions_new_place') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="mentionsNewStyle"> - {{ $t('settings.mentions_new_style') }} - </BooleanSetting> - </li> - <li> <BooleanSetting path="streaming"> {{ $t('settings.streaming') }} </BooleanSetting> diff --git a/src/components/status/status.js b/src/components/status/status.js index 3c21cb76..ac481534 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -166,29 +166,22 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, - mentions () { + mentionsLine () { + if (!this.headTailLinks) return [] + const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url)) return this.status.attentions.filter(attn => { - return attn.screen_name !== this.replyToName && - attn.screen_name !== this.status.user.screen_name + // no reply user + return attn.id !== this.status.in_reply_to_user_id && + // no self-replies + attn.statusnet_profile_url !== this.status.user.statusnet_profile_url && + // don't include if mentions is written + !writtenSet.has(attn.statusnet_profile_url) }).map(attn => ({ url: attn.statusnet_profile_url, content: attn.screen_name, userId: attn.id })) }, - alsoMentions () { - if (!this.headTailLinks) return [] - const set = new Set(this.headTailLinks.writtenMentions.map(m => m.url)) - return this.headTailLinks.writtenMentions.filter(mention => { - return !set.has(mention.url) - }) - }, - mentionsLine () { - return this.mentionsOwnLine ? this.mentions : this.alsoMentions - }, - mentionsOwnLine () { - return this.mergedConfig.mentionsOwnLine - }, hasMentionsLine () { return this.mentionsLine.length > 0 }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index a5f347a6..2684e415 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -306,7 +306,6 @@ :no-heading="noHeading" :highlight="highlight" :focused="isFocused" - :hide-mentions="mentionsOwnLine && (isReply || true)" @mediaplay="addMediaPlaying($event)" @mediapause="removeMediaPlaying($event)" @parseReady="setHeadTailLinks" diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js index 7ee965d9..f4167ac1 100644 --- a/src/components/status_body/status_body.js +++ b/src/components/status_body/status_body.js @@ -26,15 +26,16 @@ const StatusContent = { 'focused', 'noHeading', 'fullContent', - 'singleLine', - 'hideMentions' + 'singleLine' ], data () { return { showingTall: this.fullContent || (this.inConversation && this.focused), showingLongSubject: false, // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, + postLength: this.status.text.length, + parseReadyDone: false } }, computed: { @@ -49,7 +50,7 @@ const StatusContent = { // Using max-height + overflow: auto for status components resulted in false positives // very often with japanese characters, and it was very annoying. tallStatus () { - const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.status.text.length / 80 + const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80 return lengthScore > 20 }, longSubject () { @@ -87,8 +88,10 @@ const StatusContent = { }, methods: { onParseReady (event) { + if (this.parseReadyDone) return + this.parseReadyDone = true this.$emit('parseReady', event) - const { writtenMentions } = event + const { writtenMentions, invisibleMentions } = event writtenMentions .filter(mention => !mention.notifying) .forEach(mention => { @@ -99,6 +102,15 @@ const StatusContent = { const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '') this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`) }) + /* This is a bit of a hack to make current tall status detector work + * with rich mentions. Invisible mentions are detected at RichContent level + * and also we generate plaintext version of mentions by stripping tags + * so here we subtract from post length by each mention that became invisible + * via MentionsLine + */ + this.postLength = invisibleMentions.reduce((acc, mention) => { + return acc - mention.textContent.length - 1 + }, this.postLength) }, toggleShowMore () { if (this.mightHideBecauseTall) { diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue index 1001508c..a088e6bc 100644 --- a/src/components/status_body/status_body.vue +++ b/src/components/status_body/status_body.vue @@ -48,7 +48,6 @@ :html="status.raw_html" :emoji="status.emojis" :handle-links="true" - :hide-mentions="hideMentions" :greentext="mergedConfig.greentext" :attentions="status.attentions" @parseReady="onParseReady" diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index 9059642e..184f2783 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -31,8 +31,7 @@ const StatusContent = { 'focused', 'noHeading', 'fullContent', - 'singleLine', - 'hideMentions' + 'singleLine' ], computed: { hideAttachments () { diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 86b3c52a..9db176f0 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -8,11 +8,13 @@ :status="status" :compact="compact" :single-line="singleLine" - :hide-mentions="hideMentions" @parseReady="$emit('parseReady', $event)" > <div v-if="status.poll && status.poll.options"> - <poll :base-poll="status.poll" /> + <Poll + :base-poll="status.poll" + :emoji="status.emojis" + /> </div> <gallery diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 23e6358f..a453ce79 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' import Select from '../select/select.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -118,7 +119,8 @@ export default { AccountActions, ProgressButton, FollowButton, - Select + Select, + RichContent }, methods: { muteUser () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index a16f7873..794a2350 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -38,21 +38,12 @@ </router-link> <div class="user-summary"> <div class="top-line"> - <!-- eslint-disable vue/no-v-html --> - <div - v-if="user.name_html" + <RichContent :title="user.name" class="user-name" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <div - v-else - :title="user.name" - class="user-name" - > - {{ user.name }} - </div> <button v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" @@ -255,20 +246,12 @@ <span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> </div> </div> - <!-- eslint-disable vue/no-v-html --> - <p - v-if="!hideBio && user.description_html" + <RichContent + v-if="!hideBio" class="user-card-bio" - @click.prevent="linkClicked" - v-html="user.description_html" + :html="user.description_html" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <p - v-else-if="!hideBio" - class="user-card-bio" - > - {{ user.description }} - </p> </div> </div> </template> @@ -281,9 +264,10 @@ .user-card { position: relative; - &:hover .Avatar { + &:hover { --_still-image-img-visibility: visible; --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } .panel-heading { @@ -327,12 +311,12 @@ } } - p { - margin-bottom: 0; - } - &-bio { text-align: center; + display: block; + line-height: 18px; + padding: 1em; + margin: 0; a { color: $fallback--link; @@ -344,11 +328,6 @@ vertical-align: middle; max-width: 100%; max-height: 400px; - - &.emoji { - width: 32px; - height: 32px; - } } } @@ -450,13 +429,6 @@ // big one z-index: 1; - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { display: flex; } @@ -469,12 +441,7 @@ margin-right: 1em; font-size: 15px; - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + --emoji-size: 14px; } .bottom-line { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index c0b55a6c..7a475609 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' import { library } from '@fortawesome/fontawesome-svg-core' @@ -164,7 +165,8 @@ const UserProfile = { FriendList, FollowCard, TabSwitcher, - Conversation + Conversation, + RichContent } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index aef897ae..726216ff 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,20 +20,24 @@ :key="index" class="user-profile-field" > - <!-- eslint-disable vue/no-v-html --> <dt :title="user.fields_text[index].name" class="user-profile-field-name" - @click.prevent="linkClicked" - v-html="field.name" - /> + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> <dd :title="user.fields_text[index].value" class="user-profile-field-value" - @click.prevent="linkClicked" - v-html="field.value" - /> - <!-- eslint-enable vue/no-v-html --> + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> </dl> </div> <tab-switcher diff --git a/src/modules/config.js b/src/modules/config.js index db9d5ffb..bdab3f4d 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -55,8 +55,6 @@ export const defaultState = { interfaceLanguage: browserLocale, hideScopeNotice: false, useStreamingApi: false, - mentionsOwnLine: false, - mentionsNewStyle: false, sidebarRight: undefined, // instance default scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 477b861f..04bb45a4 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -56,16 +56,17 @@ export const parseUser = (data) => { output.emoji = data.emojis output.name = data.display_name - output.name_html = addEmojis(escape(data.display_name), data.emojis) + output.name_html = escape(data.display_name) output.description = data.note - output.description_html = addEmojis(data.note, data.emojis) + // TODO cleanup this shit, output.description is overriden with source data + output.description_html = data.note output.fields = data.fields output.fields_html = data.fields.map(field => { return { - name: addEmojis(escape(field.name), data.emojis), - value: addEmojis(field.value, data.emojis) + name: escape(field.name), + value: field.value } }) output.fields_text = data.fields.map(field => { @@ -240,16 +241,6 @@ export const parseAttachment = (data) => { return output } -export const addEmojis = (string, emojis) => { - const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g - return emojis.reduce((acc, emoji) => { - const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') - return acc.replace( - new RegExp(`:${regexSafeShortCode}:`, 'g'), - `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />` - ) - }, string) -} export const parseStatus = (data) => { const output = {} @@ -301,7 +292,7 @@ export const parseStatus = (data) => { if (output.poll) { output.poll.options = (output.poll.options || []).map(field => ({ ...field, - title_html: addEmojis(escape(field.title), data.emojis) + title_html: escape(field.title) })) } output.pinned = data.pinned diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js index 74103b02..5eeaa7cb 100644 --- a/src/services/html_converter/html_line_converter.service.js +++ b/src/services/html_converter/html_line_converter.service.js @@ -18,7 +18,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @return {(string|{ text: string })[]} processed html in form of a list. */ -export const convertHtmlToLines = (html) => { +export const convertHtmlToLines = (html = '') => { // Elements that are implicitly self-closing // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js index 804d35d7..6a8796c4 100644 --- a/src/services/html_converter/html_tree_converter.service.js +++ b/src/services/html_converter/html_tree_converter.service.js @@ -19,7 +19,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @return {string} processed html */ -export const convertHtmlToTree = (html) => { +export const convertHtmlToTree = (html = '') => { // Elements that are implicitly self-closing // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js index fbf8973d..b29edeab 100644 --- a/test/unit/specs/components/rich_content.spec.js +++ b/test/unit/specs/components/rich_content.spec.js @@ -2,13 +2,19 @@ import { mount, shallowMount, createLocalVue } from '@vue/test-utils' import RichContent from 'src/components/rich_content/rich_content.jsx' const localVue = createLocalVue() +const attentions = [] -const makeMention = (who) => `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>` -const stubMention = (who) => `<span class="h-card"><mentionlink-stub url="https://fake.tld/@${who}" content="@<span>${who}</span>"></mentionlink-stub></span>` -const lastMentions = (...data) => `<span class="lastMentions">${data.join('')}</span>` +const makeMention = (who) => { + attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` }) + return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>` +} const p = (...data) => `<p>${data.join('')}</p>` const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>` -const removedMentionSpan = '<span class="h-card"></span>' +const mentionsLine = (times) => [ + '<mentionsline-stub mentions="', + new Array(times).fill('[object Object]').join(','), + '"></mentionsline-stub>' +].join('') describe('RichContent', () => { it('renders simple post without exploding', () => { @@ -16,7 +22,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -39,7 +45,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -50,77 +56,15 @@ describe('RichContent', () => { expect(wrapper.html()).to.eql(compwrap(expected)) }) - it('removes mentions from the beginning of post', () => { + it('replaces mention with mentionsline', () => { const html = p( makeMention('John'), - ' how are you doing thoday?' + ' how are you doing today?' ) - const expected = p( - removedMentionSpan, - 'how are you doing thoday?' - ) - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('replaces first mention with mentionsline if hideMentions=false', () => { - const html = p( - makeMention('John'), - ' how are you doing thoday?' - ) - const expected = p( - '<span class="h-card">', - '<mentionsline-stub mentions="', - '[object Object]', - '"></mentionsline-stub>', - '</span>', - 'how are you doing thoday?' - ) - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: false, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('removes mentions from the end of the hellpost (<p>)', () => { - const html = [ - p('How are you doing today, fine gentlemen?'), - p( - makeMention('John'), - makeMention('Josh'), - makeMention('Jeremy') - ) - ].join('') - const expected = [ - p( - 'How are you doing today, fine gentlemen?' - ), - // TODO fix this extra line somehow? - p() - ].join('') - const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -128,10 +72,13 @@ describe('RichContent', () => { } }) - expect(wrapper.html()).to.eql(compwrap(expected)) + expect(wrapper.html()).to.eql(compwrap(p( + mentionsLine(1), + ' how are you doing today?' + ))) }) - it('replaces mentions at the end of the hellpost if hideMentions=false (<p>)', () => { + it('replaces mentions at the end of the hellpost', () => { const html = [ p('How are you doing today, fine gentlemen?'), p( @@ -157,184 +104,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: false, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('removes mentions from the end of the hellpost (<br>)', () => { - const html = [ - 'How are you doing today, fine gentlemen?', - [ - makeMention('John'), - makeMention('Josh'), - makeMention('Jeremy') - ].join('') - ].join('<br>') - const expected = [ - 'How are you doing today, fine gentlemen?', - // TODO fix this extra line somehow? - '<br>' - ].join('') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('removes mentions from the end of the hellpost (\\n)', () => { - const html = [ - 'How are you doing today, fine gentlemen?', - [ - makeMention('John'), - makeMention('Josh'), - makeMention('Jeremy') - ].join('') - ].join('\n') - const expected = [ - 'How are you doing today, fine gentlemen?', - // TODO fix this extra line somehow? - '' - ].join('\n') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('Does not remove mentions in the middle or at the end of text string', () => { - const html = [ - [ - makeMention('Jack'), - 'let\'s meet up with ', - makeMention('Janet') - ].join(''), - [ - 'cc: ', - makeMention('John'), - makeMention('Josh'), - makeMention('Jeremy') - ].join('') - ].join('\n') - const expected = [ - [ - removedMentionSpan, - 'let\'s meet up with ', - stubMention('Janet') - ].join(''), - [ - 'cc: ', - stubMention('John'), - stubMention('Josh'), - stubMention('Jeremy') - ].join('') - ].join('\n') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('removes mentions from the end if there\'s only one first mention', () => { - const html = [ - p( - makeMention('Todd'), - 'so anyway you are wrong' - ), - p( - makeMention('Tom'), - makeMention('Trace'), - makeMention('Theodor') - ) - ].join('') - const expected = [ - p( - removedMentionSpan, - 'so anyway you are wrong' - ), - // TODO fix this extra line somehow? - p() - ].join('') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('does not remove mentions from the end if there\'s more than one first mention', () => { - const html = [ - p( - makeMention('Zacharie'), - makeMention('Zinaide'), - 'you guys have cool names, and so do these guys: ' - ), - p( - makeMention('Watson'), - makeMention('Wallace'), - makeMention('Wakamoto') - ) - ].join('') - const expected = [ - p( - removedMentionSpan, - removedMentionSpan, - 'you guys have cool names, and so do these guys: ' - ), - p( - lastMentions( - stubMention('Watson'), - stubMention('Wallace'), - stubMention('Wakamoto') - ) - ) - ].join('') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -362,7 +132,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: true, emoji: [], @@ -386,7 +156,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: true, emoji: [], @@ -406,7 +176,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [], @@ -427,7 +197,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [{ url: 'about:blank', shortcode: 'spurdo' }], @@ -444,7 +214,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [], @@ -464,7 +234,7 @@ describe('RichContent', () => { ].join('\n') const expected = [ '<span class="greentext">>quote</span>', - stubMention('lol'), + mentionsLine(1), '<span class="greentext">>quote</span>', '<span class="greentext">>quote</span>' ].join('\n') @@ -472,6 +242,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { + attentions, handleLinks: true, greentext: true, emoji: [], @@ -496,127 +267,14 @@ describe('RichContent', () => { const expected = [ 'Bruh', 'Bruh', - [ - stubMention('foo'), - stubMention('bar'), - stubMention('baz') - ].join(''), + mentionsLine(3), 'Bruh' ].join('<br>') const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('Don\'t remove last mention if it\'s the only one', () => { - const html = [ - 'Bruh', - 'Bruh', - makeMention('foo'), - makeMention('bar'), - makeMention('baz') - ].join('<br>') - const expected = [ - 'Bruh', - 'Bruh', - stubMention('foo'), - stubMention('bar'), - stubMention('baz') - ].join('<br>') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('Don\'t remove last mentions if there are more than one first mention - remove first instead', () => { - const html = [ - [ - makeMention('foo'), - makeMention('bar') - ].join(' '), - 'Bruh', - 'Bruh', - [ - makeMention('foo'), - makeMention('bar'), - makeMention('baz') - ].join(' ') - ].join('\n') - - const expected = [ - [ - removedMentionSpan, - removedMentionSpan, - 'Bruh' // Due to trim we remove extra newline - ].join(''), - 'Bruh', - lastMentions([ - stubMention('foo'), - stubMention('bar'), - stubMention('baz') - ].join(' ')) - ].join('\n') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('Remove last mentions if there\'s just one first mention - remove all', () => { - const html = [ - [ - makeMention('foo') - ].join(' '), - 'Bruh', - 'Bruh', - [ - makeMention('foo'), - makeMention('bar'), - makeMention('baz') - ].join(' ') - ].join('\n') - - const expected = [ - [ - removedMentionSpan, - 'Bruh' // Due to trim we remove extra newline - ].join(''), - 'Bruh\n' // Can't remove this one yet - ].join('\n') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -652,7 +310,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -664,53 +322,7 @@ describe('RichContent', () => { }) it('rich contents of a mention are handled properly', () => { - const html = [ - p( - 'Testing' - ), - p( - '<a href="lol" class="mention">', - '<span>', - 'https://</span>', - '<span>', - 'lol.tld/</span>', - '<span>', - '</span>', - '</a>' - ) - ].join('') - const expected = [ - p( - 'Testing' - ), - p( - '<mentionlink-stub url="lol" content="', - '<span>', - 'https://</span>', - '<span>', - 'lol.tld/</span>', - '<span>', - '</span>', - '">', - '</mentionlink-stub>' - ) - ].join('') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - hideMentions: false, - handleLinks: true, - greentext: true, - emoji: [], - html - } - }) - - expect(wrapper.html()).to.eql(compwrap(expected)) - }) - - it('rich contents of a mention in beginning are handled properly', () => { + attentions.push({ statusnet_profile_url: 'lol' }) const html = [ p( '<a href="lol" class="mention">', @@ -729,16 +341,19 @@ describe('RichContent', () => { const expected = [ p( '<span class="MentionsLine">', - '<mentionlink-stub content="', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', '<span>', 'https://</span>', '<span>', 'lol.tld/</span>', '<span>', '</span>', - '" url="lol" class="mention-link">', - '</mentionlink-stub>', - '<!---->', // v-if placeholder + '</a>', + ' ', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<!---->', // v-if placeholder, mentionsline's extra mentions and stuff '</span>' ), p( @@ -748,11 +363,8 @@ describe('RichContent', () => { const wrapper = mount(RichContent, { localVue, - stubs: { - MentionLink: true - }, propsData: { - hideMentions: false, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -796,7 +408,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: false, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -806,4 +418,63 @@ describe('RichContent', () => { expect(wrapper.html()).to.eql(compwrap(expected)) }) + + it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => { + const amount = 20 + + const onePost = p( + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + ' i just landed in l a where are you' + ) + + const TestComponent = { + template: ` + <div v-if="!vhtml"> + ${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)} + </div> + <div v-else="vhtml"> + ${new Array(amount).fill(`<div v-html="${onePost}"/>`)} + </div> + `, + props: ['handleLinks', 'attentions', 'vhtml'] + } + console.log(1) + + const ptest = (handleLinks, vhtml) => { + const t0 = performance.now() + + const wrapper = mount(TestComponent, { + localVue, + propsData: { + attentions, + handleLinks, + vhtml + } + }) + + const t1 = performance.now() + + wrapper.destroy() + + const t2 = performance.now() + + return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item` + } + + console.log(`${amount} items with links handling:`) + console.log(ptest(true)) + console.log(`${amount} items without links handling:`) + console.log(ptest(false)) + console.log(`${amount} items plain v-html:`) + console.log(ptest(false, true)) + }) }) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 8a5a6ef9..03fb32c9 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -244,35 +244,6 @@ describe('API Entities normalizer', () => { expect(parseUser(remote)).to.have.property('is_local', false) }) - it('adds emojis to user name', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('name_html').that.contains('<img') - }) - - it('adds emojis to user bio', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('description_html').that.contains('<img') - }) - - it('adds emojis to user profile fields', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('fields_html').to.be.an('array') - - const field = parsedUser.fields_html[0] - - expect(field).to.have.property('name').that.contains('<img') - expect(field).to.have.property('value').that.contains('<img') - }) - it('removes html tags from user profile fields', () => { const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] }) @@ -338,41 +309,6 @@ describe('API Entities normalizer', () => { }) }) - describe('MastoAPI emoji adder', () => { - const emojis = makeMockEmojiMasto() - const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />' - .replace(/"/g, '\'') - const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />' - .replace(/"/g, '\'') - - it('correctly replaces shortcodes in supplied string', () => { - const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis) - expect(result).to.include(thinkHtml) - expect(result).to.include(imageHtml) - }) - - it('handles consecutive emojis correctly', () => { - const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis) - expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml) - }) - - it('Doesn\'t replace nonexistent emojis', () => { - const result = addEmojis('Admin add the :tenshi: emoji', emojis) - expect(result).to.equal('Admin add the :tenshi: emoji') - }) - - it('Doesn\'t blow up on regex special characters', () => { - const emojis = makeMockEmojiMasto([{ - shortcode: 'c++' - }, { - shortcode: '[a-z] {|}*' - }]) - const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis) - expect(result).to.include('title=\':c++:\'') - expect(result).to.include('title=\':[a-z] {|}*:\'') - }) - }) - describe('Link header pagination', () => { it('Parses min and max ids as integers', () => { const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"' |
