From 15603981f8309d979465c40175f9b3cd4f6617b4 Mon Sep 17 00:00:00 2001
From: shpuld <shp@cock.li>
Date: Wed, 30 Jan 2019 19:15:35 +0200
Subject: [PATCH] Capture clicks on statuses to hijack mention clicks, match
 mention href to user somehow

---
 src/components/status/status.js               | 15 ++++-
 src/components/status/status.vue              |  4 +-
 .../mention_matcher/mention_matcher.js        |  9 +++
 .../mention_matcher/mention_matcher.spec.js   | 63 +++++++++++++++++++
 4 files changed, 88 insertions(+), 3 deletions(-)
 create mode 100644 src/services/mention_matcher/mention_matcher.js
 create mode 100644 test/unit/specs/services/mention_matcher/mention_matcher.spec.js

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 558125df..e268ddaa 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -9,6 +9,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
 import { filter, find } from 'lodash'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
 
 const Status = {
   name: 'Status',
@@ -237,11 +238,23 @@ const Status = {
           return 'icon-globe'
       }
     },
-    linkClicked ({target}) {
+    linkClicked (event) {
+      let { target } = event
       if (target.tagName === 'SPAN') {
         target = target.parentNode
       }
       if (target.tagName === 'A') {
+        if (target.className.match(/mention/)) {
+          const href = target.getAttribute('href')
+          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
+          if (attn) {
+            event.stopPropagation()
+            event.preventDefault()
+            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
+            this.$router.push(link)
+            return
+          }
+        }
         window.open(target.href, '_blank')
       }
     },
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d88428c7..45100a46 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -24,9 +24,9 @@
 
       <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
         <div v-if="!noHeading" class="media-left">
-          <a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+          <router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
             <StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}"  :src="status.user.profile_image_url_original"/>
-          </a>
+          </router-link>
         </div>
         <div class="status-body">
           <div class="usercard media-body" v-if="userExpanded">
diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js
new file mode 100644
index 00000000..2c1ed970
--- /dev/null
+++ b/src/services/mention_matcher/mention_matcher.js
@@ -0,0 +1,9 @@
+
+export const mentionMatchesUrl = (attention, url) => {
+  if (url === attention.statusnet_profile_url) {
+    return true
+  }
+  const [namepart, instancepart] = attention.screen_name.split('@')
+  const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
+  return !!url.match(matchstring)
+}
diff --git a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js
new file mode 100644
index 00000000..4f6f58ff
--- /dev/null
+++ b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js
@@ -0,0 +1,63 @@
+import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js'
+
+const localAttn = () => ({
+  id: 123,
+  is_local: true,
+  name: 'Guy',
+  screen_name: 'person',
+  statusnet_profile_url: 'https://instance.com/users/person'
+})
+
+const externalAttn = () => ({
+  id: 123,
+  is_local: false,
+  name: 'Guy',
+  screen_name: 'person@instance.com',
+  statusnet_profile_url: 'https://instance.com/users/person'
+})
+
+describe('MentionMatcher', () => {
+  describe.only('mentionMatchesUrl', () => {
+    it('should match local mention', () => {
+      const attention = localAttn()
+      const url = 'https://instance.com/users/person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+    })
+
+    it('should not match a local mention with same name but different instance', () => {
+      const attention = localAttn()
+      const url = 'https://website.com/users/person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+    })
+
+    it('should match external pleroma mention', () => {
+      const attention = externalAttn()
+      const url = 'https://instance.com/users/person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+    })
+
+    it('should not match external pleroma mention with same name but different instance', () => {
+      const attention = externalAttn()
+      const url = 'https://website.com/users/person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+    })
+
+    it('should match external mastodon mention', () => {
+      const attention = externalAttn()
+      const url = 'https://instance.com/@person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+    })
+
+    it('should not match external mastodon mention with same name but different instance', () => {
+      const attention = externalAttn()
+      const url = 'https://website.com/@person'
+
+      expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+    })
+  })
+})