From 37e12e8d73e666b3ee07d409e28e03985a51baf4 Mon Sep 17 00:00:00 2001
From: Nolan Lawson <nolan@nolanlawson.com>
Date: Sun, 19 Aug 2018 18:03:26 -0700
Subject: [PATCH] add option to remove emoji from user display names (#450)

* add option to remove emoji from user display names

fixes #449

* slight memory perf improvement
---
 package-lock.json                             |  5 ++
 package.json                                  |  1 +
 .../profile/AccountDisplayName.html           | 16 +++++-
 routes/_pages/settings/general.html           |  5 ++
 routes/_store/store.js                        |  1 +
 routes/_utils/emojifyText.js                  | 12 ++++-
 routes/_utils/strings.js                      |  2 +-
 tests/spec/118-display-name-custom-emoji.js   | 53 ++++++++++++++++++-
 tests/utils.js                                |  2 +
 9 files changed, 92 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 2ca1470..0433bdf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4177,6 +4177,11 @@
         "minimalistic-crypto-utils": "^1.0.0"
       }
     },
+    "emoji-regex": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.0.tgz",
+      "integrity": "sha512-lnvttkzAlYW8WpFPiStPWyd/YdS02cFsYwXwWqnbKY43fMgUeUx+vzW1Zaozu34n4Fm7sxygi8+SEL6dcks/hQ=="
+    },
     "emojis-list": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
diff --git a/package.json b/package.json
index a22426f..28c1b97 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
     "chokidar": "^2.0.4",
     "cross-env": "^5.2.0",
     "css-loader": "^1.0.0",
+    "emoji-regex": "^7.0.0",
     "escape-html": "^1.0.3",
     "esm": "^3.0.77",
     "events": "^3.0.0",
diff --git a/routes/_components/profile/AccountDisplayName.html b/routes/_components/profile/AccountDisplayName.html
index 7a64502..40f25e6 100644
--- a/routes/_components/profile/AccountDisplayName.html
+++ b/routes/_components/profile/AccountDisplayName.html
@@ -5,17 +5,29 @@
   }
 </style>
 <script>
-  import { emojifyText } from '../../_utils/emojifyText'
+  import { emojifyText, removeEmoji } from '../../_utils/emojifyText'
   import { store } from '../../_store/store'
   import escapeHtml from 'escape-html'
