logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: f883d2f75cd3c404115bd2c98b6d3c8d7ff10ef6
parent b84aeff6bf288b6e5855c2be0fd78577a0f7c0e5
Author: Henry Jameson <me@hjkos.com>
Date:   Fri, 11 Jun 2021 03:11:58 +0300

better handling of hellthreads with mentions at bottom

Diffstat:

Msrc/components/mention_link/mention_link.js6------
Msrc/components/mention_link/mention_link.scss4----
Msrc/components/mentions_line/mentions_line.scss1+
Msrc/components/rich_content/rich_content.jsx198+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/components/status/status.js16+++++++---------
Msrc/components/status/status.vue6+++---
Msrc/components/status_body/status_body.js24++++++++++--------------
Msrc/components/status_body/status_body.vue13+++++++------
Msrc/components/status_content/status_content.js7++++---
Msrc/components/status_content/status_content.vue5++---
10 files changed, 138 insertions(+), 142 deletions(-)

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js @@ -28,11 +28,6 @@ const MentionLink = { userScreenName: { required: false, type: String - }, - firstMention: { - required: false, - type: Boolean, - default: false } }, methods: { @@ -89,7 +84,6 @@ const MentionLink = { { '-you': this.isYou, '-highlighted': this.highlight, - '-firstMention': this.firstMention, '-oldStyle': this.oldStyle }, this.highlightType diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss @@ -38,10 +38,6 @@ .new { margin-right: 0.25em; - &.-firstMention { - display: none; - } - &.-you { & .shortName, & .full { diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss @@ -1,5 +1,6 @@ .MentionsLine { .showMoreLess { + white-space: normal; &.-newStyle { line-height: 1.5; font-size: inherit; diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx @@ -33,22 +33,23 @@ export default Vue.component('RichContent', { default: false }, // Whether to hide last mentions (hellthreads) - hideLastMentions: { - required: false, - type: Boolean, - default: false - }, - // Whether to hide first mentions - hideFirstMentions: { + hideMentions: { required: false, type: Boolean, default: false } }, + // NEVER EVER TOUCH DATA INSIDE RENDER render (h) { // Pre-process HTML - const html = preProcessPerLine(this.html, this.greentext, this.hideLastMentions) - console.log(this.hideFirstMentions, this.hideLastMentions) + const { newHtml: html, lastMentions } = preProcessPerLine(this.html, this.greentext, this.hideLastMentions) + const firstMentions = [] // Mentions that appear in the beginning of post body + 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 const renderImage = (tag) => { return <StillImage @@ -57,20 +58,37 @@ export default Vue.component('RichContent', { /> } + const renderHashtag = (attrs, children, encounteredTextReverse) => { + const linkData = getLinkData(attrs, children, tagsIndex++) + writtenTags.push(linkData) + attrs.target = '_blank' + if (!encounteredTextReverse) { + lastTags.push(linkData) + attrs['data-parser-last'] = true + } + return <a {...{ attrs }}> + { children.map(processItem) } + </a> + } + const renderMention = (attrs, children, encounteredText) => { - return (this.hideFirstMentions && !encounteredText) - ? '' - : <MentionLink + const linkData = getLinkData(attrs, children, mentionIndex++) + writtenMentions.push(linkData) + if (!encounteredText) { + firstMentions.push(linkData) + return '' + } else { + return <MentionLink url={attrs.href} content={flattenDeep(children).join('')} - firstMention={!encounteredText} /> + } } // We stop treating mentions as "first" ones when we encounter // non-whitespace text let encounteredText = false - // Processor to use with mini_html_converter + // Processor to use with html_tree_converter const processItem = (item, index, array, what) => { // Handle text nodes - just add emoji if (typeof item === 'string') { @@ -104,12 +122,22 @@ export default Vue.component('RichContent', { if (Array.isArray(item)) { const [opener, children] = item const Tag = getTagName(opener) + const attrs = getAttrs(opener) switch (Tag) { + case 'span': // replace images with StillImage + if (attrs['class'] && attrs['class'].includes('lastMentions')) { + if (firstMentions.length > 0) { + break + } else { + return '' + } + } else { + break + } case 'img': // replace images with StillImage return renderImage(opener) case 'a': // replace mentions with MentionLink if (!this.handleLinks) break - const attrs = getAttrs(opener) if (attrs['class'] && attrs['class'].includes('mention')) { return renderMention(attrs, children, encounteredText) } else if (attrs['class'] && attrs['class'].includes('hashtag')) { @@ -132,17 +160,9 @@ export default Vue.component('RichContent', { } } } + // Processor for back direction (for finding "last" stuff, just easier this way) let encounteredTextReverse = false - const renderHashtag = (attrs, children, encounteredTextReverse) => { - attrs.target = '_blank' - if (!encounteredTextReverse) { - attrs['data-parser-last'] = true - } - return <a {...{ attrs }}> - { children.map(processItem) } - </a> - } const processItemReverse = (item, index, array, what) => { // Handle text nodes - just add emoji if (typeof item === 'string') { @@ -166,14 +186,37 @@ export default Vue.component('RichContent', { } return item } - return <span class="RichContent"> + + const event = { + firstMentions, + lastMentions, + lastTags, + writtenMentions, + writtenTags + } + + const result = <span class="RichContent"> { this.$slots.prefix } { convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() } { this.$slots.suffix } </span> + + // DO NOT MOVE TO UPDATE. BAD IDEA. + this.$emit('parseReady', event) + + return result } }) +const getLinkData = (attrs, children, index) => { + return { + index, + url: attrs.href, + hashtag: attrs['data-tag'], + content: flattenDeep(children).join('') + } +} + /** Pre-processing HTML * * Currently this does two things: @@ -183,13 +226,13 @@ export default Vue.component('RichContent', { * * @param {String} html - raw HTML to process * @param {Boolean} greentext - whether to enable greentexting or not - * @param {Boolean} removeLastMentions - whether to remove last mentions */ -export const preProcessPerLine = (html, greentext, removeLastMentions) => { - // Only mark first (last) encounter - let lastMentionsMarked = false +export const preProcessPerLine = (html, greentext) => { + const lastMentions = [] - return convertHtmlToLines(html).reverse().map((item, index, array) => { + const newHtml = convertHtmlToLines(html).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 @@ -205,6 +248,7 @@ export const preProcessPerLine = (html, greentext, removeLastMentions) => { } } + // Converting that line part into tree const tree = convertHtmlToTree(string) // If line has loose text, i.e. text outside a mention or a tag @@ -215,18 +259,23 @@ export const preProcessPerLine = (html, greentext, removeLastMentions) => { 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') { const attrs = getAttrs(opener) if (attrs['class'] && attrs['class'].includes('mention')) { + // Got mentions hasMentions = true 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') { - return [opener, [...children].reverse().map(process).reverse(), closer] + // 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] } @@ -234,82 +283,43 @@ export const preProcessPerLine = (html, greentext, removeLastMentions) => { if (typeof item === 'string') { if (item.trim() !== '') { + // only meaningful strings are loose text hasLooseText = true } return item } } - const result = [...tree].reverse().map(process).reverse() + // We now processed our tree, now we need to mark line as lastMentions + const result = [...tree].map(process) - if (removeLastMentions && hasMentions && !hasLooseText && !lastMentionsMarked) { - lastMentionsMarked = true - return '' + // Only check last (first since list is reversed) line + if (hasMentions && !hasLooseText && index === 0) { + 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('') } }).reverse().join('') + + return { newHtml, lastMentions } } export const getHeadTailLinks = (html) => { // Exported object properties - const firstMentions = [] // Mentions that appear in the beginning of post body - const lastMentions = [] // Mentions that appear at the end of post body - 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 - - let encounteredText = false - let processingFirstMentions = true - let index = 0 // unique index for vue "tag" property - - const getLinkData = (attrs, children, index) => { - return { - index, - url: attrs.href, - hashtag: attrs['data-tag'], - content: flattenDeep(children).join('') - } - } - - // Processor to use with html_tree_converter - const processItem = (item) => { - // Handle text nodes - stop treating mentions as "first" when text encountered - if (typeof item === 'string') { - const emptyText = item.trim() === '' - if (emptyText) return - if (!encounteredText) { - encounteredText = true - processingFirstMentions = false - } - // Encountered text? That means tags we've been collectings aren't "last"! - lastTags.splice(0) - lastMentions.splice(0) - return - } - // Handle tag nodes - if (Array.isArray(item)) { - const [opener, children] = item - const Tag = getTagName(opener) - if (Tag !== 'a') return children && children.forEach(processItem) - const attrs = getAttrs(opener) - if (attrs['class']) { - const linkData = getLinkData(attrs, children, index++) - if (attrs['class'].includes('mention')) { - if (processingFirstMentions) { - firstMentions.push(linkData) - } - writtenMentions.push(linkData) - lastMentions.push(linkData) - } else if (attrs['class'].includes('hashtag')) { - lastTags.push(linkData) - writtenTags.push(linkData) - } - return // Stop processing, we don't care about link's contents - } - children && children.forEach(processItem) - } - } - convertHtmlToTree(html).forEach(processItem) - return { firstMentions, writtenMentions, writtenTags, lastTags, lastMentions } } diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -19,7 +19,6 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' import { unescape, uniqBy } from 'lodash' -import { getHeadTailLinks } from 'src/components/rich_content/rich_content.jsx' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -101,7 +100,8 @@ const Status = { userExpanded: false, mediaPlaying: [], suspendable: true, - error: null + error: null, + headTailLinks: null } }, computed: { @@ -168,9 +168,6 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, - headTailLinks () { - return getHeadTailLinks(this.status.raw_html) - }, mentions () { return this.status.attentions.filter(attn => { return attn.screen_name !== this.replyToName && @@ -182,6 +179,7 @@ const Status = { })) }, 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) @@ -196,9 +194,6 @@ const Status = { hasMentionsLine () { return this.mentionsLine.length > 0 }, - hideLastMentions () { - return this.headTailLinks.firstMentions.length === 0 - }, muted () { if (this.statusoid.user.id === this.currentUser.id) return false const { status } = this @@ -346,6 +341,9 @@ const Status = { }, removeMediaPlaying (id) { this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) + }, + setHeadTailLinks (headTailLinks) { + this.headTailLinks = headTailLinks } }, watch: { @@ -356,7 +354,7 @@ const Status = { // Post is above screen, match its top to screen top window.scrollBy(0, rect.top - 100) } else if (rect.height >= (window.innerHeight - 50)) { - // Post we want to see is taller than screen so match its top to screen top + // Post we wahttp://localhost:8080/users/hj/dmsnt to see is taller than screen so match its top to screen top window.scrollBy(0, rect.top - 100) } else if (rect.bottom > window.innerHeight - 50) { // Post is below screen, match its bottom to screen bottom diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -305,11 +305,11 @@ :no-heading="noHeading" :highlight="highlight" :focused="isFocused" - :hide-first-mentions="mentionsOwnLine && isReply" - :hide-last-mentions="hideLastMentions" - :head-tail-links="headTailLinks" + :hide-mentions="mentionsOwnLine && (isReply || true)" @mediaplay="addMediaPlaying($event)" @mediapause="removeMediaPlaying($event)" + @parseReady="setHeadTailLinks" + ref="content" /> <div diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js @@ -3,6 +3,7 @@ import RichContent, { getHeadTailLinks } from 'src/components/rich_content/rich_ import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' +import { set } from 'vue' import { faFile, faMusic, @@ -27,11 +28,7 @@ const StatusContent = { 'noHeading', 'fullContent', 'singleLine', - // if this was computed at upper level it can be passed here, otherwise - // it will be in this component - 'headTailLinks', - 'hideFirstMentions', - 'hideLastMentions' + 'hideMentions' ], data () { return { @@ -39,9 +36,9 @@ const StatusContent = { showingLongSubject: false, // not as computed because it sets the initial state which will be changed later expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, - headTailLinksComputed: this.headTailLinks - ? this.headTailLinks - : getHeadTailLinks(this.status.raw_html) + headTailLinks: null, + firstMentions: [], + lastMentions: [] } }, computed: { @@ -81,12 +78,6 @@ const StatusContent = { attachmentTypes () { return this.status.attachments.map(file => fileType.fileType(file.mimetype)) }, - mentionsFirst () { - return this.headTailLinksComputed.firstMentions - }, - mentionsLast () { - return this.headTailLinksComputed.lastMentions - }, ...mapGetters(['mergedConfig']) }, components: { @@ -107,6 +98,11 @@ const StatusContent = { this.expandingSubject = !this.expandingSubject } }, + setHeadTailLinks (headTailLinks) { + set(this, 'headTailLinks', headTailLinks) + set(this, 'firstMentions', headTailLinks.firstMentions) + set(this, 'lastMentions', headTailLinks.lastMentions) + }, generateTagLink (tag) { return `/tag/${tag}` } diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue @@ -48,20 +48,21 @@ :html="status.raw_html" :emoji="status.emojis" :handle-links="true" + :hide-mentions="hideMentions" :greentext="mergedConfig.greentext" - :hide-first-mentions="hideFirstMentions" - :hide-last-mentions="hideLastMentions" + @parseReady="setHeadTailLinks" + ref="text" > <template v-slot:prefix> <MentionsLine - v-if="!hideFirstMentions && mentionsFirst" - :mentions="mentionsFirst" + v-if="!hideMentions && firstMentions && firstMentions.length > 0" + :mentions="firstMentions" /> </template> <template v-slot:suffix> <MentionsLine - v-if="!hideFirstMentions && mentionsLast" - :mentions="mentionsLast" + v-if="!hideMentions && lastMentions.length > 0 && firstMentions.length === 0" + :mentions="lastMentions" /> </template> </RichContent> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -32,9 +32,7 @@ const StatusContent = { 'noHeading', 'fullContent', 'singleLine', - 'hideFirstMentions', - 'hideLastMentions', - 'headTailLinks' + 'hideMentions' ], computed: { hideAttachments () { @@ -94,6 +92,9 @@ const StatusContent = { StatusBody }, methods: { + setHeadTailLinks (headTailLinks) { + this.$emit('parseReady', headTailLinks) + }, setMedia () { const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments return () => this.$store.dispatch('setMedia', attachments) diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -4,9 +4,8 @@ <StatusBody :status="status" :single-line="singleLine" - :hide-first-mentions="hideFirstMentions" - :hide-last-mentions="hideLastMentions" - :head-tail-links="headTailLinks" + :hide-mentions="hideMentions" + @parseReady="setHeadTailLinks" > <div v-if="status.poll && status.poll.options"> <poll :base-poll="status.poll" />