From 20ce6468520e76b0fb2931a5fac368157d950b1d Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 03:14:48 +0300
Subject: [WIP] MUCH better approach to replacing emojis with still versions

---
 src/components/status_content/status_content.vue | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 90bfaf40..c1a78db9 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,5 +1,4 @@
 <template>
-  <!-- eslint-disable vue/no-v-html -->
   <div class="StatusContent">
     <slot name="header" />
     <div
@@ -7,11 +6,11 @@
       class="summary-wrapper"
       :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
     >
-      <div
+      <RichContent
         class="media-body summary"
         @click.prevent="linkClicked"
-        v-html="status.summary_html"
-      />
+        :html="status.summary_raw_html"
+        :emoji="status.emojis"/>
       <button
         v-if="longSubject && showingLongSubject"
         class="button-unstyled -link tall-subject-hider"
@@ -40,13 +39,13 @@
       >
         {{ $t("general.show_more") }}
       </button>
-      <div
+      <RichContent
         v-if="!hideSubjectStatus"
         :class="{ 'single-line': singleLine }"
         class="status-content media-body"
         @click.prevent="linkClicked"
-        v-html="postBodyHtml"
-      />
+        :html="postBodyHtml"
+        :emoji="status.emojis"/>
       <button
         v-if="hideSubjectStatus"
         class="button-unstyled -link cw-status-hider"
@@ -127,7 +126,6 @@
     </div>
     <slot name="footer" />
   </div>
-  <!-- eslint-enable vue/no-v-html -->
 </template>
 
 <script src="./status_content.js" ></script>
-- 
cgit v1.2.3-70-g09d2


From 35dedf8416b423abb867b052d37f14becd0de34a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 12:24:11 +0300
Subject: lint

---
 src/components/emoji_input/emoji_input.vue       |  2 +-
 src/components/status_content/status_content.vue | 10 ++++++----
 2 files changed, 7 insertions(+), 5 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index e6f9a9d3..aa2950ce 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -1,9 +1,9 @@
 <template>
   <div
+    ref="root"
     v-click-outside="onClickOutside"
     class="emoji-input"
     :class="{ 'with-picker': !hideEmojiButton }"
-    ref='root'
   >
     <slot />
     <template v-if="enableEmojiPicker">
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index c1a78db9..fa095d43 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -8,9 +8,10 @@
     >
       <RichContent
         class="media-body summary"
-        @click.prevent="linkClicked"
         :html="status.summary_raw_html"
-        :emoji="status.emojis"/>
+        :emoji="status.emojis"
+        @click.prevent="linkClicked"
+      />
       <button
         v-if="longSubject && showingLongSubject"
         class="button-unstyled -link tall-subject-hider"
@@ -43,9 +44,10 @@
         v-if="!hideSubjectStatus"
         :class="{ 'single-line': singleLine }"
         class="status-content media-body"
-        @click.prevent="linkClicked"
         :html="postBodyHtml"
-        :emoji="status.emojis"/>
+        :emoji="status.emojis"
+        @click.prevent="linkClicked"
+      />
       <button
         v-if="hideSubjectStatus"
         class="button-unstyled -link cw-status-hider"
-- 
cgit v1.2.3-70-g09d2


From a2459c2187bc051c18715591119a48ccb0f308e7 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 12:49:54 +0300
Subject: move styles to richcontent

---
 src/components/rich_content/rich_content.jsx     |  9 +--
 src/components/rich_content/rich_content.scss    | 70 ++++++++++++++++++++++++
 src/components/status_content/status_content.vue | 55 +------------------
 3 files changed, 76 insertions(+), 58 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index a5c010de..7bff23cd 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -19,7 +19,8 @@ export default Vue.component('RichContent', {
   },
   render (h) {
     const renderImage = (tag) => {
-      return <StillImage {...{ attrs: getAttrs(tag) }} />
+      const attrs = getAttrs(tag)
+      return <StillImage {...{ attrs }} class="img"/>
     }
     const structure = convertHtml(this.html)
     const processItem = (item) => {
@@ -30,7 +31,7 @@ export default Vue.component('RichContent', {
             this.emoji,
             ({ shortcode, url }) => {
               return <StillImage
-                class="emoji"
+                class="emoji img"
                 src={url}
                 title={`:${shortcode}:`}
                 alt={`:${shortcode}:`}
@@ -56,8 +57,8 @@ export default Vue.component('RichContent', {
         }
       }
     }
-    return <div>
+    return <span class="RichContent">
       { structure.map(processItem) }
-    </div>
+    </span>
   }
 })
diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss
index e69de29b..2fcd3911 100644
--- a/src/components/rich_content/rich_content.scss
+++ b/src/components/rich_content/rich_content.scss
@@ -0,0 +1,70 @@
+.RichContent {
+  font-family: var(--postFont, sans-serif);
+  line-height: 1.4em;
+  white-space: pre-wrap;
+  overflow-wrap: break-word;
+  word-wrap: break-word;
+  word-break: break-word;
+
+  blockquote {
+    margin: 0.2em 0 0.2em 2em;
+    font-style: italic;
+  }
+
+  pre {
+    overflow: auto;
+  }
+
+  code,
+  samp,
+  kbd,
+  var,
+  pre {
+    font-family: var(--postCodeFont, monospace);
+  }
+
+  p {
+    margin: 0 0 1em 0;
+  }
+
+  p:last-child {
+    margin: 0 0 0 0;
+  }
+
+  h1 {
+    font-size: 1.1em;
+    line-height: 1.2em;
+    margin: 1.4em 0;
+  }
+
+  h2 {
+    font-size: 1.1em;
+    margin: 1em 0;
+  }
+
+  h3 {
+    font-size: 1em;
+    margin: 1.2em 0;
+  }
+
+  h4 {
+    margin: 1.1em 0;
+  }
+
+  .img {
+    display: inline-block;
+  }
+
+  .emoji {
+    width: 32px;
+    height: 32px;
+  }
+
+  .img,
+  video {
+    max-width: 100%;
+    max-height: 400px;
+    vertical-align: middle;
+    object-fit: contain;
+  }
+}
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index fa095d43..31cd5a2e 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -185,16 +185,11 @@ $status-margin: 0.75em;
     }
   }
 