+  import emojiRegex from 'emoji-regex'
+
+  let theEmojiRegex
 
   export default {
     store: () => store,
     computed: {
       emojis: ({ account }) => (account.emojis || []),
       accountName: ({ account }) => (account.display_name || account.username),
-      massagedAccountName: ({ accountName, emojis, $autoplayGifs }) => {
+      massagedAccountName: ({ accountName, emojis, $autoplayGifs, $omitEmojiInDisplayNames }) => {
         accountName = escapeHtml(accountName)
+
+        if ($omitEmojiInDisplayNames) { // display name emoji are annoying to some screenreader users
+          theEmojiRegex = theEmojiRegex || emojiRegex() // only init when needed
+          let emojiFreeAccountName = removeEmoji(accountName.replace(theEmojiRegex, ''), emojis).trim()
+          if (emojiFreeAccountName) {
+            return emojiFreeAccountName // only remove emoji if the resulting username is non-empty
+          }
+        }
+
         return emojifyText(accountName, emojis, $autoplayGifs)
       }
     }
diff --git a/routes/_pages/settings/general.html b/routes/_pages/settings/general.html
index 508565a..e98846e 100644
--- a/routes/_pages/settings/general.html
+++ b/routes/_pages/settings/general.html
@@ -18,6 +18,11 @@
              bind:checked="$reduceMotion" on:change="$save()">
       <label for="choice-reduce-motion">Reduce motion in UI animations</label>
     </div>
+    <div class="setting-group">
+      <input type="checkbox" id="choice-omit-emoji-in-display-names"
+             bind:checked="$omitEmojiInDisplayNames" on:change="$save()">
+      <label for="choice-omit-emoji-in-display-names">Remove emoji from user display names</label>
+    </div>
   </form>
 
 </SettingsLayout>
diff --git a/routes/_store/store.js b/routes/_store/store.js
index bccc71b..60c0578 100644
--- a/routes/_store/store.js
+++ b/routes/_store/store.js
@@ -15,6 +15,7 @@ const KEYS_TO_STORE_IN_LOCAL_STORAGE = new Set([
   'autoplayGifs',
   'markMediaAsSensitive',
   'reduceMotion',
+  'omitEmojiInDisplayNames',
   'pinnedPages',
   'composeData'
 ])
diff --git a/routes/_utils/emojifyText.js b/routes/_utils/emojifyText.js
index eb11baa..d203bba 100644
--- a/routes/_utils/emojifyText.js
+++ b/routes/_utils/emojifyText.js
@@ -1,7 +1,7 @@
 import { replaceAll } from './strings'
 
 export function emojifyText (text, emojis, autoplayGifs) {
-  if (emojis && emojis.length) {
+  if (emojis) {
     for (let emoji of emojis) {
       let urlToUse = autoplayGifs ? emoji.url : emoji.static_url
       let shortcodeWithColons = `:${emoji.shortcode}:`
@@ -15,3 +15,13 @@ export function emojifyText (text, emojis, autoplayGifs) {
   }
   return text
 }
+
+export function removeEmoji (text, emojis) {
+  if (emojis) {
+    for (let emoji of emojis) {
+      let shortcodeWithColons = `:${emoji.shortcode}:`
+      text = replaceAll(text, shortcodeWithColons, '')
+    }
+  }
+  return text
+}
diff --git a/routes/_utils/strings.js b/routes/_utils/strings.js
index dada260..c37790b 100644
--- a/routes/_utils/strings.js
+++ b/routes/_utils/strings.js
@@ -1,5 +1,5 @@
 export function replaceAll (string, replacee, replacement) {
-  if (!string.length || !replacee.length || !replacement.length) {
+  if (!string.length || !replacee.length) {
     return string
   }
   let idx
diff --git a/tests/spec/118-display-name-custom-emoji.js b/tests/spec/118-display-name-custom-emoji.js
index 5640039..ea10bad 100644
--- a/tests/spec/118-display-name-custom-emoji.js
+++ b/tests/spec/118-display-name-custom-emoji.js
@@ -1,5 +1,10 @@
 import { loginAsFoobar } from '../roles'
-import { displayNameInComposeBox, getNthStatusSelector, getUrl, sleep } from '../utils'
+import {
+  displayNameInComposeBox, generalSettingsButton, getNthStatusSelector, getUrl, homeNavButton,
+  removeEmojiFromDisplayNamesInput,
+  settingsNavButton,
+  sleep
+} from '../utils'
 import { updateUserDisplayNameAs } from '../serverActions'
 import { Selector as $ } from 'testcafe'
 
@@ -25,3 +30,49 @@ test('Cannot XSS using display name HTML', async t => {
   await t
     .expect(displayNameInComposeBox.innerText).eql('<script>alert("pwn")</script>')
 })
+
+test('Can remove emoji from user display names', async t => {
+  await updateUserDisplayNameAs('foobar', '🌈 foo :blobpats: 🌈')
+  await sleep(1000)
+  await loginAsFoobar(t)
+  await t
+    .expect(displayNameInComposeBox.innerText).eql('🌈 foo  🌈')
+    .expect($('.compose-box-display-name img').exists).ok()
+    .click(settingsNavButton)
+    .click(generalSettingsButton)
+    .click(removeEmojiFromDisplayNamesInput)
+    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
+    .click(homeNavButton)
+    .expect(displayNameInComposeBox.innerText).eql('foo')
+    .expect($('.compose-box-display-name img').exists).notOk()
+    .click(settingsNavButton)
+    .click(generalSettingsButton)
+    .click(removeEmojiFromDisplayNamesInput)
+    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
+    .click(homeNavButton)
+    .expect(displayNameInComposeBox.innerText).eql('🌈 foo  🌈')
+    .expect($('.compose-box-display-name img').exists).ok()
+})
+
+test('Cannot remove emoji from user display names if result would be empty', async t => {
+  await updateUserDisplayNameAs('foobar', '🌈 :blobpats: 🌈')
+  await sleep(1000)
+  await loginAsFoobar(t)
+  await t
+    .expect(displayNameInComposeBox.innerText).eql('🌈  🌈')
+    .expect($('.compose-box-display-name img').exists).ok()
+    .click(settingsNavButton)
+    .click(generalSettingsButton)
+    .click(removeEmojiFromDisplayNamesInput)
+    .expect(removeEmojiFromDisplayNamesInput.checked).ok()
+    .click(homeNavButton)
+    .expect(displayNameInComposeBox.innerText).eql('🌈  🌈')
+    .expect($('.compose-box-display-name img').exists).ok()
+    .click(settingsNavButton)
+    .click(generalSettingsButton)
+    .click(removeEmojiFromDisplayNamesInput)
+    .expect(removeEmojiFromDisplayNamesInput.checked).notOk()
+    .click(homeNavButton)
+    .expect(displayNameInComposeBox.innerText).eql('🌈  🌈')
+    .expect($('.compose-box-display-name img').exists).ok()
+})
diff --git a/tests/utils.js b/tests/utils.js
index 71e1031..836becd 100644
--- a/tests/utils.js
+++ b/tests/utils.js
@@ -41,6 +41,8 @@ export const followsButton = $('.account-profile-details > *:nth-child(2)')
 export const followersButton = $('.account-profile-details > *:nth-child(3)')
 export const avatarInComposeBox = $('.compose-box-avatar')
 export const displayNameInComposeBox = $('.compose-box-display-name')
+export const generalSettingsButton = $('a[href="/settings/general"]')
+export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
 
 export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({
   innerCount: el => parseInt(el.innerText, 10)