From 22c8f71945c6d114bf4db89c87eb1b166775f2d6 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 16:16:10 +0300
Subject: mention link

---
 src/components/mention_link/mention_link.vue | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 src/components/mention_link/mention_link.vue

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
new file mode 100644
index 00000000..84ed3205
--- /dev/null
+++ b/src/components/mention_link/mention_link.vue
@@ -0,0 +1,17 @@
+<template>
+  <span class="MentionLink">
+    <a v-if="!user" v-html="content" href="url" class="original"/>
+    <span v-if="user" class="new" :style="style" :class="{ '-you': isYou }" >
+      <button class="button-unstyled short" @click.prevent="onClick">
+        <span class="shortName">@<span v-html="userName" /></span> <span v-if="isYou">(You)</span>
+      </button>
+      <span class="full" v-if="userName !== userNameFull">
+        @<span v-html="userNameFull" />
+      </span>
+    </span>
+  </span>
+</template>
+
+<script src="./mention_link.js"/>
+
+<style lang="scss" src="./mention_link.scss"/>
-- 
cgit v1.2.3-70-g09d2


From aec05686d0fa300b8b8348d8d1ba2724d1dd0014 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 16:37:12 +0300
Subject: lint, fix warnings

---
 src/components/mention_link/mention_link.vue    | 30 +++++++++++++++++++++----
 src/components/status_content/status_content.js |  5 ++---
 2 files changed, 28 insertions(+), 7 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 84ed3205..80aadd3f 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,12 +1,34 @@
 <template>
   <span class="MentionLink">
-    <a v-if="!user" v-html="content" href="url" class="original"/>
-    <span v-if="user" class="new" :style="style" :class="{ '-you': isYou }" >
-      <button class="button-unstyled short" @click.prevent="onClick">
+    <!-- eslint-disable vue/no-v-html -->
+    <a
+      v-if="!user"
+      href="url"
+      class="original"
+      v-html="content"
+    />
+    <!-- eslint-enable vue/no-v-html -->
+    <span
+      v-if="user"
+      class="new"
+      :style="style"
+      :class="{ '-you': isYou }"
+    >
+      <button
+        class="button-unstyled short"
+        @click.prevent="onClick"
+      >
+        <!-- eslint-disable vue/no-v-html -->
         <span class="shortName">@<span v-html="userName" /></span> <span v-if="isYou">(You)</span>
+        <!-- eslint-enable vue/no-v-html -->
       </button>
-      <span class="full" v-if="userName !== userNameFull">
+      <span
+        v-if="userName !== userNameFull"
+        class="full"
+      >
+        <!-- eslint-disable vue/no-v-html -->
         @<span v-html="userNameFull" />