-  img, video {
+  video {
     max-width: 100%;
     max-height: 400px;
     vertical-align: middle;
     object-fit: contain;
-
-    &.emoji {
-      width: 32px;
-      height: 32px;
-    }
   }
 
   .summary-wrapper {
@@ -230,54 +225,6 @@ $status-margin: 0.75em;
   }
 
   .status-content {
-    font-family: var(--postFont, sans-serif);
-    line-height: 1.4em;
-    white-space: pre-wrap;
-    overflow-wrap: break-word;
-    word-wrap: break-word;
-    word-break: break-word;
-
-    blockquote {
-      margin: 0.2em 0 0.2em 2em;
-      font-style: italic;
-    }
-
-    pre {
-      overflow: auto;
-    }
-
-    code, samp, kbd, var, pre {
-      font-family: var(--postCodeFont, monospace);
-    }
-
-    p {
-      margin: 0 0 1em 0;
-    }
-
-    p:last-child {
-      margin: 0 0 0 0;
-    }
-
-    h1 {
-      font-size: 1.1em;
-      line-height: 1.2em;
-      margin: 1.4em 0;
-    }
-
-    h2 {
-      font-size: 1.1em;
-      margin: 1.0em 0;
-    }
-
-    h3 {
-      font-size: 1em;
-      margin: 1.2em 0;
-    }
-
-    h4 {
-      margin: 1.1em 0;
-    }
-
     &.single-line {
       white-space: nowrap;
       text-overflow: ellipsis;
-- 
cgit v1.2.3-70-g09d2


From 50aa379038771da3725e6864c40007ae12896232 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 18:41:55 +0300
Subject: new component - StatusText, to separate post's text from its
 attachments

---
 src/components/status_content/status_content.js  | 127 +-----------------
 src/components/status_content/status_content.vue | 157 ++++++-----------------
 src/components/status_text/status_text.js        | 147 +++++++++++++++++++++
 src/components/status_text/status_text.vue       |  95 ++++++++++++++
 4 files changed, 280 insertions(+), 246 deletions(-)
 create mode 100644 src/components/status_text/status_text.js
 create mode 100644 src/components/status_text/status_text.vue

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index c21afc24..67323b8a 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -1,12 +1,9 @@
 import Attachment from '../attachment/attachment.vue'
 import Poll from '../poll/poll.vue'
 import Gallery from '../gallery/gallery.vue'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
+import StatusText from 'src/components/status_text/status_text.vue'
 import LinkPreview from '../link-preview/link-preview.vue'
-import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import fileType from 'src/services/file_type/file_type.service'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
-import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
 import { mapGetters, mapState } from 'vuex'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -36,52 +33,11 @@ const StatusContent = {
     'fullContent',
     '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
-    }
-  },
   computed: {
-    localCollapseSubjectDefault () {
-      return this.mergedConfig.collapseMessageWithSubject
-    },
     hideAttachments () {
       return (this.mergedConfig.hideAttachments && !this.inConversation) ||
         (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
     },
-    // This is a bit hacky, but we want to approximate post height before rendering
-    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
-    // as well as approximate line count by counting characters and approximating ~80
-    // per line.
-    //
-    // 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.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
-      return lengthScore > 20
-    },
-    longSubject () {
-      return this.status.summary.length > 240
-    },
-    // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
-    mightHideBecauseSubject () {
-      return !!this.status.summary && this.localCollapseSubjectDefault
-    },
-    mightHideBecauseTall () {
-      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
-    },
-    hideSubjectStatus () {
-      return this.mightHideBecauseSubject && !this.expandingSubject
-    },
-    hideTallStatus () {
-      return this.mightHideBecauseTall && !this.showingTall
-    },
-    showingMore () {
-      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
-    },
     nsfwClickthrough () {
       if (!this.status.nsfw) {
         return false
@@ -119,45 +75,11 @@ const StatusContent = {
         file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
       )
     },
-    attachmentTypes () {
-      return this.status.attachments.map(file => fileType.fileType(file.mimetype))
-    },
     maxThumbnails () {
       return this.mergedConfig.maxThumbnails
     },
-    postBodyHtml () {
-      const html = this.status.raw_html
-
-      if (this.mergedConfig.greentext) {
-        try {
-          if (html.includes('&gt;')) {
-            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
-            return processHtml(html, (string) => {
-              if (string.includes('&gt;') &&
-                  string
-                    .replace(/<[^>]+?>/gi, '') // remove all tags
-                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
-                    .trim()
-                    .startsWith('&gt;')) {
-                return `<span class='greentext'>${string}</span>`
-              } else {
-                return string
-              }
-            })
-          } else {
-            return html
-          }
-        } catch (e) {
-          console.err('Failed to process status html', e)
-          return html
-        }
-      } else {
-        return html
-      }
-    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
-      betterShadow: state => state.interface.browserSupport.cssFilter,
       currentUser: state => state.users.currentUser
     })
   },
@@ -166,54 +88,9 @@ const StatusContent = {
     Poll,
     Gallery,
     LinkPreview,
-    RichContent
-  },
-  mounted () {
-    this.status.attentions.forEach(attn => {
-      const { id } = attn
-      this.$store.dispatch('fetchUserIfMissing', id)
-    })
+    StatusText
   },
   methods: {
-    linkClicked (event) {
-      const target = event.target.closest('.status-content a')
-      if (target) {
-        if (target.className.match(/mention/)) {
-          const href = target.href
-          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
-          if (attn) {
-            event.stopPropagation()
-            event.preventDefault()
-            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
-            this.$router.push(link)
-            return
-          }
-        }
-        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
-          // Extract tag name from dataset or link url
-          const tag = target.dataset.tag || extractTagFromUrl(target.href)
-          if (tag) {
-            const link = this.generateTagLink(tag)
-            this.$router.push(link)
-            return
-          }
-        }
-        window.open(target.href, '_blank')
-      }
-    },
-    toggleShowMore () {
-      if (this.mightHideBecauseTall) {
-        this.showingTall = !this.showingTall
-      } else if (this.mightHideBecauseSubject) {
-        this.expandingSubject = !this.expandingSubject
-      }
-    },
-    generateUserProfileLink (id, name) {
-      return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
-    },
-    generateTagLink (tag) {
-      return `/tag/${tag}`
-    },
     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
index 31cd5a2e..7389244a 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,131 +1,46 @@
 <template>
   <div class="StatusContent">
     <slot name="header" />
-    <div
-      v-if="status.summary_html"
-      class="summary-wrapper"
-      :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
-    >
-      <RichContent
-        class="media-body summary"
-        :html="status.summary_raw_html"
-        :emoji="status.emojis"
-        @click.prevent="linkClicked"
-      />
-      <button
-        v-if="longSubject && showingLongSubject"
-        class="button-unstyled -link tall-subject-hider"
-        @click.prevent="showingLongSubject=false"
+    <StatusText :status="status">
+      <div v-if="status.poll && status.poll.options">
+        <poll :base-poll="status.poll" />
+      </div>
+
+      <div
+        v-if="status.attachments.length !== 0"
+        class="attachments media-body"
       >
-        {{ $t("status.hide_full_subject") }}
-      </button>
-      <button
-        v-else-if="longSubject"
-        class="button-unstyled -link tall-subject-hider"
-        :class="{ 'tall-subject-hider_focused': focused }"
-        @click.prevent="showingLongSubject=true"
-      >
-        {{ $t("status.show_full_subject") }}
-      </button>
-    </div>
-    <div
-      :class="{'tall-status': hideTallStatus}"
-      class="status-content-wrapper"
-    >
-      <button
-        v-if="hideTallStatus"
-        class="button-unstyled -link tall-status-hider"
-        :class="{ 'tall-status-hider_focused': focused }"
-        @click.prevent="toggleShowMore"
-      >
-        {{ $t("general.show_more") }}
-      </button>
-      <RichContent
-        v-if="!hideSubjectStatus"
-        :class="{ 'single-line': singleLine }"
-        class="status-content media-body"
-        :html="postBodyHtml"
-        :emoji="status.emojis"
-        @click.prevent="linkClicked"
-      />
-      <button
-        v-if="hideSubjectStatus"
-        class="button-unstyled -link cw-status-hider"
-        @click.prevent="toggleShowMore"
-      >
-        {{ $t("status.show_content") }}
-        <FAIcon
-          v-if="attachmentTypes.includes('image')"
-          icon="image"
-        />
-        <FAIcon
-          v-if="attachmentTypes.includes('video')"
-          icon="video"
-        />
-        <FAIcon
-          v-if="attachmentTypes.includes('audio')"
-          icon="music"
+        <attachment
+          v-for="attachment in nonGalleryAttachments"
+          :key="attachment.id"
+          class="non-gallery"
+          :size="attachmentSize"
+          :nsfw="nsfwClickthrough"
+          :attachment="attachment"
+          :allow-play="true"
+          :set-media="setMedia()"
+          @play="$emit('mediaplay', attachment.id)"
+          @pause="$emit('mediapause', attachment.id)"
         />
-        <FAIcon
-          v-if="attachmentTypes.includes('unknown')"
-          icon="file"
+        <gallery
+          v-if="galleryAttachments.length > 0"
+          :nsfw="nsfwClickthrough"
+          :attachments="galleryAttachments"
+          :set-media="setMedia()"
         />
-        <FAIcon
-          v-if="status.poll && status.poll.options"
-          icon="poll-h"
-        />
-        <FAIcon
-          v-if="status.card"
-          icon="link"
-        />
-      </button>
-      <button
-        v-if="showingMore && !fullContent"
-        class="button-unstyled -link status-unhider"
-        @click.prevent="toggleShowMore"
+      </div>
+
+      <div
+        v-if="status.card && !noHeading"
+        class="link-preview media-body"
       >
-        {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
-      </button>
-    </div>
-
-    <div v-if="status.poll && status.poll.options && !hideSubjectStatus">
-      <poll :base-poll="status.poll" />
-    </div>
-
-    <div
-      v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
-      class="attachments media-body"
-    >
-      <attachment
-        v-for="attachment in nonGalleryAttachments"
-        :key="attachment.id"
-        class="non-gallery"
-        :size="attachmentSize"
-        :nsfw="nsfwClickthrough"
-        :attachment="attachment"
-        :allow-play="true"
-        :set-media="setMedia()"
-        @play="$emit('mediaplay', attachment.id)"
-        @pause="$emit('mediapause', attachment.id)"
-      />
-      <gallery
-        v-if="galleryAttachments.length > 0"
-        :nsfw="nsfwClickthrough"
-        :attachments="galleryAttachments"
-        :set-media="setMedia()"
-      />
-    </div>
-
-    <div
-      v-if="status.card && !hideSubjectStatus && !noHeading"
-      class="link-preview media-body"
-    >
-      <link-preview
-        :card="status.card"
-        :size="attachmentSize"
-        :nsfw="nsfwClickthrough"
-      />
-    </div>
+        <link-preview
+          :card="status.card"
+          :size="attachmentSize"
+          :nsfw="nsfwClickthrough"
+        />
+      </div>
+    </StatusText>
     <slot name="footer" />
   </div>
 </template>
diff --git a/src/components/status_text/status_text.js b/src/components/status_text/status_text.js
new file mode 100644
index 00000000..e0b11edc
--- /dev/null
+++ b/src/components/status_text/status_text.js
@@ -0,0 +1,147 @@
+import fileType from 'src/services/file_type/file_type.service'
+import RichContent from 'src/components/rich_content/rich_content.jsx'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faFile,
+  faMusic,
+  faImage,
+  faLink,
+  faPollH
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faFile,
+  faMusic,
+  faImage,
+  faLink,
+  faPollH
+)
+
+const StatusContent = {
+  name: 'StatusContent',
+  props: [
+    'status',
+    'focused',
+    'noHeading',
+    'fullContent',
+    '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
+    }
+  },
+  computed: {
+    localCollapseSubjectDefault () {
+      return this.mergedConfig.collapseMessageWithSubject
+    },
+    // This is a bit hacky, but we want to approximate post height before rendering
+    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
+    // as well as approximate line count by counting characters and approximating ~80
+    // per line.
+    //
+    // 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.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+      return lengthScore > 20
+    },
+    longSubject () {
+      return this.status.summary.length > 240
+    },
+    // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+    mightHideBecauseSubject () {
+      return !!this.status.summary && this.localCollapseSubjectDefault
+    },
+    mightHideBecauseTall () {
+      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
+    },
+    hideSubjectStatus () {
+      return this.mightHideBecauseSubject && !this.expandingSubject
+    },
+    hideTallStatus () {
+      return this.mightHideBecauseTall && !this.showingTall
+    },
+    showingMore () {
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+    },
+    postBodyHtml () {
+      const html = this.status.raw_html
+
+      if (this.mergedConfig.greentext) {
+        try {
+          if (html.includes('&gt;')) {
+            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+            return processHtml(html, (string) => {
+              if (string.includes('&gt;') &&
+                  string
+                    .replace(/<[^>]+?>/gi, '') // remove all tags
+                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+                    .trim()
+                    .startsWith('&gt;')) {
+                return `<span class='greentext'>${string}</span>`
+              } else {
+                return string
+              }
+            })
+          } else {
+            return html
+          }
+        } catch (e) {
+          console.err('Failed to process status html', e)
+          return html
+        }
+      } else {
+        return html
+      }
+    },
+    attachmentTypes () {
+      return this.status.attachments.map(file => fileType.fileType(file.mimetype))
+    },
+    ...mapGetters(['mergedConfig']),
+  },
+  components: {
+    RichContent
+  },
+  mounted () {
+    this.status.attentions.forEach(attn => {
+      const { id } = attn
+      this.$store.dispatch('fetchUserIfMissing', id)
+    })
+  },
+  methods: {
+    linkClicked (event) {
+      const target = event.target.closest('.status-content a')
+      if (target) {
+        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+          // Extract tag name from dataset or link url
+          const tag = target.dataset.tag || extractTagFromUrl(target.href)
+          if (tag) {
+            const link = this.generateTagLink(tag)
+            this.$router.push(link)
+            return
+          }
+        }
+        window.open(target.href, '_blank')
+      }
+    },
+    toggleShowMore () {
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
+      }
+    },
+    generateTagLink (tag) {
+      return `/tag/${tag}`
+    }
+  }
+}
+
+export default StatusContent
diff --git a/src/components/status_text/status_text.vue b/src/components/status_text/status_text.vue
new file mode 100644
index 00000000..bf723e15
--- /dev/null
+++ b/src/components/status_text/status_text.vue
@@ -0,0 +1,95 @@
+<template>
+  <div class="StatusText">
+    <div
+      v-if="status.summary_html"
+      class="summary-wrapper"
+      :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
+    >
+      <RichContent
+        class="media-body summary"
+        :html="status.summary_raw_html"
+        :emoji="status.emojis"
+        @click.prevent="linkClicked"
+      />
+      <button
+        v-if="longSubject && showingLongSubject"
+        class="button-unstyled -link tall-subject-hider"
+        @click.prevent="showingLongSubject=false"
+        >
+        {{ $t("status.hide_full_subject") }}
+      </button>
+      <button
+        v-else-if="longSubject"
+        class="button-unstyled -link tall-subject-hider"
+        :class="{ 'tall-subject-hider_focused': focused }"
+        @click.prevent="showingLongSubject=true"
+      >
+        {{ $t("status.show_full_subject") }}
+      </button>
+    </div>
+    <div
+      :class="{'tall-status': hideTallStatus}"
+      class="status-content-wrapper"
+      >
+      <button
+        v-if="hideTallStatus"
+        class="button-unstyled -link tall-status-hider"
+        :class="{ 'tall-status-hider_focused': focused }"
+        @click.prevent="toggleShowMore"
+      >
+        {{ $t("general.show_more") }}
+      </button>
+      <RichContent
+        v-if="!hideSubjectStatus"
+        :class="{ 'single-line': singleLine }"
+        class="status-content media-body"
+        :html="postBodyHtml"
+        :emoji="status.emojis"
+        :handleLinks="true"
+        @click.prevent="linkClicked"
+        />
+      <button
+        v-if="hideSubjectStatus"
+        class="button-unstyled -link cw-status-hider"
+        @click.prevent="toggleShowMore"
+        >
+        {{ $t("status.show_content") }}
+        <FAIcon
+          v-if="attachmentTypes.includes('image')"
+          icon="image"
+          />
+        <FAIcon
+          v-if="attachmentTypes.includes('video')"
+          icon="video"
+          />
+        <FAIcon
+          v-if="attachmentTypes.includes('audio')"
+          icon="music"
+          />
+        <FAIcon
+          v-if="attachmentTypes.includes('unknown')"
+          icon="file"
+          />
+        <FAIcon
+          v-if="status.poll && status.poll.options"
+          icon="poll-h"
+          />
+        <FAIcon
+          v-if="status.card"
+          icon="link"
+          />
+      </button>
+      <button
+        v-if="showingMore && !fullContent"
+        class="button-unstyled -link status-unhider"
+        @click.prevent="toggleShowMore"
+        >
+        {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+      </button>
+    </div>
+    <div v-if="!hideSubjectStatus">
+      <slot/>
+    </div>
+  </div>
+</template>
+<script src="./status_text.js" ></script>
-- 
cgit v1.2.3-70-g09d2


From 8e9f5d7580d5a248e280b110218aa23052827789 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 19:50:26 +0300
Subject: renamed StatusText to StatusBody for clarity, fixed chats

---
 src/components/chat_list_item/chat_list_item.js    |   9 +-
 src/components/chat_list_item/chat_list_item.scss  |   9 +-
 src/components/chat_list_item/chat_list_item.vue   |   3 +-
 src/components/chat_message/chat_message.js        |   2 +
 src/components/chat_message/chat_message.scss      |   6 +-
 src/components/chat_message/chat_message.vue       |   1 +
 .../settings_modal/tabs/theme_tab/theme_tab.js     |   2 +-
 src/components/status/status.vue                   |  10 +-
 src/components/status_body/status_body.js          | 147 +++++++++++++++++++++
 src/components/status_body/status_body.scss        | 112 ++++++++++++++++
 src/components/status_body/status_body.vue         |  95 +++++++++++++
 src/components/status_content/status_content.js    |   4 +-
 src/components/status_content/status_content.vue   | 102 +-------------
 src/components/status_text/status_text.js          | 147 ---------------------
 src/components/status_text/status_text.vue         |  95 -------------
 .../entity_normalizer/entity_normalizer.service.js |   2 +
 .../mini_html_converter.service.js                 |   2 +-
 yarn.lock                                          |  81 ++++++++++--
 18 files changed, 460 insertions(+), 369 deletions(-)
 create mode 100644 src/components/status_body/status_body.js
 create mode 100644 src/components/status_body/status_body.scss
 create mode 100644 src/components/status_body/status_body.vue
 delete mode 100644 src/components/status_text/status_text.js
 delete mode 100644 src/components/status_text/status_text.vue

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
index bee1ad53..e01b9bd4 100644
--- a/src/components/chat_list_item/chat_list_item.js
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -1,5 +1,5 @@
 import { mapState } from 'vuex'
-import StatusContent from '../status_content/status_content.vue'
+import StatusBody from '../status_content/status_content.vue'
 import fileType from 'src/services/file_type/file_type.service'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import AvatarList from '../avatar_list/avatar_list.vue'
@@ -16,7 +16,7 @@ const ChatListItem = {
     AvatarList,
     Timeago,
     ChatTitle,
-    StatusContent
+    StatusBody
   },
   computed: {
     ...mapState({
@@ -38,12 +38,15 @@ const ChatListItem = {
     },
     messageForStatusContent () {
       const message = this.chat.lastMessage
+      const messageEmojis = message ? message.emojis : []
       const isYou = message && message.account_id === this.currentUser.id
-      const content = message ? (this.attachmentInfo || message.content) : ''
+      const content = message ? (this.attachmentInfo || message.content_raw) : ''
       const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
       return {
         summary: '',
+        emojis: messageEmojis,
         statusnet_html: messagePreview,
+        raw_html: messagePreview,
         text: messagePreview,
         attachments: []
       }
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
index 9e97b28e..57332bed 100644
--- a/src/components/chat_list_item/chat_list_item.scss
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -77,18 +77,15 @@
     border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
   }
 
-  .StatusContent {
-    img.emoji {
-      width: 1.4em;
-      height: 1.4em;
-    }
+  .chat-preview-body {
+    --emoji-size: 1.4em;
   }
 
   .time-wrapper {
     line-height: 1.4em;
   }
 
-  .single-line {
+  .chat-preview-body {
     padding-right: 1em;
   }
 }
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
index cd3f436e..c7c0e878 100644
--- a/src/components/chat_list_item/chat_list_item.vue
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -29,7 +29,8 @@
         </div>
       </div>
       <div class="chat-preview">
-        <StatusContent
+        <StatusBody
+          class="chat-preview-body"
           :status="messageForStatusContent"
           :single-line="true"
         />
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index bb380f87..d126d453 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -57,6 +57,8 @@ const ChatMessage = {
     messageForStatusContent () {
       return {
         summary: '',
+        emojis: this.message.emojis,
+        raw_html: this.message.content_raw,
         statusnet_html: this.message.content,
         text: this.message.content,
         attachments: this.message.attachments
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
index e4351d3b..220355ea 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -89,8 +89,9 @@
   }
 
   .without-attachment {
-    .status-content {
-      &::after {
+    .message-content {
+      // TODO figure out how to do it properly
+      .text::after {
         margin-right: 5.4em;
         content: " ";
         display: inline-block;
@@ -162,6 +163,7 @@
   .visible {
     opacity: 1;
   }
+
 }
 
 .chat-message-date-separator {
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 0f3fc97d..d62b831d 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -71,6 +71,7 @@
               </Popover>
             </div>
             <StatusContent
+              class="message-content"
               :status="messageForStatusContent"
               :full-content="true"
             >
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 1388f74b..63416e93 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -474,7 +474,7 @@ export default {
           this.loadThemeFromLocalStorage(false, true)
           break
         case 'file':
-          console.err('Forcing snapshout from file is not supported yet')
+          console.error('Forcing snapshout from file is not supported yet')
           break
       }
       this.dismissWarning()
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index cc5fb79f..a4247cd4 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -89,7 +89,10 @@
               v-if="retweeterHtml"
               :to="retweeterProfileLink"
             >
-              <RichContent :html="retweeterHtml" :emoji="retweeterUser.emoji"/>
+              <RichContent
+                :html="retweeterHtml"
+                :emoji="retweeterUser.emoji"
+              />
             </router-link>
             <router-link
               v-else
@@ -146,7 +149,10 @@
                   class="status-username"
                   :title="status.user.name"
                 >
-                  <RichContent :html="status.user.name" :emoji="status.user.emoji" />
+                  <RichContent
+                    :html="status.user.name"
+                    :emoji="status.user.emoji"
+                  />
                 </h4>
                 <h4
                   v-else
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
new file mode 100644
index 00000000..232afccb
--- /dev/null
+++ b/src/components/status_body/status_body.js
@@ -0,0 +1,147 @@
+import fileType from 'src/services/file_type/file_type.service'
+import RichContent from 'src/components/rich_content/rich_content.jsx'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faFile,
+  faMusic,
+  faImage,
+  faLink,
+  faPollH
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faFile,
+  faMusic,
+  faImage,
+  faLink,
+  faPollH
+)
+
+const StatusContent = {
+  name: 'StatusContent',
+  props: [
+    'status',
+    'focused',
+    'noHeading',
+    'fullContent',
+    '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
+    }
+  },
+  computed: {
+    localCollapseSubjectDefault () {
+      return this.mergedConfig.collapseMessageWithSubject
+    },
+    // This is a bit hacky, but we want to approximate post height before rendering
+    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
+    // as well as approximate line count by counting characters and approximating ~80
+    // per line.
+    //
+    // 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.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+      return lengthScore > 20
+    },
+    longSubject () {
+      return this.status.summary.length > 240
+    },
+    // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+    mightHideBecauseSubject () {
+      return !!this.status.summary && this.localCollapseSubjectDefault
+    },
+    mightHideBecauseTall () {
+      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
+    },
+    hideSubjectStatus () {
+      return this.mightHideBecauseSubject && !this.expandingSubject
+    },
+    hideTallStatus () {
+      return this.mightHideBecauseTall && !this.showingTall
+    },
+    showingMore () {
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+    },
+    postBodyHtml () {
+      const html = this.status.raw_html
+
+      if (this.mergedConfig.greentext) {
+        try {
+          if (html.includes('&gt;')) {
+            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+            return processHtml(html, (string) => {
+              if (string.includes('&gt;') &&
+                  string
+                    .replace(/<[^>]+?>/gi, '') // remove all tags
+                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+                    .trim()
+                    .startsWith('&gt;')) {
+                return `<span class='greentext'>${string}</span>`
+              } else {
+                return string
+              }
+            })
+          } else {
+            return html
+          }
+        } catch (e) {
+          console.error('Failed to process status html', e)
+          return html
+        }
+      } else {
+        return html
+      }
+    },
+    attachmentTypes () {
+      return this.status.attachments.map(file => fileType.fileType(file.mimetype))
+    },
+    ...mapGetters(['mergedConfig'])
+  },
+  components: {
+    RichContent
+  },
+  mounted () {
+    this.status.attentions && this.status.attentions.forEach(attn => {
+      const { id } = attn
+      this.$store.dispatch('fetchUserIfMissing', id)
+    })
+  },
+  methods: {
+    linkClicked (event) {
+      const target = event.target.closest('.status-content a')
+      if (target) {
+        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+          // Extract tag name from dataset or link url
+          const tag = target.dataset.tag || extractTagFromUrl(target.href)
+          if (tag) {
+            const link = this.generateTagLink(tag)
+            this.$router.push(link)
+            return
+          }
+        }
+        window.open(target.href, '_blank')
+      }
+    },
+    toggleShowMore () {
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
+      }
+    },
+    generateTagLink (tag) {
+      return `/tag/${tag}`
+    }
+  }
+}
+
+export default StatusContent
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
new file mode 100644
index 00000000..6282919c
--- /dev/null
+++ b/src/components/status_body/status_body.scss
@@ -0,0 +1,112 @@
+@import '../../_variables.scss';
+
+.StatusBody {
+  .emoji {
+    --_still_image-label-scale: 0.5;
+  }
+
+  .summary {
+    display: block;
+    font-style: italic;
+    padding-bottom: 0.5em;
+  }
+
+  .text {
+    &.-single-line {
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      height: 1.4em;
+    }
+  }
+
+  .summary-wrapper {
+    margin-bottom: 0.5em;
+    border-style: solid;
+    border-width: 0 0 1px 0;
+    border-color: var(--border, $fallback--border);
+    flex-grow: 0;
+
+    &.-tall {
+      position: relative;
+
+      .summary {
+        max-height: 2em;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+
+  .text-wrapper {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+
+    &.-tall-status {
+      position: relative;
+      height: 220px;
+      overflow-x: hidden;
+      overflow-y: hidden;
+      z-index: 1;
+
+      .text {
+        min-height: 0;
+        mask:
+          linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+          linear-gradient(to top, white, white);
+
+        /* Autoprefixed seem to ignore this one, and also syntax is different */
+        -webkit-mask-composite: xor;
+        mask-composite: exclude;
+      }
+    }
+  }
+
+  & .tall-status-hider,
+  & .tall-subject-hider,
+  & .status-unhider,
+  & .cw-status-hider {
+    display: inline-block;
+    word-break: break-all;
+    width: 100%;
+    text-align: center;
+  }
+
+  .tall-status-hider {
+    position: absolute;
+    height: 70px;
+    margin-top: 150px;
+    line-height: 110px;
+    z-index: 2;
+  }
+
+  .tall-subject-hider {
+    // position: absolute;
+    padding-bottom: 0.5em;
+  }
+
+  & .status-unhider,
+  & .cw-status-hider {
+    word-break: break-all;
+
+    svg {
+      color: inherit;
+    }
+  }
+
+  .greentext {
+    color: $fallback--cGreen;
+    color: var(--postGreentext, $fallback--cGreen);
+  }
+
+  /* Not sure if this is necessary */
+  video {
+    max-width: 100%;
+    max-height: 400px;
+    vertical-align: middle;
+    object-fit: contain;
+  }
+
+}
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
new file mode 100644
index 00000000..6f982f2e
--- /dev/null
+++ b/src/components/status_body/status_body.vue
@@ -0,0 +1,95 @@
+<template>
+  <div class="StatusBody">
+    <div class="body">
+      <div
+        v-if="status.summary_html"
+        class="summary-wrapper"
+        :class="{ '-tall': (longSubject && !showingLongSubject) }"
+      >
+        <RichContent
+          class="media-body summary"
+          :html="status.summary_raw_html"
+          :emoji="status.emojis"
+          @click.prevent="linkClicked"
+        />
+        <button
+          v-if="longSubject && showingLongSubject"
+          class="button-unstyled -link tall-subject-hider"
+          @click.prevent="showingLongSubject=false"
+        >
+          {{ $t("status.hide_full_subject") }}
+        </button>
+        <button
+          v-else-if="longSubject"
+          class="button-unstyled -link tall-subject-hider"
+          @click.prevent="showingLongSubject=true"
+        >
+          {{ $t("status.show_full_subject") }}
+        </button>
+      </div>
+      <div
+        :class="{'-tall-status': hideTallStatus}"
+        class="text-wrapper"
+      >
+        <button
+          v-if="hideTallStatus"
+          class="button-unstyled -link tall-status-hider"
+          :class="{ '-focused': focused }"
+          @click.prevent="toggleShowMore"
+        >
+          {{ $t("general.show_more") }}
+        </button>
+        <RichContent
+          v-if="!hideSubjectStatus && !(singleLine && status.summary_html)"
+          :class="{ '-single-line': singleLine }"
+          class="text media-body"
+          :html="postBodyHtml"
+          :emoji="status.emojis"
+          :handle-links="true"
+          @click.prevent="linkClicked"
+        />
+        <button
+          v-if="hideSubjectStatus"
+          class="button-unstyled -link cw-status-hider"
+          @click.prevent="toggleShowMore"
+        >
+          {{ $t("status.show_content") }}
+          <FAIcon
+            v-if="attachmentTypes.includes('image')"
+            icon="image"
+          />
+          <FAIcon
+            v-if="attachmentTypes.includes('video')"
+            icon="video"
+          />
+          <FAIcon
+            v-if="attachmentTypes.includes('audio')"
+            icon="music"
+          />
+          <FAIcon
+            v-if="attachmentTypes.includes('unknown')"
+            icon="file"
+          />
+          <FAIcon
+            v-if="status.poll && status.poll.options"
+            icon="poll-h"
+          />
+          <FAIcon
+            v-if="status.card"
+            icon="link"
+          />
+        </button>
+        <button
+          v-if="showingMore && !fullContent"
+          class="button-unstyled -link status-unhider"
+          @click.prevent="toggleShowMore"
+        >
+          {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+        </button>
+      </div>
+    </div>
+    <slot v-if="!hideSubjectStatus" />
+  </div>
+</template>
+<script src="./status_body.js" ></script>
+<style lang="scss" src="./status_body.scss" />
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 67323b8a..1b80ee09 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -1,7 +1,7 @@
 import Attachment from '../attachment/attachment.vue'
 import Poll from '../poll/poll.vue'
 import Gallery from '../gallery/gallery.vue'
-import StatusText from 'src/components/status_text/status_text.vue'
+import StatusBody from 'src/components/status_body/status_body.vue'
 import LinkPreview from '../link-preview/link-preview.vue'
 import fileType from 'src/services/file_type/file_type.service'
 import { mapGetters, mapState } from 'vuex'
@@ -88,7 +88,7 @@ const StatusContent = {
     Poll,
     Gallery,
     LinkPreview,
-    StatusText
+    StatusBody
   },
   methods: {
     setMedia () {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 7389244a..0551f275 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="StatusContent">
     <slot name="header" />
-    <StatusText :status="status">
+    <StatusBody :status="status" :single-line="singleLine">
       <div v-if="status.poll && status.poll.options">
         <poll :base-poll="status.poll" />
       </div>
@@ -40,7 +40,7 @@
           :nsfw="nsfwClickthrough"
         />
       </div>
-    </StatusText>
+    </StatusBody>
     <slot name="footer" />
   </div>
 </template>
@@ -54,103 +54,5 @@ $status-margin: 0.75em;
 .StatusContent {
   flex: 1;
   min-width: 0;
-
-  .status-content-wrapper {
-    display: flex;
-    flex-direction: column;
-    flex-wrap: nowrap;
-  }
-
-  .tall-status {
-    position: relative;
-    height: 220px;
-    overflow-x: hidden;
-    overflow-y: hidden;
-    z-index: 1;
-    .status-content {
-      min-height: 0;
-      mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
-            linear-gradient(to top, white, white);
-      /* Autoprefixed seem to ignore this one, and also syntax is different */
-      -webkit-mask-composite: xor;
-      mask-composite: exclude;
-    }
-  }
-
-  .tall-status-hider {
-    display: inline-block;
-    word-break: break-all;
-    position: absolute;
-    height: 70px;
-    margin-top: 150px;
-    width: 100%;
-    text-align: center;
-    line-height: 110px;
-    z-index: 2;
-  }
-
-  .status-unhider, .cw-status-hider {
-    width: 100%;
-    text-align: center;
-    display: inline-block;
-    word-break: break-all;
-
-    svg {
-      color: inherit;
-    }
-  }
-
-  video {
-    max-width: 100%;
-    max-height: 400px;
-    vertical-align: middle;
-    object-fit: contain;
-  }
-
-  .summary-wrapper {
-    margin-bottom: 0.5em;
-    border-style: solid;
-    border-width: 0 0 1px 0;
-    border-color: var(--border, $fallback--border);
-    flex-grow: 0;
-  }
-
-  .summary {
-    font-style: italic;
-    padding-bottom: 0.5em;
-  }
-
-  .tall-subject {
-    position: relative;
-    .summary {
-      max-height: 2em;
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-    }
-  }
-
-  .tall-subject-hider {
-    display: inline-block;
-    word-break: break-all;
-    // position: absolute;
-    width: 100%;
-    text-align: center;
-    padding-bottom: 0.5em;
-  }
-
-  .status-content {
-    &.single-line {
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      overflow: hidden;
-      height: 1.4em;
-    }
-  }
-}
-
-.greentext {
-  color: $fallback--cGreen;
-  color: var(--postGreentext, $fallback--cGreen);
 }
 </style>
diff --git a/src/components/status_text/status_text.js b/src/components/status_text/status_text.js
deleted file mode 100644
index e0b11edc..00000000
--- a/src/components/status_text/status_text.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import fileType from 'src/services/file_type/file_type.service'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
-import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
-import { mapGetters } from 'vuex'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
-  faFile,
-  faMusic,
-  faImage,
-  faLink,
-  faPollH
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
-  faFile,
-  faMusic,
-  faImage,
-  faLink,
-  faPollH
-)
-
-const StatusContent = {
-  name: 'StatusContent',
-  props: [
-    'status',
-    'focused',
-    'noHeading',
-    'fullContent',
-    '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
-    }
-  },
-  computed: {
-    localCollapseSubjectDefault () {
-      return this.mergedConfig.collapseMessageWithSubject
-    },
-    // This is a bit hacky, but we want to approximate post height before rendering
-    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
-    // as well as approximate line count by counting characters and approximating ~80
-    // per line.
-    //
-    // 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.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
-      return lengthScore > 20
-    },
-    longSubject () {
-      return this.status.summary.length > 240
-    },
-    // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
-    mightHideBecauseSubject () {
-      return !!this.status.summary && this.localCollapseSubjectDefault
-    },
-    mightHideBecauseTall () {
-      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
-    },
-    hideSubjectStatus () {
-      return this.mightHideBecauseSubject && !this.expandingSubject
-    },
-    hideTallStatus () {
-      return this.mightHideBecauseTall && !this.showingTall
-    },
-    showingMore () {
-      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
-    },
-    postBodyHtml () {
-      const html = this.status.raw_html
-
-      if (this.mergedConfig.greentext) {
-        try {
-          if (html.includes('&gt;')) {
-            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
-            return processHtml(html, (string) => {
-              if (string.includes('&gt;') &&
-                  string
-                    .replace(/<[^>]+?>/gi, '') // remove all tags
-                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
-                    .trim()
-                    .startsWith('&gt;')) {
-                return `<span class='greentext'>${string}</span>`
-              } else {
-                return string
-              }
-            })
-          } else {
-            return html
-          }
-        } catch (e) {
-          console.err('Failed to process status html', e)
-          return html
-        }
-      } else {
-        return html
-      }
-    },
-    attachmentTypes () {
-      return this.status.attachments.map(file => fileType.fileType(file.mimetype))
-    },
-    ...mapGetters(['mergedConfig']),
-  },
-  components: {
-    RichContent
-  },
-  mounted () {
-    this.status.attentions.forEach(attn => {
-      const { id } = attn
-      this.$store.dispatch('fetchUserIfMissing', id)
-    })
-  },
-  methods: {
-    linkClicked (event) {
-      const target = event.target.closest('.status-content a')
-      if (target) {
-        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
-          // Extract tag name from dataset or link url
-          const tag = target.dataset.tag || extractTagFromUrl(target.href)
-          if (tag) {
-            const link = this.generateTagLink(tag)
-            this.$router.push(link)
-            return
-          }
-        }
-        window.open(target.href, '_blank')
-      }
-    },
-    toggleShowMore () {
-      if (this.mightHideBecauseTall) {
-        this.showingTall = !this.showingTall
-      } else if (this.mightHideBecauseSubject) {
-        this.expandingSubject = !this.expandingSubject
-      }
-    },
-    generateTagLink (tag) {
-      return `/tag/${tag}`
-    }
-  }
-}
-
-export default StatusContent
diff --git a/src/components/status_text/status_text.vue b/src/components/status_text/status_text.vue
deleted file mode 100644
index bf723e15..00000000
--- a/src/components/status_text/status_text.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-  <div class="StatusText">
-    <div
-      v-if="status.summary_html"
-      class="summary-wrapper"
-      :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
-    >
-      <RichContent
-        class="media-body summary"
-        :html="status.summary_raw_html"
-        :emoji="status.emojis"
-        @click.prevent="linkClicked"
-      />
-      <button
-        v-if="longSubject && showingLongSubject"
-        class="button-unstyled -link tall-subject-hider"
-        @click.prevent="showingLongSubject=false"
-        >
-        {{ $t("status.hide_full_subject") }}
-      </button>
-      <button
-        v-else-if="longSubject"
-        class="button-unstyled -link tall-subject-hider"
-        :class="{ 'tall-subject-hider_focused': focused }"
-        @click.prevent="showingLongSubject=true"
-      >
-        {{ $t("status.show_full_subject") }}
-      </button>
-    </div>
-    <div
-      :class="{'tall-status': hideTallStatus}"
-      class="status-content-wrapper"
-      >
-      <button
-        v-if="hideTallStatus"
-        class="button-unstyled -link tall-status-hider"
-        :class="{ 'tall-status-hider_focused': focused }"
-        @click.prevent="toggleShowMore"
-      >
-        {{ $t("general.show_more") }}
-      </button>
-      <RichContent
-        v-if="!hideSubjectStatus"
-        :class="{ 'single-line': singleLine }"
-        class="status-content media-body"
-        :html="postBodyHtml"
-        :emoji="status.emojis"
-        :handleLinks="true"
-        @click.prevent="linkClicked"
-        />
-      <button
-        v-if="hideSubjectStatus"
-        class="button-unstyled -link cw-status-hider"
-        @click.prevent="toggleShowMore"
-        >
-        {{ $t("status.show_content") }}
-        <FAIcon
-          v-if="attachmentTypes.includes('image')"
-          icon="image"
-          />
-        <FAIcon
-          v-if="attachmentTypes.includes('video')"
-          icon="video"
-          />
-        <FAIcon
-          v-if="attachmentTypes.includes('audio')"
-          icon="music"
-          />
-        <FAIcon
-          v-if="attachmentTypes.includes('unknown')"
-          icon="file"
-          />
-        <FAIcon
-          v-if="status.poll && status.poll.options"
-          icon="poll-h"
-          />
-        <FAIcon
-          v-if="status.card"
-          icon="link"
-          />
-      </button>
-      <button
-        v-if="showingMore && !fullContent"
-        class="button-unstyled -link status-unhider"
-        @click.prevent="toggleShowMore"
-        >
-        {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
-      </button>
-    </div>
-    <div v-if="!hideSubjectStatus">
-      <slot/>
-    </div>
-  </div>
-</template>
-<script src="./status_text.js" ></script>
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 8dc22823..13162dcf 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -448,6 +448,8 @@ export const parseChatMessage = (message) => {
   output.id = message.id
   output.created_at = new Date(message.created_at)
   output.chat_id = message.chat_id
+  output.emojis = message.emojis
+  output.content_raw = message.content
   if (message.content) {
     output.content = addEmojis(message.content, message.emojis)
   } else {
diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js
index 01f8adf8..900752cd 100644
--- a/src/services/mini_html_converter/mini_html_converter.service.js
+++ b/src/services/mini_html_converter/mini_html_converter.service.js
@@ -1,6 +1,6 @@
 /**
  * This is a not-so-tiny purpose-built HTML parser/processor. It was made for use
- * with StatusText component for purpose of replacing tags with vue components
+ * with StatusBody component for purpose of replacing tags with vue components
  *
  * known issue: doesn't handle CDATA so nested CDATA might not work well
  *
diff --git a/yarn.lock b/yarn.lock
index 23cc895b..9329cc3a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1011,23 +1011,86 @@
   resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
   integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
 
-"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
-  integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==
+"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
+  integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==
 
-"@vue/babel-plugin-transform-vue-jsx@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0"
-  integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ==
+"@vue/babel-plugin-transform-vue-jsx@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7"
+  integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA==
   dependencies:
     "@babel/helper-module-imports" "^7.0.0"
     "@babel/plugin-syntax-jsx" "^7.2.0"
-    "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
     html-tags "^2.0.0"
     lodash.kebabcase "^4.1.1"
     svg-tags "^1.0.0"
 
+"@vue/babel-preset-jsx@^1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87"
+  integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w==
+  dependencies:
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+    "@vue/babel-sugar-composition-api-inject-h" "^1.2.1"
+    "@vue/babel-sugar-composition-api-render-instance" "^1.2.4"
+    "@vue/babel-sugar-functional-vue" "^1.2.2"
+    "@vue/babel-sugar-inject-h" "^1.2.2"
+    "@vue/babel-sugar-v-model" "^1.2.3"
+    "@vue/babel-sugar-v-on" "^1.2.3"
+
+"@vue/babel-sugar-composition-api-inject-h@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb"
+  integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-composition-api-render-instance@^1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19"
+  integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-functional-vue@^1.2.2":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658"
+  integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-inject-h@^1.2.2":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa"
+  integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-v-model@^1.2.3":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2"
+  integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+    "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+    camelcase "^5.0.0"
+    html-tags "^2.0.0"
+    svg-tags "^1.0.0"
+
+"@vue/babel-sugar-v-on@^1.2.3":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada"
+  integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw==
+  dependencies:
+    "@babel/plugin-syntax-jsx" "^7.2.0"
+    "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+    camelcase "^5.0.0"
+
 "@vue/test-utils@^1.0.0-beta.26":
   version "1.0.0-beta.28"
   resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5"
-- 
cgit v1.2.3-70-g09d2


From aa38223e87bbea917e978a3efe0df49206f33f8e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 19:51:04 +0300
Subject: lint

---
 src/components/status_content/status_content.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 0551f275..0283ce2d 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,7 +1,10 @@
 <template>
   <div class="StatusContent">
     <slot name="header" />
-    <StatusBody :status="status" :single-line="singleLine">
+    <StatusBody
+      :status="status"
+      :single-line="singleLine"
+    >
       <div v-if="status.poll && status.poll.options">
         <poll :base-poll="status.poll" />
       </div>
-- 
cgit v1.2.3-70-g09d2


From 394fd462dc48654ba7743bb9c2ac378f98f75bd4 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 10 Jun 2021 13:01:00 +0300
Subject: proper cachin of headTailLinks, show mentions in notificaitons always

---
 src/components/status/status.js                  |  3 ++-
 src/components/status/status.scss                |  8 +++++---
 src/components/status/status.vue                 | 10 ++++++----
 src/components/status_body/status_body.js        | 15 ++++++---------
 src/components/status_body/status_body.vue       |  2 +-
 src/components/status_content/status_content.js  |  4 +++-
 src/components/status_content/status_content.vue |  2 ++
 7 files changed, 25 insertions(+), 19 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0498f28a..e9a5ec0d 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -142,7 +142,8 @@ const Status = {
     replyProfileLink () {
       if (this.isReply) {
         const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
-        return user && user.statusnet_profile_url
+        // FIXME Why user not found sometimes???
+        return user ? user.statusnet_profile_url : 'NOT_FOUND'
       }
     },
     retweet () { return !!this.statusoid.retweeted_status },
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 8744fa16..8f7fb71a 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -217,21 +217,23 @@ $status-margin: 0.75em;
     }
   }
 
-  .reply-to {
+  & .mentions,
+  & .reply-to {
     position: relative;
   }
 
-  .reply-to-text {
+  & .reply-to-text {
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
   }
 
-  .replies-separator {
+  .mentions-separator {
     margin-left: 0.4em;
   }
 
   .replies {
+    margin-top: 0.25em;
     line-height: 18px;
     font-size: 12px;
     display: flex;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 62ad0eac..e4c3a755 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -267,8 +267,8 @@
                   :first-mention="false"
                 />
                 <span
-                  v-if="isReply && hasMentions"
-                  class="faint replies-separator"
+                  v-if="isReply && hasMentionsLine"
+                  class="faint mentions-separator"
                 >
                   -
                 </span>
@@ -276,7 +276,7 @@
                   v-if="hasMentionsLine"
                 >
                   <span
-                    class="button-unstyled reply-to"
+                    class="button-unstyled mentions"
                     :aria-label="$t('tool_tip.reply')"
                     @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
                   >
@@ -285,7 +285,7 @@
                       icon="at"
                       />
                     <span
-                      class="faint-link reply-to-text"
+                      class="faint-link mentions-text"
                       >
                       {{ $t('status.mentions') }}
                     </span>
@@ -304,6 +304,8 @@
             :no-heading="noHeading"
             :highlight="highlight"
             :focused="isFocused"
+            :hide-first-mentions="mentionsOwnLine"
+            :head-tail-links="headTailLinks"
             @mediaplay="addMediaPlaying($event)"
             @mediapause="removeMediaPlaying($event)"
           />
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index dbabd208..c2edb601 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -29,14 +29,18 @@ const StatusContent = {
     'singleLine',
     // if this was computed at upper level it can be passed here, otherwise
     // it will be in this component
-    'headTailLinks'
+    'headTailLinks',
+    'hideFirstMentions'
   ],
   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,
+      headTailLinksComputed: this.headTailLinks
+        ? this.headTailLinks
+        : getHeadTailLinks(this.status.raw_html)
     }
   },
   computed: {
@@ -76,13 +80,6 @@ const StatusContent = {
     attachmentTypes () {
       return this.status.attachments.map(file => fileType.fileType(file.mimetype))
     },
-    mentionsOwnLine () {
-      return this.mergedConfig.mentionsOwnLine
-    },
-    headTailLinksComputed () {
-      if (this.headTailLinks) return this.headTailLinks
-      return getHeadTailLinks(this.status.raw_html)
-    },
     mentions () {
       return this.headTailLinksComputed.firstMentions
     },
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index ff919211..3c291cf6 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -52,7 +52,7 @@
           >
             <template v-slot:prefix>
               <MentionsLine
-                v-if="!mentionsOwnLine"
+                v-if="!hideFirstMentions"
                 :mentions="mentions"
                 class="mentions-line"
               />
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 1b80ee09..363a9cb0 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -31,7 +31,9 @@ const StatusContent = {
     'focused',
     'noHeading',
     'fullContent',
-    'singleLine'
+    'singleLine',
+    'hideFirstMentions',
+    'headTailLinks'
   ],
   computed: {
     hideAttachments () {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 0283ce2d..18f6e7be 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -4,6 +4,8 @@
     <StatusBody
       :status="status"
       :single-line="singleLine"
+      :hide-first-mentions="hideFirstMentions"
+      :headTailLinks="headTailLinks"
     >
       <div v-if="status.poll && status.poll.options">
         <poll :base-poll="status.poll" />
-- 
cgit v1.2.3-70-g09d2


From cc00af7a3102034b05ebcd4aa1fd01c6f467184a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 10 Jun 2021 18:52:01 +0300
Subject: Hellthread(tm) Certified

---
 src/components/mention_link/mention_link.js        |   3 -
 src/components/mention_link/mention_link.vue       |   1 -
 src/components/rich_content/rich_content.jsx       | 176 ++++++++++++++++-----
 src/components/status/status.js                    |   3 +
 src/components/status/status.vue                   |   1 +
 src/components/status_body/status_body.js          |   8 +-
 src/components/status_body/status_body.vue         |  12 +-
 src/components/status_content/status_content.js    |   1 +
 src/components/status_content/status_content.vue   |   3 +-
 .../html_converter/html_line_converter.service.js  | 102 ++++++++++++
 .../html_converter/html_tree_converter.service.js  | 146 +++++++++++++++++
 .../mini_html_converter.service.js                 | 138 ----------------
 .../tiny_post_html_processor.service.js            |  94 -----------
 .../html_converter/html_line_converter.spec.js     | 130 +++++++++++++++
 .../html_converter/html_tree_converter.spec.js     | 166 +++++++++++++++++++
 .../mini_post_html_processor.spec.js               | 166 -------------------
 .../tiny_post_html_processor.spec.js               |  96 -----------
 17 files changed, 707 insertions(+), 539 deletions(-)
 create mode 100644 src/services/html_converter/html_line_converter.service.js
 create mode 100644 src/services/html_converter/html_tree_converter.service.js
 delete mode 100644 src/services/mini_html_converter/mini_html_converter.service.js
 delete mode 100644 src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
 create mode 100644 test/unit/specs/services/html_converter/html_line_converter.spec.js
 create mode 100644 test/unit/specs/services/html_converter/html_tree_converter.spec.js
 delete mode 100644 test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
 delete mode 100644 test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 711c87d6..00b9e388 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -70,9 +70,6 @@ const MentionLink = {
     highlightClass () {
       if (this.highlight) return highlightClass(this.user)
     },
-    oldPlace () {
-      return !this.mergedConfig.mentionsOwnLine
-    },
     oldStyle () {
       return !this.mergedConfig.mentionsNewStyle
     },
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 281fab25..a65dbad3 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,7 +1,6 @@
 <template>
   <span
     class="MentionLink"
-    :class="{ '-oldPlace': oldPlace }"
   >
     <!-- eslint-disable vue/no-v-html -->
     <a
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index db24ca0e..590fea0f 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import { unescape, flattenDeep } from 'lodash'
-import { convertHtml, getTagName, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { convertHtmlToTree, getTagName, processTextForEmoji, getAttrs } 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'
 
@@ -31,11 +31,24 @@ export default Vue.component('RichContent', {
       required: false,
       type: Boolean,
       default: false
+    },
+    // Whether to hide last mentions (hellthreads)
+    hideLastMentions: {
+      required: false,
+      type: Boolean,
+      default: false
+    },
+    // Whether to hide first mentions
+    hideFirstMentions: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   render (h) {
     // Pre-process HTML
-    const html = this.greentext ? addGreentext(this.html) : this.html
+    const html = preProcessPerLine(this.html, this.greentext, this.hideLastMentions)
+    console.log(this.hideFirstMentions, this.hideLastMentions)
 
     const renderImage = (tag) => {
       return <StillImage
@@ -45,18 +58,20 @@ export default Vue.component('RichContent', {
     }
 
     const renderMention = (attrs, children, encounteredText) => {
-      return <MentionLink
-        url={attrs.href}
-        content={flattenDeep(children).join('')}
-        firstMention={!encounteredText}
-      />
+      return (this.hideFirstMentions && !encounteredText)
+        ? ''
+        : <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
-    const processItem = (item) => {
+    const processItem = (item, index, array, what) => {
       // Handle text nodes - just add emoji
       if (typeof item === 'string') {
         const emptyText = item.trim() === ''
@@ -69,7 +84,7 @@ export default Vue.component('RichContent', {
           encounteredText = true
         }
         if (item.includes(':')) {
-          return processTextForEmoji(
+          unescapedItem = processTextForEmoji(
             unescapedItem,
             this.emoji,
             ({ shortcode, url }) => {
@@ -81,9 +96,8 @@ export default Vue.component('RichContent', {
               />
             }
           )
-        } else {
-          return unescapedItem
         }
+        return unescapedItem
       }
 
       // Handle tag nodes
@@ -98,6 +112,8 @@ export default Vue.component('RichContent', {
             const attrs = getAttrs(opener)
             if (attrs['class'] && attrs['class'].includes('mention')) {
               return renderMention(attrs, children, encounteredText)
+            } else if (attrs['class'] && attrs['class'].includes('hashtag')) {
+              return item // We'll handle it later
             } else {
               attrs.target = '_blank'
               return <a {...{ attrs }}>
@@ -116,43 +132,129 @@ 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') {
+        const emptyText = item.trim() === ''
+        if (emptyText) return encounteredTextReverse ? item : item.trim()
+        if (!encounteredTextReverse) encounteredTextReverse = true
+        return item
+      } else if (Array.isArray(item)) {
+        // Handle tag nodes
+        const [opener, children] = item
+        const Tag = getTagName(opener)
+        switch (Tag) {
+          case 'a': // replace mentions with MentionLink
+            if (!this.handleLinks) break
+            const attrs = getAttrs(opener)
+            // should only be this
+            if (attrs['class'] && attrs['class'].includes('hashtag')) {
+              return renderHashtag(attrs, children, encounteredTextReverse)
+            }
+        }
+      }
+      return item
+    }
     return <span class="RichContent">
       { this.$slots.prefix }
-      { convertHtml(html).map(processItem) }
+      { convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() }
       { this.$slots.suffix }
     </span>
   }
 })
 
-export const addGreentext = (html) => {
-  try {
-    if (html.includes('&gt;')) {
-      // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
-      return processHtml(html, (string) => {
-        if (
-          string.includes('&gt;') && string
-            .replace(/<[^>]+?>/gi, '') // remove all tags
-            .replace(/@\w+/gi, '') // remove mentions (even failed ones)
-            .trim()
-            .startsWith('&gt;')
-        ) {
-          return `<span class='greentext'>${string}</span>`
+/** Pre-processing HTML
+ *
+ * Currently this does two things:
+ * - 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} removeLastMentions - whether to remove last mentions
+ */
+export const preProcessPerLine = (html, greentext, removeLastMentions) => {
+  // Only mark first (last) encounter
+  let lastMentionsMarked = false
+
+  return convertHtmlToLines(html).reverse().map((item, index, array) => {
+    if (!item.text) return item
+    const string = item.text
+
+    // Greentext stuff
+    if (greentext && (string.includes('&gt;') || string.includes('&lt;'))) {
+      const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
+        .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+        .trim()
+      if (cleanedString.startsWith('&gt;')) {
+        return `<span class='greentext'>${string}</span>`
+      } else if (cleanedString.startsWith('&lt;')) {
+        return `<span class='cyantext'>${string}</span>`
+      }
+    }
+
+    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 hasMentions = false
+    const process = (item) => {
+      if (Array.isArray(item)) {
+        const [opener, children, closer] = item
+        const tag = getTagName(opener)
+        if (tag === 'a') {
+          const attrs = getAttrs(opener)
+          if (attrs['class'] && attrs['class'].includes('mention')) {
+            hasMentions = true
+            return [opener, children, closer]
+          } else {
+            hasLooseText = true
+            return [opener, children, closer]
+          }
+        } else if (tag === 'span' || tag === 'p') {
+          return [opener, [...children].reverse().map(process).reverse(), closer]
         } else {
-          return string
+          hasLooseText = true
+          return [opener, children, closer]
+        }
+      }
+
+      if (typeof item === 'string') {
+        if (item.trim() !== '') {
+          hasLooseText = true
         }
-      })
+        return item
+      }
+    }
+
+    const result = [...tree].reverse().map(process).reverse()
+
+    if (removeLastMentions && hasMentions && !hasLooseText && !lastMentionsMarked) {
+      lastMentionsMarked = true
+      return ''
     } else {
-      return html
+      return flattenDeep(result).join('')
     }
-  } catch (e) {
-    console.error('Failed to process status html', e)
-    return html
-  }
+  }).reverse().join('')
 }
 
 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
@@ -170,7 +272,7 @@ export const getHeadTailLinks = (html) => {
     }
   }
 
-  // Processor to use with mini_html_converter
+  // 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') {
@@ -182,6 +284,7 @@ export const getHeadTailLinks = (html) => {
       }
       // Encountered text? That means tags we've been collectings aren't "last"!
       lastTags.splice(0)
+      lastMentions.splice(0)
       return
     }
     // Handle tag nodes
@@ -197,6 +300,7 @@ export const getHeadTailLinks = (html) => {
             firstMentions.push(linkData)
           }
           writtenMentions.push(linkData)
+          lastMentions.push(linkData)
         } else if (attrs['class'].includes('hashtag')) {
           lastTags.push(linkData)
           writtenTags.push(linkData)
@@ -206,6 +310,6 @@ export const getHeadTailLinks = (html) => {
       children && children.forEach(processItem)
     }
   }
-  convertHtml(html).forEach(processItem)
-  return { firstMentions, writtenMentions, writtenTags, lastTags }
+  convertHtmlToTree(html).forEach(processItem)
+  return { firstMentions, writtenMentions, writtenTags, lastTags, lastMentions }
 }
diff --git a/src/components/status/status.js b/src/components/status/status.js
index e9a5ec0d..bab818fc 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -196,6 +196,9 @@ 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
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 7cc25be9..0190d864 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -306,6 +306,7 @@
             :highlight="highlight"
             :focused="isFocused"
             :hide-first-mentions="mentionsOwnLine && isReply"
+            :hide-last-mentions="hideLastMentions"
             :head-tail-links="headTailLinks"
             @mediaplay="addMediaPlaying($event)"
             @mediapause="removeMediaPlaying($event)"
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index c2edb601..2fc9abbf 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -30,7 +30,8 @@ const StatusContent = {
     // if this was computed at upper level it can be passed here, otherwise
     // it will be in this component
     'headTailLinks',
-    'hideFirstMentions'
+    'hideFirstMentions',
+    'hideLastMentions'
   ],
   data () {
     return {
@@ -80,9 +81,12 @@ const StatusContent = {
     attachmentTypes () {
       return this.status.attachments.map(file => fileType.fileType(file.mimetype))
     },
-    mentions () {
+    mentionsFirst () {
       return this.headTailLinksComputed.firstMentions
     },
+    mentionsLast () {
+      return this.headTailLinksComputed.lastMentions
+    },
     ...mapGetters(['mergedConfig'])
   },
   components: {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 4df29934..bd599a8c 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -49,11 +49,19 @@
             :emoji="status.emojis"
             :handle-links="true"
             :greentext="mergedConfig.greentext"
+            :hide-first-mentions="hideFirstMentions"
+            :hide-last-mentions="hideLastMentions"
           >
             <template v-slot:prefix>
               <MentionsLine
-                v-if="!hideFirstMentions"
-                :mentions="mentions"
+                v-if="!hideFirstMentions && mentionsFirst"
+                :mentions="mentionsFirst"
+              />
+            </template>
+            <template v-slot:suffix>
+              <MentionsLine
+                v-if="!hideFirstMentions && mentionsLast"
+                :mentions="mentionsLast"
               />
             </template>
           </RichContent>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 363a9cb0..64cc6d44 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -33,6 +33,7 @@ const StatusContent = {
     'fullContent',
     'singleLine',
     'hideFirstMentions',
+    'hideLastMentions',
     'headTailLinks'
   ],
   computed: {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 18f6e7be..c32bbbfb 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -5,7 +5,8 @@
       :status="status"
       :single-line="singleLine"
       :hide-first-mentions="hideFirstMentions"
-      :headTailLinks="headTailLinks"
+      :hide-last-mentions="hideLastMentions"
+      :head-tail-links="headTailLinks"
     >
       <div v-if="status.poll && status.poll.options">
         <poll :base-poll="status.poll" />
diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js
new file mode 100644
index 00000000..80482c9a
--- /dev/null
+++ b/src/services/html_converter/html_line_converter.service.js
@@ -0,0 +1,102 @@
+/**
+ * This is a tiny purpose-built HTML parser/processor. This basically detects
+ * any type of visual newline and converts entire HTML into a array structure.
+ *
+ * Text nodes are represented as object with single property - text - containing
+ * the visual line. Intended usage is to process the array with .map() in which
+ * map function returns a string and resulting array can be converted back to html
+ * with a .join('').
+ *
+ * Generally this isn't very useful except for when you really need to either
+ * modify visual lines (greentext i.e. simple quoting) or do something with
+ * first/last line.
+ *
+ * known issue: doesn't handle CDATA so nested CDATA might not work well
+ *
+ * @param {Object} input - input data
+ * @return {(string|{ text: string })[]} processed html in form of a list.
+ */
+export const convertHtmlToLines = (html) => {
+  const handledTags = new Set(['p', 'br', 'div'])
+  const openCloseTags = new Set(['p', 'div'])
+
+  let buffer = [] // Current output buffer
+  const level = [] // How deep we are in tags and which tags were there
+  let textBuffer = '' // Current line content
+  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
+
+  // Extracts tag name from tag, i.e. <span a="b"> => span
+  const getTagName = (tag) => {
+    const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
+    return result && (result[1] || result[2])
+  }
+
+  const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
+    if (textBuffer.trim().length > 0) {
+      buffer.push({ text: textBuffer })
+    } else {
+      buffer.push(textBuffer)
+    }
+    textBuffer = ''
+  }
+
+  const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
+    flush()
+    buffer.push(tag)
+  }
+
+  const handleOpen = (tag) => { // handles opening tags
+    flush()
+    buffer.push(tag)
+    level.push(tag)
+  }
+
+  const handleClose = (tag) => { // handles closing tags
+    flush()
+    buffer.push(tag)
+    if (level[level.length - 1] === tag) {
+      level.pop()
+    }
+  }
+
+  for (let i = 0; i < html.length; i++) {
+    const char = html[i]
+    if (char === '<' && tagBuffer === null) {
+      tagBuffer = char
+    } else if (char !== '>' && tagBuffer !== null) {
+      tagBuffer += char
+    } else if (char === '>' && tagBuffer !== null) {
+      tagBuffer += char
+      const tagFull = tagBuffer
+      tagBuffer = null
+      const tagName = getTagName(tagFull)
+      if (handledTags.has(tagName)) {
+        if (tagName === 'br') {
+          handleBr(tagFull)
+        } else if (openCloseTags.has(tagName)) {
+          if (tagFull[1] === '/') {
+            handleClose(tagFull)
+          } else if (tagFull[tagFull.length - 2] === '/') {
+            // self-closing
+            handleBr(tagFull)
+          } else {
+            handleOpen(tagFull)
+          }
+        }
+      } else {
+        textBuffer += tagFull
+      }
+    } else if (char === '\n') {
+      handleBr(char)
+    } else {
+      textBuffer += char
+    }
+  }
+  if (tagBuffer) {
+    textBuffer += tagBuffer
+  }
+
+  flush()
+
+  return buffer
+}
diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js
new file mode 100644
index 00000000..badd473a
--- /dev/null
+++ b/src/services/html_converter/html_tree_converter.service.js
@@ -0,0 +1,146 @@
+/**
+ * This is a not-so-tiny purpose-built HTML parser/processor. This parses html
+ * and converts it into a tree structure representing tag openers/closers and
+ * children.
+ *
+ * Structure follows this pattern: [opener, [...children], closer] except root
+ * node which is just [...children]. Text nodes can only be within children and
+ * are represented as strings.
+ *
+ * Intended use is to convert HTML structure and then recursively iterate over it
+ * most likely using a map. Very useful for dynamically rendering html replacing
+ * tags with JSX elements in a render function.
+ *
+ * known issue: doesn't handle CDATA so CDATA might not work well
+ * known issue: doesn't handle HTML comments
+ *
+ * @param {Object} input - input data
+ * @return {string} processed 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([
+    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
+    'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
+  ])
+  // TODO For future - also parse HTML5 multi-source components?
+
+  const buffer = [] // Current output buffer
+  const levels = [['', buffer]] // How deep we are in tags and which tags were there
+  let textBuffer = '' // Current line content
+  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
+
+  const getCurrentBuffer = () => {
+    return levels[levels.length - 1][1]
+  }
+
+  const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
+    if (textBuffer === '') return
+    getCurrentBuffer().push(textBuffer)
+    textBuffer = ''
+  }
+
+  const handleSelfClosing = (tag) => {
+    getCurrentBuffer().push([tag])
+  }
+
+  const handleOpen = (tag) => {
+    const curBuf = getCurrentBuffer()
+    const newLevel = [tag, []]
+    levels.push(newLevel)
+    curBuf.push(newLevel)
+  }
+
+  const handleClose = (tag) => {
+    const currentTag = levels[levels.length - 1]
+    if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
+      currentTag.push(tag)
+      levels.pop()
+    } else {
+      getCurrentBuffer().push(tag)
+    }
+  }
+
+  for (let i = 0; i < html.length; i++) {
+    const char = html[i]
+    if (char === '<' && tagBuffer === null) {
+      flushText()
+      tagBuffer = char
+    } else if (char !== '>' && tagBuffer !== null) {
+      tagBuffer += char
+    } else if (char === '>' && tagBuffer !== null) {
+      tagBuffer += char
+      const tagFull = tagBuffer
+      tagBuffer = null
+      const tagName = getTagName(tagFull)
+      if (tagFull[1] === '/') {
+        handleClose(tagFull)
+      } else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
+        // self-closing
+        handleSelfClosing(tagFull)
+      } else {
+        handleOpen(tagFull)
+      }
+    } else {
+      textBuffer += char
+    }
+  }
+  if (tagBuffer) {
+    textBuffer += tagBuffer
+  }
+
+  flushText()
+  return buffer
+}
+
+// Extracts tag name from tag, i.e. <span a="b"> => span
+export const getTagName = (tag) => {
+  const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
+  return result && (result[1] || result[2])
+}
+
+export const processTextForEmoji = (text, emojis, processor) => {
+  const buffer = []
+  let textBuffer = ''
+  for (let i = 0; i < text.length; i++) {
+    const char = text[i]
+    if (char === ':') {
+      const next = text.slice(i + 1)
+      let found = false
+      for (let emoji of emojis) {
+        if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
+          found = emoji
+          break
+        }
+      }
+      if (found) {
+        buffer.push(textBuffer)
+        textBuffer = ''
+        buffer.push(processor(found))
+        i += found.shortcode.length + 1
+      } else {
+        textBuffer += char
+      }
+    } else {
+      textBuffer += char
+    }
+  }
+  if (textBuffer) buffer.push(textBuffer)
+  return buffer
+}
+
+export const getAttrs = tag => {
+  const innertag = tag
+    .substring(1, tag.length - 1)
+    .replace(new RegExp('^' + getTagName(tag)), '')
+    .replace(/\/?$/, '')
+    .trim()
+  const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
+    .map(([trash, key, value]) => [key, value])
+    .map(([k, v]) => {
+      if (!v) return [k, true]
+      return [k, v.substring(1, v.length - 1)]
+    })
+  return Object.fromEntries(attrs)
+}
diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/mini_html_converter/mini_html_converter.service.js
deleted file mode 100644
index 900752cd..00000000
--- a/src/services/mini_html_converter/mini_html_converter.service.js
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * This is a not-so-tiny purpose-built HTML parser/processor. It was made for use
- * with StatusBody component for purpose of replacing tags with vue components
- *
- * known issue: doesn't handle CDATA so nested CDATA might not work well
- *
- * @param {Object} input - input data
- * @param {(string) => string} lineProcessor - function that will be called on every line
- * @param {{ key[string]: (string) => string}} tagProcessor - map of processors for tags
- * @return {string} processed html
- */
-export const convertHtml = (html) => {
-  // Elements that are implicitly self-closing
-  // https://developer.mozilla.org/en-US/docs/Glossary/empty_element
-  const emptyElements = new Set([
-    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
-    'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
-  ])
-  // TODO For future - also parse HTML5 multi-source components?
-
-  const buffer = [] // Current output buffer
-  const levels = [['', buffer]] // How deep we are in tags and which tags were there
-  let textBuffer = '' // Current line content
-  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
-
-  const getCurrentBuffer = () => {
-    return levels[levels.length - 1][1]
-  }
-
-  const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
-    if (textBuffer === '') return
-    getCurrentBuffer().push(textBuffer)
-    textBuffer = ''
-  }
-
-  const handleSelfClosing = (tag) => {
-    getCurrentBuffer().push([tag])
-  }
-
-  const handleOpen = (tag) => {
-    const curBuf = getCurrentBuffer()
-    const newLevel = [tag, []]
-    levels.push(newLevel)
-    curBuf.push(newLevel)
-  }
-
-  const handleClose = (tag) => {
-    const currentTag = levels[levels.length - 1]
-    if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
-      currentTag.push(tag)
-      levels.pop()
-    } else {
-      getCurrentBuffer().push(tag)
-    }
-  }
-
-  for (let i = 0; i < html.length; i++) {
-    const char = html[i]
-    if (char === '<' && tagBuffer === null) {
-      flushText()
-      tagBuffer = char
-    } else if (char !== '>' && tagBuffer !== null) {
-      tagBuffer += char
-    } else if (char === '>' && tagBuffer !== null) {
-      tagBuffer += char
-      const tagFull = tagBuffer
-      tagBuffer = null
-      const tagName = getTagName(tagFull)
-      if (tagFull[1] === '/') {
-        handleClose(tagFull)
-      } else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
-        // self-closing
-        handleSelfClosing(tagFull)
-      } else {
-        handleOpen(tagFull)
-      }
-    } else {
-      textBuffer += char
-    }
-  }
-  if (tagBuffer) {
-    textBuffer += tagBuffer
-  }
-
-  flushText()
-  return buffer
-}
-
-// Extracts tag name from tag, i.e. <span a="b"> => span
-export const getTagName = (tag) => {
-  const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
-  return result && (result[1] || result[2])
-}
-
-export const processTextForEmoji = (text, emojis, processor) => {
-  const buffer = []
-  let textBuffer = ''
-  for (let i = 0; i < text.length; i++) {
-    const char = text[i]
-    if (char === ':') {
-      const next = text.slice(i + 1)
-      let found = false
-      for (let emoji of emojis) {
-        if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
-          found = emoji
-          break
-        }
-      }
-      if (found) {
-        buffer.push(textBuffer)
-        textBuffer = ''
-        buffer.push(processor(found))
-        i += found.shortcode.length + 1
-      } else {
-        textBuffer += char
-      }
-    } else {
-      textBuffer += char
-    }
-  }
-  if (textBuffer) buffer.push(textBuffer)
-  return buffer
-}
-
-export const getAttrs = tag => {
-  const innertag = tag
-    .substring(1, tag.length - 1)
-    .replace(new RegExp('^' + getTagName(tag)), '')
-    .replace(/\/?$/, '')
-    .trim()
-  const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
-    .map(([trash, key, value]) => [key, value])
-    .map(([k, v]) => {
-      if (!v) return [k, true]
-      return [k, v.substring(1, v.length - 1)]
-    })
-  return Object.fromEntries(attrs)
-}
diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
deleted file mode 100644
index de6f20ef..00000000
--- a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
- * allows it to be processed, useful for greentexting, mostly
- *
- * known issue: doesn't handle CDATA so nested CDATA might not work well
- *
- * @param {Object} input - input data
- * @param {(string) => string} processor - function that will be called on every line
- * @return {string} processed html
- */
-export const processHtml = (html, processor) => {
-  const handledTags = new Set(['p', 'br', 'div'])
-  const openCloseTags = new Set(['p', 'div'])
-
-  let buffer = '' // Current output buffer
-  const level = [] // How deep we are in tags and which tags were there
-  let textBuffer = '' // Current line content
-  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
-
-  // Extracts tag name from tag, i.e. <span a="b"> => span
-  const getTagName = (tag) => {
-    const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
-    return result && (result[1] || result[2])
-  }
-
-  const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
-    if (textBuffer.trim().length > 0) {
-      buffer += processor(textBuffer)
-    } else {
-      buffer += textBuffer
-    }
-    textBuffer = ''
-  }
-
-  const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
-    flush()
-    buffer += tag
-  }
-
-  const handleOpen = (tag) => { // handles opening tags
-    flush()
-    buffer += tag
-    level.push(tag)
-  }
-
-  const handleClose = (tag) => { // handles closing tags
-    flush()
-    buffer += tag
-    if (level[level.length - 1] === tag) {
-      level.pop()
-    }
-  }
-
-  for (let i = 0; i < html.length; i++) {
-    const char = html[i]
-    if (char === '<' && tagBuffer === null) {
-      tagBuffer = char
-    } else if (char !== '>' && tagBuffer !== null) {
-      tagBuffer += char
-    } else if (char === '>' && tagBuffer !== null) {
-      tagBuffer += char
-      const tagFull = tagBuffer
-      tagBuffer = null
-      const tagName = getTagName(tagFull)
-      if (handledTags.has(tagName)) {
-        if (tagName === 'br') {
-          handleBr(tagFull)
-        } else if (openCloseTags.has(tagName)) {
-          if (tagFull[1] === '/') {
-            handleClose(tagFull)
-          } else if (tagFull[tagFull.length - 2] === '/') {
-            // self-closing
-            handleBr(tagFull)
-          } else {
-            handleOpen(tagFull)
-          }
-        }
-      } else {
-        textBuffer += tagFull
-      }
-    } else if (char === '\n') {
-      handleBr(char)
-    } else {
-      textBuffer += char
-    }
-  }
-  if (tagBuffer) {
-    textBuffer += tagBuffer
-  }
-
-  flush()
-
-  return buffer
-}
diff --git a/test/unit/specs/services/html_converter/html_line_converter.spec.js b/test/unit/specs/services/html_converter/html_line_converter.spec.js
new file mode 100644
index 00000000..82cb4170
--- /dev/null
+++ b/test/unit/specs/services/html_converter/html_line_converter.spec.js
@@ -0,0 +1,130 @@
+import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
+
+const mapOnlyText = (processor) => (input) => input.text ? processor(input.text) : input
+
+describe('TinyPostHTMLProcessor', () => {
+  describe('with processor that keeps original line should not make any changes to HTML when', () => {
+    const processorKeep = (line) => line
+    it('fed with regular HTML with newlines', () => {
+      const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with possibly broken HTML with invalid tags/composition', () => {
+      const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with very broken HTML with broken composition', () => {
+      const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with sorta valid HTML but tags aren\'t closed', () => {
+      const inputOutput = 'just leaving a <div> hanging'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with not really HTML at this point... tags that aren\'t finished', () => {
+      const inputOutput = 'do you expect me to finish this <div class='
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
+      const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with maybe valid HTML? self-closing divs and ps', () => {
+      const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+
+    it('fed with valid XHTML containing a CDATA', () => {
+      const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
+      const result = convertHtmlToLines(inputOutput)
+      const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
+      expect(comparableResult).to.eql(inputOutput)
+    })
+  })
+  describe('with processor that replaces lines with word "_" should match expected line when', () => {
+    const processorReplace = (line) => '_'
+    it('fed with regular HTML with newlines', () => {
+      const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
+      const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with possibly broken HTML with invalid tags/composition', () => {
+      const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
+      const output = '_'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with very broken HTML with broken composition', () => {
+      const input = '</p> lmao what </div> whats going on <div> wha <p>'
+      const output = '</p>_</div>_<div>_<p>'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with sorta valid HTML but tags aren\'t closed', () => {
+      const input = 'just leaving a <div> hanging'
+      const output = '_<div>_'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with not really HTML at this point... tags that aren\'t finished', () => {
+      const input = 'do you expect me to finish this <div class='
+      const output = '_'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
+      const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
+      const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with maybe valid HTML? self-closing divs and ps', () => {
+      const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
+      const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+
+    it('fed with valid XHTML containing a CDATA', () => {
+      const input = 'Yes, it is me, <![CDATA[DIO]]>'
+      const output = '_'
+      const result = convertHtmlToLines(input)
+      const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
+      expect(comparableResult).to.eql(output)
+    })
+  })
+})
diff --git a/test/unit/specs/services/html_converter/html_tree_converter.spec.js b/test/unit/specs/services/html_converter/html_tree_converter.spec.js
new file mode 100644
index 00000000..a54745c3
--- /dev/null
+++ b/test/unit/specs/services/html_converter/html_tree_converter.spec.js
@@ -0,0 +1,166 @@
+import { convertHtmlToTree, processTextForEmoji, getAttrs } from 'src/services/html_converter/html_tree_converter.service.js'
+
+describe('MiniHtmlConverter', () => {
+  describe('convertHtmlToTree', () => {
+    it('converts html into a tree structure', () => {
+      const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
+      expect(convertHtmlToTree(input)).to.eql([
+        '1 ',
+        [
+          '<p>',
+          ['2'],
+          '</p>'
+        ],
+        ' ',
+        [
+          '<b>',
+          [
+            '3',
+            ['<img src="a">'],
+            '4'
+          ],
+          '</b>'
+        ],
+        '5'
+      ])
+    })
+    it('converts html to tree while preserving tag formatting', () => {
+      const input = '1 <p >2</p><b >3<img   src="a">4</b>5'
+      expect(convertHtmlToTree(input)).to.eql([
+        '1 ',
+        [
+          '<p >',
+          ['2'],
+          '</p>'
+        ],
+        [
+          '<b >',
+          [
+            '3',
+            ['<img   src="a">'],
+            '4'
+          ],
+          '</b>'
+        ],
+        '5'
+      ])
+    })
+    it('converts semi-broken html', () => {
+      const input = '1 <br> 2 <p> 42'
+      expect(convertHtmlToTree(input)).to.eql([
+        '1 ',
+        ['<br>'],
+        ' 2 ',
+        [
+          '<p>',
+          [' 42']
+        ]
+      ])
+    })
+    it('realistic case 1', () => {
+      const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
+      expect(convertHtmlToTree(input)).to.eql([
+        [
+          '<p>',
+          [
+            [
+              '<span class="h-card">',
+              [
+                [
+                  '<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">',
+                  [
+                    '@',
+                    [
+                      '<span>',
+                      [
+                        'benis'
+                      ],
+                      '</span>'
+                    ]
+                  ],
+                  '</a>'
+                ]
+              ],
+              '</span>'
+            ],
+            ' ',
+            [
+              '<span class="h-card">',
+              [
+                [
+                  '<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">',
+                  [
+                    '@',
+                    [
+                      '<span>',
+                      [
+                        'hj'
+                      ],
+                      '</span>'
+                    ]
+                  ],
+                  '</a>'
+                ]
+              ],
+              '</span>'
+            ],
+            ' nice'
+          ],
+          '</p>'
+        ]
+      ])
+    })
+    it('realistic case 2', () => {
+      const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
+      expect(convertHtmlToTree(inputOutput)).to.eql([
+        'Country improv: give me a city',
+        [
+          '<br/>'
+        ],
+        'Audience: Memphis',
+        [
+          '<br/>'
+        ],
+        'Improv troupe: come on, a better one',
+        [
+          '<br/>'
+        ],
+        'Audience: el paso'
+      ])
+    })
+  })
+
+  describe('processTextForEmoji', () => {
+    it('processes all emoji in text', () => {
+      const input = 'Hello from finland! :lol: We have best water! :lmao:'
+      const emojis = [
+        { shortcode: 'lol', src: 'LOL' },
+        { shortcode: 'lmao', src: 'LMAO' }
+      ]
+      const processor = ({ shortcode, src }) => ({ shortcode, src })
+      expect(processTextForEmoji(input, emojis, processor)).to.eql([
+        'Hello from finland! ',
+        { shortcode: 'lol', src: 'LOL' },
+        ' We have best water! ',
+        { shortcode: 'lmao', src: 'LMAO' }
+      ])
+    })
+    it('leaves text as is', () => {
+      const input = 'Number one: that\'s terror'
+      const emojis = []
+      const processor = ({ shortcode, src }) => ({ shortcode, src })
+      expect(processTextForEmoji(input, emojis, processor)).to.eql([
+        'Number one: that\'s terror'
+      ])
+    })
+  })
+
+  describe('getAttrs', () => {
+    it('extracts arguments from tag', () => {
+      const input = '<img src="boop" cool ebin=\'true\'>'
+      const output = { src: 'boop', cool: true, ebin: 'true' }
+
+      expect(getAttrs(input)).to.eql(output)
+    })
+  })
+})
diff --git a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
deleted file mode 100644
index 8df2fbc3..00000000
--- a/test/unit/specs/services/tiny_post_html_processor/mini_post_html_processor.spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import { convertHtml, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
-
-describe('MiniHtmlConverter', () => {
-  describe('convertHtml', () => {
-    it('converts html into a tree structure', () => {
-      const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
-      expect(convertHtml(input)).to.eql([
-        '1 ',
-        [
-          '<p>',
-          ['2'],
-          '</p>'
-        ],
-        ' ',
-        [
-          '<b>',
-          [
-            '3',
-            ['<img src="a">'],
-            '4'
-          ],
-          '</b>'
-        ],
-        '5'
-      ])
-    })
-    it('converts html to tree while preserving tag formatting', () => {
-      const input = '1 <p >2</p><b >3<img   src="a">4</b>5'
-      expect(convertHtml(input)).to.eql([
-        '1 ',
-        [
-          '<p >',
-          ['2'],
-          '</p>'
-        ],
-        [
-          '<b >',
-          [
-            '3',
-            ['<img   src="a">'],
-            '4'
-          ],
-          '</b>'
-        ],
-        '5'
-      ])
-    })
-    it('converts semi-broken html', () => {
-      const input = '1 <br> 2 <p> 42'
-      expect(convertHtml(input)).to.eql([
-        '1 ',
-        ['<br>'],
-        ' 2 ',
-        [
-          '<p>',
-          [' 42']
-        ]
-      ])
-    })
-    it('realistic case 1', () => {
-      const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
-      expect(convertHtml(input)).to.eql([
-        [
-          '<p>',
-          [
-            [
-              '<span class="h-card">',
-              [
-                [
-                  '<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">',
-                  [
-                    '@',
-                    [
-                      '<span>',
-                      [
-                        'benis'
-                      ],
-                      '</span>'
-                    ]
-                  ],
-                  '</a>'
-                ]
-              ],
-              '</span>'
-            ],
-            ' ',
-            [
-              '<span class="h-card">',
-              [
-                [
-                  '<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">',
-                  [
-                    '@',
-                    [
-                      '<span>',
-                      [
-                        'hj'
-                      ],
-                      '</span>'
-                    ]
-                  ],
-                  '</a>'
-                ]
-              ],
-              '</span>'
-            ],
-            ' nice'
-          ],
-          '</p>'
-        ]
-      ])
-    })
-    it('realistic case 2', () => {
-      const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
-      expect(convertHtml(inputOutput)).to.eql([
-        'Country improv: give me a city',
-        [
-          '<br/>'
-        ],
-        'Audience: Memphis',
-        [
-          '<br/>'
-        ],
-        'Improv troupe: come on, a better one',
-        [
-          '<br/>'
-        ],
-        'Audience: el paso'
-      ])
-    })
-  })
-
-  describe('processTextForEmoji', () => {
-    it('processes all emoji in text', () => {
-      const input = 'Hello from finland! :lol: We have best water! :lmao:'
-      const emojis = [
-        { shortcode: 'lol', src: 'LOL' },
-        { shortcode: 'lmao', src: 'LMAO' }
-      ]
-      const processor = ({ shortcode, src }) => ({ shortcode, src })
-      expect(processTextForEmoji(input, emojis, processor)).to.eql([
-        'Hello from finland! ',
-        { shortcode: 'lol', src: 'LOL' },
-        ' We have best water! ',
-        { shortcode: 'lmao', src: 'LMAO' }
-      ])
-    })
-    it('leaves text as is', () => {
-      const input = 'Number one: that\'s terror'
-      const emojis = []
-      const processor = ({ shortcode, src }) => ({ shortcode, src })
-      expect(processTextForEmoji(input, emojis, processor)).to.eql([
-        'Number one: that\'s terror'
-      ])
-    })
-  })
-
-  describe('getAttrs', () => {
-    it('extracts arguments from tag', () => {
-      const input = '<img src="boop" cool ebin=\'true\'>'
-      const output = { src: 'boop', cool: true, ebin: 'true' }
-
-      expect(getAttrs(input)).to.eql(output)
-    })
-  })
-})
diff --git a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js
deleted file mode 100644
index f301429d..00000000
--- a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
-
-describe('TinyPostHTMLProcessor', () => {
-  describe('with processor that keeps original line should not make any changes to HTML when', () => {
-    const processorKeep = (line) => line
-    it('fed with regular HTML with newlines', () => {
-      const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with possibly broken HTML with invalid tags/composition', () => {
-      const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with very broken HTML with broken composition', () => {
-      const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with sorta valid HTML but tags aren\'t closed', () => {
-      const inputOutput = 'just leaving a <div> hanging'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with not really HTML at this point... tags that aren\'t finished', () => {
-      const inputOutput = 'do you expect me to finish this <div class='
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
-      const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with maybe valid HTML? self-closing divs and ps', () => {
-      const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-
-    it('fed with valid XHTML containing a CDATA', () => {
-      const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
-      expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
-    })
-  })
-  describe('with processor that replaces lines with word "_" should match expected line when', () => {
-    const processorReplace = (line) => '_'
-    it('fed with regular HTML with newlines', () => {
-      const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
-      const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with possibly broken HTML with invalid tags/composition', () => {
-      const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
-      const output = '_'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with very broken HTML with broken composition', () => {
-      const input = '</p> lmao what </div> whats going on <div> wha <p>'
-      const output = '</p>_</div>_<div>_<p>'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with sorta valid HTML but tags aren\'t closed', () => {
-      const input = 'just leaving a <div> hanging'
-      const output = '_<div>_'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with not really HTML at this point... tags that aren\'t finished', () => {
-      const input = 'do you expect me to finish this <div class='
-      const output = '_'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
-      const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
-      const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with maybe valid HTML? self-closing divs and ps', () => {
-      const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
-      const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-
-    it('fed with valid XHTML containing a CDATA', () => {
-      const input = 'Yes, it is me, <![CDATA[DIO]]>'
-      const output = '_'
-      expect(processHtml(input, processorReplace)).to.eql(output)
-    })
-  })
-})
-- 
cgit v1.2.3-70-g09d2


From f883d2f75cd3c404115bd2c98b6d3c8d7ff10ef6 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 11 Jun 2021 03:11:58 +0300
Subject: better handling of hellthreads with mentions at bottom

---
 src/components/mention_link/mention_link.js      |   6 -
 src/components/mention_link/mention_link.scss    |   4 -
 src/components/mentions_line/mentions_line.scss  |   1 +
 src/components/rich_content/rich_content.jsx     | 198 ++++++++++++-----------
 src/components/status/status.js                  |  16 +-
 src/components/status/status.vue                 |   6 +-
 src/components/status_body/status_body.js        |  24 ++-
 src/components/status_body/status_body.vue       |  13 +-
 src/components/status_content/status_content.js  |   7 +-
 src/components/status_content/status_content.vue |   5 +-
 10 files changed, 138 insertions(+), 142 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 00b9e388..eec116db 100644
--- 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
index 9df1ccfe..1be3e7c5 100644
--- 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
index 735502de..59f75fbb 100644
--- 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
index 590fea0f..8972c494 100644
--- 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
index bab818fc..5b178c2e 100644
--- 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
index 0190d864..507e4192 100644
--- 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
index 2fc9abbf..7433619b 100644
--- 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
index bd599a8c..68f6701f 100644
--- 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
index 64cc6d44..11a4974b 100644
--- 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
index c32bbbfb..feb34d2c 100644
--- 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" />
-- 
cgit v1.2.3-70-g09d2


From a3c703bd37fc55c2377cb0bf099af6a21ea07e8e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 14 Jun 2021 02:52:41 +0300
Subject: compact notifs

---
 src/components/gallery/gallery.vue               |  5 ++-
 src/components/notification/notification.vue     |  3 +-
 src/components/status_body/status_body.js        |  1 +
 src/components/status_body/status_body.scss      | 56 ++++++++++++++++++++++++
 src/components/status_body/status_body.vue       |  7 ++-
 src/components/status_content/status_content.js  |  1 +
 src/components/status_content/status_content.vue | 13 +++++-
 7 files changed, 81 insertions(+), 5 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index ca91c9c1..3e967e9c 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -36,7 +36,10 @@
   height: 0;
   width: 100%;
   flex-grow: 1;
-  margin-top: 0.5em;
+
+  &:not(:first-child) {
+    margin-top: 0.5em;
+  }
 
   .gallery-row-inner {
     position: absolute;
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 0081dee4..396ae0e1 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -181,8 +181,9 @@
           </router-link>
         </div>
         <template v-else>
-          <status-content
+          <StatusContent
             class="faint"
+            :compact="true"
             :status="notification.action"
           />
         </template>
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 26491e1b..6dc028a6 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -23,6 +23,7 @@ library.add(
 const StatusContent = {
   name: 'StatusContent',
   props: [
+    'compact',
     'status',
     'focused',
     'noHeading',
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 81a687f1..a93c92e0 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -1,11 +1,17 @@
 @import '../../_variables.scss';
 
 .StatusBody {
+  display: flex;
+  flex-direction: column;
 
   .emoji {
     --_still_image-label-scale: 0.5;
   }
 
+  .attachments {
+    margin-top: 0.5em;
+  }
+
   & .text,
   & .summary {
     font-family: var(--postFont, sans-serif);
@@ -115,4 +121,54 @@
   .cyantext {
     color: var(--postCyantext, $fallback--cBlue);
   }
+
+  &.-compact {
+    flex-direction: row;
+
+    --emoji-size: 16px;
+
+    & .body,
+    & .attachments {
+      max-height: 3.25em;
+    }
+
+    .body {
+      overflow: hidden;
+      white-space: normal;
+      min-width: 5em;
+      flex: 5 1 auto;
+      mask-size: auto 3.5em, auto auto;
+      mask-position: 0 0, 0 0;
+      mask-repeat: repeat-x, repeat;
+      mask-image:
+        linear-gradient(to top, white 0.5em, transparent 2.5em),
+        linear-gradient(to top, white, white);
+
+      /* Autoprefixed seem to ignore this one, and also syntax is different */
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+    }
+
+    .attachments {
+      margin-top: 0;
+      flex: 1 1 auto;
+      min-width: 5em;
+      height: 100%;
+    }
+
+    .summary-wrapper {
+      .summary::after {
+        content: ': ';
+      }
+
+      line-height: inherit;
+      margin: 0;
+      border: none;
+      display: inline-block;
+    }
+
+    .text-wrapper {
+      display: inline-block;
+    }
+  }
 }
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 3dc4916c..6609d989 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -1,5 +1,8 @@
 <template>
-  <div class="StatusBody">
+<div
+  class="StatusBody"
+  :class="{ '-compact': compact }"
+>
     <div class="body">
       <div
         v-if="status.summary_raw_html"
@@ -8,6 +11,7 @@
       >
         <RichContent
           class="media-body summary"
+          :single-line="compact"
           :html="status.summary_raw_html"
           :emoji="status.emojis"
         />
@@ -52,6 +56,7 @@
             :html="status.raw_html"
             :emoji="status.emojis"
             :handle-links="true"
+            :single-line="compact"
             :greentext="mergedConfig.greentext"
             @parseReady="setHeadTailLinks"
           />
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 11a4974b..4d6f0a14 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -28,6 +28,7 @@ const StatusContent = {
   name: 'StatusContent',
   props: [
     'status',
+    'compact',
     'focused',
     'noHeading',
     'fullContent',
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index feb34d2c..06edd780 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,8 +1,12 @@
 <template>
-  <div class="StatusContent">
+<div
+  class="StatusContent"
+  :class="{ '-compact': compact }"
+>
     <slot name="header" />
     <StatusBody
       :status="status"
+      :compact="compact"
       :single-line="singleLine"
       :hide-mentions="hideMentions"
       @parseReady="setHeadTailLinks"
@@ -36,7 +40,7 @@
       </div>
 
       <div
-        v-if="status.card && !noHeading"
+        v-if="status.card && !noHeading && !compact"
         class="link-preview media-body"
       >
         <link-preview
@@ -59,5 +63,10 @@ $status-margin: 0.75em;
 .StatusContent {
   flex: 1;
   min-width: 0;
+  &.-compact {
+    flex {
+      display: flex;
+    }
+  }
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From c21b1cf89840297a781e6adc66cc195b8741cac6 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 14 Jun 2021 10:30:08 +0300
Subject: do the impossible, fix the unfixable

---
 src/components/rich_content/rich_content.jsx       | 16 +++-
 src/components/status_body/status_body.js          | 12 +--
 src/components/status_body/status_body.scss        |  2 +-
 src/components/status_body/status_body.vue         | 31 +++-----
 src/components/status_content/status_content.js    |  3 -
 src/components/status_content/status_content.vue   |  2 +-
 test/unit/specs/components/rich_content.spec.js    | 91 +++++++++++++++++++++-
 .../html_converter/html_line_converter.spec.js     |  2 +-
 8 files changed, 118 insertions(+), 41 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index ffb36f50..4144d895 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -5,6 +5,7 @@ import { convertHtmlToTree } from 'src/services/html_converter/html_tree_convert
 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 './rich_content.scss'
 
@@ -51,6 +52,11 @@ export default Vue.component('RichContent', {
       required: false,
       type: Boolean,
       default: false
+    },
+    hideMentions: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   // NEVER EVER TOUCH DATA INSIDE RENDER
@@ -64,6 +70,7 @@ export default Vue.component('RichContent', {
     // unique index for vue "tag" property
     let mentionIndex = 0
     let tagsIndex = 0
+    let firstMentionReplaced = false
 
     const renderImage = (tag) => {
       return <StillImage
@@ -90,7 +97,12 @@ export default Vue.component('RichContent', {
       writtenMentions.push(linkData)
       if (!encounteredText) {
         firstMentions.push(linkData)
-        return ''
+        if (!firstMentionReplaced && !this.hideMentions) {
+          firstMentionReplaced = true
+          return <MentionsLine mentions={ firstMentions } />
+        } else {
+          return ''
+        }
       } else {
         return <MentionLink
           url={attrs.href}
@@ -143,7 +155,7 @@ export default Vue.component('RichContent', {
               if (firstMentions.length > 1 && lastMentions.length > 1) {
                 break
               } else {
-                return ''
+                return !this.hideMentions ? <MentionsLine mentions={lastMentions} /> : ''
               }
             } else {
               break
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 26491e1b..9ee7a109 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -1,6 +1,5 @@
 import fileType from 'src/services/file_type/file_type.service'
 import RichContent from 'src/components/rich_content/rich_content.jsx'
-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'
@@ -36,9 +35,6 @@ const StatusContent = {
       showingLongSubject: false,
       // not as computed because it sets the initial state which will be changed later
       expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
-      headTailLinks: null,
-      firstMentions: [],
-      lastMentions: []
     }
   },
   computed: {
@@ -81,8 +77,7 @@ const StatusContent = {
     ...mapGetters(['mergedConfig'])
   },
   components: {
-    RichContent,
-    MentionsLine
+    RichContent
   },
   mounted () {
     this.status.attentions && this.status.attentions.forEach(attn => {
@@ -98,11 +93,6 @@ 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.scss b/src/components/status_body/status_body.scss
index 81a687f1..c7732bfe 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -62,7 +62,7 @@
       overflow-y: hidden;
       z-index: 1;
 
-      .rich-content-wrapper {
+      .media-body {
         min-height: 0;
         mask:
           linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 3dc4916c..2be46303 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -38,28 +38,17 @@
         >
           {{ $t("general.show_more") }}
         </button>
-        <span
+        <RichContent
           v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
-          class="rich-content-wrapper"
-        >
-          <MentionsLine
-            v-if="!hideMentions && firstMentions && firstMentions.length > 0"
-            :mentions="firstMentions"
-          />
-          <RichContent
-            :class="{ '-single-line': singleLine }"
-            class="text media-body"
-            :html="status.raw_html"
-            :emoji="status.emojis"
-            :handle-links="true"
-            :greentext="mergedConfig.greentext"
-            @parseReady="setHeadTailLinks"
-          />
-          <MentionsLine
-            v-if="!hideMentions && lastMentions.length > 1 && firstMentions.length <= 1"
-            :mentions="lastMentions"
-          />
-        </span>
+          :class="{ '-single-line': singleLine }"
+          class="text media-body"
+          :html="status.raw_html"
+          :emoji="status.emojis"
+          :handle-links="true"
+          :hide-mentions="hideMentions"
+          :greentext="mergedConfig.greentext"
+          @parseReady="$emit('parseReady', $event)"
+        />
 
         <button
           v-if="hideSubjectStatus"
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 11a4974b..51895ef6 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -92,9 +92,6 @@ 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
index feb34d2c..2e71757d 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -5,7 +5,7 @@
       :status="status"
       :single-line="singleLine"
       :hide-mentions="hideMentions"
-      @parseReady="setHeadTailLinks"
+      @parseReady="$emit('parseReady', $event)"
     >
       <div v-if="status.poll && status.poll.options">
         <poll :base-poll="status.poll" />
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 835fbea2..c6ec4315 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -16,6 +16,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -38,6 +39,34 @@ describe('RichContent', () => {
     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: [],
@@ -68,6 +97,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -78,6 +108,44 @@ describe('RichContent', () => {
     expect(wrapper.html()).to.eql(compwrap(expected))
   })
 
+  it('replaces mentions at the end of the hellpost if hideMentions=false (<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(
+        '<mentionsline-stub mentions="',
+        '[object Object],',
+        '[object Object],',
+        '[object Object]',
+        '"></mentionsline-stub>'
+      )
+    ].join('')
+
+    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?',
@@ -96,6 +164,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -124,6 +193,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -165,6 +235,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -199,6 +270,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -240,6 +312,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -267,6 +340,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: false,
         greentext: true,
         emoji: [],
@@ -290,6 +364,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: false,
         greentext: true,
         emoji: [],
@@ -309,6 +384,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [],
@@ -329,6 +405,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
@@ -345,6 +422,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [],
@@ -407,6 +485,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -425,10 +504,18 @@ describe('RichContent', () => {
       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: [],
@@ -436,7 +523,7 @@ describe('RichContent', () => {
       }
     })
 
-    expect(wrapper.html()).to.eql(compwrap(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', () => {
@@ -471,6 +558,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -506,6 +594,7 @@ describe('RichContent', () => {
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
+        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
diff --git a/test/unit/specs/services/html_converter/html_line_converter.spec.js b/test/unit/specs/services/html_converter/html_line_converter.spec.js
index c8c89700..de7c7fc2 100644
--- a/test/unit/specs/services/html_converter/html_line_converter.spec.js
+++ b/test/unit/specs/services/html_converter/html_line_converter.spec.js
@@ -11,7 +11,7 @@ const mapOnlyText = (processor) => (input) => {
   }
 }
 
-describe.only('html_line_converter', () => {
+describe('html_line_converter', () => {
   describe('with processor that keeps original line should not make any changes to HTML when', () => {
     const processorKeep = (line) => line
     it('fed with regular HTML with newlines', () => {
-- 
cgit v1.2.3-70-g09d2


From e654fead23ebb457f81e8642c65e1f3e98ee0027 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 17 Jun 2021 16:29:46 +0300
Subject: refactored attachments and gallery. All attachments now are in
 gallery.

---
 src/components/attachment/attachment.js          |  41 ++-
 src/components/attachment/attachment.vue         | 307 ++++++-----------------
 src/components/chat_message/chat_message.scss    |   4 -
 src/components/flash/flash.js                    |   5 +-
 src/components/flash/flash.vue                   |   7 -
 src/components/gallery/gallery.js                |  65 +++--
 src/components/gallery/gallery.vue               | 175 +++++++++----
 src/components/status_content/status_content.js  |  20 +-
 src/components/status_content/status_content.vue |  31 +--
 src/i18n/en.json                                 |   6 +-
 10 files changed, 296 insertions(+), 365 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 8849f501..06928ca6 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -11,7 +11,9 @@ import {
   faImage,
   faVideo,
   faPlayCircle,
-  faTimes
+  faTimes,
+  faStop,
+  faSearchPlus
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -20,7 +22,9 @@ library.add(
   faImage,
   faVideo,
   faPlayCircle,
-  faTimes
+  faTimes,
+  faStop,
+  faSearchPlus
 )
 
 const Attachment = {
@@ -28,7 +32,6 @@ const Attachment = {
     'attachment',
     'nsfw',
     'size',
-    'allowPlay',
     'setMedia',
     'naturalSizeLoad'
   ],
@@ -40,7 +43,8 @@ const Attachment = {
       loading: false,
       img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
       modalOpen: false,
-      showHidden: false
+      showHidden: false,
+      flashLoaded: false
     }
   },
   components: {
@@ -49,9 +53,22 @@ const Attachment = {
     VideoAttachment
   },
   computed: {
+    classNames () {
+      return [
+        {
+          '-loading': this.loading,
+          '-nsfw-placeholder': this.hidden
+        },
+        '-' + this.type,
+        `-${this.useContainFit ? 'contain' : 'cover'}-fit`
+      ]
+    },
     usePlaceholder () {
       return this.size === 'hide' || this.type === 'unknown'
     },
+    useContainFit () {
+      return this.$store.getters.mergedConfig.useContainFit
+    },
     placeholderName () {
       if (this.attachment.description === '' || !this.attachment.description) {
         return this.type.toUpperCase()
@@ -79,10 +96,6 @@ const Attachment = {
     isSmall () {
       return this.size === 'small'
     },
-    fullwidth () {
-      if (this.size === 'hide') return false
-      return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
-    },
     useModal () {
       const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
         : this.mergedConfig.playVideosInModal
@@ -100,12 +113,20 @@ const Attachment = {
     },
     openModal (event) {
       if (this.useModal) {
-        event.stopPropagation()
-        event.preventDefault()
         this.setMedia()
         this.$store.dispatch('setCurrent', this.attachment)
       }
     },
+    openModalForce (event) {
+      this.setMedia()
+      this.$store.dispatch('setCurrent', this.attachment)
+    },
+    stopFlash () {
+      this.$refs.flash.closePlayer()
+    },
+    setFlashLoaded (event) {
+      this.flashLoaded = event
+    },
     toggleHidden (event) {
       if (
         (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index f80badfd..fe9e9398 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -1,7 +1,8 @@
 <template>
-  <div
+  <button
     v-if="usePlaceholder"
-    :class="{ 'fullwidth': fullwidth }"
+    class="Attachment -placeholder button-unstyled"
+    :class="classNames"
     @click="openModal"
   >
     <a
@@ -15,16 +16,16 @@
       <FAIcon :icon="placeholderIconClass" />
       <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
     </a>
-  </div>
+  </button>
   <div
     v-else
     v-show="!isEmpty"
-    class="attachment"
-    :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
+    class="Attachment"
+    :class="classNames"
   >
     <a
       v-if="hidden"
-      class="image-attachment"
+      class="image-container"
       :href="attachment.url"
       :alt="attachment.description"
       :title="attachment.description"
@@ -34,7 +35,6 @@
         :key="nsfwImage"
         class="nsfw"
         :src="nsfwImage"
-        :class="{'small': isSmall}"
       >
       <FAIcon
         v-if="type === 'video'"
@@ -42,21 +42,40 @@
         icon="play-circle"
       />
     </a>
-    <button
-      v-if="nsfw && hideNsfwLocal && !hidden"
-      class="button-unstyled hider"
-      @click.prevent="toggleHidden"
-    >
-      <FAIcon icon="times" />
-    </button>
+    <div
+      class="attachment-buttons"
+      v-if="!hidden"
+      >
+      <button
+        v-if="type === 'flash' && flashLoaded"
+        class="button-unstyled attachment-button"
+        @click.prevent="stopFlash"
+      >
+        <FAIcon icon="stop" />
+      </button>
+      <button
+        v-if="!useModal"
+        class="button-unstyled attachment-button"
+        @click.prevent="openModalForce"
+      >
+        <FAIcon icon="search-plus" />
+      </button>
+      <button
+        v-if="nsfw && hideNsfwLocal"
+        class="button-unstyled attachment-button"
+        @click.prevent="toggleHidden"
+      >
+        <FAIcon icon="times" />
+      </button>
+    </div>
 
     <a
       v-if="type === 'image' && (!hidden || preloadImage)"
-      class="image-attachment"
-      :class="{'hidden': hidden && preloadImage }"
+      class="image-container"
+      :class="{'-hidden': hidden && preloadImage }"
       :href="attachment.url"
       target="_blank"
-      @click="openModal"
+      @click.stop.prevent="openModal"
     >
       <StillImage
         class="image"
@@ -71,37 +90,43 @@
     <a
       v-if="type === 'video' && !hidden"
       class="video-container"
-      :class="{'small': isSmall}"
-      :href="allowPlay ? undefined : attachment.url"
-      @click="openModal"
+      :href="attachment.url"
+      @click.stop.prevent="openModal"
     >
       <VideoAttachment
         class="video"
         :attachment="attachment"
-        :controls="allowPlay"
+        :controls="!useModal"
         @play="$emit('play')"
         @pause="$emit('pause')"
       />
       <FAIcon
-        v-if="!allowPlay"
+        v-if="useModal"
         class="play-icon"
         icon="play-circle"
       />
     </a>
 
-    <audio
-      v-if="type === 'audio'"
-      :src="attachment.url"
-      :alt="attachment.description"
-      :title="attachment.description"
-      controls
-      @play="$emit('play')"
-      @pause="$emit('pause')"
-    />
+    <a
+      v-if="type === 'audio' && !hidden"
+      class="audio-container"
+      :href="attachment.url"
+      @click.stop.prevent="openModal"
+    >
+      <audio
+        v-if="type === 'audio'"
+        :src="attachment.url"
+        :alt="attachment.description"
+        :title="attachment.description"
+        controls
+        @play="$emit('play')"
+        @pause="$emit('pause')"
+      />
+    </a>
 
     <div
       v-if="type === 'html' && attachment.oembed"
-      class="oembed"
+      class="oembed-container"
       @click.prevent="linkClicked"
     >
       <div
@@ -118,211 +143,23 @@
       </div>
     </div>
 
-    <Flash
-      v-if="type === 'flash'"
-      :src="attachment.large_thumb_url || attachment.url"
-    />
+    <a
+      v-if="type === 'flash' && !hidden"
+      class="flash-container"
+      :href="attachment.url"
+      @click.stop.prevent="openModal"
+    >
+      <Flash
+        class="flash"
+        ref="flash"
+        :src="attachment.large_thumb_url || attachment.url"
+        @playerOpened="setFlashLoaded(true)"
+        @playerClosed="setFlashLoaded(false)"
+      />
+    </a>
   </div>
 </template>
 
 <script src="./attachment.js"></script>
 
-<style lang="scss">
-@import '../../_variables.scss';
-
-.attachments {
-  display: flex;
-  flex-wrap: wrap;
-
-  .non-gallery {
-    max-width: 100%;
-  }
-
-  .placeholder {
-    display: inline-block;
-    padding: 0.3em 1em 0.3em 0;
-    color: $fallback--link;
-    color: var(--postLink, $fallback--link);
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    max-width: 100%;
-
-    svg {
-      color: inherit;
-    }
-  }
-
-  .nsfw-placeholder {
-    cursor: pointer;
-
-    &.loading {
-      cursor: progress;
-    }
-  }
-
-  .attachment {
-    position: relative;
-    margin-top: 0.5em;
-    align-self: flex-start;
-    line-height: 0;
-
-    border-style: solid;
-    border-width: 1px;
-    border-radius: $fallback--attachmentRadius;
-    border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
-    border-color: $fallback--border;
-    border-color: var(--border, $fallback--border);
-    overflow: hidden;
-  }
-
-  .non-gallery.attachment {
-    &.flash,
-    &.video {
-      flex: 1 0 40%;
-    }
-    .nsfw {
-      height: 260px;
-    }
-    .small {
-      height: 120px;
-      flex-grow: 0;
-    }
-    .video {
-      height: 260px;
-      display: flex;
-    }
-    video {
-      max-height: 100%;
-      object-fit: contain;
-    }
-  }
-
-  .fullwidth {
-    flex-basis: 100%;
-  }
-  // fixes small gap below video
-  &.video {
-    line-height: 0;
-  }
-
-  .video-container {
-    display: flex;
-    max-height: 100%;
-  }
-
-  .video {
-    width: 100%;
-    height: 100%;
-  }
-
-  .play-icon {
-    position: absolute;
-    font-size: 64px;
-    top: calc(50% - 32px);
-    left: calc(50% - 32px);
-    color: rgba(255, 255, 255, 0.75);
-    text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
-  }
-
-  .play-icon::before {
-    margin: 0;
-  }
-
-  &.html {
-    flex-basis: 90%;
-    width: 100%;
-    display: flex;
-  }
-
-  .hider {
-    position: absolute;
-    right: 0;
-    margin: 10px;
-    padding: 0;
-    z-index: 4;
-    border-radius: $fallback--tooltipRadius;
-    border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
-    text-align: center;
-    width: 2em;
-    height: 2em;
-    font-size: 1.25em;
-    // TODO: theming? hard to theme with unknown background image color
-    background: rgba(230, 230, 230, 0.7);
-    .svg-inline--fa {
-      color: rgba(0, 0, 0, 0.6);
-    }
-    &:hover .svg-inline--fa {
-      color: rgba(0, 0, 0, 0.9);
-    }
-  }
-
-  video {
-    z-index: 0;
-  }
-
-  audio {
-    width: 100%;
-  }
-
-  img.media-upload {
-    line-height: 0;
-    max-height: 200px;
-    max-width: 100%;
-  }
-
-  .oembed {
-    line-height: 1.2em;
-    flex: 1 0 100%;
-    width: 100%;
-    margin-right: 15px;
-    display: flex;
-
-    img {
-      width: 100%;
-    }
-
-    .image {
-      flex: 1;
-      img {
-        border: 0px;
-        border-radius: 5px;
-        height: 100%;
-        object-fit: cover;
-      }
-    }
-
-    .text {
-      flex: 2;
-      margin: 8px;
-      word-break: break-all;
-      h1 {
-        font-size: 14px;
-        margin: 0px;
-      }
-    }
-  }
-
-  .image-attachment {
-    &,
-    & .image {
-      width: 100%;
-      height: 100%;
-    }
-
-    &.hidden {
-      display: none;
-    }
-
-    .nsfw {
-      object-fit: cover;
-      width: 100%;
-      height: 100%;
-    }
-
-    img {
-      image-orientation: from-image; // NOTE: only FF supports this
-    }
-  }
-}
-</style>
+<style src="./attachment.scss" lang="scss"></style>
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
index fcfa7c8a..1dbe1cad 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -62,10 +62,6 @@
     &.with-media {
       width: 100%;
 
-      .gallery-row {
-        overflow: hidden;
-      }
-
       .status {
         width: 100%;
       }
diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js
index d03384c7..87f940a7 100644
--- a/src/components/flash/flash.js
+++ b/src/components/flash/flash.js
@@ -39,12 +39,13 @@ const Flash = {
           this.player = 'error'
         })
         this.ruffleInstance = player
+        this.$emit('playerOpened')
       })
     },
     closePlayer () {
-      console.log(this.ruffleInstance)
-      this.ruffleInstance.remove()
+      this.ruffleInstance && this.ruffleInstance.remove()
       this.player = false
+      this.$emit('playerClosed')
     }
   }
 }
diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue
index d20d037b..5a77d235 100644
--- a/src/components/flash/flash.vue
+++ b/src/components/flash/flash.vue
@@ -36,13 +36,6 @@
         </p>
       </span>
     </button>
-    <button
-      v-if="player"
-      class="button-unstyled hider"
-      @click="closePlayer"
-    >
-      <FAIcon icon="stop" />
-    </button>
   </div>
 </template>
 
diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js
index f856fd0a..134ea77e 100644
--- a/src/components/gallery/gallery.js
+++ b/src/components/gallery/gallery.js
@@ -1,15 +1,17 @@
 import Attachment from '../attachment/attachment.vue'
-import { chunk, last, dropRight, sumBy } from 'lodash'
+import { sumBy } from 'lodash'
 
 const Gallery = {
   props: [
     'attachments',
     'nsfw',
-    'setMedia'
+    'setMedia',
+    'size'
   ],
   data () {
     return {
-      sizes: {}
+      sizes: {},
+      hidingLong: true
     }
   },
   components: { Attachment },
@@ -18,26 +20,54 @@ const Gallery = {
       if (!this.attachments) {
         return []
       }
-      const rows = chunk(this.attachments, 3)
-      if (last(rows).length === 1 && rows.length > 1) {
-        // if 1 attachment on last row -> add it to the previous row instead
-        const lastAttachment = last(rows)[0]
-        const allButLastRow = dropRight(rows)
-        last(allButLastRow).push(lastAttachment)
-        return allButLastRow
+      console.log(this.size)
+      if (this.size === 'hide') {
+        return this.attachments.map(item => ({ minimal: true, items: [item] }))
       }
+      const rows = this.attachments.reduce((acc, attachment, i) => {
+        if (attachment.mimetype.includes('audio')) {
+          return [...acc, { audio: true, items: [attachment] }, { items: [] }]
+        }
+        const maxPerRow = 3
+        const attachmentsRemaining = this.attachments.length - i - 1
+        const currentRow = acc[acc.length - 1].items
+        if (
+          currentRow.length <= maxPerRow ||
+            attachmentsRemaining === 1
+        ) {
+          currentRow.push(attachment)
+        }
+        if (currentRow.length === maxPerRow && attachmentsRemaining > 1) {
+          return [...acc, { items: [] }]
+        } else {
+          return acc
+        }
+      }, [{ items: [] }]).filter(_ => _.items.length > 0)
       return rows
     },
-    useContainFit () {
-      return this.$store.getters.mergedConfig.useContainFit
+    attachmentsDimensionalScore () {
+      return this.rows.reduce((acc, row) => {
+        return acc + (row.audio ? 0.25 : (1 / (row.items.length + 0.6)))
+      }, 0)
+    },
+    tooManyAttachments () {
+      if (this.size === 'hide') {
+        return this.attachments.length > 8
+      } else {
+        return this.attachmentsDimensionalScore > 1
+      }
     }
   },
   methods: {
     onNaturalSizeLoad (id, size) {
       this.$set(this.sizes, id, size)
     },
-    rowStyle (itemsPerRow) {
-      return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
+    rowStyle (row) {
+      if (row.audio) {
+        return { 'padding-bottom': '25%' } // fixed reduced height for audio
+      } else if (!row.minimal) {
+        return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
+      }
     },
     itemStyle (id, row) {
       const total = sumBy(row, item => this.getAspectRatio(item.id))
@@ -46,6 +76,13 @@ const Gallery = {
     getAspectRatio (id) {
       const size = this.sizes[id]
       return size ? size.width / size.height : 1
+    },
+    toggleHidingLong (event) {
+      this.hidingLong = event
+    },
+    openGallery () {
+      this.setMedia()
+      this.$store.dispatch('setCurrent', this.attachments[0])
     }
   }
 }
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index ca91c9c1..8e08e514 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -1,26 +1,74 @@
 <template>
   <div
+    class="Gallery"
     ref="galleryContainer"
-    style="width: 100%;"
+    :class="{ '-long': tooManyAttachments && hidingLong  }"
   >
+    <div class="gallery-rows">
+      <div
+        v-for="(row, index) in rows"
+        :key="index"
+        class="gallery-row"
+        :style="rowStyle(row)"
+        :class="{ '-audio': row.audio, '-minimal': row.minimal }"
+      >
+        <div class="gallery-row-inner">
+          <attachment
+            v-for="attachment in row.items"
+            class="gallery-item"
+            :key="attachment.id"
+            :set-media="setMedia"
+            :nsfw="nsfw"
+            :attachment="attachment"
+            :allow-play="false"
+            :size="size"
+            :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
+            :style="itemStyle(attachment.id, row.items)"
+          />
+        </div>
+      </div>
+    </div>
     <div
-      v-for="(row, index) in rows"
-      :key="index"
-      class="gallery-row"
-      :style="rowStyle(row.length)"
-      :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
+      v-if="tooManyAttachments"
+      class="many-attachments"
     >
-      <div class="gallery-row-inner">
-        <attachment
-          v-for="attachment in row"
-          :key="attachment.id"
-          :set-media="setMedia"
-          :nsfw="nsfw"
-          :attachment="attachment"
-          :allow-play="false"
-          :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
-          :style="itemStyle(attachment.id, row)"
-        />
+      <div class="many-attachments-text">
+        {{ $t("status.many_attachments", { number: attachments.length })}}
+      </div>
+      <div class="many-attachments-buttons">
+        <span
+          v-if="!hidingLong"
+          class="many-attachments-button"
+        >
+          <button
+            class="button-unstyled -link"
+            @click="toggleHidingLong(true)"
+          >
+            {{ $t("status.collapse_attachments") }}
+          </button>
+        </span>
+        <span
+          v-if="hidingLong"
+          class="many-attachments-button"
+        >
+          <button
+            class="button-unstyled -link"
+            @click="toggleHidingLong(false)"
+          >
+            {{ $t("status.show_all_attachments") }}
+          </button>
+        </span>
+        <span
+          class="many-attachments-button"
+          v-if="hidingLong"
+        >
+          <button
+            class="button-unstyled -link"
+            @click="openGallery"
+          >
+            {{ $t("status.open_gallery") }}
+          </button>
+        </span>
       </div>
     </div>
   </div>
@@ -31,12 +79,64 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.gallery-row {
-  position: relative;
-  height: 0;
-  width: 100%;
-  flex-grow: 1;
-  margin-top: 0.5em;
+.Gallery {
+  .gallery-rows {
+    display: flex;
+    flex-direction: column;
+  }
+
+  .gallery-row {
+    position: relative;
+    height: 0;
+    width: 100%;
+    flex-grow: 1;
+    margin-top: 0.5em;
+  }
+
+  &.-long {
+    .gallery-rows {
+      max-height: 25em;
+      overflow: hidden;
+      mask:
+        linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+        linear-gradient(to top, white, white);
+
+      /* Autoprefixed seem to ignore this one, and also syntax is different */
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+    }
+  }
+
+  .many-attachments-text {
+    text-align: center;
+    line-height: 2;
+  }
+
+  .many-attachments-buttons {
+    display: flex;
+  }
+
+  .many-attachments-button {
+    display: flex;
+    flex: 1;
+    justify-content: center;
+    line-height: 2;
+
+    button {
+      padding: 0 2em;
+    }
+  }
+
+  .gallery-row {
+
+    &.-minimal {
+      height: auto;
+
+      .gallery-row-inner {
+        position: relative;
+      }
+    }
+  }
 
   .gallery-row-inner {
     position: absolute;
@@ -50,7 +150,7 @@
     align-content: stretch;
   }
 
-  .gallery-row-inner .attachment {
+  .gallery-item {
     margin: 0 0.5em 0 0;
     flex-grow: 1;
     height: 100%;
@@ -61,32 +161,5 @@
       margin: 0;
     }
   }
-
-  .image-attachment {
-    width: 100%;
-    height: 100%;
-  }
-
-  .video-container {
-    height: 100%;
-  }
-
-  &.contain-fit {
-    img,
-    video,
-    canvas {
-      object-fit: contain;
-      height: 100%;
-    }
-  }
-
-  &.cover-fit {
-    img,
-    video,
-    canvas {
-      object-fit: cover;
-    }
-  }
 }
-
 </style>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 51895ef6..49f9d7f8 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -58,24 +58,6 @@ const StatusContent = {
       }
       return 'normal'
     },
-    galleryTypes () {
-      if (this.attachmentSize === 'hide') {
-        return []
-      }
-      return this.mergedConfig.playVideosInModal
-        ? ['image', 'video']
-        : ['image']
-    },
-    galleryAttachments () {
-      return this.status.attachments.filter(
-        file => fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
-    nonGalleryAttachments () {
-      return this.status.attachments.filter(
-        file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
     maxThumbnails () {
       return this.mergedConfig.maxThumbnails
     },
@@ -93,7 +75,7 @@ const StatusContent = {
   },
   methods: {
     setMedia () {
-      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+      const attachments = this.status.attachments
       return () => this.$store.dispatch('setMedia', attachments)
     }
   }
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 2e71757d..0f999da6 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -11,29 +11,16 @@
         <poll :base-poll="status.poll" />
       </div>
 
-      <div
-        v-if="status.attachments.length !== 0"
+      <gallery
         class="attachments media-body"
-      >
-        <attachment
-          v-for="attachment in nonGalleryAttachments"
-          :key="attachment.id"
-          class="non-gallery"
-          :size="attachmentSize"
-          :nsfw="nsfwClickthrough"
-          :attachment="attachment"
-          :allow-play="true"
-          :set-media="setMedia()"
-          @play="$emit('mediaplay', attachment.id)"
-          @pause="$emit('mediapause', attachment.id)"
-        />
-        <gallery
-          v-if="galleryAttachments.length > 0"
-          :nsfw="nsfwClickthrough"
-          :attachments="galleryAttachments"
-          :set-media="setMedia()"
-        />
-      </div>
+        v-if="status.attachments.length !== 0"
+        :nsfw="nsfwClickthrough"
+        :attachments="status.attachments"
+        :set-media="setMedia()"
+        :size="attachmentSize"
+        @play="$emit('mediaplay', attachment.id)"
+        @pause="$emit('mediapause', attachment.id)"
+      />
 
       <div
         v-if="status.card && !noHeading"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 86870447..a4f24c64 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -717,7 +717,11 @@
     "nsfw": "NSFW",
     "expand": "Expand",
     "you": "(You)",
-    "plus_more": "+{number} more"
+    "plus_more": "+{number} more",
+    "many_attachments": "Post has {number} attachment(s)",
+    "collapse_attachments": "Collapse attachments",
+    "show_all_attachments": "Show all attachments",
+    "open_gallery": "Open gallery"
   },
   "user_card": {
     "approve": "Approve",
-- 
cgit v1.2.3-70-g09d2


From 90345f158ff2eb12e619dbd7406f2fa81c379637 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 18 Jun 2021 02:01:37 +0300
Subject: gallery now supports flash, fixes for flash component. refactored
 media modal

---
 src/components/attachment/attachment.js          |  9 +++++----
 src/components/flash/flash.vue                   | 21 ++++++++++++---------
 src/components/media_modal/media_modal.js        |  8 +++++---
 src/components/media_modal/media_modal.vue       |  7 +++++++
 src/components/status_content/status_content.js  |  6 ------
 src/components/status_content/status_content.vue |  2 +-
 src/components/user_card/user_card.js            |  2 +-
 src/modules/media_viewer.js                      |  9 +++++----
 8 files changed, 36 insertions(+), 28 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 06928ca6..3ea96a7a 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -113,13 +113,14 @@ const Attachment = {
     },
     openModal (event) {
       if (this.useModal) {
-        this.setMedia()
-        this.$store.dispatch('setCurrent', this.attachment)
+        this.$emit('setMedia')
+        this.$store.dispatch('setCurrentMedia', this.attachment)
       }
     },
     openModalForce (event) {
-      this.setMedia()
-      this.$store.dispatch('setCurrent', this.attachment)
+      this.$emit('setMedia')
+      this.$store.dispatch('setCurrentMedia', this.attachment)
+    },
     },
     stopFlash () {
       this.$refs.flash.closePlayer()
diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue
index 5a77d235..95f71950 100644
--- a/src/components/flash/flash.vue
+++ b/src/components/flash/flash.vue
@@ -44,8 +44,9 @@
 <style lang="scss">
 @import '../../_variables.scss';
 .Flash {
+  display: inline-block;
   width: 100%;
-  height: 260px;
+  height: 100%;
   position: relative;
 
   .player {
@@ -53,6 +54,16 @@
     width: 100%;
   }
 
+  .placeholder {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: var(--bg);
+    color: var(--link);
+  }
+
   .hider {
     top: 0;
   }
@@ -69,13 +80,5 @@
     display: none;
     visibility: 'hidden';
   }
-
-  .placeholder {
-    height: 100%;
-    flex: 1;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
 }
 </style>
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
index e7384c93..b6919366 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -3,6 +3,7 @@ import VideoAttachment from '../video_attachment/video_attachment.vue'
 import Modal from '../modal/modal.vue'
 import fileTypeService from '../../services/file_type/file_type.service.js'
 import GestureService from '../../services/gesture_service/gesture_service'
+import Flash from 'src/components/flash/flash.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faChevronLeft,
@@ -18,7 +19,8 @@ const MediaModal = {
   components: {
     StillImage,
     VideoAttachment,
-    Modal
+    Modal,
+    Flash
   },
   computed: {
     showing () {
@@ -67,13 +69,13 @@ const MediaModal = {
     goPrev () {
       if (this.canNavigate) {
         const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
-        this.$store.dispatch('setCurrent', this.media[prevIndex])
+        this.$store.dispatch('setCurrentMedia', this.media[prevIndex])
       }
     },
     goNext () {
       if (this.canNavigate) {
         const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
-        this.$store.dispatch('setCurrent', this.media[nextIndex])
+        this.$store.dispatch('setCurrentMedia', this.media[nextIndex])
       }
     },
     handleKeyupEvent (e) {
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 54bc5335..a578bc71 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -28,6 +28,13 @@
       :title="currentMedia.description"
       controls
     />
+    <Flash
+      v-if="type === 'flash'"
+      class="modal-image"
+      :src="currentMedia.url"
+      :alt="currentMedia.description"
+      :title="currentMedia.description"
+    />
     <button
       v-if="canNavigate"
       :title="$t('media_modal.previous')"
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 49f9d7f8..c085c992 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -72,12 +72,6 @@ const StatusContent = {
     Gallery,
     LinkPreview,
     StatusBody
-  },
-  methods: {
-    setMedia () {
-      const attachments = this.status.attachments
-      return () => this.$store.dispatch('setMedia', attachments)
-    }
   }
 }
 
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 0f999da6..a9511fbe 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -16,8 +16,8 @@
         v-if="status.attachments.length !== 0"
         :nsfw="nsfwClickthrough"
         :attachments="status.attachments"
-        :set-media="setMedia()"
         :size="attachmentSize"
+        @setMedia="onMedia"
         @play="$emit('mediaplay', attachment.id)"
         @pause="$emit('mediapause', attachment.id)"
       />
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index d9fb64d1..23e6358f 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -159,7 +159,7 @@ export default {
         mimetype: 'image'
       }
       this.$store.dispatch('setMedia', [attachment])
-      this.$store.dispatch('setCurrent', attachment)
+      this.$store.dispatch('setCurrentMedia', attachment)
     },
     mentionUser () {
       this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js
index 721c25e6..ebcba01d 100644
--- a/src/modules/media_viewer.js
+++ b/src/modules/media_viewer.js
@@ -1,4 +1,5 @@
 import fileTypeService from '../services/file_type/file_type.service.js'
+const supportedTypes = new Set(['image', 'video', 'audio', 'flash'])
 
 const mediaViewer = {
   state: {
@@ -10,7 +11,7 @@ const mediaViewer = {
     setMedia (state, media) {
       state.media = media
     },
-    setCurrent (state, index) {
+    setCurrentMedia (state, index) {
       state.activated = true
       state.currentIndex = index
     },
@@ -22,13 +23,13 @@ const mediaViewer = {
     setMedia ({ commit }, attachments) {
       const media = attachments.filter(attachment => {
         const type = fileTypeService.fileType(attachment.mimetype)
-        return type === 'image' || type === 'video' || type === 'audio'
+        return supportedTypes.has(type)
       })
       commit('setMedia', media)
     },
-    setCurrent ({ commit, state }, current) {
+    setCurrentMedia ({ commit, state }, current) {
       const index = state.media.indexOf(current)
-      commit('setCurrent', index || 0)
+      commit('setCurrentMedia', index || 0)
     },
     closeMediaViewer ({ commit }) {
       commit('close')
-- 
cgit v1.2.3-70-g09d2


From f35c090caaccf272c12358a2a02df71b9212d16c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 18 Jun 2021 02:27:32 +0300
Subject: merged in compact notifs and improved upon it

---
 src/components/gallery/gallery.js                    | 10 +++++++---
 src/components/gallery/gallery.vue                   |  2 +-
 src/components/post_status_form/post_status_form.vue |  1 -
 src/components/status_body/status_body.scss          |  2 +-
 src/components/status_body/status_body.vue           |  5 ++++-
 src/components/status_content/status_content.vue     |  1 +
 6 files changed, 14 insertions(+), 7 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js
index cca67dbd..ab13f698 100644
--- a/src/components/gallery/gallery.js
+++ b/src/components/gallery/gallery.js
@@ -6,6 +6,7 @@ const Gallery = {
     'attachments',
     'limitRows',
     'descriptions',
+    'limit',
     'nsfw',
     'setMedia',
     'size',
@@ -26,11 +27,14 @@ const Gallery = {
       if (!this.attachments) {
         return []
       }
+      console.log(this.limit)
+      const attachments = this.limit > 0
+        ? this.attachments.slice(0, this.limit)
+        : this.attachments
       if (this.size === 'hide') {
-        return this.attachments.map(item => ({ minimal: true, items: [item] }))
+        return attachments.map(item => ({ minimal: true, items: [item] }))
       }
-      const rows = this.attachments.reduce((acc, attachment, i) => {
-        if (this.size === 'small' && acc.length === 2) return acc
+      const rows = attachments.reduce((acc, attachment, i) => {
         if (attachment.mimetype.includes('audio')) {
           return [...acc, { audio: true, items: [attachment] }, { items: [] }]
         }
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index cedf64d3..a1f1c26c 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -25,7 +25,7 @@
             :remove="removeAttachment"
             :edit="editAttachment"
             :description="descriptions && descriptions[attachment.id]"
-            :hideDescription="tooManyAttachments && hidingLong"
+            :hideDescription="size === 'small' || tooManyAttachments && hidingLong"
             :style="itemStyle(attachment.id, row.items)"
             @setMedia="onMedia"
             @naturalSizeLoad="onNaturalSizeLoad"
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index c6f84a4b..88ca4c9c 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -298,7 +298,6 @@
         :editable="true"
         :editAttachment="editAttachment"
         :removeAttachment="removeMediaFile"
-        size="small"
         @play="$emit('mediaplay', attachment.id)"
         @pause="$emit('mediapause', attachment.id)"
       />
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 516ced9d..5be21171 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -151,7 +151,7 @@
 
     .attachments {
       margin-top: 0;
-      flex: 1 1 auto;
+      flex: 1 1 0;
       min-width: 5em;
       height: 100%;
     }
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 2be46303..3eb13ce2 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -1,5 +1,8 @@
 <template>
-  <div class="StatusBody">
+<div
+  class="StatusBody"
+  :class="{ '-compact': compact }"
+  >
     <div class="body">
       <div
         v-if="status.summary_raw_html"
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index c1533b19..90ce130d 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -20,6 +20,7 @@
         v-if="status.attachments.length !== 0"
         :nsfw="nsfwClickthrough"
         :attachments="status.attachments"
+        :limit="this.compact ? 1 : 0"
         :size="attachmentSize"
         @setMedia="onMedia"
         @play="$emit('mediaplay', attachment.id)"
-- 
cgit v1.2.3-70-g09d2


From bfe31e20eaddaa9435c98962c53c35f4184ed5fe Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 18 Jun 2021 14:12:50 +0300
Subject: better compact attachments

---
 src/components/attachment/attachment.js          | 17 +++++++++++++----
 src/components/gallery/gallery.vue               |  5 ++++-
 src/components/status_body/status_body.scss      |  6 +++---
 src/components/status_content/status_content.vue |  5 -----
 4 files changed, 20 insertions(+), 13 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index a80c5c2e..84656ffa 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -68,6 +68,7 @@ const Attachment = {
           '-nsfw-placeholder': this.hidden
         },
         '-' + this.type,
+        '-' + this.size,
         `-${this.useContainFit ? 'contain' : 'cover'}-fit`
       ]
     },
@@ -102,10 +103,18 @@ const Attachment = {
       return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
     },
     useModal () {
-      const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
-        : this.mergedConfig.playVideosInModal
-          ? ['image', 'video']
-          : ['image']
+      let modalTypes = []
+      switch (this.size) {
+        case 'hide':
+        case 'small':
+          modalTypes = ['image', 'video', 'audio', 'flash']
+          break
+        default:
+          modalTypes = this.mergedConfig.playVideosInModal
+            ? ['image', 'video', 'flash']
+            : ['image']
+          break
+      }
       return modalTypes.includes(this.type)
     },
     ...mapGetters(['mergedConfig'])
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index a1f1c26c..65175366 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -95,7 +95,10 @@
     height: 0;
     width: 100%;
     flex-grow: 1;
-    margin-top: 0.5em;
+
+    &:not(:first-child) {
+      margin-top: 0.5em;
+    }
   }
 
   &.-long {
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 5be21171..f261108e 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -123,6 +123,7 @@
   }
 
   &.-compact {
+    align-items: top;
     flex-direction: row;
 
     --emoji-size: 16px;
@@ -140,9 +141,7 @@
       mask-size: auto 3.5em, auto auto;
       mask-position: 0 0, 0 0;
       mask-repeat: repeat-x, repeat;
-      mask-image:
-        linear-gradient(to top, white 0.5em, transparent 2.5em),
-        linear-gradient(to top, white, white);
+      mask-image: linear-gradient(to bottom, white 2em, transparent 3em);
 
       /* Autoprefixed seem to ignore this one, and also syntax is different */
       -webkit-mask-composite: xor;
@@ -154,6 +153,7 @@
       flex: 1 1 0;
       min-width: 5em;
       height: 100%;
+      margin-left: 0.5em;
     }
 
     .summary-wrapper {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 90ce130d..573eadf5 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -51,10 +51,5 @@ $status-margin: 0.75em;
 .StatusContent {
   flex: 1;
   min-width: 0;
-  &.-compact {
-    flex {
-      display: flex;
-    }
-  }
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From b67db47c888dd45c49a49b82e7922c2bf40ed61c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 22 Jun 2021 20:47:35 +0300
Subject: lint

---
 src/components/attachment/attachment.vue             | 16 ++++++++--------
 src/components/gallery/gallery.vue                   | 12 ++++++------
 src/components/post_status_form/post_status_form.vue |  4 ++--
 src/components/status_body/status_body.vue           |  6 +++---
 src/components/status_content/status_content.js      |  1 -
 src/components/status_content/status_content.vue     | 12 ++++++------
 6 files changed, 25 insertions(+), 26 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index e1e700c4..98d9c3ec 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -15,12 +15,12 @@
       @click.prevent=""
     >
       <FAIcon :icon="placeholderIconClass" />
-      <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ this.edit ? '' : placeholderName }}
+      <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
     </a>
     <div
-      class="attachment-buttons"
       v-if="edit || remove"
-      >
+      class="attachment-buttons"
+    >
       <button
         v-if="remove"
         class="button-unstyled attachment-button"
@@ -48,13 +48,13 @@
     </div>
   </button>
   <div
+    v-else
     class="Attachment"
     :class="classNames"
-    v-else
   >
     <div
-      class="attachment-wrapper"
       v-show="!isEmpty"
+      class="attachment-wrapper"
     >
       <a
         v-if="hidden"
@@ -76,9 +76,9 @@
         />
       </a>
       <div
-        class="attachment-buttons"
         v-if="!hidden"
-        >
+        class="attachment-buttons"
+      >
         <button
           v-if="type === 'flash' && flashLoaded"
           class="button-unstyled attachment-button"
@@ -190,8 +190,8 @@
         @click.stop.prevent="openModal"
       >
         <Flash
-          class="flash"
           ref="flash"
+          class="flash"
           :src="attachment.large_thumb_url || attachment.url"
           @playerOpened="setFlashLoaded(true)"
           @playerClosed="setFlashLoaded(false)"
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index 5a2de68d..18b94d58 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -1,8 +1,8 @@
 <template>
   <div
-    class="Gallery"
     ref="galleryContainer"
-    :class="{ '-long': tooManyAttachments && hidingLong  }"
+    class="Gallery"
+    :class="{ '-long': tooManyAttachments && hidingLong }"
   >
     <div class="gallery-rows">
       <div
@@ -18,8 +18,8 @@
         >
           <attachment
             v-for="attachment in row.items"
-            class="gallery-item"
             :key="attachment.id"
+            class="gallery-item"
             :nsfw="nsfw"
             :attachment="attachment"
             :allow-play="false"
@@ -28,7 +28,7 @@
             :remove="removeAttachment"
             :edit="editAttachment"
             :description="descriptions && descriptions[attachment.id]"
-            :hideDescription="size === 'small' || tooManyAttachments && hidingLong"
+            :hide-description="size === 'small' || tooManyAttachments && hidingLong"
             :style="itemStyle(attachment.id, row.items)"
             @setMedia="onMedia"
             @naturalSizeLoad="onNaturalSizeLoad"
@@ -41,7 +41,7 @@
       class="many-attachments"
     >
       <div class="many-attachments-text">
-        {{ $t("status.many_attachments", { number: attachments.length })}}
+        {{ $t("status.many_attachments", { number: attachments.length }) }}
       </div>
       <div class="many-attachments-buttons">
         <span
@@ -67,8 +67,8 @@
           </button>
         </span>
         <span
-          class="many-attachments-button"
           v-if="hidingLong"
+          class="many-attachments-button"
         >
           <button
             class="button-unstyled -link"
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 5ff191cf..87e6d2ab 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -296,8 +296,8 @@
         :descriptions="newStatus.mediaDescriptions"
         :set-media="() => $store.dispatch('setMedia', newStatus.files)"
         :editable="true"
-        :editAttachment="editAttachment"
-        :removeAttachment="removeMediaFile"
+        :edit-attachment="editAttachment"
+        :remove-attachment="removeMediaFile"
         @play="$emit('mediaplay', attachment.id)"
         @pause="$emit('mediapause', attachment.id)"
       />
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 91ab1797..1001508c 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -1,7 +1,7 @@
 <template>
-<div
-  class="StatusBody"
-  :class="{ '-compact': compact }"
+  <div
+    class="StatusBody"
+    :class="{ '-compact': compact }"
   >
     <div class="body">
       <div
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 55f701d0..9059642e 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -3,7 +3,6 @@ import Poll from '../poll/poll.vue'
 import Gallery from '../gallery/gallery.vue'
 import StatusBody from 'src/components/status_body/status_body.vue'
 import LinkPreview from '../link-preview/link-preview.vue'
-import fileType from 'src/services/file_type/file_type.service'
 import { mapGetters, mapState } from 'vuex'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 573eadf5..86b3c52a 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,8 +1,8 @@
 <template>
-<div
-  class="StatusContent"
-  :class="{ '-compact': compact }"
->
+  <div
+    class="StatusContent"
+    :class="{ '-compact': compact }"
+  >
     <slot name="header" />
     <StatusBody
       :status="status"
@@ -16,11 +16,11 @@
       </div>
 
       <gallery
-        class="attachments media-body"
         v-if="status.attachments.length !== 0"
+        class="attachments media-body"
         :nsfw="nsfwClickthrough"
         :attachments="status.attachments"
-        :limit="this.compact ? 1 : 0"
+        :limit="compact ? 1 : 0"
         :size="attachmentSize"
         @setMedia="onMedia"
         @play="$emit('mediaplay', attachment.id)"
-- 
cgit v1.2.3-70-g09d2


From 2cfff1b8b9c642e9284483d6658312d9a3763417 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
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/mention_link/mention_link.js        |   6 +-
 src/components/mention_link/mention_link.scss      |  41 ---
 src/components/mention_link/mention_link.vue       |   3 +-
 src/components/mentions_line/mentions_line.js      |  15 +-
 src/components/mentions_line/mentions_line.scss    |  15 +-
 src/components/mentions_line/mentions_line.vue     |   6 +-
 src/components/rich_content/rich_content.jsx       | 128 ++-----
 src/components/settings_modal/tabs/general_tab.vue |  10 -
 src/components/status/status.js                    |   5 +-
 src/components/status/status.vue                   |   1 -
 src/components/status_body/status_body.js          |   3 +-
 src/components/status_body/status_body.vue         |   1 -
 src/components/status_content/status_content.js    |   3 +-
 src/components/status_content/status_content.vue   |   1 -
 src/modules/config.js                              |   2 -
 test/unit/specs/components/rich_content.spec.js    | 399 +--------------------
 16 files changed, 55 insertions(+), 584 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index eec116db..a60a8040 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -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..33e25391 100644
--- a/src/components/mentions_line/mentions_line.js
+++ b/src/components/mentions_line/mentions_line.js
@@ -14,11 +14,8 @@ const MentionsLine = {
     MentionLink
   },
   computed: {
-    oldStyle () {
-      return !this.mergedConfig.mentionsNewStyle
-    },
     limit () {
-      return 6
+      return 2
     },
     mentionsComputed () {
       return this.mentions.slice(0, this.limit)
@@ -29,16 +26,6 @@ const MentionsLine = {
     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/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 <StillImage
@@ -98,41 +94,32 @@ 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 === 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 +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 ? <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 (attrs['class'].includes('h-card')) {
+              return ['', children.map(processItem), '']
+            }
         }
 
         if (children !== undefined) {
@@ -246,8 +230,6 @@ export default Vue.component('RichContent', {
     </span>
 
     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 ['<span class="lastMentions">', flattenDeep(result).join(''), '</span>'].join('')
-    } else {
-      return flattenDeep(result).join('')
-    }
+    return flattenDeep(result).join('')
   }).reverse().join('')
 
   return { newHtml, lastMentions }
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
@@ -41,16 +41,6 @@
             {{ $t('settings.collapse_subject') }}
           </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') }}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 3c21cb76..8aa73a22 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -184,10 +184,7 @@ const Status = {
       })
     },
     mentionsLine () {
-      return this.mentionsOwnLine ? this.mentions : this.alsoMentions
-    },
-    mentionsOwnLine () {
-      return this.mergedConfig.mentionsOwnLine
+      return this.alsoMentions
     },
     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 d1736ba3..8757154d 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -25,8 +25,7 @@ const StatusContent = {
     'focused',
     'noHeading',
     'fullContent',
-    'singleLine',
-    'hideMentions'
+    'singleLine'
   ],
   data () {
     return {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index d77ccd54..9f01c470 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -45,7 +45,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 51895ef6..1b80ee09 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 2e71757d..23c90913 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -4,7 +4,6 @@
     <StatusBody
       :status="status"
       :single-line="singleLine"
-      :hide-mentions="hideMentions"
       @parseReady="$emit('parseReady', $event)"
     >
       <div v-if="status.poll && status.poll.options">
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/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 9ac9bedb..dfc229c0 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -8,11 +8,13 @@ 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 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 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', () => {
@@ -21,7 +23,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -45,7 +46,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -56,20 +56,15 @@ describe('RichContent', () => {
     expect(wrapper.html()).to.eql(compwrap(expected))
   })
 
-  it('removes mentions from the beginning of post', () => {
+  it('replaces first mention with mentionsline', () => {
     const html = p(
       makeMention('John'),
-      ' how are you doing thoday?'
-    )
-    const expected = p(
-      removedMentionSpan,
-      'how are you doing thoday?'
+      ' how are you doing today?'
     )
     const wrapper = shallowMount(RichContent, {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -77,70 +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 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: {
-        attentions,
-        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: {
-        attentions,
-        hideMentions: true,
-        handleLinks: true,
-        greentext: true,
-        emoji: [],
-        html
-      }
-    })
-
-    expect(wrapper.html()).to.eql(compwrap(expected))
-  })
-
-  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(
@@ -167,189 +105,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -378,7 +133,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: false,
         greentext: true,
         emoji: [],
@@ -403,7 +157,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: false,
         greentext: true,
         emoji: [],
@@ -424,7 +177,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [],
@@ -446,7 +198,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
@@ -464,7 +215,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: false,
         greentext: false,
         emoji: [],
@@ -484,7 +234,7 @@ describe('RichContent', () => {
     ].join('\n')
     const expected = [
       '<span class="greentext">&gt;quote</span>',
-      stubMention('lol'),
+      mentionsLine(1)
       '<span class="greentext">&gt;quote</span>',
       '<span class="greentext">&gt;quote</span>'
     ].join('\n')
@@ -517,11 +267,7 @@ describe('RichContent', () => {
     const expected = [
       'Bruh',
       'Bruh',
-      [
-        stubMention('foo'),
-        stubMention('bar'),
-        stubMention('baz')
-      ].join(''),
+      mentionsLine(3)
       'Bruh'
     ].join('<br>')
 
@@ -529,119 +275,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        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: {
-        attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -678,7 +311,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: true,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -726,7 +358,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: false,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -781,7 +412,6 @@ describe('RichContent', () => {
       },
       propsData: {
         attentions,
-        hideMentions: false,
         handleLinks: true,
         greentext: true,
         emoji: [],
@@ -826,7 +456,6 @@ describe('RichContent', () => {
       localVue,
       propsData: {
         attentions,
-        hideMentions: false,
         handleLinks: true,
         greentext: true,
         emoji: [],
-- 
cgit v1.2.3-70-g09d2


From 6c6df29ed3b76ee5f2e6ba43a6185ff66b91aace Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 13 Aug 2021 12:19:57 +0300
Subject: support richcontent in polls

---
 src/components/poll/poll.js                                 | 10 +++++++---
 src/components/poll/poll.vue                                |  6 ++----
 src/components/status_content/status_content.vue            |  2 +-
 src/services/entity_normalizer/entity_normalizer.service.js |  2 +-
 4 files changed, 11 insertions(+), 9 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

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..6e0234b3 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -17,8 +17,7 @@
           <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 +41,7 @@
           :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/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 23c90913..3f65e64a 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -7,7 +7,7 @@
       @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>
 
       <div
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 477b861f..c80ea487 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -301,7 +301,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
-- 
cgit v1.2.3-70-g09d2


From 4c974f5ca21c6a514d0dfc09a0aeab7789da95c5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 13 Aug 2021 13:06:42 +0300
Subject: richcontent support in polls, user cards and user profiles

---
 src/components/basic_user_card/basic_user_card.js  |  4 +-
 src/components/basic_user_card/basic_user_card.vue | 12 ++---
 src/components/notification/notification.js        |  4 +-
 src/components/notification/notification.scss      |  2 +
 src/components/notification/notification.vue       | 14 ++---
 src/components/notifications/notifications.scss    |  7 ---
 src/components/poll/poll.vue                       | 12 ++++-
 src/components/rich_content/rich_content.scss      |  1 +
 src/components/status_content/status_content.vue   |  5 +-
 src/components/user_card/user_card.js              |  4 +-
 src/components/user_card/user_card.vue             | 61 +++++-----------------
 src/components/user_profile/user_profile.js        |  4 +-
 src/components/user_profile/user_profile.vue       | 20 ++++---
 .../entity_normalizer/entity_normalizer.service.js |  5 +-
 14 files changed, 69 insertions(+), 86 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

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/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 0081dee4..634ec8ee 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.vue b/src/components/poll/poll.vue
index 6e0234b3..63b44e4f 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -17,7 +17,11 @@
           <span class="result-percentage">
             {{ percentageForOption(option.votes_count) }}%
           </span>
-          <RichContent :html="option.title_html" :handle-links="false" :emoji="emoji" />
+          <RichContent
+            :html="option.title_html"
+            :handle-links="false"
+            :emoji="emoji"
+          />
         </div>
         <div
           class="result-fill"
@@ -41,7 +45,11 @@
           :value="index"
         >
         <label class="option-vote">
-          <RichContent :html="option.title_html" :handle-links="false" :emoji="emoji" />
+          <RichContent
+            :html="option.title_html"
+            :handle-links="false"
+            :emoji="emoji"
+          />
         </label>
       </div>
     </div>
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/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 3f65e64a..5cebc697 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -7,7 +7,10 @@
       @parseReady="$emit('parseReady', $event)"
     >
       <div v-if="status.poll && status.poll.options">
-        <poll :base-poll="status.poll" :emoji="status.emojis" />
+        <Poll
+          :base-poll="status.poll"
+          :emoji="status.emojis"
+        />
       </div>
 
       <div
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index d9fb64d1..98e6c2f2 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/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index c80ea487..d84e3422 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -56,10 +56,11 @@ 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 => {
-- 
cgit v1.2.3-70-g09d2


From 6aa6f6f300d2f9de1e6c70a67b9459ae4c4b5a8f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 15 Aug 2021 17:53:35 +0300
Subject: fix console errors

---
 src/components/status_content/status_content.vue | 1 -
 1 file changed, 1 deletion(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 9db176f0..ea32890e 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -24,7 +24,6 @@
         :attachments="status.attachments"
         :limit="compact ? 1 : 0"
         :size="attachmentSize"
-        @setMedia="onMedia"
         @play="$emit('mediaplay', attachment.id)"
         @pause="$emit('mediapause', attachment.id)"
       />
-- 
cgit v1.2.3-70-g09d2


From 07c12ae16273498209ce557b59ea610e40365ad9 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 15 Aug 2021 18:26:37 +0300
Subject: replace poll with an icon in notifications

---
 src/components/status_content/status_content.vue | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index ea32890e..69635aad 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -10,13 +10,20 @@
       :single-line="singleLine"
       @parseReady="$emit('parseReady', $event)"
     >
-      <div v-if="status.poll && status.poll.options">
+      <div v-if="status.poll && status.poll.options && !compact">
         <Poll
           :base-poll="status.poll"
           :emoji="status.emojis"
         />
       </div>
 
+      <div v-else-if="status.poll && status.poll.options && compact">
+        <FAIcon
+          icon="poll-h"
+          size="2x"
+        />
+      </div>
+
       <gallery
         v-if="status.attachments.length !== 0"
         class="attachments media-body"
-- 
cgit v1.2.3-70-g09d2

