From 2cfff1b8b9c642e9284483d6658312d9a3763417 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 12 Aug 2021 02:49:37 +0300 Subject: remove new options for style and separate line, now groups all chained mentions on a mentionsline regardless of placement. fixes spacing --- src/components/rich_content/rich_content.jsx | 128 ++++++--------------------- 1 file changed, 29 insertions(+), 99 deletions(-) (limited to 'src/components/rich_content/rich_content.jsx') diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index cd73f2e5..4b7d4d37 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -56,25 +56,21 @@ 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, this.handleLinks) + let currentMentions = null // Current chain of mentions, we group all mentions together + // to collapse too many mentions in a row + const lastTags = [] // Tags that appear at the end of post body const writtenMentions = [] // All mentions that appear in post body 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 { 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 - } else { - return '' - } + if (currentMentions === null) { + currentMentions = [] + } + currentMentions.push(linkData) + if (currentMentions.length === 1) { + return } else { - return + 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 +143,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 ? : '' - } - } 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 (attrs['class'].includes('h-card')) { + return ['', children.map(processItem), ''] + } } if (children !== undefined) { @@ -246,8 +230,6 @@ export default Vue.component('RichContent', { const event = { - firstMentions, - lastMentions, lastTags, writtenMentions, writtenTags @@ -284,15 +266,12 @@ export const preProcessPerLine = (html, greentext, handleLinks) => { const lastMentions = [] 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 ( @@ -316,42 +295,19 @@ 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') { + 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 } } @@ -359,33 +315,7 @@ export const preProcessPerLine = (html, greentext, handleLinks) => { // 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 ['', flattenDeep(result).join(''), ''].join('') - } else { - return flattenDeep(result).join('') - } + return flattenDeep(result).join('') }).reverse().join('') return { newHtml, lastMentions } -- cgit v1.2.3-70-g09d2 From add5921b8b3579b153bef6ee2b1916227016d200 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 12 Aug 2021 19:37:04 +0300 Subject: fix tests, add performance test (skipped, doesn't assert anything), tweak max mentions count --- src/components/mention_link/mention_link.js | 2 +- src/components/mentions_line/mentions_line.js | 2 +- src/components/rich_content/rich_content.jsx | 2 +- test/unit/specs/components/rich_content.spec.js | 126 +++++++++++++----------- 4 files changed, 72 insertions(+), 60 deletions(-) (limited to 'src/components/rich_content/rich_content.jsx') diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index a60a8040..4d27fe6d 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -41,7 +41,7 @@ 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??? diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js index 33e25391..83eeea4c 100644 --- a/src/components/mentions_line/mentions_line.js +++ b/src/components/mentions_line/mentions_line.js @@ -15,7 +15,7 @@ const MentionsLine = { }, computed: { limit () { - return 2 + return 5 }, mentionsComputed () { return this.mentions.slice(0, this.limit) diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 4b7d4d37..32b737ec 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -159,7 +159,7 @@ export default Vue.component('RichContent', { return item // We'll handle it later } case 'span': - if (attrs['class'].includes('h-card')) { + if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { return ['', children.map(processItem), ''] } } diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js index dfc229c0..b29edeab 100644 --- a/test/unit/specs/components/rich_content.spec.js +++ b/test/unit/specs/components/rich_content.spec.js @@ -56,7 +56,7 @@ describe('RichContent', () => { expect(wrapper.html()).to.eql(compwrap(expected)) }) - it('replaces first mention with mentionsline', () => { + it('replaces mention with mentionsline', () => { const html = p( makeMention('John'), ' how are you doing today?' @@ -234,7 +234,7 @@ describe('RichContent', () => { ].join('\n') const expected = [ '>quote', - mentionsLine(1) + mentionsLine(1), '>quote', '>quote' ].join('\n') @@ -267,7 +267,7 @@ describe('RichContent', () => { const expected = [ 'Bruh', 'Bruh', - mentionsLine(3) + mentionsLine(3), 'Bruh' ].join('
') @@ -322,53 +322,6 @@ describe('RichContent', () => { }) it('rich contents of a mention are handled properly', () => { - const html = [ - p( - 'Testing' - ), - p( - '', - '', - 'https://', - '', - 'lol.tld/', - '', - '', - '' - ) - ].join('') - const expected = [ - p( - 'Testing' - ), - p( - '', - '' - ) - ].join('') - - const wrapper = shallowMount(RichContent, { - localVue, - propsData: { - attentions, - 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( @@ -388,16 +341,19 @@ describe('RichContent', () => { const expected = [ p( '', - '', + '', '', 'https://', '', 'lol.tld/', '', '', - '" url="lol" class="mention-link">', - '', - '', // v-if placeholder + '', + ' ', + '', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '', + '', // v-if placeholder, mentionsline's extra mentions and stuff '' ), p( @@ -407,9 +363,6 @@ describe('RichContent', () => { const wrapper = mount(RichContent, { localVue, - stubs: { - MentionLink: true - }, propsData: { attentions, handleLinks: true, @@ -465,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: ` +
+ ${new Array(amount).fill(``)} +
+
+ ${new Array(amount).fill(`
`)} +
+ `, + 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)) + }) }) -- cgit v1.2.3-70-g09d2 From 530ac4442b498c90c73533d2a03ae5b7d6875900 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 15 Aug 2021 02:41:53 +0300 Subject: removed useless code, review change, fixed bug with tall statuses --- src/components/mention_link/mention_link.js | 2 +- src/components/mentions_line/mentions_line.js | 9 ++-- src/components/rich_content/rich_content.jsx | 69 +++++++++++---------------- src/components/status_body/status_body.js | 16 +++++-- 4 files changed, 45 insertions(+), 51 deletions(-) (limited to 'src/components/rich_content/rich_content.jsx') diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 4d27fe6d..65c62baa 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -45,7 +45,7 @@ const MentionLink = { }, 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] diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js index 83eeea4c..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,14 +16,11 @@ const MentionsLine = { MentionLink }, computed: { - limit () { - return 5 - }, 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 diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 32b737ec..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 -s - * - Removes mentions from beginning and end (hellthread style only) + * - Groups all mentions into , this affects all mentions regardles + * of where they are (beginning/middle/end), even single mentions are converted + * to a containing single . * - Replaces emoji shortcodes with 'd images. * - * Stuff like removing mentions from beginning and end is done so that they could - * be either replaced by collapsible 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. @@ -61,12 +59,13 @@ export default Vue.component('RichContent', { // NEVER EVER TOUCH DATA INSIDE RENDER render (h) { // Pre-process HTML - const { newHtml: html } = preProcessPerLine(this.html, this.greentext, this.handleLinks) + const { newHtml: html } = preProcessPerLine(this.html, this.greentext) let currentMentions = null // Current chain of mentions, we group all mentions together - // to collapse too many mentions in a row 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 @@ -99,6 +98,9 @@ export default Vue.component('RichContent', { currentMentions = [] } currentMentions.push(linkData) + if (currentMentions.length > MENTIONS_LIMIT) { + invisibleMentions.push(linkData) + } if (currentMentions.length === 1) { return } else { @@ -232,7 +234,8 @@ export default Vue.component('RichContent', { const event = { lastTags, writtenMentions, - writtenTags + writtenTags, + invisibleMentions } // DO NOT MOVE TO UPDATE. BAD IDEA. @@ -243,27 +246,32 @@ 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']) const lines = convertHtmlToLines(html) @@ -277,7 +285,7 @@ export const preProcessPerLine = (html, greentext, handleLinks) => { 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('<')) @@ -292,31 +300,8 @@ export const preProcessPerLine = (html, greentext, handleLinks) => { } } - // Converting that line part into tree - const tree = convertHtmlToTree(string) - - const process = (item) => { - if (Array.isArray(item)) { - const [opener, children, closer] = item - const tag = getTagName(opener) - if (tag === 'span' || tag === 'p') { - // For span and p we need to go deeper - return [opener, [...children].map(process), closer] - } else { - return [opener, children, closer] - } - } - - if (typeof item === 'string') { - return item - } - } - - // We now processed our tree, now we need to mark line as lastMentions - const result = [...tree].map(process) - - return flattenDeep(result).join('') + return string }).reverse().join('') - return { newHtml, lastMentions } + return { newHtml } } diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js index 8757154d..b941765c 100644 --- a/src/components/status_body/status_body.js +++ b/src/components/status_body/status_body.js @@ -32,7 +32,8 @@ const StatusContent = { 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 } }, computed: { @@ -47,7 +48,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(/ 20 }, longSubject () { @@ -86,7 +87,7 @@ const StatusContent = { methods: { onParseReady (event) { this.$emit('parseReady', event) - const { writtenMentions } = event + const { writtenMentions, invisibleMentions } = event writtenMentions .filter(mention => !mention.notifying) .forEach(mention => { @@ -97,6 +98,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) { -- cgit v1.2.3-70-g09d2