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: [PATCH] mention link

---
 src/components/mention_link/mention_link.js   | 71 +++++++++++++++++++
 src/components/mention_link/mention_link.scss | 49 +++++++++++++
 src/components/mention_link/mention_link.vue  | 17 +++++
 src/components/rich_content/rich_content.jsx  | 16 ++++-
 .../status_content/status_content.js          |  7 ++
 src/modules/users.js                          |  4 ++
 6 files changed, 161 insertions(+), 3 deletions(-)
 create mode 100644 src/components/mention_link/mention_link.js
 create mode 100644 src/components/mention_link/mention_link.scss
 create mode 100644 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
new file mode 100644
index 0000000000..ade598d8ba
--- /dev/null
+++ b/src/components/mention_link/mention_link.js
@@ -0,0 +1,71 @@
+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'
+
+const MentionLink = {
+  name: 'MentionLink',
+  props: {
+    url: {
+      required: true,
+      type: String
+    },
+    content: {
+      required: true,
+      type: String
+    },
+    origattrs: {
+      required: true,
+      type: Object
+    }
+  },
+  methods: {
+    onClick () {
+      const link = generateProfileLink(this.user.id, this.user.screen_name)
+      this.$router.push(link)
+    }
+  },
+  computed: {
+    user () {
+      return this.$store.getters.findUserByUrl(this.url)
+    },
+    isYou () {
+      // FIXME why user !== currentUser???
+      return this.user.screen_name === this.currentUser.screen_name
+    },
+    userName () {
+      return this.userNameFullUi.split('@')[0]
+    },
+    userNameFull () {
+      return this.user.screen_name
+    },
+    userNameFullUi () {
+      return this.user.screen_name_ui
+    },
+    highlight () {
+      return this.mergedConfig.highlight[this.user.screen_name]
+    },
+    bg () {
+      if (this.highlight) return this.highlight.color
+    },
+    text () {
+      if (this.bg) {
+        const linkColor = this.mergedConfig.customTheme.colors.link
+        const color = getTextColor(convert(this.bg).rgb, convert(linkColor).rgb)
+        return rgb2hex(color)
+      }
+    },
+    style () {
+      return [
+        this.bg && `--mention-bg: ${this.bg}`,
+        this.text && `--mention-text: ${this.text}`
+      ].filter(_ => _).join('; ')
+    },
+    ...mapGetters(['mergedConfig']),
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    })
+  }
+}
+
+export default MentionLink
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
new file mode 100644
index 0000000000..237d92052c
--- /dev/null
+++ b/src/components/mention_link/mention_link.scss
@@ -0,0 +1,49 @@
+.MentionLink {
+  position: relative;
+  white-space: normal;
+  display: inline-block;
+
+  & .new,
+  & .original,
+  & .full {
+    padding: 0 2px;
+    margin: 0 -2px;
+    display: inline-block;
+    border-radius: 2px;
+  }
+
+  .original {
+    opacity: 0.5;
+  }
+
+  .full {
+    pointer-events: none;
+    position: absolute;
+    opacity: 0;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    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;
+  }
+
+  .new {
+    background-color: var(--mention-bg);
+    color: var(--mention-text, var(--link));
+
+    &.-you {
+      & .shortName,
+      & .full {
+        font-weight: 600;
+      }
+    }
+  }
+
+  &:hover .new .full {
+    opacity: 1;
+  }
+}
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
new file mode 100644
index 0000000000..84ed320506
--- /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"/>
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index 7bff23cd19..00cf6623f8 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -1,7 +1,8 @@
 import Vue from 'vue'
-import { unescape } from 'lodash'
+import { unescape, flattenDeep } from 'lodash'
 import { convertHtml, getTagName, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
 import StillImage from 'src/components/still-image/still-image.vue'
+import MentionLink from 'src/components/mention_link/mention_link.vue'
 
 import './rich_content.scss'
 
@@ -22,6 +23,9 @@ export default Vue.component('RichContent', {
       const attrs = getAttrs(tag)
       return <StillImage {...{ attrs }} class="img"/>
     }
+    const renderMention = (attrs, children) => {
+      return <MentionLink url={attrs.href} content={flattenDeep(children).join('')} origattrs={attrs}/>
+    }
     const structure = convertHtml(this.html)
     const processItem = (item) => {
       if (typeof item === 'string') {
@@ -45,8 +49,14 @@ export default Vue.component('RichContent', {
       if (Array.isArray(item)) {
         const [opener, children] = item
         const Tag = getTagName(opener)
-        if (Tag === 'img') {
-          return renderImage(opener)
+        switch (Tag) {
+          case 'img':
+            return renderImage(opener)
+          case 'a':
+            const attrs = getAttrs(opener)
+            if (attrs['class'] && attrs['class'].includes('mention')) {
+              return renderMention(attrs, children)
+            }
         }
         if (children !== undefined) {
           return <Tag {...{ attrs: getAttrs(opener) }}>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 571f1a78e6..79408e38fb 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -168,6 +168,13 @@ const StatusContent = {
     LinkPreview,
     RichContent
   },
+  mounted () {
+    const { attentions } = this.status
+    attentions.forEach(attn => {
+      const { id } = attn
+      this.$store.state.api.backendInteractor.fetchUserIfMissing(this.$store, id)
+    })
+  },
   methods: {
     linkClicked (event) {
       const target = event.target.closest('.status-content a')
diff --git a/src/modules/users.js b/src/modules/users.js
index 2b416f94f4..9ed068973c 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -246,6 +246,10 @@ export const getters = {
     }
     return result
   },
+  findUserByUrl: state => query => {
+    return state.users
+      .find(u => u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
+  },
   relationship: state => id => {
     const rel = id && state.relationships[id]
     return rel || { id, loading: true }
-- 
GitLab