+        <!-- eslint-enable vue/no-v-html -->
       </span>
     </span>
   </span>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 79408e38..c21afc24 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -169,10 +169,9 @@ const StatusContent = {
     RichContent
   },
   mounted () {
-    const { attentions } = this.status
-    attentions.forEach(attn => {
+    this.status.attentions.forEach(attn => {
       const { id } = attn
-      this.$store.state.api.backendInteractor.fetchUserIfMissing(this.$store, id)
+      this.$store.dispatch('fetchUserIfMissing', id)
     })
   },
   methods: {
-- 
cgit v1.2.3-70-g09d2


From 8045d1866ecb08a606a2891a9f179c1e7e968e9b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 20:25:09 +0300
Subject: localization

---
 src/components/mention_link/mention_link.vue | 2 +-
 src/i18n/en.json                             | 3 ++-
 src/i18n/fi.json                             | 3 ++-
 3 files changed, 5 insertions(+), 3 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 80aadd3f..ea356315 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -19,7 +19,7 @@
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <span class="shortName">@<span v-html="userName" /></span> <span v-if="isYou">(You)</span>
+        <span class="shortName">@<span v-html="userName" /></span> <span v-if="isYou">{{ $t('status.you')}}</span>
         <!-- eslint-enable vue/no-v-html -->
       </button>
       <span
diff --git a/src/i18n/en.json b/src/i18n/en.json
index aa440ac1..d64adaff 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -711,7 +711,8 @@
     "hide_content": "Hide content",
     "status_deleted": "This post was deleted",
     "nsfw": "NSFW",
-    "expand": "Expand"
+    "expand": "Expand",
+    "you": "(You)"
   },
   "user_card": {
     "approve": "Approve",
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 2524f278..ebcad804 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -579,7 +579,8 @@
     "hide_full_subject": "Piilota koko otsikko",
     "show_content": "Näytä sisältö",
     "hide_content": "Piilota sisältö",
-    "status_deleted": "Poistettu viesti"
+    "status_deleted": "Poistettu viesti",
+    "you": "(sinä)"
   },
   "user_card": {
     "approve": "Hyväksy",
-- 
cgit v1.2.3-70-g09d2


From 7d6fc044fbcf2c6f82f759605a0fd5c1aebae55a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Jun 2021 23:42:04 +0300
Subject: new mentions look

---
 src/components/mention_link/mention_link.js       | 28 +++----
 src/components/mention_link/mention_link.scss     | 94 ++++++++++++++++++++---
 src/components/mention_link/mention_link.vue      | 12 +--
 src/services/user_highlighter/user_highlighter.js | 14 +++-
 4 files changed, 114 insertions(+), 34 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index ade598d8..d26ae337 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -1,7 +1,6 @@
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
-import { getTextColor, rgb2hex } from 'src/services/color_convert/color_convert.js'
 import { mapGetters, mapState } from 'vuex'
-import { convert } from 'chromatism'
+import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 
 const MentionLink = {
   name: 'MentionLink',
@@ -45,21 +44,22 @@ const MentionLink = {
     highlight () {
       return this.mergedConfig.highlight[this.user.screen_name]
     },
-    bg () {
-      if (this.highlight) return this.highlight.color
+    highlightType () {
+      return this.highlight && ('-' + this.highlight.type)
     },
-    text () {
-      if (this.bg) {
-        const linkColor = this.mergedConfig.customTheme.colors.link
-        const color = getTextColor(convert(this.bg).rgb, convert(linkColor).rgb)
-        return rgb2hex(color)
-      }
+    highlightClass () {
+      if (this.highlight) return highlightClass(this.user)
     },
     style () {
-      return [
-        this.bg && `--mention-bg: ${this.bg}`,
-        this.text && `--mention-text: ${this.text}`
-      ].filter(_ => _).join('; ')
+      if (this.highlight) {
+        const {
+          backgroundColor,
+          backgroundPosition,
+          backgroundImage,
+          ...rest
+        } = highlightStyle(this.highlight)
+        return rest
+      }
     },
     ...mapGetters(['mergedConfig']),
     ...mapState({
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 237d9205..3a3e58d1 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -2,12 +2,10 @@
   position: relative;
   white-space: normal;
   display: inline-block;
+  color: var(--link);
 
   & .new,
-  & .original,
-  & .full {
-    padding: 0 2px;
-    margin: 0 -2px;
+  & .original {
     display: inline-block;
     border-radius: 2px;
   }
@@ -17,24 +15,95 @@
   }
 
   .full {
-    pointer-events: none;
     position: absolute;
+    display: inline-block;
+    pointer-events: none;
     opacity: 0;
-    top: 0;
-    bottom: 0;
+    top: 100%;
     left: 0;
+    height: 100%;
     word-wrap: normal;
     white-space: nowrap;
     transition: opacity 0.2s ease;
-    background-color: var(--mention-bg, var(--popover));
-    color: var(--mention-text, var(--link));
     z-index: 1;
+    margin-top: 0.25em;
+    padding: 0.5em;
   }
 
-  .new {
-    background-color: var(--mention-bg);
-    color: var(--mention-text, var(--link));
+  & .short,
+  & .full {
+    &::before {
+      content: '@';
+    }
+  }
+
+  & .new {
+    &,
+    &.-striped,
+    &.-solid,
+    &.-side {
+      .full {
+      }
+      .short {
+        &::before {
+          display: inline-block;
+          height: 50%;
+          font-size: 90%;
+          line-height: 1;
+          vertical-align: 6%;
+        }
+      }
+
+      .you {
+        padding-right: 0.25em;
+      }
+      .short {
+        padding-left: 0.25em;
+        padding-right: 0;
+        padding-top: 0;
+        padding-bottom: 0;
+      }
+
+      .userName {
+        color: var(--link);
+        margin-left: 0.25em;
+        padding-left: 0.25em;
+        padding-right: 0.25em;
+        padding-top: 0;
+        padding-bottom: 0;
+      }
+    }
+
+    &.-striped {
+      & .userName,
+      & .full {
+        background-image:
+          repeating-linear-gradient(
+            135deg,
+            var(--____highlight-tintColor),
+            var(--____highlight-tintColor) 5px,
+            var(--____highlight-tintColor2) 5px,
+            var(--____highlight-tintColor2) 10px
+          );
+      }
+    }
 
+    &.-solid {
+      & .userName,
+      & .full {
+        background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
+      }
+    }
+
+    &.-side {
+      & .userName,
+      & .userNameFull {
+        box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
+      }
+    }
+  }
+
+  .new {
     &.-you {
       & .shortName,
       & .full {
@@ -45,5 +114,6 @@
 
   &:hover .new .full {
     opacity: 1;
+    pointer-events: initial;
   }
 }
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index ea356315..5a8506eb 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -12,22 +12,24 @@
       v-if="user"
       class="new"
       :style="style"
-      :class="{ '-you': isYou }"
+      :class="[{ '-you': isYou }, highlightType]"
     >
       <button
-        class="button-unstyled short"
+        class="short"
+        :class="highlight ? 'button-default' : 'button-default' "
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <span class="shortName">@<span v-html="userName" /></span> <span v-if="isYou">{{ $t('status.you')}}</span>
+        <span class="shortName"><span class="userName" v-html="userName" /></span><span class="you" v-if="isYou">{{ $t('status.you')}}</span>
         <!-- eslint-enable vue/no-v-html -->
       </button>
       <span
         v-if="userName !== userNameFull"
-        class="full"
+        class="full popover-default"
+        :class="[highlightType]"
       >
         <!-- eslint-disable vue/no-v-html -->
-        @<span v-html="userNameFull" />
+        <span class="userNameFull" v-html="userNameFull" />
         <!-- eslint-enable vue/no-v-html -->
       </span>
     </span>
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
index b91c0f78..3b07592e 100644
--- a/src/services/user_highlighter/user_highlighter.js
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -8,6 +8,11 @@ const highlightStyle = (prefs) => {
   const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
   const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
   const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
+  const customProps = {
+    '--____highlight-solidColor': solidColor,
+    '--____highlight-tintColor': tintColor,
+    '--____highlight-tintColor2': tintColor2
+  }
   if (type === 'striped') {
     return {
       backgroundImage: [
@@ -17,11 +22,13 @@ const highlightStyle = (prefs) => {
         `${tintColor2} 20px,`,
         `${tintColor2} 40px`
       ].join(' '),
-      backgroundPosition: '0 0'
+      backgroundPosition: '0 0',
+      ...customProps
     }
   } else if (type === 'solid') {
     return {
-      backgroundColor: tintColor2
+      backgroundColor: tintColor2,
+      ...customProps
     }
   } else if (type === 'side') {
     return {
@@ -31,7 +38,8 @@ const highlightStyle = (prefs) => {
         `${solidColor} 2px,`,
         `transparent 6px`
       ].join(' '),
-      backgroundPosition: '0 0'
+      backgroundPosition: '0 0',
+      ...customProps
     }
   }
 }
-- 
cgit v1.2.3-70-g09d2


From e6d5ddcbb69ff93e711227672caaf208cf872c21 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 00:03:59 +0300
Subject: better modifier, no background for unhighlighted mentions

---
 src/components/mention_link/mention_link.scss | 13 ++++++++-----
 src/components/mention_link/mention_link.vue  |  2 +-
 2 files changed, 9 insertions(+), 6 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 37717424..4e061be9 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -37,13 +37,15 @@
     }
   }
 
-  & .new {
+  .new:not(.-highlighted) {
+    .short {
+      background: none;
+    }
+  }
+  .new {
     &,
-    &.-striped,
-    &.-solid,
-    &.-side {
+    &.-highlighted {
       .short {
-        font-size: 90%;
         line-height: 1.5;
         vertical-align: middle;
 
@@ -58,6 +60,7 @@
       .you {
         padding-right: 0.25em;
       }
+
       .short {
         padding-left: 0.25em;
         padding-right: 0;
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 5a8506eb..a38faa7b 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -12,7 +12,7 @@
       v-if="user"
       class="new"
       :style="style"
-      :class="[{ '-you': isYou }, highlightType]"
+      :class="[{ '-you': isYou, '-highlighted': highlight }, highlightType]"
     >
       <button
         class="short"
-- 
cgit v1.2.3-70-g09d2


From 0583a6b8638afc6b3ea07adceecdb42ca9188251 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 10:13:40 +0300
Subject: moved transparent button styles into button itself

---
 src/App.scss                                  | 4 ++++
 src/components/mention_link/mention_link.scss | 5 -----
 src/components/mention_link/mention_link.vue  | 4 ++--
 3 files changed, 6 insertions(+), 7 deletions(-)

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

diff --git a/src/App.scss b/src/App.scss
index 45071ba2..bc027f4f 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -88,6 +88,10 @@ a {
   font-family: sans-serif;
   font-family: var(--interfaceFont, sans-serif);
 
+  &.-sublime {
+    background: transparent;
+  }
+
   i[class*=icon-],
   .svg-inline--fa {
     color: $fallback--text;
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 4a9fab1c..78a9816c 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -38,11 +38,6 @@
     }
   }
 
-  .new:not(.-highlighted) {
-    .short {
-      background: none;
-    }
-  }
   .new {
     &,
     &.-highlighted {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index a38faa7b..d8d45a1b 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -15,8 +15,8 @@
       :class="[{ '-you': isYou, '-highlighted': highlight }, highlightType]"
     >
       <button
-        class="short"
-        :class="highlight ? 'button-default' : 'button-default' "
+        class="short button-default"
+        :class="{ '-sublime': !highlight }"
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-- 
cgit v1.2.3-70-g09d2


From 3abd357694c29c84d213d7d2a7a954ab2a591da0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 11:38:44 +0300
Subject: moving mentions into separate row

---
 src/components/mention_link/mention_link.js   | 20 +++++++++++--
 src/components/mention_link/mention_link.scss |  5 ++++
 src/components/mention_link/mention_link.vue  |  2 +-
 src/components/rich_content/rich_content.jsx  | 19 +++++++++---
 src/components/status/status.js               | 22 ++++++++++----
 src/components/status/status.scss             |  3 +-
 src/components/status/status.vue              | 43 ++++++++++++++++++++++-----
 src/i18n/en.json                              |  1 +
 8 files changed, 94 insertions(+), 21 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index d26ae337..465c7d00 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -14,8 +14,14 @@ const MentionLink = {
       type: String
     },
     origattrs: {
-      required: true,
-      type: Object
+      required: false,
+      type: Object,
+      default: {}
+    },
+    firstMention: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   methods: {
@@ -61,6 +67,16 @@ const MentionLink = {
         return rest
       }
     },
+    classnames () {
+      return [
+        {
+          '-you': this.isYou,
+          '-highlighted': this.highlight,
+          '-firstMention': this.firstMention
+        },
+        this.highlightType
+      ]
+    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       currentUser: state => state.users.currentUser
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 78a9816c..dec11014 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -39,10 +39,15 @@
   }
 
   .new {
+    &.-firstMention {
+      display: none;
+    }
+
     &,
     &.-highlighted {
       .short {
         line-height: 1.5;
+        font-size: inherit;
 
         &::before {
           display: inline-block;
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index d8d45a1b..05f6fd91 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -12,7 +12,7 @@
       v-if="user"
       class="new"
       :style="style"
-      :class="[{ '-you': isYou, '-highlighted': highlight }, highlightType]"
+      :class="classnames"
     >
       <button
         class="short button-default"
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index c15877c8..86a318bd 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -33,21 +33,32 @@ export default Vue.component('RichContent', {
         class="img"
       />
     }
-    const renderMention = (attrs, children) => {
+    const renderMention = (attrs, children, encounteredText) => {
       return <MentionLink
         url={attrs.href}
         content={flattenDeep(children).join('')}
+        firstMention={!encounteredText}
         origattrs={attrs}
       />
     }
 
+    let encounteredText = false
     // Processor to use with mini_html_converter
     const processItem = (item) => {
       // Handle text noes - just add emoji
       if (typeof item === 'string') {
+        const emptyText = item.trim()
+        if (!emptyText) {
+          return encounteredText ? item : item.trim()
+        }
+        let unescapedItem = unescape(item)
+        if (!encounteredText) {
+          unescapedItem = unescapedItem.trimStart()
+          encounteredText = true
+        }
         if (item.includes(':')) {
           return processTextForEmoji(
-            unescape(item),
+            unescapedItem,
             this.emoji,
             ({ shortcode, url }) => {
               return <StillImage
@@ -59,7 +70,7 @@ export default Vue.component('RichContent', {
             }
           )
         } else {
-          return unescape(item)
+          return unescapedItem
         }
       }
       // Handle tag nodes
@@ -73,7 +84,7 @@ export default Vue.component('RichContent', {
             if (!this.handleLinks) break
             const attrs = getAttrs(opener)
             if (attrs['class'] && attrs['class'].includes('mention')) {
-              return renderMention(attrs, children)
+              return renderMention(attrs, children, encounteredText)
             }
         }
         // Render tag as is
diff --git a/src/components/status/status.js b/src/components/status/status.js
index b6414ad8..54b54a90 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,6 +13,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
 import StatusPopover from '../status_popover/status_popover.vue'
 import UserListPopover from '../user_list_popover/user_list_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
+import MentionLink from 'src/components/mention_link/mention_link.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import { muteWordHits } from '../../services/status_parser/status_parser.js'
@@ -33,7 +34,8 @@ import {
   faStar,
   faEyeSlash,
   faEye,
-  faThumbtack
+  faThumbtack,
+  faAt,
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -50,7 +52,8 @@ library.add(
   faEllipsisH,
   faEyeSlash,
   faEye,
-  faThumbtack
+  faThumbtack,
+  faAt
 )
 
 const Status = {
@@ -70,7 +73,8 @@ const Status = {
     UserListPopover,
     EmojiReactions,
     StatusContent,
-    RichContent
+    RichContent,
+    MentionLink
   },
   props: [
     'statusoid',
@@ -133,9 +137,7 @@ const Status = {
       return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
     },
     replyProfileLink () {
-      if (this.isReply) {
-        return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)
-      }
+      return this.$store.getters.findUser(this.status.in_reply_to_screen_name).statusnet_profile_url
     },
     retweet () { return !!this.statusoid.retweeted_status },
     retweeterUser () { return this.statusoid.user },
@@ -159,6 +161,14 @@ const Status = {
     muteWordHits () {
       return muteWordHits(this.status, this.muteWords)
     },
+    mentions () {
+      return this.statusoid.attentions.filter(attn => {
+        return attn.screen_name !== this.replyToName
+      })
+    },
+    hasMentions () {
+      return this.mentions.length > 0
+    },
     muted () {
       if (this.statusoid.user.id === this.currentUser.id) return false
       const { status } = this
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 82088943..8744fa16 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -155,7 +155,8 @@ $status-margin: 0.75em;
     margin-right: 0.2em;
   }
 
-  .heading-reply-row {
+  & .heading-mentions-row,
+  & .heading-reply-row {
     position: relative;
     align-content: baseline;
     font-size: 12px;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index a4247cd4..092d3834 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -222,6 +222,35 @@
               </span>
             </div>
 
+            <div v-if="hasMentions" class="heading-mentions-row">
+              <div
+                class="mentions"
+              >
+                <span
+                  class="button-unstyled reply-to"
+                  :aria-label="$t('tool_tip.reply')"
+                  @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
+                >
+                  <FAIcon
+                    class="fa-scale-110 fa-old-padding"
+                    icon="at"
+                  />
+                  <span
+                    class="faint-link reply-to-text"
+                  >
+                    {{ $t('status.mentions') }}
+                  </span>
+                </span>
+                <MentionLink
+                  v-for="mention in mentions"
+                  class="mention-link"
+                  :key="mention.statusnet_profile_url"
+                  :content="mention.statusnet_profile_url"
+                  :url="mention.statusnet_profile_url"
+                  :first-mention="false"
+                />
+              </div>
+            </div>
             <div class="heading-reply-row">
               <div
                 v-if="isReply"
@@ -258,13 +287,13 @@
                 >
                   <span class="reply-to-text">{{ $t('status.reply_to') }}</span>
                 </span>
-                <router-link
-                  class="reply-to-link"
-                  :title="replyToName"
-                  :to="replyProfileLink"
-                >
-                  {{ replyToName }}
-                </router-link>
+
+                <MentionLink
+                  class="mention-link"
+                  :content="replyToName"
+                  :url="replyProfileLink"
+                  :first-mention="false"
+                />
                 <span
                   v-if="replies && replies.length"
                   class="faint replies-separator"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d64adaff..cd366132 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -697,6 +697,7 @@
     "unbookmark": "Unbookmark",
     "delete_confirm": "Do you really want to delete this status?",
     "reply_to": "Reply to",
+    "mentions": "Mentions",
     "replies_list": "Replies:",
     "mute_conversation": "Mute conversation",
     "unmute_conversation": "Unmute conversation",
-- 
cgit v1.2.3-70-g09d2


From 9ea370033af450c0d5e4650a7c10f0e1ffec911c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 12:58:28 +0300
Subject: configurable mentions placement

---
 src/components/mention_link/mention_link.js        | 14 ++---
 src/components/mention_link/mention_link.scss      | 22 ++++----
 src/components/mention_link/mention_link.vue       |  9 ++--
 src/components/rich_content/rich_content.jsx       |  1 -
 src/components/settings_modal/tabs/general_tab.vue | 10 ++++
 src/components/status/status.js                    |  8 ++-
 src/components/status/status.vue                   | 60 +++++++++++-----------
 src/i18n/en.json                                   |  2 +
 src/modules/config.js                              |  2 +
 9 files changed, 75 insertions(+), 53 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 465c7d00..8a93e8a3 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -13,11 +13,6 @@ const MentionLink = {
       required: true,
       type: String
     },
-    origattrs: {
-      required: false,
-      type: Object,
-      default: {}
-    },
     firstMention: {
       required: false,
       type: Boolean,
@@ -56,6 +51,12 @@ const MentionLink = {
     highlightClass () {
       if (this.highlight) return highlightClass(this.user)
     },
+    oldPlace () {
+      return this.mergedConfig.mentionsOldPlace
+    },
+    oldStyle () {
+      return this.mergedConfig.mentionsOldStyle
+    },
     style () {
       if (this.highlight) {
         const {
@@ -72,7 +73,8 @@ const MentionLink = {
         {
           '-you': this.isYou,
           '-highlighted': this.highlight,
-          '-firstMention': this.firstMention
+          '-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 dec11014..261aeaef 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -3,6 +3,7 @@
   white-space: normal;
   display: inline-block;
   color: var(--link);
+  margin-right: 0.25em;
 
   & .new,
   & .original {
@@ -33,23 +34,25 @@
   & .short,
   & .full {
     &::before {
-      color: var(--faint);
       content: '@';
     }
   }
 
   .new {
-    &.-firstMention {
-      display: none;
+    &.-you {
+      & .shortName,
+      & .full {
+        font-weight: 600;
+      }
     }
 
-    &,
-    &.-highlighted {
+    &:not(.-oldStyle) {
       .short {
         line-height: 1.5;
         font-size: inherit;
 
         &::before {
+          color: var(--faint);
           display: inline-block;
           height: 50%;
           line-height: 1;
@@ -111,12 +114,9 @@
     }
   }
 
-  .new {
-    &.-you {
-      & .shortName,
-      & .full {
-        font-weight: 600;
-      }
+  &:not(.-oldPlace) {
+    .new.-firstMention {
+      display: none;
     }
   }
 
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 05f6fd91..0dae1f53 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,5 +1,8 @@
 <template>
-  <span class="MentionLink">
+<span
+  class="MentionLink"
+  :class="{ '-oldPlace': oldPlace }"
+>
     <!-- eslint-disable vue/no-v-html -->
     <a
       v-if="!user"
@@ -15,8 +18,8 @@
       :class="classnames"
     >
       <button
-        class="short button-default"
-        :class="{ '-sublime': !highlight }"
+        class="short"
+        :class="[{ '-sublime': !highlight }, oldStyle ? 'button-unstyled' : 'button-default']"
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index 86a318bd..26a64f28 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -38,7 +38,6 @@ export default Vue.component('RichContent', {
         url={attrs.href}
         content={flattenDeep(children).join('')}
         firstMention={!encounteredText}
-        origattrs={attrs}
       />
     }
 
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index bdb29edf..c8bab869 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -36,6 +36,16 @@
             {{ $t('settings.collapse_subject') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="mentionsOldPlace">
+            {{ $t('settings.mentions_old_place') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="mentionsOldStyle">
+            {{ $t('settings.mentions_old_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 54b54a90..7d2ec514 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -105,6 +105,9 @@ const Status = {
     muteWords () {
       return this.mergedConfig.muteWords
     },
+    mentionsOldPlace () {
+      return this.mergedConfig.mentionsOldPlace
+    },
     showReasonMutedThread () {
       return (
         this.status.thread_muted ||
@@ -137,7 +140,7 @@ const Status = {
       return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
     },
     replyProfileLink () {
-      return this.$store.getters.findUser(this.status.in_reply_to_screen_name).statusnet_profile_url
+      return this.$store.getters.findUser(this.status.in_reply_to_user_id).statusnet_profile_url
     },
     retweet () { return !!this.statusoid.retweeted_status },
     retweeterUser () { return this.statusoid.user },
@@ -163,7 +166,8 @@ const Status = {
     },
     mentions () {
       return this.statusoid.attentions.filter(attn => {
-        return attn.screen_name !== this.replyToName
+        return attn.screen_name !== this.replyToName &&
+          attn.screen_name !== this.statusoid.user.screen_name
       })
     },
     hasMentions () {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 092d3834..d0fb150d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -221,36 +221,6 @@
                 </button>
               </span>
             </div>
-
-            <div v-if="hasMentions" class="heading-mentions-row">
-              <div
-                class="mentions"
-              >
-                <span
-                  class="button-unstyled reply-to"
-                  :aria-label="$t('tool_tip.reply')"
-                  @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
-                >
-                  <FAIcon
-                    class="fa-scale-110 fa-old-padding"
-                    icon="at"
-                  />
-                  <span
-                    class="faint-link reply-to-text"
-                  >
-                    {{ $t('status.mentions') }}
-                  </span>
-                </span>
-                <MentionLink
-                  v-for="mention in mentions"
-                  class="mention-link"
-                  :key="mention.statusnet_profile_url"
-                  :content="mention.statusnet_profile_url"
-                  :url="mention.statusnet_profile_url"
-                  :first-mention="false"
-                />
-              </div>
-            </div>
             <div class="heading-reply-row">
               <div
                 v-if="isReply"
@@ -320,6 +290,36 @@
                 </StatusPopover>
               </div>
             </div>
+
+            <div v-if="hasMentions && !mentionsOldPlace" class="heading-mentions-row">
+              <div
+                class="mentions"
+              >
+                <span
+                  class="button-unstyled reply-to"
+                  :aria-label="$t('tool_tip.reply')"
+                  @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
+                >
+                  <FAIcon
+                    class="fa-scale-110 fa-old-padding"
+                    icon="at"
+                  />
+                  <span
+                    class="faint-link reply-to-text"
+                  >
+                    {{ $t('status.mentions') }}
+                  </span>
+                </span>
+                <MentionLink
+                  v-for="mention in mentions"
+                  class="mention-link"
+                  :key="mention.statusnet_profile_url"
+                  :content="mention.statusnet_profile_url"
+                  :url="mention.statusnet_profile_url"
+                  :first-mention="false"
+                />
+              </div>
+            </div>
           </div>
 
           <StatusContent
diff --git a/src/i18n/en.json b/src/i18n/en.json
index cd366132..da2f6ddf 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -259,6 +259,8 @@
     "security": "Security",
     "setting_changed": "Setting is different from default",
     "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
+    "mentions_old_style": "Old style mentions",
+    "mentions_old_place": "Leave mentions inside post",
     "mfa": {
       "otp": "OTP",
       "setup_otp": "Setup OTP",
diff --git a/src/modules/config.js b/src/modules/config.js
index 0f596750..6e21e38b 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -54,6 +54,8 @@ export const defaultState = {
   interfaceLanguage: browserLocale,
   hideScopeNotice: false,
   useStreamingApi: false,
+  mentionsOldPlace: false,
+  mentionsOldStyle: false,
   sidebarRight: undefined, // instance default
   scopeCopy: undefined, // instance default
   subjectLineBehavior: undefined, // instance default
-- 
cgit v1.2.3-70-g09d2


From 2f383c2c0197b94b30fdc4c5e0c742c7e104be20 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 14:34:47 +0300
Subject: moved mentions into a separate component - MentionLine, added
 collapsing of mentions when there's too many of 'em

---
 src/components/mention_link/mention_link.scss   | 10 ++---
 src/components/mention_link/mention_link.vue    |  3 +-
 src/components/mentions_line/mentions_line.js   | 51 +++++++++++++++++++++++++
 src/components/mentions_line/mentions_line.scss | 15 ++++++++
 src/components/mentions_line/mentions_line.vue  | 42 ++++++++++++++++++++
 src/components/status/status.js                 | 10 +++--
 src/components/status/status.vue                | 10 ++---
 src/components/status_body/status_body.js       | 10 ++++-
 src/components/status_body/status_body.vue      | 25 ++++++++----
 src/i18n/en.json                                |  3 +-
 10 files changed, 151 insertions(+), 28 deletions(-)
 create mode 100644 src/components/mentions_line/mentions_line.js
 create mode 100644 src/components/mentions_line/mentions_line.scss
 create mode 100644 src/components/mentions_line/mentions_line.vue

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 5b5218f7..eed4d5be 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -40,6 +40,10 @@
   .new {
     margin-right: 0.25em;
 
+    &.-firstMention {
+      display: none;
+    }
+
     &.-you {
       & .shortName,
       & .full {
@@ -115,12 +119,6 @@
     }
   }
 
-  &:not(.-oldPlace) {
-    .new.-firstMention {
-      display: none;
-    }
-  }
-
   &:hover .new .full {
     opacity: 1;
     pointer-events: initial;
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 0dae1f53..d2f4129d 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -23,7 +23,8 @@
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <span class="shortName"><span class="userName" v-html="userName" /></span><span class="you" v-if="isYou">{{ $t('status.you')}}</span>
+        <span class="shortName"><span class="userName" v-html="userName" /></span>
+        <span class="you" v-if="isYou">{{ $t('status.you') }}</span>
         <!-- eslint-enable vue/no-v-html -->
       </button>
       <span
diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js
new file mode 100644
index 00000000..837935ec
--- /dev/null
+++ b/src/components/mentions_line/mentions_line.js
@@ -0,0 +1,51 @@
+import MentionLink from 'src/components/mention_link/mention_link.vue'
+import { mapGetters } from 'vuex'
+
+const MentionsLine = {
+  name: 'MentionsLine',
+  props: {
+    attentions: {
+      required: true,
+      type: Object
+    }
+  },
+  data: () => ({ expanded: false }),
+  components: {
+    MentionLink
+  },
+  computed: {
+    oldStyle () {
+      return this.mergedConfig.mentionsOldStyle
+    },
+    limit () {
+      return 1
+    },
+    mentions () {
+      return this.attentions.slice(0, this.limit)
+    },
+    extraMentions () {
+      return this.attentions.slice(this.limit)
+    },
+    manyMentions () {
+      return this.extraMentions.length > 0
+    },
+    buttonClasses () {
+      return [
+        this.oldStyle
+          ? 'button-unstyled'
+          : 'button-default -sublime',
+        this.oldStyle
+          ? '-oldStyle'
+          : '-newStyle'
+      ]
+    },
+    ...mapGetters(['mergedConfig']),
+  },
+  methods: {
+    toggleShowMore () {
+      this.expanded = !this.expanded
+    }
+  }
+}
+
+export default MentionsLine
diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss
new file mode 100644
index 00000000..735502de
--- /dev/null
+++ b/src/components/mentions_line/mentions_line.scss
@@ -0,0 +1,15 @@
+.MentionsLine {
+  .showMoreLess {
+    &.-newStyle {
+      line-height: 1.5;
+      font-size: inherit;
+      display: inline-block;
+      padding-top: 0;
+      padding-bottom: 0;
+    }
+
+    &.-oldStyle {
+      color: var(--link);
+    }
+  }
+}
diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue
new file mode 100644
index 00000000..6d114f2d
--- /dev/null
+++ b/src/components/mentions_line/mentions_line.vue
@@ -0,0 +1,42 @@
+<template>
+<span class="MentionsLine">
+  <MentionLink
+    v-for="mention in mentions"
+    class="mention-link"
+    :key="mention.statusnet_profile_url"
+    :content="mention.statusnet_profile_url"
+    :url="mention.statusnet_profile_url"
+    :first-mention="false"
+  /><span v-if="manyMentions" class="extraMentions">
+    <span
+      v-if="expanded"
+      class="fullExtraMentions"
+    >
+      <MentionLink
+        v-for="mention in extraMentions"
+        class="mention-link"
+        :key="mention.statusnet_profile_url"
+        :content="mention.statusnet_profile_url"
+        :url="mention.statusnet_profile_url"
+        :first-mention="false"
+      />
+    </span><button
+      v-if="!expanded"
+      class="showMoreLess"
+      :class="buttonClasses"
+      @click="toggleShowMore"
+    >
+      {{ $t('status.plus_more', { number: extraMentions.length })}}
+    </button><button
+      v-if="expanded"
+      class="showMoreLess"
+      :class="buttonClasses"
+      @click="toggleShowMore"
+    >
+      {{ $t('general.show_less')}}
+    </button>
+  </span>
+</span>
+</template>
+<script src="./mentions_line.js" ></script>
+<style lang="scss" src="./mentions_line.scss" />
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 7d2ec514..d921f625 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,6 +13,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
 import StatusPopover from '../status_popover/status_popover.vue'
 import UserListPopover from '../user_list_popover/user_list_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
+import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
 import MentionLink from 'src/components/mention_link/mention_link.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
@@ -74,7 +75,8 @@ const Status = {
     EmojiReactions,
     StatusContent,
     RichContent,
-    MentionLink
+    MentionLink,
+    MentionsLine
   },
   props: [
     'statusoid',
@@ -105,9 +107,6 @@ const Status = {
     muteWords () {
       return this.mergedConfig.muteWords
     },
-    mentionsOldPlace () {
-      return this.mergedConfig.mentionsOldPlace
-    },
     showReasonMutedThread () {
       return (
         this.status.thread_muted ||
@@ -164,6 +163,9 @@ const Status = {
     muteWordHits () {
       return muteWordHits(this.status, this.muteWords)
     },
+    mentionsOldPlace () {
+      return this.mergedConfig.mentionsOldPlace
+    },
     mentions () {
       return this.statusoid.attentions.filter(attn => {
         return attn.screen_name !== this.replyToName &&
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d0fb150d..faf67328 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -310,13 +310,9 @@
                     {{ $t('status.mentions') }}
                   </span>
                 </span>
-                <MentionLink
-                  v-for="mention in mentions"
-                  class="mention-link"
-                  :key="mention.statusnet_profile_url"
-                  :content="mention.statusnet_profile_url"
-                  :url="mention.statusnet_profile_url"
-                  :first-mention="false"
+                <MentionsLine
+                  :attentions="mentions"
+                  class="mentions-line"
                 />
               </div>
             </div>
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 232afccb..0c1aa88e 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -1,5 +1,6 @@
 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 { 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'
@@ -104,10 +105,17 @@ const StatusContent = {
     attachmentTypes () {
       return this.status.attachments.map(file => fileType.fileType(file.mimetype))
     },
+    mentionsOldPlace () {
+      return this.mergedConfig.mentionsOldPlace
+    },
+    mentions () {
+      return this.status.attentions
+    },
     ...mapGetters(['mergedConfig'])
   },
   components: {
-    RichContent
+    RichContent,
+    MentionsLine
   },
   mounted () {
     this.status.attentions && this.status.attentions.forEach(attn => {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 6f982f2e..92e47118 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -39,15 +39,24 @@
         >
           {{ $t("general.show_more") }}
         </button>
-        <RichContent
+        <span
           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"
-        />
+        >
+          <MentionsLine
+            v-if="mentionsOldPlace"
+            :attentions="status.attentions"
+            class="mentions-line"
+            />
+          <RichContent
+            :class="{ '-single-line': singleLine }"
+            class="text media-body"
+            :html="postBodyHtml"
+            :emoji="status.emojis"
+            :handle-links="true"
+            @click.prevent="linkClicked"
+          />
+        </span>
+
         <button
           v-if="hideSubjectStatus"
           class="button-unstyled -link cw-status-hider"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index da2f6ddf..de1f23b4 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -715,7 +715,8 @@
     "status_deleted": "This post was deleted",
     "nsfw": "NSFW",
     "expand": "Expand",
-    "you": "(You)"
+    "you": "(You)",
+    "plus_more": "+{number} more"
   },
   "user_card": {
     "approve": "Approve",
-- 
cgit v1.2.3-70-g09d2


From 7ae85c8318add44af1c8f91c9738a2d715da2f3e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Jun 2021 14:51:42 +0300
Subject: change defaults

---
 src/components/mention_link/mention_link.js        |  4 +-
 src/components/mention_link/mention_link.vue       | 23 +++++--
 src/components/mentions_line/mentions_line.js      |  4 +-
 src/components/mentions_line/mentions_line.vue     | 75 +++++++++++-----------
 src/components/settings_modal/tabs/general_tab.vue |  8 +--
 src/components/status/status.js                    |  4 +-
 src/components/status/status.vue                   |  5 +-
 src/components/status_body/status_body.js          |  4 +-
 src/components/status_body/status_body.vue         |  2 +-
 src/modules/config.js                              |  4 +-
 10 files changed, 74 insertions(+), 59 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 8a93e8a3..8ad84a51 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -52,10 +52,10 @@ const MentionLink = {
       if (this.highlight) return highlightClass(this.user)
     },
     oldPlace () {
-      return this.mergedConfig.mentionsOldPlace
+      return !this.mergedConfig.mentionsOwnLine
     },
     oldStyle () {
-      return this.mergedConfig.mentionsOldStyle
+      return !this.mergedConfig.mentionsNewStyle
     },
     style () {
       if (this.highlight) {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index d2f4129d..55f49507 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,8 +1,8 @@
 <template>
-<span
-  class="MentionLink"
-  :class="{ '-oldPlace': oldPlace }"
->
+  <span
+    class="MentionLink"
+    :class="{ '-oldPlace': oldPlace }"
+  >
     <!-- eslint-disable vue/no-v-html -->
     <a
       v-if="!user"
@@ -23,8 +23,14 @@
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <span class="shortName"><span class="userName" v-html="userName" /></span>
-        <span class="you" v-if="isYou">{{ $t('status.you') }}</span>
+        <span class="shortName"><span
+          class="userName"
+          v-html="userName"
+        /></span>
+        <span
+          v-if="isYou"
+          class="you"
+        >{{ $t('status.you') }}</span>
         <!-- eslint-enable vue/no-v-html -->
       </button>
       <span
@@ -33,7 +39,10 @@
         :class="[highlightType]"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <span class="userNameFull" v-html="userNameFull" />
+        <span
+          class="userNameFull"
+          v-html="userNameFull"
+        />
         <!-- eslint-enable vue/no-v-html -->
       </span>
     </span>
diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js
index 2e84d4c5..6f347058 100644
--- a/src/components/mentions_line/mentions_line.js
+++ b/src/components/mentions_line/mentions_line.js
@@ -15,7 +15,7 @@ const MentionsLine = {
   },
   computed: {
     oldStyle () {
-      return this.mergedConfig.mentionsOldStyle
+      return !this.mergedConfig.mentionsNewStyle
     },
     limit () {
       return 6
@@ -39,7 +39,7 @@ const MentionsLine = {
           : '-newStyle'
       ]
     },
-    ...mapGetters(['mergedConfig']),
+    ...mapGetters(['mergedConfig'])
   },
   methods: {
     toggleShowMore () {
diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue
index 6d114f2d..58f3de6f 100644
--- a/src/components/mentions_line/mentions_line.vue
+++ b/src/components/mentions_line/mentions_line.vue
@@ -1,42 +1,45 @@
 <template>
-<span class="MentionsLine">
-  <MentionLink
-    v-for="mention in mentions"
-    class="mention-link"
-    :key="mention.statusnet_profile_url"
-    :content="mention.statusnet_profile_url"
-    :url="mention.statusnet_profile_url"
-    :first-mention="false"
-  /><span v-if="manyMentions" class="extraMentions">
-    <span
-      v-if="expanded"
-      class="fullExtraMentions"
+  <span class="MentionsLine">
+    <MentionLink
+      v-for="mention in mentions"
+      :key="mention.statusnet_profile_url"
+      class="mention-link"
+      :content="mention.statusnet_profile_url"
+      :url="mention.statusnet_profile_url"
+      :first-mention="false"
+    /><span
+      v-if="manyMentions"
+      class="extraMentions"
     >
-      <MentionLink
-        v-for="mention in extraMentions"
-        class="mention-link"
-        :key="mention.statusnet_profile_url"
-        :content="mention.statusnet_profile_url"
-        :url="mention.statusnet_profile_url"
-        :first-mention="false"
-      />
-    </span><button
-      v-if="!expanded"
-      class="showMoreLess"
-      :class="buttonClasses"
-      @click="toggleShowMore"
-    >
-      {{ $t('status.plus_more', { number: extraMentions.length })}}
-    </button><button
-      v-if="expanded"
-      class="showMoreLess"
-      :class="buttonClasses"
-      @click="toggleShowMore"
-    >
-      {{ $t('general.show_less')}}
-    </button>
+      <span
+        v-if="expanded"
+        class="fullExtraMentions"
+      >
+        <MentionLink
+          v-for="mention in extraMentions"
+          :key="mention.statusnet_profile_url"
+          class="mention-link"
+          :content="mention.statusnet_profile_url"
+          :url="mention.statusnet_profile_url"
+          :first-mention="false"
+        />
+      </span><button
+        v-if="!expanded"
+        class="showMoreLess"
+        :class="buttonClasses"
+        @click="toggleShowMore"
+      >
+        {{ $t('status.plus_more', { number: extraMentions.length }) }}
+      </button><button
+        v-if="expanded"
+        class="showMoreLess"
+        :class="buttonClasses"
+        @click="toggleShowMore"
+      >
+        {{ $t('general.show_less') }}
+      </button>
+    </span>
   </span>
-</span>
 </template>
 <script src="./mentions_line.js" ></script>
 <style lang="scss" src="./mentions_line.scss" />
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index c8bab869..e62df290 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -37,13 +37,13 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="mentionsOldPlace">
-            {{ $t('settings.mentions_old_place') }}
+          <BooleanSetting path="mentionsOwnLine">
+            {{ $t('settings.mentions_new_place') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="mentionsOldStyle">
-            {{ $t('settings.mentions_old_style') }}
+          <BooleanSetting path="mentionsNewStyle">
+            {{ $t('settings.mentions_new_style') }}
           </BooleanSetting>
         </li>
         <li>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d921f625..3954df00 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -163,8 +163,8 @@ const Status = {
     muteWordHits () {
       return muteWordHits(this.status, this.muteWords)
     },
-    mentionsOldPlace () {
-      return this.mergedConfig.mentionsOldPlace
+    mentionsOwnLine () {
+      return this.mergedConfig.mentionsOwnLine
     },
     mentions () {
       return this.statusoid.attentions.filter(attn => {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index faf67328..969e2bc8 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -291,7 +291,10 @@
               </div>
             </div>
 
-            <div v-if="hasMentions && !mentionsOldPlace" class="heading-mentions-row">
+            <div
+              v-if="hasMentions && mentionsOwnLine"
+              class="heading-mentions-row"
+            >
               <div
                 class="mentions"
               >
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 0c1aa88e..3c092ac7 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -105,8 +105,8 @@ const StatusContent = {
     attachmentTypes () {
       return this.status.attachments.map(file => fileType.fileType(file.mimetype))
     },
-    mentionsOldPlace () {
-      return this.mergedConfig.mentionsOldPlace
+    mentionsOwnLine () {
+      return this.mergedConfig.mentionsOwnLine
     },
     mentions () {
       return this.status.attentions
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 92e47118..fadbeaf3 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -43,7 +43,7 @@
           v-if="!hideSubjectStatus && !(singleLine && status.summary_html)"
         >
           <MentionsLine
-            v-if="mentionsOldPlace"
+            v-if="!mentionsOwnLine"
             :attentions="status.attentions"
             class="mentions-line"
             />
diff --git a/src/modules/config.js b/src/modules/config.js
index 6e21e38b..ad9c8be5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -54,8 +54,8 @@ export const defaultState = {
   interfaceLanguage: browserLocale,
   hideScopeNotice: false,
   useStreamingApi: false,
-  mentionsOldPlace: false,
-  mentionsOldStyle: false,
+  mentionsOwnLine: false,
+  mentionsNewStyle: false,
   sidebarRight: undefined, // instance default
   scopeCopy: undefined, // instance default
   subjectLineBehavior: undefined, // instance default
-- 
cgit v1.2.3-70-g09d2


From 6bff7cc6efd1a7def75f4eef316fdcd84f0d3209 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 10 Jun 2021 13:29:59 +0300
Subject: use icon instead of symbol for @ in mentions links

---
 src/components/mention_link/mention_link.js   |  8 ++++++++
 src/components/mention_link/mention_link.scss | 21 +++++++--------------
 src/components/mention_link/mention_link.vue  |  5 +++++
 3 files changed, 20 insertions(+), 14 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 559250c5..711c87d6 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -1,6 +1,14 @@
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { mapGetters, mapState } from 'vuex'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+  faAt
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faAt
+)
 
 const MentionLink = {
   name: 'MentionLink',
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index a18475a1..9560e205 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -33,10 +33,6 @@
   & .short,
   & .full {
     white-space: nowrap;
-
-    &::before {
-      content: '@';
-    }
   }
 
   .new {
@@ -55,15 +51,19 @@
 
     &:not(.-oldStyle) {
       .short {
+        padding-left: 0.25em;
+        padding-right: 0;
+        padding-top: 0;
+        padding-bottom: 0;
         line-height: 1.5;
         font-size: inherit;
 
-        &::before {
+        .at {
           color: var(--faint);
+          opacity: 0.8;
           display: inline-block;
           height: 50%;
           line-height: 1;
-          vertical-align: 6%;
         }
       }
 
@@ -71,18 +71,11 @@
         padding-right: 0.25em;
       }
 
-      .short {
-        padding-left: 0.25em;
-        padding-right: 0;
-        padding-top: 0;
-        padding-bottom: 0;
-      }
-
       .userName {
         display: inline-block;
         color: var(--link);
         line-height: inherit;
-        margin-left: 0.125em;
+        margin-left: 0;
         padding-left: 0.125em;
         padding-right: 0.25em;
         padding-top: 0;
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 55f49507..281fab25 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -22,6 +22,11 @@
         :class="[{ '-sublime': !highlight }, oldStyle ? 'button-unstyled' : 'button-default']"
         @click.prevent="onClick"
       >
+        <FAIcon
+          size="s"
+          icon="at"
+          class="at"
+        />
         <!-- eslint-disable vue/no-v-html -->
         <span class="shortName"><span
           class="userName"
-- 
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/mention_link/mention_link.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 b84aeff6bf288b6e5855c2be0fd78577a0f7c0e5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 10 Jun 2021 18:52:23 +0300
Subject: stylistic changes

---
 src/components/mention_link/mention_link.scss | 16 +++++++++++++---
 src/components/mention_link/mention_link.vue  |  5 ++---
 2 files changed, 15 insertions(+), 6 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 9560e205..9df1ccfe 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -49,6 +49,17 @@
       }
     }
 
+    .at {
+      color: var(--link);
+      opacity: 0.8;
+      display: inline-block;
+      height: 50%;
+      line-height: 1;
+      padding: 0 0.1em;
+      vertical-align: -25%;
+      margin: 0;
+    }
+
     &:not(.-oldStyle) {
       .short {
         padding-left: 0.25em;
@@ -61,9 +72,8 @@
         .at {
           color: var(--faint);
           opacity: 0.8;
-          display: inline-block;
-          height: 50%;
-          line-height: 1;
+          padding-right: 0.25em;
+          vertical-align: -20%;
         }
       }
 
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index a65dbad3..062ad4bc 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -21,13 +21,12 @@
         :class="[{ '-sublime': !highlight }, oldStyle ? 'button-unstyled' : 'button-default']"
         @click.prevent="onClick"
       >
+        <!-- eslint-disable vue/no-v-html -->
         <FAIcon
           size="s"
           icon="at"
           class="at"
-        />
-        <!-- eslint-disable vue/no-v-html -->
-        <span class="shortName"><span
+        /><span class="shortName"><span
           class="userName"
           v-html="userName"
         /></span>
-- 
cgit v1.2.3-70-g09d2


From f819227bed8ab34e775ce5c0501457bf94c45fed Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 11 Jun 2021 11:49:32 +0300
Subject: fixed console errors, improved user-selecting, added cyantexting

---
 src/components/mention_link/mention_link.scss | 5 +++++
 src/components/mention_link/mention_link.vue  | 4 ++--
 src/components/status_body/status_body.scss   | 4 ++++
 src/services/theme_data/pleromafe.js          | 6 ++++++
 4 files changed, 17 insertions(+), 2 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 1be3e7c5..5f5da98f 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -28,6 +28,11 @@
     z-index: 1;
     margin-top: 0.25em;
     padding: 0.5em;
+    user-select: all;
+  }
+
+  .short {
+    user-select: none;
   }
 
   & .short,
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 062ad4bc..3449f4bd 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -23,7 +23,7 @@
       >
         <!-- eslint-disable vue/no-v-html -->
         <FAIcon
-          size="s"
+          size="sm"
           icon="at"
           class="at"
         /><span class="shortName"><span
@@ -44,7 +44,7 @@
         <!-- eslint-disable vue/no-v-html -->
         <span
           class="userNameFull"
-          v-html="userNameFull"
+          v-text="'@' + userNameFull"
         />
         <!-- eslint-enable vue/no-v-html -->
       </span>
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 10b4c186..310185ae 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -112,6 +112,10 @@
     color: var(--postGreentext, $fallback--cGreen);
   }
 
+  .cyantext {
+    color: var(--postCyantext, $fallback--cBlue);
+  }
+
   /* Not sure if this is necessary */
   video {
     max-width: 100%;
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 14aac975..c2983be7 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
     textColor: 'preserve'
   },
 
+  postCyantext: {
+    depends: ['cBlue'],
+    layer: 'bg',
+    textColor: 'preserve'
+  },
+
   border: {
     depends: ['fg'],
     opacity: 'border',
-- 
cgit v1.2.3-70-g09d2


From 418f029789f5e1cc22fd7db4f269088633d90050 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 12 Jun 2021 20:42:17 +0300
Subject: review + fixes

---
 src/components/mention_link/mention_link.vue       |  2 --
 src/components/mentions_line/mentions_line.scss    |  1 +
 src/components/rich_content/rich_content.jsx       | 21 ++++++++++++++++
 .../settings_modal/tabs/theme_tab/theme_tab.js     |  2 +-
 src/components/status/status.js                    |  6 ++---
 src/components/status_body/status_body.scss        |  8 -------
 src/components/status_body/status_body.vue         |  4 ++--
 .../entity_normalizer/entity_normalizer.service.js |  6 -----
 test/unit/specs/components/rich_content.spec.js    | 28 ++++++++++++++++++++++
 .../entity_normalizer/entity_normalizer.spec.js    |  8 -------
 10 files changed, 55 insertions(+), 31 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 3449f4bd..e4d395fa 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -41,12 +41,10 @@
         class="full popover-default"
         :class="[highlightType]"
       >
-        <!-- eslint-disable vue/no-v-html -->
         <span
           class="userNameFull"
           v-text="'@' + userNameFull"
         />
-        <!-- eslint-enable vue/no-v-html -->
       </span>
     </span>
   </span>
diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss
index 59f75fbb..90d1e0a4 100644
--- a/src/components/mentions_line/mentions_line.scss
+++ b/src/components/mentions_line/mentions_line.scss
@@ -1,6 +1,7 @@
 .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 ef15aaeb..0aae7a55 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -8,6 +8,25 @@ import MentionLink from 'src/components/mention_link/mention_link.vue'
 
 import './rich_content.scss'
 
+/**
+ * RichContent, The Über-powered component for rendering Post HTML.
+ *
+ * This takes post HTML and does multiple things to it:
+ * - Converts mention links to <MentionLink>-s
+ * - Removes mentions from beginning and end (hellthread style only)
+ * - Replaces emoji shortcodes with <StillImage>'d images.
+ *
+ * Stuff like removing mentions from beginning and end is done so that they could
+ * be either replaced by collapsible <MentionsLine>  or moved to separate place.
+ * There are two problems with this component's architecture:
+ * 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
+ *    proven to be a massive overcomplication due to amount of things done here.
+ * 2. We need to output both render and some extra data, which seems to be imp-
+ *    possible in vue. Current solution is to emit 'parseReady' event when parsing
+ *    is done within render() function.
+ *
+ * Apart from that one small hiccup with emit in render this _should_ be vue3-ready
+ */
 export default Vue.component('RichContent', {
   name: 'RichContent',
   props: {
@@ -241,8 +260,10 @@ export const preProcessPerLine = (html, greentext, handleLinks) => {
         .replace(/@\w+/gi, '') // remove mentions (even failed ones)
         .trim()
       if (cleanedString.startsWith('&gt;')) {
+        nonEmptyIndex += 1
         return `<span class='greentext'>${string}</span>`
       } else if (cleanedString.startsWith('&lt;')) {
+        nonEmptyIndex += 1
         return `<span class='cyantext'>${string}</span>`
       }
     }
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 63416e93..85749045 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.error('Forcing snapshout from file is not supported yet')
+          console.error('Forcing snapshot from file is not supported yet')
           break
       }
       this.dismissWarning()
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 5b178c2e..ae734493 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -35,8 +35,7 @@ import {
   faStar,
   faEyeSlash,
   faEye,
-  faThumbtack,
-  faAt
+  faThumbtack
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -53,8 +52,7 @@ library.add(
   faEllipsisH,
   faEyeSlash,
   faEye,
-  faThumbtack,
-  faAt
+  faThumbtack
 )
 
 const Status = {
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index da5d4dd3..81a687f1 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -115,12 +115,4 @@
   .cyantext {
     color: var(--postCyantext, $fallback--cBlue);
   }
-
-  /* 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
index aac44e42..0eb11ad0 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -2,7 +2,7 @@
   <div class="StatusBody">
     <div class="body">
       <div
-        v-if="status.summary_html"
+        v-if="status.summary_raw_html"
         class="summary-wrapper"
         :class="{ '-tall': (longSubject && !showingLongSubject) }"
       >
@@ -39,7 +39,7 @@
           {{ $t("general.show_more") }}
         </button>
         <span
-          v-if="!hideSubjectStatus && !(singleLine && status.summary_html)"
+          v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
           class="rich-content-wrapper"
         >
           <MentionsLine
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 613ed566..477b861f 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -296,7 +296,6 @@ export const parseStatus = (data) => {
     }
 
     output.summary_raw_html = escape(data.spoiler_text)
-    output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
     output.external_url = data.url
     output.poll = data.poll
     if (output.poll) {
@@ -449,11 +448,6 @@ export const parseChatMessage = (message) => {
   output.chat_id = message.chat_id
   output.emojis = message.emojis
   output.content = message.content
-  if (message.content) {
-    output.content = addEmojis(message.content, message.emojis)
-  } else {
-    output.content = ''
-  }
   if (message.attachment) {
     output.attachments = [parseAttachment(message.attachment)]
   } else {
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 05c0b259..f2c3f04e 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -354,4 +354,32 @@ describe('RichContent', () => {
 
     expect(wrapper.html()).to.eql(compwrap(html))
   })
+
+  it('Greentext + last mentions', () => {
+    const html = [
+      '&gt;quote',
+      makeMention('lol'),
+      '&gt;quote',
+      '&gt;quote'
+    ].join('\n')
+    const expected = [
+      '<span class="greentext">&gt;quote</span>',
+      stubMention('lol'),
+      '<span class="greentext">&gt;quote</span>',
+      '<span class="greentext">&gt;quote</span>'
+    ].join('\n')
+
+    const wrapper = shallowMount(RichContent, {
+      localVue,
+      propsData: {
+        handleLinks: true,
+        greentext: true,
+        emoji: [],
+        html
+      }
+    })
+
+    expect(wrapper.html()).to.eql(compwrap(expected))
+  })
+
 })
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index c8965785..8a5a6ef9 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -231,14 +231,6 @@ describe('API Entities normalizer', () => {
         expect(parsedRepeat).to.have.property('retweeted_status')
         expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
       })
-
-      it('adds emojis to subject line', () => {
-        const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
-
-        const parsedPost = parseStatus(post)
-
-        expect(parsedPost).to.have.property('summary_html').that.contains('<img')
-      })
     })
   })
 
-- 
cgit v1.2.3-70-g09d2


From a258182522e85c31fe9dfbfbddf7a314ca36d0ca Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 22 Jun 2021 20:16:26 +0300
Subject: fix non-notifying mentions and original mention display

---
 src/components/mention_link/mention_link.vue |  3 ++-
 src/components/rich_content/rich_content.jsx | 12 ++++++++++--
 src/components/status_body/status_body.js    | 14 ++++++++++++++
 src/components/status_body/status_body.vue   |  3 ++-
 4 files changed, 28 insertions(+), 4 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index e4d395fa..514b7475 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -5,8 +5,9 @@
     <!-- eslint-disable vue/no-v-html -->
     <a
       v-if="!user"
-      href="url"
+      :href="url"
       class="original"
+      target="_blank"
       v-html="content"
     />
     <!-- eslint-enable vue/no-v-html -->
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index 79acd1de..cd73f2e5 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -36,6 +36,10 @@ export default Vue.component('RichContent', {
       required: true,
       type: String
     },
+    attentions: {
+      required: false,
+      default: () => []
+    },
     // Emoji object, as in status.emojis, note the "s" at the end...
     emoji: {
       required: true,
@@ -91,8 +95,12 @@ export default Vue.component('RichContent', {
       </a>
     }
 
-    const renderMention = (attrs, children, encounteredText) => {
+    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)
@@ -148,7 +156,7 @@ export default Vue.component('RichContent', {
         const Tag = getTagName(opener)
         const attrs = getAttrs(opener)
         switch (Tag) {
-          case 'span': // replace images with StillImage
+          case 'span': // Replace last mentions class with mentionsline
             if (attrs['class'] && attrs['class'].includes('lastMentions')) {
               if (firstMentions.length > 1 && lastMentions.length > 1) {
                 break
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 601c963b..d1736ba3 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -85,6 +85,20 @@ const StatusContent = {
     })
   },
   methods: {
+    onParseReady (event) {
+      this.$emit('parseReady', event)
+      const { writtenMentions } = event
+      writtenMentions
+        .filter(mention => !mention.notifying)
+        .forEach(mention => {
+          const { content, url } = mention
+          const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags
+          if (!cleanedString.startsWith('@')) return
+          const handle = cleanedString.slice(1)
+          const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
+          this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
+        })
+    },
     toggleShowMore () {
       if (this.mightHideBecauseTall) {
         this.showingTall = !this.showingTall
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 2be46303..d77ccd54 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -47,7 +47,8 @@
           :handle-links="true"
           :hide-mentions="hideMentions"
           :greentext="mergedConfig.greentext"
-          @parseReady="$emit('parseReady', $event)"
+          :attentions="status.attentions"
+          @parseReady="onParseReady"
         />
 
         <button
-- 
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/mention_link/mention_link.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 39494439d359fa058079c9325efdff244a7e7890 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 23 Aug 2021 20:57:21 +0300
Subject: very minimalist hashtaglink implementation, also you can middle-click
 mentions now.

---
 src/components/hashtag_link/hashtag_link.js     | 36 +++++++++++++++++++++++++
 src/components/hashtag_link/hashtag_link.scss   |  6 +++++
 src/components/hashtag_link/hashtag_link.vue    | 19 +++++++++++++
 src/components/mention_link/mention_link.vue    |  5 ++--
 src/components/rich_content/rich_content.jsx    | 13 ++++-----
 test/unit/specs/components/rich_content.spec.js |  8 +++---
 6 files changed, 75 insertions(+), 12 deletions(-)
 create mode 100644 src/components/hashtag_link/hashtag_link.js
 create mode 100644 src/components/hashtag_link/hashtag_link.scss
 create mode 100644 src/components/hashtag_link/hashtag_link.vue

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

diff --git a/src/components/hashtag_link/hashtag_link.js b/src/components/hashtag_link/hashtag_link.js
new file mode 100644
index 00000000..a2433c2a
--- /dev/null
+++ b/src/components/hashtag_link/hashtag_link.js
@@ -0,0 +1,36 @@
+import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+
+const HashtagLink = {
+  name: 'HashtagLink',
+  props: {
+    url: {
+      required: true,
+      type: String
+    },
+    content: {
+      required: true,
+      type: String
+    },
+    tag: {
+      required: false,
+      type: String,
+      default: ''
+    }
+  },
+  methods: {
+    onClick () {
+      const tag = this.tag || extractTagFromUrl(this.url)
+      if (tag) {
+        const link = this.generateTagLink(tag)
+        this.$router.push(link)
+      } else {
+        window.open(this.url, '_blank')
+      }
+    },
+    generateTagLink (tag) {
+      return `/tag/${tag}`
+    }
+  }
+}
+
+export default HashtagLink
diff --git a/src/components/hashtag_link/hashtag_link.scss b/src/components/hashtag_link/hashtag_link.scss
new file mode 100644
index 00000000..78e8fb99
--- /dev/null
+++ b/src/components/hashtag_link/hashtag_link.scss
@@ -0,0 +1,6 @@
+.HashtagLink {
+  position: relative;
+  white-space: normal;
+  display: inline-block;
+  color: var(--link);
+}
diff --git a/src/components/hashtag_link/hashtag_link.vue b/src/components/hashtag_link/hashtag_link.vue
new file mode 100644
index 00000000..918ed26b
--- /dev/null
+++ b/src/components/hashtag_link/hashtag_link.vue
@@ -0,0 +1,19 @@
+<template>
+  <span
+    class="HashtagLink"
+  >
+    <!-- eslint-disable vue/no-v-html -->
+    <a
+      :href="url"
+      class="original"
+      target="_blank"
+      @click.prevent="onClick"
+      v-html="content"
+    />
+    <!-- eslint-enable vue/no-v-html -->
+  </span>
+</template>
+
+<script src="./hashtag_link.js"/>
+
+<style lang="scss" src="./hashtag_link.scss"/>
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 625eb727..a22b486c 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -17,8 +17,9 @@
       :style="style"
       :class="classnames"
     >
-      <button
+      <a
         class="short button-unstyled"
+        :href="url"
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
@@ -35,7 +36,7 @@
           class="you"
         >{{ $t('status.you') }}</span>
         <!-- eslint-enable vue/no-v-html -->
-      </button>
+      </a>
       <span
         v-if="userName !== userNameFull"
         class="full popover-default"
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index d98947df..f52b0370 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 MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
+import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
 
 import './rich_content.scss'
 
@@ -83,13 +84,10 @@ 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)
       }
-      return <a {...{ attrs }}>
-        { children.map(processItem) }
-      </a>
+      return <HashtagLink {...{ props: linkData }}/>
     }
 
     const renderMention = (attrs, children) => {
@@ -211,7 +209,10 @@ export default Vue.component('RichContent', {
             if (!this.handleLinks) break
             const attrs = getAttrs(opener)
             // should only be this
-            if (attrs['class'] && attrs['class'].includes('hashtag')) {
+            if (
+              (attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style
+                (attrs['rel'] === 'tag') // Mastodon style
+            ) {
               return renderHashtag(attrs, children, encounteredTextReverse)
             } else {
               attrs.target = '_blank'
@@ -275,7 +276,7 @@ const getLinkData = (attrs, children, index) => {
   return {
     index,
     url: attrs.href,
-    hashtag: attrs['data-tag'],
+    tag: attrs['data-tag'],
     content: flattenDeep(children).join(''),
     textContent
   }
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index b29edeab..f6c478a9 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -300,10 +300,10 @@ describe('RichContent', () => {
       '<p>',
       '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
       'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
-      ' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou" target="_blank">',
-      '#nou</a>',
-      ' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap" target="_blank">',
-      '#screencap</a>',
+      ' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
+      '</hashtaglink-stub>',
+      ' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
+      '</hashtaglink-stub>',
       ' </p>'
     ].join('')
 
-- 
cgit v1.2.3-70-g09d2


From 76547fe66d1771f5bff732a34b0547f890f4621a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 00:37:39 -0500
Subject: Add a pref for whether to display mention as icon or text

---
 src/components/mention_link/mention_link.js        | 3 +++
 src/components/mention_link/mention_link.vue       | 9 +++++----
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/modules/config.js                              | 1 +
 src/modules/instance.js                            | 1 +
 5 files changed, 15 insertions(+), 4 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 65c62baa..637641af 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -85,6 +85,9 @@ const MentionLink = {
         this.highlightType
       ]
     },
+    useAtIcon () {
+      return this.mergedConfig.useAtIcon
+    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       currentUser: state => state.users.currentUser
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index a22b486c..f633bf50 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -24,13 +24,14 @@
       >
         <!-- eslint-disable vue/no-v-html -->
         <FAIcon
+          v-if="useAtIcon"
           size="sm"
           icon="at"
           class="at"
-        /><span class="shortName"><span
-          class="userName"
-          v-html="userName"
-        /></span>
+        /><span class="shortName">{{ !useAtIcon ? '@' : '' }}<span
+            class="userName"
+            v-html="userName"
+          /></span>
         <span
           v-if="isYou"
           class="you"
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index bd3ee84e..5fec2d89 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -147,6 +147,11 @@
             {{ $t('settings.greentext') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="useAtIcon">
+            {{ $t('settings.use_at_icon') }}
+          </BooleanSetting>
+        </li>
       </ul>
     </div>
 
diff --git a/src/modules/config.js b/src/modules/config.js
index c79302b5..43c8b92f 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -70,6 +70,7 @@ export const defaultState = {
   useOneClickNsfw: false,
   useContainFit: false,
   greentext: undefined, // instance default
+  useAtIcon: undefined, // instance default
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 60038f08..aaaf7acf 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -20,6 +20,7 @@ const defaultState = {
   background: '/static/aurora_borealis.jpg',
   collapseMessageWithSubject: false,
   greentext: false,
+  useAtIcon: false,
   hideFilteredStatuses: false,
   // bad name: actually hides posts of muted USERS
   hideMutedPosts: false,
-- 
cgit v1.2.3-70-g09d2


From 1d4b1b296e8ee37f119f419df49791d99fef4774 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 01:16:33 -0500
Subject: Add pref for whether to display full user names and tooltips

---
 src/components/mention_link/mention_link.js        | 20 ++++++++++++++++++++
 src/components/mention_link/mention_link.vue       | 16 +++++++++++-----
 src/components/settings_modal/tabs/general_tab.js  |  5 +++++
 src/components/settings_modal/tabs/general_tab.vue | 19 +++++++++++++++++++
 src/modules/config.js                              |  5 ++++-
 src/modules/instance.js                            |  5 +++++
 6 files changed, 64 insertions(+), 6 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 637641af..f71c7716 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -50,6 +50,10 @@ const MentionLink = {
     userName () {
       return this.user && this.userNameFullUi.split('@')[0]
     },
+    serverName () {
+      // XXX assumed that domain does not contain @
+      return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain)
+    },
     userNameFull () {
       return this.user && this.user.screen_name
     },
@@ -88,6 +92,22 @@ const MentionLink = {
     useAtIcon () {
       return this.mergedConfig.useAtIcon
     },
+    isRemote () {
+      return this.userName !== this.userNameFull
+    },
+    shouldShowFullUserName () {
+      const conf = this.mergedConfig.mentionLinkDisplay
+      if (conf === 'short') {
+        return false
+      } else if (conf === 'full') {
+        return true
+      } else { // full_for_remote
+        return this.isRemote
+      }
+    },
+    shouldShowTooltip () {
+      return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
+    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       currentUser: state => state.users.currentUser
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index f633bf50..fe16cbf5 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -28,10 +28,16 @@
           size="sm"
           icon="at"
           class="at"
-        /><span class="shortName">{{ !useAtIcon ? '@' : '' }}<span
-            class="userName"
-            v-html="userName"
-          /></span>
+        /><span
+          class="shortName"
+        >{{ !useAtIcon ? '@' : '' }}<span
+          class="userName"
+          v-html="userName"
+        /><span
+          v-if="shouldShowFullUserName"
+          class="serverName"
+          v-html="'@' + serverName"
+        /></span>
         <span
           v-if="isYou"
           class="you"
@@ -39,7 +45,7 @@
         <!-- eslint-enable vue/no-v-html -->
       </a>
       <span
-        v-if="userName !== userNameFull"
+        v-if="shouldShowTooltip"
         class="full popover-default"
         :class="[highlightType]"
       >
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index eeda61bf..952c328d 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -20,6 +20,11 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
       })),
+      mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.mention_link_display_${mode}`)
+      })),
       loopSilentAvailable:
       // Firefox
       Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 5fec2d89..8fce1eee 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -152,6 +152,25 @@
             {{ $t('settings.use_at_icon') }}
           </BooleanSetting>
         </li>
+        <li>
+          <ChoiceSetting
+            id="mentionLinkDisplay"
+            path="mentionLinkDisplay"
+            :options="mentionLinkDisplayOptions"
+          >
+            {{ $t('settings.mention_link_display') }}
+          </ChoiceSetting>
+        </li>
+        <ul
+          v-if="mentionLinkDisplay === 'short'"
+          class="setting-list suboptions"
+        >
+          <li>
+            <BooleanSetting path="mentionLinkShowTooltip">
+              {{ $t('settings.mention_link_show_tooltip') }}
+            </BooleanSetting>
+          </li>
+        </ul>
       </ul>
     </div>
 
diff --git a/src/modules/config.js b/src/modules/config.js
index 43c8b92f..e1a49a7d 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -11,7 +11,8 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
  */
 export const multiChoiceProperties = [
   'postContentType',
-  'subjectLineBehavior'
+  'subjectLineBehavior',
+  'mentionLinkDisplay' // short | full_for_remote | full
 ]
 
 export const defaultState = {
@@ -71,6 +72,8 @@ export const defaultState = {
   useContainFit: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
+  mentionLinkDisplay: undefined, // instance default
+  mentionLinkShowTooltip: undefined, // instance default
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index aaaf7acf..200a7a6f 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -21,6 +21,8 @@ const defaultState = {
   collapseMessageWithSubject: false,
   greentext: false,
   useAtIcon: false,
+  mentionLinkDisplay: 'short',
+  mentionLinkShowTooltip: true,
   hideFilteredStatuses: false,
   // bad name: actually hides posts of muted USERS
   hideMutedPosts: false,
@@ -101,6 +103,9 @@ const instance = {
       return instanceDefaultProperties
         .map(key => [key, state[key]])
         .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
+    },
+    instanceDomain (state) {
+      return new URL(state.server).hostname
     }
   },
   actions: {
-- 
cgit v1.2.3-70-g09d2


From 95007059d16cfed51b0f3d5c17fbbbe4464a71ed Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 01:37:20 -0500
Subject: Style properly usernames without tooltips

---
 src/components/mention_link/mention_link.scss | 8 ++++----
 src/components/mention_link/mention_link.vue  | 9 +++++----
 2 files changed, 9 insertions(+), 8 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index ec2689f8..44e1363a 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -27,7 +27,7 @@
     user-select: all;
   }
 
-  .short {
+  .short.-with-tooltip {
     user-select: none;
   }
 
@@ -56,7 +56,7 @@
     }
 
     &.-striped {
-      & .userName,
+      & .shortName,
       & .full {
         background-image:
           repeating-linear-gradient(
@@ -70,14 +70,14 @@
     }
 
     &.-solid {
-      & .userName,
+      & .shortName,
       & .full {
         background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
       }
     }
 
     &.-side {
-      & .userName,
+      & .shortName,
       & .userNameFull {
         box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
       }
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index fe16cbf5..c9baee10 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -19,18 +19,19 @@
     >
       <a
         class="short button-unstyled"
+        :class="{ '-with-tooltip': shouldShowTooltip }"
         :href="url"
         @click.prevent="onClick"
       >
         <!-- eslint-disable vue/no-v-html -->
-        <FAIcon
+        <span
+          class="shortName"
+        ><FAIcon
           v-if="useAtIcon"
           size="sm"
           icon="at"
           class="at"
-        /><span
-          class="shortName"
-        >{{ !useAtIcon ? '@' : '' }}<span
+        />{{ !useAtIcon ? '@' : '' }}<span
           class="userName"
           v-html="userName"
         /><span
-- 
cgit v1.2.3-70-g09d2


From 9fde13c9685e934c8f610c96d481457b3d37c389 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 02:05:41 -0500
Subject: Add option to display user avatar in mention link

---
 src/components/mention_link/mention_link.js        | 7 +++++++
 src/components/mention_link/mention_link.scss      | 9 +++++++++
 src/components/mention_link/mention_link.vue       | 5 +++++
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/modules/config.js                              | 1 +
 src/modules/instance.js                            | 1 +
 6 files changed, 28 insertions(+)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index f71c7716..426dbe97 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -1,6 +1,7 @@
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import { mapGetters, mapState } from 'vuex'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
+import UserAvatar from '../user_avatar/user_avatar.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faAt
@@ -12,6 +13,9 @@ library.add(
 
 const MentionLink = {
   name: 'MentionLink',
+  components: {
+    UserAvatar
+  },
   props: {
     url: {
       required: true,
@@ -108,6 +112,9 @@ const MentionLink = {
     shouldShowTooltip () {
       return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
     },
+    shouldShowAvatar () {
+      return this.mergedConfig.mentionLinkShowAvatar
+    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       currentUser: state => state.users.currentUser
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 5f467582..be00093a 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -1,3 +1,5 @@
+@import '../../_variables.scss';
+
 .MentionLink {
   position: relative;
   white-space: normal;
@@ -10,6 +12,13 @@
     border-radius: 2px;
   }
 
+  .mention-avatar {
+    border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+    width: 1.5em;
+    height: 1.5em;
+    vertical-align: middle;
+  }
+
   .full {
     position: absolute;
     display: inline-block;
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index c9baee10..8e78243a 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -23,6 +23,11 @@
         :href="url"
         @click.prevent="onClick"
       >
+        <UserAvatar
+          v-if="shouldShowAvatar"
+          class="mention-avatar"
+          :user="user"
+        />
         <!-- eslint-disable vue/no-v-html -->
         <span
           class="shortName"
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 8fce1eee..b4495941 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -171,6 +171,11 @@
             </BooleanSetting>
           </li>
         </ul>
+        <li>
+          <BooleanSetting path="mentionLinkShowAvatar">
+            {{ $t('settings.mention_link_show_avatar') }}
+          </BooleanSetting>
+        </li>
       </ul>
     </div>
 
diff --git a/src/modules/config.js b/src/modules/config.js
index e1a49a7d..cfb753ce 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -74,6 +74,7 @@ export const defaultState = {
   useAtIcon: undefined, // instance default
   mentionLinkDisplay: undefined, // instance default
   mentionLinkShowTooltip: undefined, // instance default
+  mentionLinkShowAvatar: undefined, // instance default
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 200a7a6f..6c8823c3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -23,6 +23,7 @@ const defaultState = {
   useAtIcon: false,
   mentionLinkDisplay: 'short',
   mentionLinkShowTooltip: true,
+  mentionLinkShowAvatar: true,
   hideFilteredStatuses: false,
   // bad name: actually hides posts of muted USERS
   hideMutedPosts: false,
-- 
cgit v1.2.3-70-g09d2


From aaf0b985ad0cf23ace370d2de0d8402df4a4af0e Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 02:10:42 -0500
Subject: Make avatar unselectable

---
 src/components/mention_link/mention_link.scss | 2 ++
 src/components/mention_link/mention_link.vue  | 5 ++---
 2 files changed, 4 insertions(+), 3 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index be00093a..9a285ad8 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -17,6 +17,8 @@
     width: 1.5em;
     height: 1.5em;
     vertical-align: middle;
+    user-select: none;
+    margin-right: 0.2em;
   }
 
   .full {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 8e78243a..c0d71322 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -23,13 +23,12 @@
         :href="url"
         @click.prevent="onClick"
       >
+        <!-- eslint-disable vue/no-v-html -->
         <UserAvatar
           v-if="shouldShowAvatar"
           class="mention-avatar"
           :user="user"
-        />
-        <!-- eslint-disable vue/no-v-html -->
-        <span
+        /><span
           class="shortName"
         ><FAIcon
           v-if="useAtIcon"
-- 
cgit v1.2.3-70-g09d2


From 0c60b31eee3e31810ebd41bc186a9825a39cd821 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 10 Jan 2022 02:31:26 -0500
Subject: Add option to fade domains in mention link

---
 src/components/mention_link/mention_link.js        |  3 +++
 src/components/mention_link/mention_link.scss      |  8 ++++++++
 src/components/mention_link/mention_link.vue       | 15 +++++++++++++--
 src/components/settings_modal/tabs/general_tab.vue |  5 +++++
 src/modules/config.js                              |  1 +
 src/modules/instance.js                            |  1 +
 6 files changed, 31 insertions(+), 2 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 426dbe97..5209907d 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -115,6 +115,9 @@ const MentionLink = {
     shouldShowAvatar () {
       return this.mergedConfig.mentionLinkShowAvatar
     },
+    shouldFadeDomain () {
+      return this.mergedConfig.mentionLinkFadeDomain
+    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       currentUser: state => state.users.currentUser
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 9a285ad8..03306dcc 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -100,4 +100,12 @@
     opacity: 1;
     pointer-events: initial;
   }
+
+  .serverName.-faded {
+    color: var(--faintLink, $fallback--link);
+  }
+
+  .full .-faded {
+    color: var(--faint, $fallback--faint);
+  }
 }
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index c0d71322..ac20eb5a 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -41,6 +41,7 @@
         /><span
           v-if="shouldShowFullUserName"
           class="serverName"
+          :class="{ '-faded': shouldFadeDomain }"
           v-html="'@' + serverName"
         /></span>
         <span
@@ -56,8 +57,18 @@
       >
         <span
           class="userNameFull"
-          v-text="'@' + userNameFull"
-        />
+        >
+          <!-- eslint-disable vue/no-v-html -->
+          @<span
+            class="userName"
+            v-html="userName"
+          /><span
+            class="serverName"
+            :class="{ '-faded': shouldFadeDomain }"
+            v-html="'@' + serverName"
+          />
+          <!-- eslint-enable vue/no-v-html -->
+        </span>
       </span>
     </span>
   </span>
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index b4495941..50599eae 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -176,6 +176,11 @@
             {{ $t('settings.mention_link_show_avatar') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="mentionLinkFadeDomain">
+            {{ $t('settings.mention_link_fade_domain') }}
+          </BooleanSetting>
+        </li>
       </ul>
     </div>
 
diff --git a/src/modules/config.js b/src/modules/config.js
index cfb753ce..9f2d4ef3 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -75,6 +75,7 @@ export const defaultState = {
   mentionLinkDisplay: undefined, // instance default
   mentionLinkShowTooltip: undefined, // instance default
   mentionLinkShowAvatar: undefined, // instance default
+  mentionLinkFadeDomain: undefined, // instance default
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 6c8823c3..2859b00b 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -24,6 +24,7 @@ const defaultState = {
   mentionLinkDisplay: 'short',
   mentionLinkShowTooltip: true,
   mentionLinkShowAvatar: true,
+  mentionLinkFadeDomain: true,
   hideFilteredStatuses: false,
   // bad name: actually hides posts of muted USERS
   hideMutedPosts: false,
-- 
cgit v1.2.3-70-g09d2


From bfb3a4364be3f71f7c46056aeb8972f04a072703 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 3 Feb 2022 22:34:57 +0200
Subject: options to disable (You)s and highlighting of yourself

---
 src/components/mention_link/mention_link.js        |  8 +++++++-
 src/components/mention_link/mention_link.vue       |  4 ++--
 src/components/settings_modal/tabs/general_tab.vue | 10 ++++++++++
 src/i18n/en.json                                   |  2 ++
 src/modules/config.js                              |  2 ++
 src/modules/instance.js                            |  2 ++
 6 files changed, 25 insertions(+), 3 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 5209907d..55eea195 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -87,7 +87,7 @@ const MentionLink = {
     classnames () {
       return [
         {
-          '-you': this.isYou,
+          '-you': this.isYou && this.shouldBoldenYou,
           '-highlighted': this.highlight
         },
         this.highlightType
@@ -115,6 +115,12 @@ const MentionLink = {
     shouldShowAvatar () {
       return this.mergedConfig.mentionLinkShowAvatar
     },
+    shouldShowYous () {
+      return this.mergedConfig.mentionLinkShowYous
+    },
+    shouldBoldenYou () {
+      return this.mergedConfig.mentionLinkBoldenYou
+    },
     shouldFadeDomain () {
       return this.mergedConfig.mentionLinkFadeDomain
     },
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index ac20eb5a..d8ab79fb 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -45,8 +45,8 @@
           v-html="'@' + serverName"
         /></span>
         <span
-          v-if="isYou"
-          class="you"
+          v-if="isYou && shouldShowYous"
+          :class="{ '-you': shouldBoldenYou }"
         >{{ $t('status.you') }}</span>
         <!-- eslint-enable vue/no-v-html -->
       </a>
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 44b1ac92..eba3b268 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -147,6 +147,11 @@
             {{ $t('settings.greentext') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="mentionLinkShowYous">
+            {{ $t('settings.show_yous') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
@@ -181,6 +186,11 @@
               {{ $t('settings.mention_link_fade_domain') }}
             </BooleanSetting>
           </li>
+          <li>
+            <BooleanSetting path="mentionLinkBoldenYou">
+              {{ $t('settings.mention_link_bolden_you') }}
+            </BooleanSetting>
+          </li>
         </ul>
       </ul>
     </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 209fd184..8fd189c6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -493,8 +493,10 @@
     "mention_link_show_tooltip": "Show full user names as tooltip for remote users",
     "mention_link_show_avatar": "Show user avatar beside the link",
     "mention_link_fade_domain": "Fade domains (e.g. @example.org in @foo@example.org)",
+    "mention_link_bolden_you": "Bolden text of your mention",
     "fun": "Fun",
     "greentext": "Meme arrows",
+    "show_yous": "Show (You)s",
     "notifications": "Notifications",
     "notification_setting_filters": "Filters",
     "notification_setting_block_from_strangers": "Block notifications from users who you do not follow",
diff --git a/src/modules/config.js b/src/modules/config.js
index 9f2d4ef3..20979174 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -76,6 +76,8 @@ export const defaultState = {
   mentionLinkShowTooltip: undefined, // instance default
   mentionLinkShowAvatar: undefined, // instance default
   mentionLinkFadeDomain: undefined, // instance default
+  mentionLinkShowYous: undefined, // instance default
+  mentionLinkBoldenYou: undefined, // instance default
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index d686f258..1abd784f 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -25,6 +25,8 @@ const defaultState = {
   mentionLinkShowTooltip: true,
   mentionLinkShowAvatar: false,
   mentionLinkFadeDomain: true,
+  mentionLinkShowYous: false,
+  mentionLinkBoldenYou: true,
   hideFilteredStatuses: false,
   // bad name: actually hides posts of muted USERS
   hideMutedPosts: false,
-- 
cgit v1.2.3-70-g09d2


From 4c7edfc9a9865003483bc4b78d29bb554e7901ad Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 9 Feb 2022 23:34:06 +0200
Subject: more spacing/wrapping fixes

---
 src/components/mention_link/mention_link.scss   | 1 +
 src/components/mention_link/mention_link.vue    | 8 +++-----
 src/components/mentions_line/mentions_line.scss | 1 +
 src/components/status_body/status_body.scss     | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index c7db7fb9..a4326296 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -5,6 +5,7 @@
   white-space: normal;
   display: inline;
   color: var(--link);
+  word-break: normal;
 
   & .new,
   & .original {
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index d8ab79fb..13786c3a 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -43,14 +43,12 @@
           class="serverName"
           :class="{ '-faded': shouldFadeDomain }"
           v-html="'@' + serverName"
-        /></span>
-        <span
+        /></span><span
           v-if="isYou && shouldShowYous"
           :class="{ '-you': shouldBoldenYou }"
-        >{{ $t('status.you') }}</span>
+        > {{ $t('status.you') }}</span>
         <!-- eslint-enable vue/no-v-html -->
-      </a>
-      <span
+      </a><span
         v-if="shouldShowTooltip"
         class="full popover-default"
         :class="[highlightType]"
diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss
index 1a485860..9650a9a4 100644
--- a/src/components/mentions_line/mentions_line.scss
+++ b/src/components/mentions_line/mentions_line.scss
@@ -2,6 +2,7 @@
   word-break: break-all;
 
   .showMoreLess {
+    margin-left: 0.5em;
     white-space: normal;
     color: var(--link);
   }
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index 51623a26..c7732bfe 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -9,7 +9,7 @@
   & .text,
   & .summary {
     font-family: var(--postFont, sans-serif);
-    white-space: normal;
+    white-space: pre-wrap;
     overflow-wrap: break-word;
     word-wrap: break-word;
     word-break: break-word;
-- 
cgit v1.2.3-70-g09d2


From 17d6f1b53b343fee3ed0ae6137846cd9f21097ec Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 11 Feb 2022 15:06:12 +0200
Subject: more spacing fixes

---
 src/components/mention_link/mention_link.vue | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

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

diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 13786c3a..3562f511 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -9,9 +9,7 @@
       class="original"
       target="_blank"
       v-html="content"
-    />
-    <!-- eslint-enable vue/no-v-html -->
-    <span
+    /><!-- eslint-enable vue/no-v-html --><span
       v-if="user"
       class="new"
       :style="style"
-- 
cgit v1.2.3-70-g09d2

