From 036882d27c22d768782c208a12625116ddc6ae7e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 12 Aug 2019 13:18:37 +0300
Subject: rename for consistency

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

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
new file mode 100644
index 00000000..70d08700
--- /dev/null
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -0,0 +1,161 @@
+<template>
+  <div class="emoji-dropdown-menu panel panel-default">
+    <div class="panel-heading emoji-tabs">
+      <span
+        v-for="(value, key) in emojis"
+        :key="key"
+        class="emoji-tabs-item"
+        :class="{'active': activeGroup === key}"
+        :title="value.text"
+        @click.prevent="highlight(key)"
+      >
+        <i :class="value.icon" />
+      </span>
+    </div>
+    <div class="panel-body emoji-dropdown-menu-content">
+      <div class="emoji-search">
+        <input
+          v-model="keyword"
+          type="text"
+          class="form-control"
+        >
+      </div>
+      <div
+        ref="emoji-groups"
+        class="emoji-groups"
+        @scroll="scrolledGroup"
+      >
+        <div
+          v-for="(value, key) in emojis"
+          :key="key"
+          class="emoji-group"
+        >
+          <h6
+            :ref="'group-' + key"
+            class="emoji-group-title"
+          >
+            {{ value.text }}
+          </h6>
+          <span
+            v-for="emoji in value.emojis"
+            :key="key + emoji.displayText"
+            :title="emoji.displayText"
+            class="emoji-item"
+            @click="onEmoji(emoji)"
+          >
+            <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
+            <img
+              v-else
+              :src="emoji.imageUrl"
+            >
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./emoji_picker.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.emoji {
+  &-dropdown {
+    position: absolute;
+    right: 0;
+    top: 28px;
+    z-index: 1;
+
+    &-toggle {
+      cursor: pointer;
+      position: absolute;
+      top: -23px;
+      right: 2px;
+      line-height: 1;
+
+      i {
+        font-size: 18px;
+      }
+    }
+
+    &-menu {
+      position: absolute;
+      z-index: 1;
+      right: 0;
+      width: 300px;
+      height: 300px;
+      display: flex;
+      flex-direction: column;
+      margin: 0 !important;
+
+      &-content {
+        flex: 1 1 1px;
+        display: flex;
+        flex-direction: column;
+      }
+    }
+  }
+
+  &-tabs {
+    &-item {
+      padding: 0 5px;
+
+      &.active {
+        border-bottom: 4px solid;
+
+        i {
+          color: $fallback--lightText;
+          color: var(--lightText, $fallback--lightText);
+        }
+      }
+    }
+  }
+
+  &-search {
+    padding: 5px;
+
+    input {
+      width: 100%;
+    }
+  }
+
+  &-groups {
+    flex: 1 1 1px;
+    overflow: auto;
+    position: relative;
+  }
+
+  &-group {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    width: 100%;
+
+    &-title {
+      font-size: 12px;
+      width: 100%;
+      margin: 0;
+      padding: 5px;
+    }
+  }
+
+  &-item {
+    width: 32px;
+    height: 32px;
+    box-sizing: border-box;
+    display: flex;
+    font-size: 32px;
+    align-items: center;
+    justify-content: center;
+    margin: 2px;
+    cursor: pointer;
+
+    img {
+      max-width: 100%;
+      max-height: 100%;
+    }
+  }
+
+}
+</style>
-- 
cgit v1.2.3-70-g09d2


From 579b5c9e77154db5fe1bc712969b6fa39830442f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 12 Aug 2019 14:20:08 +0300
Subject: initial attempts at making emoji-picker somewhat extensible

---
 src/components/emoji_picker/emoji_picker.js   |   6 +-
 src/components/emoji_picker/emoji_picker.scss |  83 +++++++++++
 src/components/emoji_picker/emoji_picker.vue  | 201 +++++++-------------------
 3 files changed, 144 insertions(+), 146 deletions(-)
 create mode 100644 src/components/emoji_picker/emoji_picker.scss

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 92d517b7..e25f98ff 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -6,7 +6,8 @@ const EmojiPicker = {
   data () {
     return {
       keyword: '',
-      activeGroup: 'standard'
+      activeGroup: 'standard',
+      showingAdditional: false
     }
   },
   methods: {
@@ -28,6 +29,9 @@ const EmojiPicker = {
           this.activeGroup = key
         }
       })
+    },
+    toggleAdditional (value) {
+      this.showingAdditional = value
     }
   },
   computed: {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
new file mode 100644
index 00000000..72889441
--- /dev/null
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -0,0 +1,83 @@
+@import '../../_variables.scss';
+
+.emoji-picker {
+  position: absolute;
+  z-index: 1;
+  right: 0;
+  width: 300px;
+  height: 300px;
+  display: flex;
+  flex-direction: column;
+  margin: 0 !important;
+
+  .emoji {
+    &-tabs {
+      &-item {
+        padding: 0 5px;
+
+        &.active {
+          border-bottom: 4px solid;
+
+          i {
+            color: $fallback--lightText;
+            color: var(--lightText, $fallback--lightText);
+          }
+        }
+      }
+    }
+
+    &-content {
+      display: flex;
+      flex-direction: column;
+    }
+
+    &-search {
+      padding: 5px;
+      flex: 0 0 1px;
+
+      input {
+        width: 100%;
+      }
+    }
+
+    &-groups {
+      flex: 1 1 1px;
+      position: relative;
+      overflow: auto;
+    }
+
+    &-group {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      padding: 5px;
+      justify-content: space-between;
+
+      &-title {
+        font-size: 12px;
+        width: 100%;
+        margin: 0;
+      }
+    }
+
+    &-item {
+      width: 32px;
+      height: 32px;
+      box-sizing: border-box;
+      display: flex;
+      font-size: 32px;
+      align-items: center;
+      justify-content: center;
+      margin: 2px;
+
+      cursor: pointer;
+
+      img {
+        max-width: 100%;
+        max-height: 100%;
+      }
+    }
+
+  }
+
+}
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 70d08700..ec1702f3 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,161 +1,72 @@
 <template>
-  <div class="emoji-dropdown-menu panel panel-default">
-    <div class="panel-heading emoji-tabs">
-      <span
-        v-for="(value, key) in emojis"
-        :key="key"
-        class="emoji-tabs-item"
-        :class="{'active': activeGroup === key}"
-        :title="value.text"
-        @click.prevent="highlight(key)"
-      >
-        <i :class="value.icon" />
+  <div class="emoji-picker panel panel-default">
+    <div class="panel-heading">
+      <span class="emoji-tabs">
+        <span
+          v-for="(value, key) in emojis"
+          :key="key"
+          class="emoji-tabs-item"
+          :class="{'active': activeGroup === key}"
+          :title="value.text"
+          @click.prevent="highlight(key)"
+        >
+          <i :class="value.icon" />
+        </span>
+      </span>
+      <span class="additional-tabs">
+        <slot name="tabs" />
       </span>
     </div>
     <div class="panel-body emoji-dropdown-menu-content">
-      <div class="emoji-search">
-        <input
-          v-model="keyword"
-          type="text"
-          class="form-control"
-        >
-      </div>
       <div
-        ref="emoji-groups"
-        class="emoji-groups"
-        @scroll="scrolledGroup"
+        v-if="!showingAdditional"
+        class="emoji-content"
       >
+        <div class="emoji-search">
+          <input
+            v-model="keyword"
+            type="text"
+            class="form-control"
+          >
+        </div>
         <div
-          v-for="(value, key) in emojis"
-          :key="key"
-          class="emoji-group"
+          ref="emoji-groups"
+          class="emoji-groups"
+          @scroll="scrolledGroup"
         >
-          <h6
-            :ref="'group-' + key"
-            class="emoji-group-title"
-          >
-            {{ value.text }}
-          </h6>
-          <span
-            v-for="emoji in value.emojis"
-            :key="key + emoji.displayText"
-            :title="emoji.displayText"
-            class="emoji-item"
-            @click="onEmoji(emoji)"
+          <div
+            v-for="(value, key) in emojis"
+            :key="key"
+            class="emoji-group"
           >
-            <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
-            <img
-              v-else
-              :src="emoji.imageUrl"
+            <h6
+              :ref="'group-' + key"
+              class="emoji-group-title"
             >
-          </span>
+              {{ value.text }}
+            </h6>
+            <span
+              v-for="emoji in value.emojis"
+              :key="key + emoji.displayText"
+              :title="emoji.displayText"
+              class="emoji-item"
+              @click="onEmoji(emoji)"
+            >
+              <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
+              <img
+                v-else
+                :src="emoji.imageUrl"
+              >
+            </span>
+          </div>
         </div>
       </div>
+      <div v-if="showingAdditional" class="additional-tabs-content">
+        <slot name="tab-content" />
+      </div>
     </div>
-  </div>
+</div>
 </template>
 
 <script src="./emoji_picker.js"></script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.emoji {
-  &-dropdown {
-    position: absolute;
-    right: 0;
-    top: 28px;
-    z-index: 1;
-
-    &-toggle {
-      cursor: pointer;
-      position: absolute;
-      top: -23px;
-      right: 2px;
-      line-height: 1;
-
-      i {
-        font-size: 18px;
-      }
-    }
-
-    &-menu {
-      position: absolute;
-      z-index: 1;
-      right: 0;
-      width: 300px;
-      height: 300px;
-      display: flex;
-      flex-direction: column;
-      margin: 0 !important;
-
-      &-content {
-        flex: 1 1 1px;
-        display: flex;
-        flex-direction: column;
-      }
-    }
-  }
-
-  &-tabs {
-    &-item {
-      padding: 0 5px;
-
-      &.active {
-        border-bottom: 4px solid;
-
-        i {
-          color: $fallback--lightText;
-          color: var(--lightText, $fallback--lightText);
-        }
-      }
-    }
-  }
-
-  &-search {
-    padding: 5px;
-
-    input {
-      width: 100%;
-    }
-  }
-
-  &-groups {
-    flex: 1 1 1px;
-    overflow: auto;
-    position: relative;
-  }
-
-  &-group {
-    display: flex;
-    align-items: center;
-    flex-wrap: wrap;
-    width: 100%;
-
-    &-title {
-      font-size: 12px;
-      width: 100%;
-      margin: 0;
-      padding: 5px;
-    }
-  }
-
-  &-item {
-    width: 32px;
-    height: 32px;
-    box-sizing: border-box;
-    display: flex;
-    font-size: 32px;
-    align-items: center;
-    justify-content: center;
-    margin: 2px;
-    cursor: pointer;
-
-    img {
-      max-width: 100%;
-      max-height: 100%;
-    }
-  }
-
-}
-</style>
+<style lang="scss" src="./emoji_picker.scss"></style>
-- 
cgit v1.2.3-70-g09d2


From 5851f97eb058b3e2df91f9122ba899bc7e4affaf Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 12 Aug 2019 20:01:38 +0300
Subject: fixed a lot of bugs with emoji picker, improved relevant components

---
 src/components/emoji_input/emoji_input.js          | 33 ++++++++-
 src/components/emoji_input/emoji_input.vue         | 16 ++---
 src/components/emoji_picker/emoji_picker.js        | 82 +++++++++++++++++-----
 src/components/emoji_picker/emoji_picker.scss      | 78 +++++++++++++++-----
 src/components/emoji_picker/emoji_picker.vue       | 60 ++++++++++------
 .../post_status_form/post_status_form.js           | 25 ++-----
 .../post_status_form/post_status_form.vue          | 26 ++++---
 src/components/sticker_picker/sticker_picker.js    |  4 +-
 src/components/sticker_picker/sticker_picker.vue   | 72 +++++++++----------
 src/components/tab_switcher/tab_switcher.js        | 26 ++++++-
 src/components/tab_switcher/tab_switcher.scss      | 11 +++
 src/i18n/en.json                                   |  9 ++-
 12 files changed, 296 insertions(+), 146 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5a9d1406..5ff27b20 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -58,6 +58,16 @@ const EmojiInput = {
       required: false,
       type: Boolean,
       default: false
+    },
+    emojiPickerExternalTrigger: {
+      required: false,
+      type: Boolean,
+      default: false
+    },
+    stickerPicker: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   data () {
@@ -95,9 +105,6 @@ const EmojiInput = {
     textAtCaret () {
       return (this.wordAtCaret || {}).word || ''
     },
-    pickerIconBottom () {
-      return this.input && this.input.tag === 'textarea'
-    },
     wordAtCaret () {
       if (this.value && this.caret) {
         const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
@@ -133,6 +140,9 @@ const EmojiInput = {
     }
   },
   methods: {
+    triggerShowPicker () {
+      this.showPicker = true
+    },
     togglePicker () {
       this.showPicker = !this.showPicker
     },
@@ -148,6 +158,15 @@ const EmojiInput = {
         this.value.substring(this.caret)
       ].join('')
       this.$emit('input', newValue)
+      const position = this.caret + insertion.length
+
+      this.$nextTick(function () {
+        // Re-focus inputbox after clicking suggestion
+        this.input.elm.focus()
+        // Set selection right after the replacement instead of the very end
+        this.input.elm.setSelectionRange(position, position)
+        this.caret = position
+      })
     },
     replaceText (e, suggestion) {
       const len = this.suggestions.length || 0
@@ -264,6 +283,14 @@ const EmojiInput = {
     onClickOutside () {
       this.showPicker = false
     },
+    onStickerUploaded (e) {
+      this.showPicker = false
+      this.$emit('sticker-uploaded', e)
+    },
+    onStickerUploadFailed (e) {
+      this.showPicker = false
+      this.$emit('sticker-upload-Failed', e)
+    },
     setCaret ({ target: { selectionStart } }) {
       this.caret = selectionStart
     },
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 3ca12af1..b077e6e9 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -6,8 +6,8 @@
     <slot />
     <template v-if="emojiPicker">
       <div
+        v-if="!emojiPickerExternalTrigger"
         class="emoji-picker-icon"
-        :class="pickerIconBottom ? 'picker-icon-bottom': 'picker-icon-right'"
         @click.prevent="togglePicker"
       >
         <i class="icon-smile" />
@@ -16,8 +16,11 @@
         v-if="emojiPicker"
         ref="picker"
         :class="{ hide: !showPicker }"
+        :sticker-picker="stickerPicker"
         class="emoji-picker-panel"
         @emoji="insert"
+        @sticker-uploaded="onStickerUploaded"
+        @sticker-upload-failed="onStickerUploadFailed"
       />
     </template>
     <div
@@ -62,6 +65,8 @@
 
   .emoji-picker-icon {
     position: absolute;
+    top: 0;
+    right: 0;
     margin: 0 .25em;
     font-size: 16px;
     cursor: pointer;
@@ -70,15 +75,6 @@
       color: $fallback--text;
       color: var(--text, $fallback--text);
     }
-
-    &.picker-icon-bottom {
-      bottom: 0;
-      left: 0;
-    }
-    &.picker-icon-right {
-      top: 0;
-      right: 0;
-    }
   }
   .emoji-picker-panel {
     position: absolute;
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index e25f98ff..0a64f759 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,15 +1,26 @@
+
 const filterByKeyword = (list, keyword = '') => {
   return list.filter(x => x.displayText.includes(keyword))
 }
 
 const EmojiPicker = {
+  props: {
+    stickerPicker: {
+      required: false,
+      type: Boolean,
+      default: false
+    }
+  },
   data () {
     return {
       keyword: '',
-      activeGroup: 'standard',
-      showingAdditional: false
+      activeGroup: 'custom',
+      showingStickers: false
     }
   },
+  components: {
+    StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
+  },
   methods: {
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
@@ -19,37 +30,72 @@ const EmojiPicker = {
     highlight (key) {
       const ref = this.$refs['group-' + key]
       const top = ref[0].offsetTop
-      this.$refs['emoji-groups'].scrollTop = top + 1
+      this.setShowStickers(false)
       this.activeGroup = key
+      this.$nextTick(() => {
+        this.$refs['emoji-groups'].scrollTop = top + 1
+      })
     },
     scrolledGroup (e) {
-      const top = e.target.scrollTop
-      Object.keys(this.emojis).forEach(key => {
-        if (this.$refs['group-' + key][0].offsetTop < top) {
-          this.activeGroup = key
-        }
+      const target = (e && e.target) || this.$refs['emoji-groups']
+      const top = target.scrollTop + 5
+      this.$nextTick(() => {
+        this.emojisView.forEach(group => {
+          const ref = this.$refs['group-' + group.id]
+          if (ref[0].offsetTop <= top) {
+            this.activeGroup = group.id
+          }
+        })
       })
     },
-    toggleAdditional (value) {
-      this.showingAdditional = value
+    toggleStickers () {
+      this.showingStickers = !this.showingStickers
+    },
+    setShowStickers (value) {
+      this.showingStickers = value
+    },
+    onStickerUploaded (e) {
+      this.$emit('sticker-uploaded', e)
+    },
+    onStickerUploadFailed (e) {
+      this.$emit('sticker-upload-failed', e)
+    }
+  },
+  watch: {
+    keyword () {
+      this.scrolledGroup()
     }
   },
   computed: {
+    activeGroupView () {
+      return this.showingStickers ? '' : this.activeGroup
+    },
+    stickersAvailable () {
+      if (this.$store.state.instance.stickers) {
+        return this.$store.state.instance.stickers.length > 0
+      }
+      return 0
+    },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
       const customEmojis = this.$store.state.instance.customEmoji || []
-      return {
-        custom: {
-          text: 'Custom',
-          icon: 'icon-picture',
+      return [
+        {
+          id: 'custom',
+          text: this.$t('emoji.custom'),
+          icon: 'icon-smile',
           emojis: filterByKeyword(customEmojis, this.keyword)
         },
-        standard: {
-          text: 'Standard',
-          icon: 'icon-star',
+        {
+          id: 'standard',
+          text: this.$t('emoji.unicode'),
+          icon: 'icon-picture',
           emojis: filterByKeyword(standardEmojis, this.keyword)
         }
-      }
+      ]
+    },
+    emojisView () {
+      return this.emojis.filter(value => value.emojis.length > 0)
     }
   }
 }
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 72889441..6c13e82b 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -1,39 +1,78 @@
 @import '../../_variables.scss';
 
 .emoji-picker {
+  display: flex;
+  flex-direction: column;
   position: absolute;
-  z-index: 1;
   right: 0;
-  width: 300px;
+  left: 0;
   height: 300px;
-  display: flex;
-  flex-direction: column;
   margin: 0 !important;
+  z-index: 1;
 
-  .emoji {
-    &-tabs {
-      &-item {
-        padding: 0 5px;
+  .panel-body {
+    display: flex;
+    flex-direction: column;
+    flex: 1 1 0;
+    min-height: 0px;
+  }
+
+  .additional-tabs {
+    border-left: 1px solid;
+    border-left-color: $fallback--icon;
+    border-left-color: var(--icon, $fallback--icon);
+    padding-left: 5px;
+    flex: 0 0 0;
+  }
+
+  .emoji-tabs {
+    flex: 1 1 0;
+  }
+
+  .additional-tabs,
+  .emoji-tabs {
+    &-item {
+      padding: 0 5px;
+      cursor: pointer;
+      font-size: 24px;
 
-        &.active {
-          border-bottom: 4px solid;
+      &.disabled {
+        opacity: 0.5;
+        pointer-events: none;
+      }
+      &.active {
+        border-bottom: 4px solid;
 
-          i {
-            color: $fallback--lightText;
-            color: var(--lightText, $fallback--lightText);
-          }
+        i {
+          color: $fallback--lightText;
+          color: var(--lightText, $fallback--lightText);
         }
       }
     }
+  }
 
+  .sticker-picker {
+    flex: 1 1 0
+  }
+
+  .stickers,
+  .emoji {
     &-content {
       display: flex;
       flex-direction: column;
+      flex: 1 1 0;
+      min-height: 0;
+
+      &.hidden {
+        display: none
+      }
     }
+  }
 
+  .emoji {
     &-search {
       padding: 5px;
-      flex: 0 0 1px;
+      flex: 0 0 0;
 
       input {
         width: 100%;
@@ -50,13 +89,16 @@
       display: flex;
       align-items: center;
       flex-wrap: wrap;
-      padding: 5px;
-      justify-content: space-between;
+      padding-left: 5px;
+      justify-content: left;
 
       &-title {
         font-size: 12px;
         width: 100%;
         margin: 0;
+        &.disabled {
+          display: none;
+        }
       }
     }
 
@@ -68,7 +110,7 @@
       font-size: 32px;
       align-items: center;
       justify-content: center;
-      margin: 2px;
+      margin: 4px;
 
       cursor: pointer;
 
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index ec1702f3..12b1569e 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,30 +3,44 @@
     <div class="panel-heading">
       <span class="emoji-tabs">
         <span
-          v-for="(value, key) in emojis"
-          :key="key"
+          v-for="group in emojis"
+          :key="group.id"
           class="emoji-tabs-item"
-          :class="{'active': activeGroup === key}"
-          :title="value.text"
-          @click.prevent="highlight(key)"
+          :class="{
+            active: activeGroupView === group.id,
+            disabled: group.emojis.length === 0
+          }"
+          :title="group.text"
+          @click.prevent="highlight(group.id)"
         >
-          <i :class="value.icon" />
+          <i :class="group.icon" />
         </span>
       </span>
-      <span class="additional-tabs">
-        <slot name="tabs" />
+      <span
+        v-if="stickerPicker"
+        class="additional-tabs"
+      >
+        <span
+          class="stickers-tab-icon additional-tabs-item"
+          :class="{active: showingStickers}"
+          :title="$t('emoji.stickers')"
+          @click.prevent="toggleStickers"
+        >
+          <i class="icon-star" />
+        </span>
       </span>
     </div>
-    <div class="panel-body emoji-dropdown-menu-content">
+    <div class="panel-body">
       <div
-        v-if="!showingAdditional"
         class="emoji-content"
+        :class="{hidden: showingStickers}"
       >
         <div class="emoji-search">
           <input
             v-model="keyword"
             type="text"
             class="form-control"
+            :placeholder="$t('emoji.search_emoji')"
           >
         </div>
         <div
@@ -35,22 +49,22 @@
           @scroll="scrolledGroup"
         >
           <div
-            v-for="(value, key) in emojis"
-            :key="key"
+            v-for="group in emojisView"
+            :key="group.id"
             class="emoji-group"
           >
             <h6
-              :ref="'group-' + key"
+              :ref="'group-' + group.id"
               class="emoji-group-title"
             >
-              {{ value.text }}
+              {{ group.text }}
             </h6>
             <span
-              v-for="emoji in value.emojis"
-              :key="key + emoji.displayText"
+              v-for="emoji in group.emojis"
+              :key="group.id + emoji.displayText"
               :title="emoji.displayText"
               class="emoji-item"
-              @click="onEmoji(emoji)"
+              @click.stop.prevent="onEmoji(emoji)"
             >
               <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
               <img
@@ -61,11 +75,17 @@
           </div>
         </div>
       </div>
-      <div v-if="showingAdditional" class="additional-tabs-content">
-        <slot name="tab-content" />
+      <div
+        v-if="showingStickers"
+        class="stickers-content"
+      >
+        <sticker-picker
+          @uploaded="onStickerUploaded"
+          @upload-failed="onStickerUploadFailed"
+        />
       </div>
     </div>
-</div>
+  </div>
 </template>
 
 <script src="./emoji_picker.js"></script>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index f646aeb5..1359e75a 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -3,7 +3,6 @@ import MediaUpload from '../media_upload/media_upload.vue'
 import ScopeSelector from '../scope_selector/scope_selector.vue'
 import EmojiInput from '../emoji_input/emoji_input.vue'
 import PollForm from '../poll/poll_form.vue'
-import StickerPicker from '../sticker_picker/sticker_picker.vue'
 import fileTypeService from '../../services/file_type/file_type.service.js'
 import { reject, map, uniqBy } from 'lodash'
 import suggestor from '../emoji_input/suggestor.js'
@@ -35,7 +34,6 @@ const PostStatusForm = {
     MediaUpload,
     EmojiInput,
     PollForm,
-    StickerPicker,
     ScopeSelector
   },
   mounted () {
@@ -84,8 +82,7 @@ const PostStatusForm = {
         contentType
       },
       caret: 0,
-      pollFormVisible: false,
-      stickerPickerVisible: false
+      pollFormVisible: false
     }
   },
   computed: {
@@ -161,12 +158,6 @@ const PostStatusForm = {
     safeDMEnabled () {
       return this.$store.state.instance.safeDM
     },
-    stickersAvailable () {
-      if (this.$store.state.instance.stickers) {
-        return this.$store.state.instance.stickers.length > 0
-      }
-      return 0
-    },
     pollsAvailable () {
       return this.$store.state.instance.pollsAvailable &&
         this.$store.state.instance.pollLimits.max_options >= 2
@@ -222,7 +213,6 @@ const PostStatusForm = {
             poll: {}
           }
           this.pollFormVisible = false
-          this.stickerPickerVisible = false
           this.$refs.mediaUpload.clearFile()
           this.clearPollForm()
           this.$emit('posted')
@@ -239,7 +229,6 @@ const PostStatusForm = {
     addMediaFile (fileInfo) {
       this.newStatus.files.push(fileInfo)
       this.enableSubmit()
-      this.stickerPickerVisible = false
     },
     removeMediaFile (fileInfo) {
       let index = this.newStatus.files.indexOf(fileInfo)
@@ -293,20 +282,16 @@ const PostStatusForm = {
         target.style.height = null
       }
     },
+    showEmoji () {
+      this.$refs['textarea'].focus()
+      this.$refs['emoji-input'].triggerShowPicker()
+    },
     clearError () {
       this.error = null
     },
     changeVis (visibility) {
       this.newStatus.visibility = visibility
     },
-    toggleStickerPicker () {
-      this.stickerPickerVisible = !this.stickerPickerVisible
-    },
-    clearStickerPicker () {
-      if (this.$refs.stickerPicker) {
-        this.$refs.stickerPicker.clear()
-      }
-    },
     togglePollForm () {
       this.pollFormVisible = !this.pollFormVisible
     },
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index e691acad..ad2c2218 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -74,10 +74,15 @@
           >
         </EmojiInput>
         <EmojiInput
+          ref="emoji-input"
           v-model="newStatus.status"
           :suggest="emojiUserSuggestor"
-          emoji-picker
           class="form-control main-input"
+          emoji-picker
+          emoji-picker-external-trigger
+          sticker-picker
+          @sticker-uploaded="addMediaFile"
+          @sticker-upload-failed="uploadFailed"
         >
           <textarea
             ref="textarea"
@@ -160,14 +165,12 @@
             @upload-failed="uploadFailed"
           />
           <div
-            v-if="stickersAvailable"
-            class="sticker-icon"
+            class="emoji-icon"
           >
             <i
-              :title="$t('stickers.add_sticker')"
-              class="icon-picture btn btn-default"
-              :class="{ selected: stickerPickerVisible }"
-              @click="toggleStickerPicker"
+              :title="$t('emoji.add_emoji')"
+              class="icon-smile btn btn-default"
+              @click.stop.prevent="showEmoji"
             />
           </div>
           <div
@@ -260,11 +263,6 @@
         <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
       </div>
     </form>
-    <sticker-picker
-      v-if="stickerPickerVisible"
-      ref="stickerPicker"
-      @uploaded="addMediaFile"
-    />
   </div>
 </template>
 
@@ -327,7 +325,7 @@
     }
   }
 
-  .poll-icon, .sticker-icon {
+  .poll-icon, .emoji-icon {
     font-size: 26px;
     flex: 1;
 
@@ -337,7 +335,7 @@
     }
   }
 
-  .sticker-icon {
+  .emoji-icon {
     flex: 0;
     min-width: 50px;
   }
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
index a6dcded3..8daf3f07 100644
--- a/src/components/sticker_picker/sticker_picker.js
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -3,9 +3,9 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
 
 const StickerPicker = {
-  components: [
+  components: {
     TabSwitcher
-  ],
+  },
   data () {
     return {
       meta: {
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
index 938204c8..323855b9 100644
--- a/src/components/sticker_picker/sticker_picker.vue
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -2,32 +2,30 @@
   <div
     class="sticker-picker"
   >
-    <div
-      class="sticker-picker-panel"
+    <tab-switcher
+      class="tab-switcher"
+      :render-only-focused="true"
+      scrollable-tabs
     >
-      <tab-switcher
-        :render-only-focused="true"
+      <div
+        v-for="stickerpack in pack"
+        :key="stickerpack.path"
+        :image-tooltip="stickerpack.meta.title"
+        :image="stickerpack.path + stickerpack.meta.tabIcon"
+        class="sticker-picker-content"
       >
         <div
-          v-for="stickerpack in pack"
-          :key="stickerpack.path"
-          :image-tooltip="stickerpack.meta.title"
-          :image="stickerpack.path + stickerpack.meta.tabIcon"
-          class="sticker-picker-content"
+          v-for="sticker in stickerpack.meta.stickers"
+          :key="sticker"
+          class="sticker"
+          @click.stop.prevent="pick(stickerpack.path + sticker, stickerpack.meta.title)"
         >
-          <div
-            v-for="sticker in stickerpack.meta.stickers"
-            :key="sticker"
-            class="sticker"
-            @click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
+          <img
+            :src="stickerpack.path + sticker"
           >
-            <img
-              :src="stickerpack.path + sticker"
-            >
-          </div>
         </div>
-      </tab-switcher>
-    </div>
+      </div>
+    </tab-switcher>
   </div>
 </template>
 
@@ -37,22 +35,24 @@
 @import '../../_variables.scss';
 
 .sticker-picker {
-  .sticker-picker-panel {
-    display: inline-block;
-    width: 100%;
-    .sticker-picker-content {
-      max-height: 300px;
-      overflow-y: scroll;
-      overflow-x: auto;
-      .sticker {
-        display: inline-block;
-        width: 20%;
-        height: 20%;
-        img {
-          width: 100%;
-          &:hover {
-            filter: drop-shadow(0 0 5px var(--link, $fallback--link));
-          }
+  width: 100%;
+  position: relative;
+  .tab-switcher {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+  }
+  .sticker-picker-content {
+    .sticker {
+      display: inline-block;
+      width: 20%;
+      height: 20%;
+      img {
+        width: 100%;
+        &:hover {
+          filter: drop-shadow(0 0 5px var(--link, $fallback--link));
         }
       }
     }
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index a5fe019c..99428044 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,7 +4,26 @@ import './tab_switcher.scss'
 
 export default Vue.component('tab-switcher', {
   name: 'TabSwitcher',
-  props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
+  props: {
+    renderOnlyFocused: {
+      required: false,
+      type: Boolean,
+      default: false
+    },
+    onSwitch: {
+      required: false,
+      type: Function
+    },
+    customActive: {
+      required: false,
+      type: String
+    },
+    scrollableTabs: {
+      required: false,
+      type: Boolean,
+      default: false
+    }
+  },
   data () {
     return {
       active: this.$slots.default.findIndex(_ => _.tag)
@@ -18,7 +37,8 @@ export default Vue.component('tab-switcher', {
   },
   methods: {
     activateTab (index, dataset) {
-      return () => {
+      return (e) => {
+        e.preventDefault()
         if (typeof this.onSwitch === 'function') {
           this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
         }
@@ -85,7 +105,7 @@ export default Vue.component('tab-switcher', {
         <div class="tabs">
           {tabs}
         </div>
-        <div class="contents">
+        <div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
           {contents}
         </div>
       </div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 4eeb42e0..3e5eacd5 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -1,10 +1,21 @@
 @import '../../_variables.scss';
 
 .tab-switcher {
+  display: flex;
+  flex-direction: column;
+
   .contents {
+    flex: 1 0 auto;
+    min-height: 0px;
+
     .hidden {
       display: none;
     }
+
+    &.scrollable-tabs {
+      flex-basis: 0;
+      overflow-y: auto;
+    }
   }
   .tabs {
     display: flex;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 60a3e284..13f7168f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,8 +106,13 @@
     "expired": "Poll ended {0} ago",
     "not_enough_options": "Too few unique options in poll"
   },
-  "stickers": {
-    "add_sticker": "Add Sticker"
+  "emoji": {
+    "stickers": "Stickers",
+    "emoji": "Emoji",
+    "search_emoji": "Search for an emoji",
+    "add_emoji": "Insert emoji",
+    "custom": "Custom emoji",
+    "unicode": "Unicode emoji"
   },
   "interactions": {
     "favs_repeats": "Repeats and Favorites",
-- 
cgit v1.2.3-70-g09d2


From 14df84d89bfc58f564697b7d72d7c66134c64697 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 8 Sep 2019 15:51:17 +0300
Subject: fixed some bugs, added spam mode, minor collateral fixes

---
 src/components/emoji_input/emoji_input.js          | 28 +++++++++++++-----
 src/components/emoji_picker/emoji_picker.js        |  7 +++--
 src/components/emoji_picker/emoji_picker.scss      | 33 +++++++++++++++++-----
 src/components/emoji_picker/emoji_picker.vue       | 16 +++++++++--
 .../post_status_form/post_status_form.js           |  2 +-
 .../post_status_form/post_status_form.vue          |  2 +-
 src/components/status/status.vue                   |  2 +-
 src/components/user_panel/user_panel.vue           |  2 +-
 src/i18n/en.json                                   |  1 +
 9 files changed, 69 insertions(+), 24 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5ff27b20..b1640753 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -77,7 +77,9 @@ const EmojiInput = {
       caret: 0,
       focused: false,
       blurTimeout: null,
-      showPicker: false
+      showPicker: false,
+      spamMode: false,
+      disableClickOutside: false
     }
   },
   components: {
@@ -100,7 +102,7 @@ const EmojiInput = {
         }))
     },
     showSuggestions () {
-      return this.focused && this.suggestions && this.suggestions.length > 0
+      return this.focused && this.suggestions && this.suggestions.length > 0 && !this.showPicker
     },
     textAtCaret () {
       return (this.wordAtCaret || {}).word || ''
@@ -142,6 +144,13 @@ const EmojiInput = {
   methods: {
     triggerShowPicker () {
       this.showPicker = true
+      // This temporarily disables "click outside" handler
+      // since external trigger also means click originates
+      // from outside, thus preventing picker from opening
+      this.disableClickOutside = true
+      setTimeout(() => {
+        this.disableClickOutside = false
+      }, 0)
     },
     togglePicker () {
       this.showPicker = !this.showPicker
@@ -151,12 +160,13 @@ const EmojiInput = {
       this.$emit('input', newValue)
       this.caret = 0
     },
-    insert (insertion) {
+    insert ({ insertion, spamMode }) {
       const newValue = [
         this.value.substring(0, this.caret),
         insertion,
         this.value.substring(this.caret)
       ].join('')
+      this.spamMode = spamMode
       this.$emit('input', newValue)
       const position = this.caret + insertion.length
 
@@ -191,7 +201,7 @@ const EmojiInput = {
     },
     cycleBackward (e) {
       const len = this.suggestions.length || 0
-      if (len > 0) {
+      if (len > 1) {
         this.highlighted -= 1
         if (this.highlighted < 0) {
           this.highlighted = this.suggestions.length - 1
@@ -203,7 +213,7 @@ const EmojiInput = {
     },
     cycleForward (e) {
       const len = this.suggestions.length || 0
-      if (len > 0) {
+      if (len > 1) {
         this.highlighted += 1
         if (this.highlighted >= len) {
           this.highlighted = 0
@@ -234,7 +244,10 @@ const EmojiInput = {
         this.blurTimeout = null
       }
 
-      this.showPicker = false
+      console.log(this.spamMode)
+      if (!this.spamMode) {
+        this.showPicker = false
+      }
       this.focused = true
       this.setCaret(e)
       this.resize()
@@ -280,7 +293,8 @@ const EmojiInput = {
       this.resize()
       this.$emit('input', e.target.value)
     },
-    onClickOutside () {
+    onClickOutside (e) {
+      if (this.disableClickOutside) return
       this.showPicker = false
     },
     onStickerUploaded (e) {
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 0a64f759..bce5026e 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -13,9 +13,11 @@ const EmojiPicker = {
   },
   data () {
     return {
+      labelKey: String(Math.random() * 100000),
       keyword: '',
       activeGroup: 'custom',
-      showingStickers: false
+      showingStickers: false,
+      spamMode: false
     }
   },
   components: {
@@ -24,8 +26,7 @@ const EmojiPicker = {
   methods: {
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
-      this.$emit('emoji', ` ${value} `)
-      this.open = false
+      this.$emit('emoji', { insertion: ` ${value} `, spamMode: this.spamMode })
     },
     highlight (key) {
       const ref = this.$refs['group-' + key]
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 6c13e82b..079eb362 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -10,29 +10,48 @@
   margin: 0 !important;
   z-index: 1;
 
-  .panel-body {
+  .spam-mode {
+    padding: 7px;
+    line-height: normal;
+  }
+  .spam-mode-label {
+    padding: 7px;
+  }
+
+  .heading {
+    display: flex;
+    height: 32px;
+    padding: 10px 7px 5px;
+  }
+
+  .content {
     display: flex;
     flex-direction: column;
     flex: 1 1 0;
     min-height: 0px;
   }
 
+  .emoji-tabs {
+    flex-grow: 1;
+  }
+
   .additional-tabs {
     border-left: 1px solid;
     border-left-color: $fallback--icon;
     border-left-color: var(--icon, $fallback--icon);
-    padding-left: 5px;
+    padding-left: 7px;
     flex: 0 0 0;
   }
 
-  .emoji-tabs {
-    flex: 1 1 0;
-  }
-
   .additional-tabs,
   .emoji-tabs {
+    display: block;
+    min-width: 0;
+    flex-basis: auto;
+    flex-shrink: 1;
+
     &-item {
-      padding: 0 5px;
+      padding: 0 7px;
       cursor: pointer;
       font-size: 24px;
 
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 12b1569e..901520aa 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,6 +1,6 @@
 <template>
-  <div class="emoji-picker panel panel-default">
-    <div class="panel-heading">
+  <div class="emoji-picker panel panel-default panel-body">
+    <div class="heading">
       <span class="emoji-tabs">
         <span
           v-for="group in emojis"
@@ -30,7 +30,7 @@
         </span>
       </span>
     </div>
-    <div class="panel-body">
+    <div class="content">
       <div
         class="emoji-content"
         :class="{hidden: showingStickers}"
@@ -74,6 +74,16 @@
             </span>
           </div>
         </div>
+        <div
+          class="spam-mode"
+          >
+          <input
+            :id="labelKey + 'spam-mode'"
+            v-model="spamMode"
+            type="checkbox"
+            >
+          <label class="spam-mode-label" :for="labelKey + 'spam-mode'">{{ $t('emoji.spam') }}</label>
+        </div>
       </div>
       <div
         v-if="showingStickers"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1359e75a..d468be76 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -282,7 +282,7 @@ const PostStatusForm = {
         target.style.height = null
       }
     },
-    showEmoji () {
+    showEmojiPicker () {
       this.$refs['textarea'].focus()
       this.$refs['emoji-input'].triggerShowPicker()
     },
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index ad2c2218..026cb8fe 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -170,7 +170,7 @@
             <i
               :title="$t('emoji.add_emoji')"
               class="icon-smile btn btn-default"
-              @click.stop.prevent="showEmoji"
+              @click="showEmojiPicker"
             />
           </div>
           <div
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 64218f6e..771615f3 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -413,7 +413,7 @@
         v-if="replying"
         class="container"
       >
-        <post-status-form
+        <PostStatusForm
           class="reply-body"
           :reply-to="status.id"
           :attentions="status.attentions"
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index c92630e3..5cdb2914 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -11,7 +11,7 @@
         rounded="top"
       />
       <div class="panel-footer">
-        <post-status-form v-if="user" />
+        <PostStatusForm v-if="user" />
       </div>
     </div>
     <auth-form
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 12920b92..e74469ed 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -109,6 +109,7 @@
   "emoji": {
     "stickers": "Stickers",
     "emoji": "Emoji",
+    "spam": "Keep open after adding emoji",
     "search_emoji": "Search for an emoji",
     "add_emoji": "Insert emoji",
     "custom": "Custom emoji",
-- 
cgit v1.2.3-70-g09d2


From 83f45167b61d5f10f00ea8e3e3e946c72e5fc33f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 8 Sep 2019 16:56:54 +0300
Subject: added emoji zoom for picker

---
 src/components/emoji_picker/emoji_picker.js   |  8 ++++++++
 src/components/emoji_picker/emoji_picker.scss | 18 ++++++++++++++++++
 src/components/emoji_picker/emoji_picker.vue  | 22 +++++++++++++++++++---
 3 files changed, 45 insertions(+), 3 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index bce5026e..03870a99 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -17,6 +17,7 @@ const EmojiPicker = {
       keyword: '',
       activeGroup: 'custom',
       showingStickers: false,
+      zoomEmoji: false,
       spamMode: false
     }
   },
@@ -60,6 +61,13 @@ const EmojiPicker = {
     },
     onStickerUploadFailed (e) {
       this.$emit('sticker-upload-failed', e)
+    },
+    setZoomEmoji (e, emoji) {
+      this.zoomEmoji = emoji
+      const { x, y } = e.target.getBoundingClientRect()
+      console.log(e.target)
+      this.$refs['zoom-portal'].style.left = (x - 32) + 'px'
+      this.$refs['zoom-portal'].style.top = (y - 32) + 'px'
     }
   },
   watch: {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index e3a83bdd..872af2de 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -10,6 +10,20 @@
   margin: 0 !important;
   z-index: 1;
 
+  .zoom-portal {
+    position: fixed;
+    pointer-events: none;
+    width: 96px;
+    height: 96px;
+    font-size: 96px;
+    line-height: 96px;
+    z-index: 10;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
   .spam-mode {
     padding: 7px;
     line-height: normal;
@@ -135,6 +149,10 @@
 
       cursor: pointer;
 
+      &:hover {
+        opacity: 0
+      }
+
       img {
         max-width: 100%;
         max-height: 100%;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 901520aa..5a8961d2 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -65,12 +65,16 @@
               :title="emoji.displayText"
               class="emoji-item"
               @click.stop.prevent="onEmoji(emoji)"
-            >
-              <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
+              @mouseenter="setZoomEmoji($event, emoji)"
+              @mouseleave="setZoomEmoji($event, false)"
+              >
+              <span v-if="!emoji.imageUrl">
+                {{ emoji.replacement }}
+              </span>
               <img
                 v-else
                 :src="emoji.imageUrl"
-              >
+                >
             </span>
           </div>
         </div>
@@ -95,6 +99,18 @@
         />
       </div>
     </div>
+    <div ref="zoom-portal" class="zoom-portal">
+      <span v-if="zoomEmoji">
+        <span v-if="!zoomEmoji.imageUrl">
+          {{ zoomEmoji.replacement }}
+        </span>
+        <img
+          v-else
+          :key="zoomEmoji.imageUrl"
+          :src="zoomEmoji.imageUrl"
+          >
+      </span>
+    </div>
   </div>
 </template>
 
-- 
cgit v1.2.3-70-g09d2


From 4f0195b05c3124104b3e09d4b9d1f73c9d74440a Mon Sep 17 00:00:00 2001
From: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Tue, 10 Sep 2019 18:39:45 +0000
Subject: Apply suggestion to src/components/emoji_picker/emoji_picker.vue

---
 src/components/emoji_picker/emoji_picker.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 5a8961d2..1c91c8b7 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -67,7 +67,7 @@
               @click.stop.prevent="onEmoji(emoji)"
               @mouseenter="setZoomEmoji($event, emoji)"
               @mouseleave="setZoomEmoji($event, false)"
-              >
+            >
               <span v-if="!emoji.imageUrl">
                 {{ emoji.replacement }}
               </span>
-- 
cgit v1.2.3-70-g09d2


From 0d8b68632b02565e5ba7a833e91e41daabb4a1dc Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 12 Sep 2019 19:15:22 +0300
Subject: Remove emoji zoom

---
 src/components/emoji_picker/emoji_picker.js          |  7 -------
 src/components/emoji_picker/emoji_picker.scss        | 19 -------------------
 src/components/emoji_picker/emoji_picker.vue         | 20 ++------------------
 src/components/status/status.vue                     | 19 -------------------
 .../entity_normalizer/entity_normalizer.service.js   |  2 +-
 .../entity_normalizer/entity_normalizer.spec.js      |  4 ++--
 6 files changed, 5 insertions(+), 66 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index d9d41bf6..bce5026e 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -17,7 +17,6 @@ const EmojiPicker = {
       keyword: '',
       activeGroup: 'custom',
       showingStickers: false,
-      zoomEmoji: false,
       spamMode: false
     }
   },
@@ -61,12 +60,6 @@ const EmojiPicker = {
     },
     onStickerUploadFailed (e) {
       this.$emit('sticker-upload-failed', e)
-    },
-    setZoomEmoji (e, emoji) {
-      this.zoomEmoji = emoji
-      const { x, y } = e.target.getBoundingClientRect()
-      this.$refs['zoom-portal'].style.left = (x - 32) + 'px'
-      this.$refs['zoom-portal'].style.top = (y - 32) + 'px'
     }
   },
   watch: {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 7e76aa52..472db35b 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -10,21 +10,6 @@
   margin: 0 !important;
   z-index: 1;
 
-  .zoom-portal {
-    position: fixed;
-    pointer-events: none;
-    width: 96px;
-    height: 96px;
-    font-size: 96px;
-    line-height: 96px;
-    z-index: 10;
-    img {
-      object-fit: contain;
-      width: 100%;
-      height: 100%;
-    }
-  }
-
   .spam-mode {
     padding: 7px;
     line-height: normal;
@@ -150,10 +135,6 @@
 
       cursor: pointer;
 
-      &:hover {
-        opacity: 0
-      }
-
       img {
         object-fit: contain;
         max-width: 100%;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 1c91c8b7..901520aa 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -65,16 +65,12 @@
               :title="emoji.displayText"
               class="emoji-item"
               @click.stop.prevent="onEmoji(emoji)"
-              @mouseenter="setZoomEmoji($event, emoji)"
-              @mouseleave="setZoomEmoji($event, false)"
             >
-              <span v-if="!emoji.imageUrl">
-                {{ emoji.replacement }}
-              </span>
+              <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
               <img
                 v-else
                 :src="emoji.imageUrl"
-                >
+              >
             </span>
           </div>
         </div>
@@ -99,18 +95,6 @@
         />
       </div>
     </div>
-    <div ref="zoom-portal" class="zoom-portal">
-      <span v-if="zoomEmoji">
-        <span v-if="!zoomEmoji.imageUrl">
-          {{ zoomEmoji.replacement }}
-        </span>
-        <img
-          v-else
-          :key="zoomEmoji.imageUrl"
-          :src="zoomEmoji.imageUrl"
-          >
-      </span>
-    </div>
   </div>
 </template>
 
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index a0756ae4..771615f3 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -696,22 +696,6 @@ $status-margin: 0.75em;
     line-height: 1.4em;
     white-space: pre-wrap;
 
-    .emoji-container {
-      display: inline-block;
-      width: 32px;
-      height: 32px;
-      z-index: 1;
-
-      &:hover {
-        z-index: 2;
-
-        .emoji {
-          transform: scale(3);
-          z-index: 2;
-        }
-      }
-    }
-
     img, video {
       max-width: 100%;
       max-height: 400px;
@@ -721,9 +705,6 @@ $status-margin: 0.75em;
       &.emoji {
         width: 32px;
         height: 32px;
-        transition: transform 200ms;
-        transform: scale(1);
-        pointer-events: none;
       }
     }
 
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 17b4a6e0..6cc1851d 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -194,7 +194,7 @@ export const addEmojis = (string, emojis) => {
   return emojis.reduce((acc, emoji) => {
     return acc.replace(
       new RegExp(`:${emoji.shortcode}:`, 'g'),
-      `<span class='emoji-container'><img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' /></span>`
+      `<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
     )
   }, string)
 }
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 24aef5cd..20e03cb0 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -323,9 +323,9 @@ describe('API Entities normalizer', () => {
 
   describe('MastoAPI emoji adder', () => {
     const emojis = makeMockEmojiMasto()
-    const imageHtml = '<span class="emoji-container"><img src="https://example.com/image.png" alt="image" title="image" class="emoji" /></span>'
+    const imageHtml = '<img src="https://example.com/image.png" alt="image" title="image" class="emoji" />'
       .replace(/"/g, '\'')
-    const thinkHtml = '<span class="emoji-container"><img src="https://example.com/think.png" alt="thinking" title="thinking" class="emoji" /></span>'
+    const thinkHtml = '<img src="https://example.com/think.png" alt="thinking" title="thinking" class="emoji" />'
       .replace(/"/g, '\'')
 
     it('correctly replaces shortcodes in supplied string', () => {
-- 
cgit v1.2.3-70-g09d2


From 3505e53756562b4d7b304d0da9910c4528b25959 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 12 Sep 2019 20:36:43 +0300
Subject: review/naming + bugfix for stickers

---
 src/components/emoji_input/emoji_input.js            | 17 ++++++++++++++---
 src/components/emoji_input/emoji_input.vue           |  8 ++++----
 src/components/emoji_picker/emoji_picker.js          |  5 ++++-
 src/components/emoji_picker/emoji_picker.vue         |  2 +-
 src/components/post_status_form/post_status_form.vue |  8 ++++----
 5 files changed, 27 insertions(+), 13 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 6dc006f8..94af6e2f 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -54,17 +54,28 @@ const EmojiInput = {
       required: true,
       type: String
     },
-    emojiPicker: {
+    enableEmojiPicker: {
+      /**
+       * Enables emoji picker support, this implies that custom emoji are supported
+       */
       required: false,
       type: Boolean,
       default: false
     },
-    emojiPickerExternalTrigger: {
+    hideEmojiButton: {
+      /**
+    enableStickerPicker: {
+       * intended to use with external picker trigger, i.e. you have a button outside
+       * input that will open up the picker, see triggerShowPicker()
+       */
       required: false,
       type: Boolean,
       default: false
     },
-    stickerPicker: {
+    enableStickerPicker: {
+      /**
+       * Enables sticker picker support, only makes sense when enableEmojiPicker=true
+       */
       required: false,
       type: Boolean,
       default: false
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index b077e6e9..53b38573 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -4,19 +4,19 @@
     class="emoji-input"
   >
     <slot />
-    <template v-if="emojiPicker">
+    <template v-if="enableEmojiPicker">
       <div
-        v-if="!emojiPickerExternalTrigger"
+        v-if="!hideEmojiButton"
         class="emoji-picker-icon"
         @click.prevent="togglePicker"
       >
         <i class="icon-smile" />
       </div>
       <EmojiPicker
-        v-if="emojiPicker"
+        v-if="enableEmojiPicker"
         ref="picker"
         :class="{ hide: !showPicker }"
-        :sticker-picker="stickerPicker"
+        :enable-sticker-picker="enableStickerPicker"
         class="emoji-picker-panel"
         @emoji="insert"
         @sticker-uploaded="onStickerUploaded"
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index bce5026e..570ace13 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -5,7 +5,7 @@ const filterByKeyword = (list, keyword = '') => {
 
 const EmojiPicker = {
   props: {
-    stickerPicker: {
+    enableStickerPicker: {
       required: false,
       type: Boolean,
       default: false
@@ -97,6 +97,9 @@ const EmojiPicker = {
     },
     emojisView () {
       return this.emojis.filter(value => value.emojis.length > 0)
+    },
+    stickerPickerEnabled () {
+      return (this.$store.state.instance.stickers || []).length !== 0
     }
   }
 }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 901520aa..5c139d0e 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -17,7 +17,7 @@
         </span>
       </span>
       <span
-        v-if="stickerPicker"
+        v-if="stickerPickerEnabled"
         class="additional-tabs"
       >
         <span
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 026cb8fe..0e0b0e60 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -61,7 +61,7 @@
         <EmojiInput
           v-if="newStatus.spoilerText || alwaysShowSubject"
           v-model="newStatus.spoilerText"
-          emoji-picker
+          enable-emoji-picker
           :suggest="emojiSuggestor"
           class="form-control"
         >
@@ -78,9 +78,9 @@
           v-model="newStatus.status"
           :suggest="emojiUserSuggestor"
           class="form-control main-input"
-          emoji-picker
-          emoji-picker-external-trigger
-          sticker-picker
+          enable-emoji-picker
+          hide-emoji-button
+          enable-sticker-picker
           @sticker-uploaded="addMediaFile"
           @sticker-upload-failed="uploadFailed"
         >
-- 
cgit v1.2.3-70-g09d2


From f0cb6fe03faa1994fb667255d4051fe611b2afbc Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 15 Sep 2019 01:16:54 +0300
Subject: Fixed scrollability not being obvious, added fade effect

---
 src/components/emoji_picker/emoji_picker.js   |  8 ++++++++
 src/components/emoji_picker/emoji_picker.scss | 18 +++++++++++++++++-
 src/components/emoji_picker/emoji_picker.vue  |  1 +
 3 files changed, 26 insertions(+), 1 deletion(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 570ace13..8c60916b 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -17,6 +17,7 @@ const EmojiPicker = {
       keyword: '',
       activeGroup: 'custom',
       showingStickers: false,
+      groupsScrolledClass: 'scrolled-top',
       spamMode: false
     }
   },
@@ -40,6 +41,13 @@ const EmojiPicker = {
     scrolledGroup (e) {
       const target = (e && e.target) || this.$refs['emoji-groups']
       const top = target.scrollTop + 5
+      if (target.scrollTop <= 5) {
+        this.groupsScrolledClass = 'scrolled-top'
+      } else if (target.scrollTop >= target.scrollTopMax - 5) {
+        this.groupsScrolledClass = 'scrolled-bottom'
+      } else {
+        this.groupsScrolledClass = 'scrolled-middle'
+      }
       this.$nextTick(() => {
         this.emojisView.forEach(group => {
           const ref = this.$refs['group-' + group.id]
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 472db35b..8c07fd27 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -6,7 +6,7 @@
   position: absolute;
   right: 0;
   left: 0;
-  height: 300px;
+  height: 320px;
   margin: 0 !important;
   z-index: 1;
 
@@ -104,6 +104,22 @@
       flex: 1 1 1px;
       position: relative;
       overflow: auto;
+      mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+            linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+            linear-gradient(to top, white, white);
+      transition: mask-size 150ms;
+      mask-size: 100% 20px, 100% 20px, auto;
+      // Autoprefixed seem to ignore this one, and also syntax is different
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+      &.scrolled {
+        &-top {
+          mask-size: 100% 20px, 100% 0, auto;
+        }
+        &-bottom {
+          mask-size: 100% 0, 100% 20px, auto;
+        }
+      }
     }
 
     &-group {
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 5c139d0e..8bc7c382 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -46,6 +46,7 @@
         <div
           ref="emoji-groups"
           class="emoji-groups"
+          :class="groupsScrolledClass"
           @scroll="scrolledGroup"
         >
           <div
-- 
cgit v1.2.3-70-g09d2


From e366adbb6ca83f526c71d26ed03e20790b21af74 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 15 Sep 2019 12:09:19 +0300
Subject: updated logic for padding with spaces, improved spam mode

---
 src/components/emoji_input/emoji_input.js     | 29 ++++++++++++++++++++++++---
 src/components/emoji_picker/emoji_picker.js   |  2 +-
 src/components/emoji_picker/emoji_picker.scss |  4 +++-
 src/components/emoji_picker/emoji_picker.vue  |  6 +++++-
 src/i18n/en.json                              |  2 +-
 5 files changed, 36 insertions(+), 7 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 94af6e2f..41ee239c 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -178,14 +178,37 @@ const EmojiInput = {
       this.caret = 0
     },
     insert ({ insertion, spamMode }) {
+      const before = this.value.substring(0, this.caret) || ''
+      const after = this.value.substring(this.caret) || ''
+
+      /* Using a bit more smart approach to padding emojis with spaces:
+       * - put a space before cursor if there isn't one already, unless we
+       *   are at the beginning of post or in spam mode
+       * - put a space after emoji if there isn't one already unless we are
+       *   in spam mode
+       *
+       * The idea is that when you put a cursor somewhere in between sentence
+       * inserting just ' :emoji: ' will add more spaces to post which might
+       * break the flow/spacing, as well as the case where user ends sentence
+       * with a space before adding emoji.
+       *
+       * Spam mode is intended for creating multi-part emojis and overall spamming
+       * them, masto seem to be rendering :emoji::emoji: correctly now so why not
+       */
+      const isSpaceRegex = /\s/
+      const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && !spamMode > 0 ? ' ' : ''
+      const spaceAfter = !isSpaceRegex.exec(after[0]) && !spamMode ? ' ' : ''
+
       const newValue = [
-        this.value.substring(0, this.caret),
+        before,
+        spaceBefore,
         insertion,
-        this.value.substring(this.caret)
+        spaceAfter,
+        after
       ].join('')
       this.spamMode = spamMode
       this.$emit('input', newValue)
-      const position = this.caret + insertion.length
+      const position = this.caret + (insertion + spaceAfter + spaceBefore).length
 
       this.$nextTick(function () {
         // Re-focus inputbox after clicking suggestion
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 8c60916b..cb93f0c1 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -27,7 +27,7 @@ const EmojiPicker = {
   methods: {
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
-      this.$emit('emoji', { insertion: ` ${value} `, spamMode: this.spamMode })
+      this.$emit('emoji', { insertion: value, spamMode: this.spamMode })
     },
     highlight (key) {
       const ref = this.$refs['group-' + key]
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 8c07fd27..09438898 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -15,7 +15,8 @@
     line-height: normal;
   }
   .spam-mode-label {
-    padding: 7px;
+    padding: 0 7px;
+    display: flex;
   }
 
   .heading {
@@ -104,6 +105,7 @@
       flex: 1 1 1px;
       position: relative;
       overflow: auto;
+      user-select: none;
       mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
             linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
             linear-gradient(to top, white, white);
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 8bc7c382..b32d0862 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -83,7 +83,11 @@
             v-model="spamMode"
             type="checkbox"
             >
-          <label class="spam-mode-label" :for="labelKey + 'spam-mode'">{{ $t('emoji.spam') }}</label>
+          <label class="spam-mode-label" :for="labelKey + 'spam-mode'">
+            <div class="spam-mode-label-text">
+              {{ $t('emoji.spam') }}
+            </div>
+          </label>
         </div>
       </div>
       <div
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e74469ed..7676e7a8 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -109,7 +109,7 @@
   "emoji": {
     "stickers": "Stickers",
     "emoji": "Emoji",
-    "spam": "Keep open after adding emoji",
+    "spam": "Keep picker open, don't separate emoji with spaces",
     "search_emoji": "Search for an emoji",
     "add_emoji": "Insert emoji",
     "custom": "Custom emoji",
-- 
cgit v1.2.3-70-g09d2


From 7b4cb387345c0e278a6cfe5bbff5265c09281567 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 23 Sep 2019 20:29:01 +0300
Subject: split spam mode into two separate options (one in settings page)

---
 src/components/emoji_input/emoji_input.js      | 15 +++++++++------
 src/components/emoji_picker/emoji_picker.js    |  4 ++--
 src/components/emoji_picker/emoji_picker.scss  |  4 ++--
 src/components/emoji_picker/emoji_picker.vue   | 12 ++++++------
 src/components/settings/settings.js            |  4 ++++
 src/components/settings/settings.vue           |  8 ++++++++
 src/i18n/en.json                               |  3 ++-
 src/modules/config.js                          |  1 +
 test/unit/specs/components/emoji_input.spec.js | 18 +++++++++---------
 9 files changed, 43 insertions(+), 26 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 86ff9707..5f90d7f4 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -89,7 +89,7 @@ const EmojiInput = {
       blurTimeout: null,
       showPicker: false,
       temporarilyHideSuggestions: false,
-      spamMode: false,
+      keepOpen: false,
       disableClickOutside: false
     }
   },
@@ -97,6 +97,9 @@ const EmojiInput = {
     EmojiPicker
   },
   computed: {
+    padEmoji () {
+      return this.$store.state.config.padEmoji
+    },
     suggestions () {
       const firstchar = this.textAtCaret.charAt(0)
       if (this.textAtCaret === firstchar) { return [] }
@@ -176,7 +179,7 @@ const EmojiInput = {
       this.$emit('input', newValue)
       this.caret = 0
     },
-    insert ({ insertion, spamMode }) {
+    insert ({ insertion, keepOpen }) {
       const before = this.value.substring(0, this.caret) || ''
       const after = this.value.substring(this.caret) || ''
 
@@ -195,8 +198,8 @@ const EmojiInput = {
        * them, masto seem to be rendering :emoji::emoji: correctly now so why not
        */
       const isSpaceRegex = /\s/
-      const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && !spamMode > 0 ? ' ' : ''
-      const spaceAfter = !isSpaceRegex.exec(after[0]) && !spamMode ? ' ' : ''
+      const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : ''
+      const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : ''
 
       const newValue = [
         before,
@@ -205,7 +208,7 @@ const EmojiInput = {
         spaceAfter,
         after
       ].join('')
-      this.spamMode = spamMode
+      this.keepOpen = keepOpen
       this.$emit('input', newValue)
       const position = this.caret + (insertion + spaceAfter + spaceBefore).length
 
@@ -283,7 +286,7 @@ const EmojiInput = {
         this.blurTimeout = null
       }
 
-      if (!this.spamMode) {
+      if (!this.keepOpen) {
         this.showPicker = false
       }
       this.focused = true
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index cb93f0c1..824412dd 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -18,7 +18,7 @@ const EmojiPicker = {
       activeGroup: 'custom',
       showingStickers: false,
       groupsScrolledClass: 'scrolled-top',
-      spamMode: false
+      keepOpen: false
     }
   },
   components: {
@@ -27,7 +27,7 @@ const EmojiPicker = {
   methods: {
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
-      this.$emit('emoji', { insertion: value, spamMode: this.spamMode })
+      this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
     },
     highlight (key) {
       const ref = this.$refs['group-' + key]
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 09438898..b0ed00e9 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -10,11 +10,11 @@
   margin: 0 !important;
   z-index: 1;
 
-  .spam-mode {
+  .keep-open {
     padding: 7px;
     line-height: normal;
   }
-  .spam-mode-label {
+  .keep-open-label {
     padding: 0 7px;
     display: flex;
   }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index b32d0862..6c43dd97 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -76,16 +76,16 @@
           </div>
         </div>
         <div
-          class="spam-mode"
+          class="keep-open"
           >
           <input
-            :id="labelKey + 'spam-mode'"
-            v-model="spamMode"
+            :id="labelKey + 'keep-open'"
+            v-model="keepOpen"
             type="checkbox"
             >
-          <label class="spam-mode-label" :for="labelKey + 'spam-mode'">
-            <div class="spam-mode-label-text">
-              {{ $t('emoji.spam') }}
+          <label class="keep-open-label" :for="labelKey + 'keep-open'">
+            <div class="keep-open-label-text">
+              {{ $t('emoji.keep_open') }}
             </div>
           </label>
         </div>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index c4aa45b2..b6540d7e 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -16,6 +16,7 @@ const settings = {
 
     return {
       hideAttachmentsLocal: user.hideAttachments,
+      padEmojiLocal: user.padEmoji,
       hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
       maxThumbnails: user.maxThumbnails,
       hideNsfwLocal: user.hideNsfw,
@@ -127,6 +128,9 @@ const settings = {
     hideAttachmentsLocal (value) {
       this.$store.dispatch('setOption', { name: 'hideAttachments', value })
     },
+    padEmojiLocal (value) {
+      this.$store.dispatch('setOption', { name: 'padEmoji', value })
+    },
     hideAttachmentsInConvLocal (value) {
       this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
     },
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 744ec566..6d87a060 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -198,6 +198,14 @@
                   >
                   <label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
                 </li>
+                <li>
+                  <input
+                    id="padEmoji"
+                    v-model="padEmojiLocal"
+                    type="checkbox"
+                  >
+                  <label for="padEmoji">{{ $t('settings.pad_emoji') }}</label>
+                </li>
               </ul>
             </div>
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7676e7a8..20d4ed22 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -109,7 +109,7 @@
   "emoji": {
     "stickers": "Stickers",
     "emoji": "Emoji",
-    "spam": "Keep picker open, don't separate emoji with spaces",
+    "keep_open": "Keep picker open",
     "search_emoji": "Search for an emoji",
     "add_emoji": "Insert emoji",
     "custom": "Custom emoji",
@@ -232,6 +232,7 @@
     "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
     "delete_account_instructions": "Type your password in the input below to confirm account deletion.",
     "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
+    "pad_emoji": "Pad emoji with spaces when adding from picker",
     "export_theme": "Save preset",
     "filtering": "Filtering",
     "filtering_explanation": "All statuses containing these words will be muted, one per line",
diff --git a/src/modules/config.js b/src/modules/config.js
index 2bfad8f6..cf04d14f 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -7,6 +7,7 @@ const defaultState = {
   colors: {},
   hideMutedPosts: undefined, // instance default
   collapseMessageWithSubject: undefined, // instance default
+  padEmoji: true,
   hideAttachments: false,
   hideAttachmentsInConv: false,
   maxThumbnails: 16,
diff --git a/test/unit/specs/components/emoji_input.spec.js b/test/unit/specs/components/emoji_input.spec.js
index 5f24331a..13a59961 100644
--- a/test/unit/specs/components/emoji_input.spec.js
+++ b/test/unit/specs/components/emoji_input.spec.js
@@ -26,7 +26,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: initialString.length })
-      wrapper.vm.insert({ insertion: '(test)', spamMode: false })
+      wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
       expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
     })
 
@@ -36,7 +36,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: initialString.length })
-      wrapper.vm.insert({ insertion: '(test)', spamMode: false })
+      wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
       expect(wrapper.emitted().input[0][0]).to.eql('Testing (test) ')
     })
 
@@ -46,7 +46,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: 0 })
-      wrapper.vm.insert({ insertion: '(test)', spamMode: false })
+      wrapper.vm.insert({ insertion: '(test)', keepOpen: false })
       expect(wrapper.emitted().input[0][0]).to.eql('(test) Testing')
     })
 
@@ -56,7 +56,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: 6 })
-      wrapper.vm.insert({ insertion: ':ebin:', spamMode: false })
+      wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
       expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
     })
 
@@ -66,7 +66,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: 7 })
-      wrapper.vm.insert({ insertion: ':ebin:', spamMode: false })
+      wrapper.vm.insert({ insertion: ':ebin:', keepOpen: false })
       expect(wrapper.emitted().input[0][0]).to.eql('Spurdo :ebin: Sparde')
     })
 
@@ -76,7 +76,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: initialString.length })
-      wrapper.vm.insert({ insertion: ':spam:', spamMode: true })
+      wrapper.vm.insert({ insertion: ':spam:', keepOpen: true })
       expect(wrapper.emitted().input[0][0]).to.eql('Eat some spam!:spam:')
     })
 
@@ -86,7 +86,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: 0 })
-      wrapper.vm.insert({ insertion: '1234', spamMode: false })
+      wrapper.vm.insert({ insertion: '1234', keepOpen: false })
       vue.nextTick(() => {
         expect(wrapper.vm.caret).to.eql(5)
         done()
@@ -99,7 +99,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: initialString.length })
-      wrapper.vm.insert({ insertion: '1234', spamMode: false })
+      wrapper.vm.insert({ insertion: '1234', keepOpen: false })
       vue.nextTick(() => {
         expect(wrapper.vm.caret).to.eql(10)
         done()
@@ -112,7 +112,7 @@ describe('EmojiInput', () => {
       const input = wrapper.find('input')
       input.setValue(initialString)
       wrapper.setData({ caret: initialString.length })
-      wrapper.vm.insert({ insertion: '1234', spamMode: true })
+      wrapper.vm.insert({ insertion: '1234', keepOpen: true })
       vue.nextTick(() => {
         expect(wrapper.vm.caret).to.eql(8)
         done()
-- 
cgit v1.2.3-70-g09d2


From 2f26e8acc5395753f4d649d9a1bfbe356717e9ac Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 25 Sep 2019 20:23:55 +0300
Subject: eslint

---
 src/components/emoji_input/emoji_input.js            | 2 +-
 src/components/emoji_picker/emoji_picker.vue         | 9 ++++++---
 src/components/post_status_form/post_status_form.vue | 6 +++---
 src/components/user_card/user_card.vue               | 7 +++++--
 4 files changed, 15 insertions(+), 9 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f30cd1e4..a586b819 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -401,7 +401,7 @@ const EmojiInput = {
       this.$emit('input', e.target.value)
     },
     onClickInput (e) {
-      this.showPicker = false;
+      this.showPicker = false
     },
     onClickOutside (e) {
       if (this.disableClickOutside) return
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 6c43dd97..42f20130 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -77,13 +77,16 @@
         </div>
         <div
           class="keep-open"
-          >
+        >
           <input
             :id="labelKey + 'keep-open'"
             v-model="keepOpen"
             type="checkbox"
-            >
-          <label class="keep-open-label" :for="labelKey + 'keep-open'">
+          >
+          <label
+            class="keep-open-label"
+            :for="labelKey + 'keep-open'"
+          >
             <div class="keep-open-label-text">
               {{ $t('emoji.keep_open') }}
             </div>
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 5f9e21ae..4916d988 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -1,7 +1,7 @@
 <template>
-<div
-  class="post-status-form"
-  ref="root"
+  <div
+    ref="root"
+    class="post-status-form"
   >
     <form
       autocomplete="off"
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index c2626260..629d1fd0 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -3,8 +3,11 @@
     class="user-card"
     :class="classes"
   >
-    <div :class="{ 'hide-bio': hideBio }" :style="style" class="background-image">
-    </div>
+    <div
+      :class="{ 'hide-bio': hideBio }"
+      :style="style"
+      class="background-image"
+    />
     <div class="panel-heading">
       <div class="user-info">
         <div class="container">
-- 
cgit v1.2.3-70-g09d2


From e7532464e386b908321c8c202249c3855e83d722 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Oct 2019 20:43:23 +0300
Subject: Replaced most trivial checkboxes with Checkbox component

---
 src/components/checkbox/checkbox.vue               |   8 +-
 src/components/emoji_picker/emoji_picker.js        |   4 +-
 src/components/emoji_picker/emoji_picker.scss      |   4 -
 src/components/emoji_picker/emoji_picker.vue       |  20 +-
 .../post_status_form/post_status_form.js           |   4 +-
 .../post_status_form/post_status_form.vue          |   9 +-
 src/components/settings/settings.js                |   4 +-
 src/components/settings/settings.vue               | 266 +++++++--------------
 src/components/style_switcher/style_switcher.js    |   4 +-
 src/components/style_switcher/style_switcher.vue   |  45 ++--
 10 files changed, 122 insertions(+), 246 deletions(-)

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

diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 441f815c..6e164c1b 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -40,12 +40,16 @@ export default {
 .checkbox {
   position: relative;
   display: inline-block;
-  padding-left: 1.2em;
   min-height: 1.2em;
 
+  &-indicator {
+    position: relative;
+    padding-left: 1.2em;
+  }
+
   &-indicator::before {
     position: absolute;
-    left: 0;
+    right: 0;
     top: 0;
     display: block;
     content: '✔';
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 824412dd..f300a6b8 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,3 +1,4 @@
+import Checkbox from '../checkbox/checkbox.vue'
 
 const filterByKeyword = (list, keyword = '') => {
   return list.filter(x => x.displayText.includes(keyword))
@@ -22,7 +23,8 @@ const EmojiPicker = {
     }
   },
   components: {
-    StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
+    StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
+    Checkbox
   },
   methods: {
     onEmoji (emoji) {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index b0ed00e9..d99539b0 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -14,10 +14,6 @@
     padding: 7px;
     line-height: normal;
   }
-  .keep-open-label {
-    padding: 0 7px;
-    display: flex;
-  }
 
   .heading {
     display: flex;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 42f20130..b974fce9 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -75,22 +75,10 @@
             </span>
           </div>
         </div>
-        <div
-          class="keep-open"
-        >
-          <input
-            :id="labelKey + 'keep-open'"
-            v-model="keepOpen"
-            type="checkbox"
-          >
-          <label
-            class="keep-open-label"
-            :for="labelKey + 'keep-open'"
-          >
-            <div class="keep-open-label-text">
-              {{ $t('emoji.keep_open') }}
-            </div>
-          </label>
+        <div class="keep-open">
+          <Checkbox v-model="keepOpen">
+            {{ $t('emoji.keep_open') }}
+          </Checkbox>
         </div>
       </div>
       <div
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index db501dc1..9659f296 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -8,6 +8,7 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j
 import { reject, map, uniqBy } from 'lodash'
 import suggestor from '../emoji_input/suggestor.js'
 import { mapGetters } from 'vuex'
+import Checkbox from '../checkbox/checkbox.vue'
 
 const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
   let allAttentions = [...attentions]
@@ -36,7 +37,8 @@ const PostStatusForm = {
     MediaUpload,
     EmojiInput,
     PollForm,
-    ScopeSelector
+    ScopeSelector,
+    Checkbox
   },
   mounted () {
     this.resize(this.$refs.textarea)
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 4916d988..237ed725 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -261,12 +261,9 @@
         v-if="newStatus.files.length > 0"
         class="upload_settings"
       >
-        <input
-          id="filesSensitive"
-          v-model="newStatus.nsfw"
-          type="checkbox"
-        >
-        <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
+        <Checkbox v-model="newStatus.nsfw">
+          {{ $t('post_status.attachments_sensitive') }}
+        </Checkbox>
       </div>
     </form>
   </div>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 98ceb164..c49083f9 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -6,6 +6,7 @@ import StyleSwitcher from '../style_switcher/style_switcher.vue'
 import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
 import { extractCommit } from '../../services/version/version.service'
 import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
+import Checkbox from '../checkbox/checkbox.vue'
 
 const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
 const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
@@ -35,7 +36,8 @@ const settings = {
   components: {
     TabSwitcher,
     StyleSwitcher,
-    InterfaceLanguageSwitcher
+    InterfaceLanguageSwitcher,
+    Checkbox
   },
   computed: {
     user () {
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index e1a06fe2..a83489d2 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -36,12 +36,9 @@
                   <interface-language-switcher />
                 </li>
                 <li v-if="instanceSpecificPanelPresent">
-                  <input
-                    id="hideISP"
-                    v-model="hideISP"
-                    type="checkbox"
-                  >
-                  <label for="hideISP">{{ $t('settings.hide_isp') }}</label>
+                  <Checkbox v-model="hideISP">
+                    {{ $t('settings.hide_isp') }}
+                  </Checkbox>
                 </li>
               </ul>
             </div>
@@ -49,58 +46,42 @@
               <h2>{{ $t('nav.timeline') }}</h2>
               <ul class="setting-list">
                 <li>
-                  <input
-                    id="hideMutedPosts"
-                    v-model="hideMutedPosts"
-                    type="checkbox"
-                  >
-                  <label for="hideMutedPosts">{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}</label>
+                  <Checkbox v-model="hideMutedPosts">
+                    {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="collapseMessageWithSubject"
-                    v-model="collapseMessageWithSubject"
-                    type="checkbox"
-                  >
-                  <label for="collapseMessageWithSubject">{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}</label>
+                  <Checkbox v-model="collapseMessageWithSubject">
+                    {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="streaming"
-                    v-model="streaming"
-                    type="checkbox"
-                  >
-                  <label for="streaming">{{ $t('settings.streaming') }}</label>
+                  <Checkbox v-model="streaming">
+                    {{ $t('settings.streaming') }}
+                  </Checkbox>
                   <ul
                     class="setting-list suboptions"
                     :class="[{disabled: !streaming}]"
                   >
                     <li>
-                      <input
-                        id="pauseOnUnfocused"
+                      <Checkbox
                         v-model="pauseOnUnfocused"
                         :disabled="!streaming"
-                        type="checkbox"
                       >
-                      <label for="pauseOnUnfocused">{{ $t('settings.pause_on_unfocused') }}</label>
+                        {{ $t('settings.pause_on_unfocused') }}
+                      </Checkbox>
                     </li>
                   </ul>
                 </li>
                 <li>
-                  <input
-                    id="autoload"
-                    v-model="autoLoad"
-                    type="checkbox"
-                  >
-                  <label for="autoload">{{ $t('settings.autoload') }}</label>
+                  <Checkbox v-model="autoLoad">
+                    {{ $t('settings.autoload') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="hoverPreview"
-                    v-model="hoverPreview"
-                    type="checkbox"
-                  >
-                  <label for="hoverPreview">{{ $t('settings.reply_link_preview') }}</label>
+                  <Checkbox v-model="hoverPreview">
+                    {{ $t('settings.reply_link_preview') }}
+                  </Checkbox>
                 </li>
               </ul>
             </div>
@@ -109,24 +90,14 @@
               <h2>{{ $t('settings.composing') }}</h2>
               <ul class="setting-list">
                 <li>
-                  <input
-                    id="scopeCopy"
-                    v-model="scopeCopy"
-                    type="checkbox"
-                  >
-                  <label for="scopeCopy">
+                  <Checkbox v-model="scopeCopy">
                     {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
-                  </label>
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="subjectHide"
-                    v-model="alwaysShowSubjectInput"
-                    type="checkbox"
-                  >
-                  <label for="subjectHide">
+                  <Checkbox v-model="alwaysShowSubjectInput">
                     {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
-                  </label>
+                  </Checkbox>
                 </li>
                 <li>
                   <div>
@@ -181,30 +152,19 @@
                   </div>
                 </li>
                 <li>
-                  <input
-                    id="minimalScopesMode"
-                    v-model="minimalScopesMode"
-                    type="checkbox"
-                  >
-                  <label for="minimalScopesMode">
+                  <Checkbox v-model="minimalScopesMode">
                     {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
-                  </label>
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="autohideFloatingPostButton"
-                    v-model="autohideFloatingPostButton"
-                    type="checkbox"
-                  >
-                  <label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
+                  <Checkbox v-model="autohideFloatingPostButton">
+                    {{ $t('settings.autohide_floating_post_button') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="padEmoji"
-                    v-model="padEmoji"
-                    type="checkbox"
-                  >
-                  <label for="padEmoji">{{ $t('settings.pad_emoji') }}</label>
+                  <Checkbox v-model="padEmoji">
+                    {{ $t('settings.pad_emoji') }}
+                  </Checkbox>
                 </li>
               </ul>
             </div>
@@ -213,23 +173,19 @@
               <h2>{{ $t('settings.attachments') }}</h2>
               <ul class="setting-list">
                 <li>
-                  <input
-                    id="hideAttachments"
-                    v-model="hideAttachments"
-                    type="checkbox"
-                  >
-                  <label for="hideAttachments">{{ $t('settings.hide_attachments_in_tl') }}</label>
+                  <Checkbox v-model="hideAttachments">
+                    {{ $t('settings.hide_attachments_in_tl') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="hideAttachmentsInConv"
-                    v-model="hideAttachmentsInConv"
-                    type="checkbox"
-                  >
-                  <label for="hideAttachmentsInConv">{{ $t('settings.hide_attachments_in_convo') }}</label>
+                  <Checkbox v-model="hideAttachmentsInConv">
+                    {{ $t('settings.hide_attachments_in_convo') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <label for="maxThumbnails">{{ $t('settings.max_thumbnails') }}</label>
+                  <label for="maxThumbnails">
+                    {{ $t('settings.max_thumbnails') }}
+                  </label>
                   <input
                     id="maxThumbnails"
                     v-model.number="maxThumbnails"
@@ -240,60 +196,48 @@
                   >
                 </li>
                 <li>
-                  <input
-                    id="hideNsfw"
-                    v-model="hideNsfw"
-                    type="checkbox"
-                  >
-                  <label for="hideNsfw">{{ $t('settings.nsfw_clickthrough') }}</label>
+                  <Checkbox v-model="hideNsfw">
+                    {{ $t('settings.nsfw_clickthrough') }}
+                  </Checkbox>
                 </li>
                 <ul class="setting-list suboptions">
                   <li>
-                    <input
-                      id="preloadImage"
+                    <Checkbox
                       v-model="preloadImage"
                       :disabled="!hideNsfw"
-                      type="checkbox"
                     >
-                    <label for="preloadImage">{{ $t('settings.preload_images') }}</label>
+                      {{ $t('settings.preload_images') }}
+                    </Checkbox>
                   </li>
                   <li>
-                    <input
-                      id="useOneClickNsfw"
+                    <Checkbox
                       v-model="useOneClickNsfw"
                       :disabled="!hideNsfw"
-                      type="checkbox"
                     >
-                    <label for="useOneClickNsfw">{{ $t('settings.use_one_click_nsfw') }}</label>
+                      {{ $t('settings.use_one_click_nsfw') }}
+                    </Checkbox>
                   </li>
                 </ul>
                 <li>
-                  <input
-                    id="stopGifs"
-                    v-model="stopGifs"
-                    type="checkbox"
-                  >
-                  <label for="stopGifs">{{ $t('settings.stop_gifs') }}</label>
+                  <Checkbox v-model="stopGifs">
+                    {{ $t('settings.stop_gifs') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="loopVideo"
-                    v-model="loopVideo"
-                    type="checkbox"
-                  >
-                  <label for="loopVideo">{{ $t('settings.loop_video') }}</label>
+                  <Checkbox v-model="loopVideo">
+                    {{ $t('settings.loop_video') }}
+                  </Checkbox>
                   <ul
                     class="setting-list suboptions"
                     :class="[{disabled: !streaming}]"
                   >
                     <li>
-                      <input
-                        id="loopVideoSilentOnly"
+                      <Checkbox
                         v-model="loopVideoSilentOnly"
                         :disabled="!loopVideo || !loopSilentAvailable"
-                        type="checkbox"
                       >
-                      <label for="loopVideoSilentOnly">{{ $t('settings.loop_video_silent_only') }}</label>
+                        {{ $t('settings.loop_video_silent_only') }}
+                      </Checkbox>
                       <div
                         v-if="!loopSilentAvailable"
                         class="unavailable"
@@ -304,20 +248,14 @@
                   </ul>
                 </li>
                 <li>
-                  <input
-                    id="playVideosInModal"
-                    v-model="playVideosInModal"
-                    type="checkbox"
-                  >
-                  <label for="playVideosInModal">{{ $t('settings.play_videos_in_modal') }}</label>
+                  <Checkbox v-model="playVideosInModal">
+                    {{ $t('settings.play_videos_in_modal') }}
+                  </Checkbox>
                 </li>
                 <li>
-                  <input
-                    id="useContainFit"
-                    v-model="useContainFit"
-                    type="checkbox"
-                  >
-                  <label for="useContainFit">{{ $t('settings.use_contain_fit') }}</label>
+                  <Checkbox v-model="useContainFit">
+                    {{ $t('settings.use_contain_fit') }}
+                  </Checkbox>
                 </li>
               </ul>
             </div>
@@ -326,14 +264,9 @@
               <h2>{{ $t('settings.notifications') }}</h2>
               <ul class="setting-list">
                 <li>
-                  <input
-                    id="webPushNotifications"
-                    v-model="webPushNotifications"
-                    type="checkbox"
-                  >
-                  <label for="webPushNotifications">
+                  <Checkbox v-model="webPushNotifications">
                     {{ $t('settings.enable_web_push_notifications') }}
-                  </label>
+                  </Checkbox>
                 </li>
               </ul>
             </div>
@@ -351,44 +284,24 @@
                 <span class="label">{{ $t('settings.notification_visibility') }}</span>
                 <ul class="option-list">
                   <li>
-                    <input
-                      id="notification-visibility-likes"
-                      v-model="notificationVisibility.likes"
-                      type="checkbox"
-                    >
-                    <label for="notification-visibility-likes">
+                    <Checkbox v-model="notificationVisibility.likes">
                       {{ $t('settings.notification_visibility_likes') }}
-                    </label>
+                    </Checkbox>
                   </li>
                   <li>
-                    <input
-                      id="notification-visibility-repeats"
-                      v-model="notificationVisibility.repeats"
-                      type="checkbox"
-                    >
-                    <label for="notification-visibility-repeats">
+                    <Checkbox v-model="notificationVisibility.repeats">
                       {{ $t('settings.notification_visibility_repeats') }}
-                    </label>
+                    </Checkbox>
                   </li>
                   <li>
-                    <input
-                      id="notification-visibility-follows"
-                      v-model="notificationVisibility.follows"
-                      type="checkbox"
-                    >
-                    <label for="notification-visibility-follows">
+                    <Checkbox v-model="notificationVisibility.follows">
                       {{ $t('settings.notification_visibility_follows') }}
-                    </label>
+                    </Checkbox>
                   </li>
                   <li>
-                    <input
-                      id="notification-visibility-mentions"
-                      v-model="notificationVisibility.mentions"
-                      type="checkbox"
-                    >
-                    <label for="notification-visibility-mentions">
+                    <Checkbox v-model="notificationVisibility.mentions">
                       {{ $t('settings.notification_visibility_mentions') }}
-                    </label>
+                    </Checkbox>
                   </li>
                 </ul>
               </div>
@@ -413,24 +326,14 @@
                 </label>
               </div>
               <div>
-                <input
-                  id="hidePostStats"
-                  v-model="hidePostStats"
-                  type="checkbox"
-                >
-                <label for="hidePostStats">
+                <Checkbox v-model="hidePostStats">
                   {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
-                </label>
+                </Checkbox>
               </div>
               <div>
-                <input
-                  id="hideUserStats"
-                  v-model="hideUserStats"
-                  type="checkbox"
-                >
-                <label for="hideUserStats">
+                <Checkbox v-model="hideUserStats">
                   {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
-                </label>
+                </Checkbox>
               </div>
             </div>
             <div class="setting-item">
@@ -442,14 +345,9 @@
                 />
               </div>
               <div>
-                <input
-                  id="hideFilteredStatuses"
-                  v-model="hideFilteredStatuses"
-                  type="checkbox"
-                >
-                <label for="hideFilteredStatuses">
+                <Checkbox v-model="hideFilteredStatuses">
                   {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
-                </label>
+                </Checkbox>
               </div>
             </div>
           </div>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index c19aaaec..6e2a1d7b 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -10,6 +10,7 @@ import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
 import Preview from './preview.vue'
 import ExportImport from '../export_import/export_import.vue'
+import Checkbox from '../checkbox/checkbox.vue'
 
 // List of color values used in v1
 const v1OnlyNames = [
@@ -338,7 +339,8 @@ export default {
     FontControl,
     TabSwitcher,
     Preview,
-    ExportImport
+    ExportImport,
+    Checkbox
   },
   methods: {
     setCustomTheme () {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index d24394a4..944debab 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -42,44 +42,29 @@
       </div>
       <div class="save-load-options">
         <span class="keep-option">
-          <input
-            id="keep-color"
-            v-model="keepColor"
-            type="checkbox"
-          >
-          <label for="keep-color">{{ $t('settings.style.switcher.keep_color') }}</label>
+          <Checkbox v-model="keepColor">
+            {{ $t('settings.style.switcher.keep_color') }}
+          </Checkbox>
         </span>
         <span class="keep-option">
-          <input
-            id="keep-shadows"
-            v-model="keepShadows"
-            type="checkbox"
-          >
-          <label for="keep-shadows">{{ $t('settings.style.switcher.keep_shadows') }}</label>
+          <Checkbox v-model="keepShadows">
+            {{ $t('settings.style.switcher.keep_shadows') }}
+          </Checkbox>
         </span>
         <span class="keep-option">
-          <input
-            id="keep-opacity"
-            v-model="keepOpacity"
-            type="checkbox"
-          >
-          <label for="keep-opacity">{{ $t('settings.style.switcher.keep_opacity') }}</label>
+          <Checkbox v-model="keepOpacity">
+            {{ $t('settings.style.switcher.keep_opacity') }}
+          </Checkbox>
         </span>
         <span class="keep-option">
-          <input
-            id="keep-roundness"
-            v-model="keepRoundness"
-            type="checkbox"
-          >
-          <label for="keep-roundness">{{ $t('settings.style.switcher.keep_roundness') }}</label>
+          <Checkbox v-model="keepRoundness">
+            {{ $t('settings.style.switcher.keep_roundness') }}
+          </Checkbox>
         </span>
         <span class="keep-option">
-          <input
-            id="keep-fonts"
-            v-model="keepFonts"
-            type="checkbox"
-          >
-          <label for="keep-fonts">{{ $t('settings.style.switcher.keep_fonts') }}</label>
+          <Checkbox v-model="keepFonts">
+            {{ $t('settings.style.switcher.keep_fonts') }}
+          </Checkbox>
         </span>
         <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
       </div>
-- 
cgit v1.2.3-70-g09d2


From 6511a744a29c760aa2613b0902db55be3b59dfa5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 7 Oct 2019 23:46:40 +0300
Subject: arbitrary limit with option to overcome it

---
 src/App.scss                                     | 12 ++++
 src/_variables.scss                              |  1 +
 src/components/emoji_picker/emoji_picker.js      | 80 ++++++++++++++++--------
 src/components/emoji_picker/emoji_picker.scss    | 24 ++++---
 src/components/emoji_picker/emoji_picker.vue     | 11 ++++
 src/components/style_switcher/style_switcher.js  |  3 +
 src/components/style_switcher/style_switcher.vue |  7 +++
 src/i18n/en.json                                 |  5 +-
 src/services/style_setter/style_setter.js        |  5 ++
 9 files changed, 113 insertions(+), 35 deletions(-)

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

diff --git a/src/App.scss b/src/App.scss
index 2190f91a..fe271ce1 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -658,6 +658,18 @@ nav {
       color: var(--alertErrorPanelText, $fallback--text);
     }
   }
+
+  &.warning {
+    background-color: $fallback--alertWarning;
+    background-color: var(--alertWarning, $fallback--alertWarning);
+    color: $fallback--text;
+    color: var(--alertWarningText, $fallback--text);
+
+    .panel-heading & {
+      color: $fallback--text;
+      color: var(--alertWarningPanelText, $fallback--text);
+    }
+  }
 }
 
 .faint {
diff --git a/src/_variables.scss b/src/_variables.scss
index 150e4fb5..e18101f0 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -17,6 +17,7 @@ $fallback--cGreen: #0fa00f;
 $fallback--cOrange: orange;
 
 $fallback--alertError: rgba(211,16,20,.5);
+$fallback--alertWarning: rgba(111,111,20,.5);
 
 $fallback--panelRadius: 10px;
 $fallback--checkboxRadius: 2px;
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 81f7437f..11dbdf44 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,5 +1,9 @@
 import { set } from 'vue'
 
+const LOAD_EMOJI_BY = 50
+const LOAD_EMOJI_INTERVAL = 100
+const LOAD_EMOJI_SANE_AMOUNT = 500
+
 const filterByKeyword = (list, keyword = '') => {
   return list.filter(x => x.displayText.includes(keyword))
 }
@@ -21,13 +25,17 @@ const EmojiPicker = {
       groupsScrolledClass: 'scrolled-top',
       keepOpen: false,
       customEmojiBuffer: [],
-      customEmojiInterval: null,
-      customEmojiCounter: 0
+      customEmojiTimeout: null,
+      customEmojiCounter: 0,
+      customEmojiLoadAllConfirmed: false
     }
   },
   components: {
     StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
   },
+  mounted () {
+    this.startEmojiLoad()
+  },
   methods: {
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
@@ -61,35 +69,39 @@ const EmojiPicker = {
         })
       })
     },
-    restartInterval () {
-      const customEmojis = filterByKeyword(
-        this.$store.state.instance.customEmoji || [],
-        this.keyword
-      )
-      const amount = 50
-      const interval = 100
+    loadEmojiInsane () {
+      this.customEmojiLoadAllConfirmed = true
+      this.continueEmojiLoad()
+    },
+    loadEmoji () {
+      const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
+      const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
+            !this.customEmojiLoadAllConfirmed
 
-      if (this.customEmojiInterval) {
-        window.clearInterval(this.customEmojiInterval)
+      if (allLoaded || saneLoaded) {
+        return
       }
-      window.setTimeout(
-        window.clearInterval(this.customEmojiInterval),
-        1000
+
+      this.customEmojiBuffer.push(
+        ...this.filteredEmoji.slice(
+          this.customEmojiCounter,
+          this.customEmojiCounter + LOAD_EMOJI_BY
+        )
       )
+      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
+      this.customEmojiCounter += LOAD_EMOJI_BY
+    },
+    startEmojiLoad () {
+      if (this.customEmojiTimeout) {
+        window.clearTimeout(this.customEmojiTimeout)
+      }
+
       set(this, 'customEmojiBuffer', [])
       this.customEmojiCounter = 0
-      this.customEmojiInterval = window.setInterval(() => {
-        console.log(this.customEmojiBuffer.length)
-        console.log(customEmojis.length)
-        if (this.customEmojiBuffer.length < customEmojis.length) {
-          this.customEmojiBuffer.push(
-            ...customEmojis.slice(this.customEmojiCounter, this.customEmojiCounter + amount)
-          )
-        } else {
-          window.clearInterval(this.customEmojiInterval)
-        }
-        this.customEmojiCounter += amount
-      }, interval)
+      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
+    },
+    continueEmojiLoad () {
+      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
     },
     toggleStickers () {
       this.showingStickers = !this.showingStickers
@@ -107,7 +119,7 @@ const EmojiPicker = {
   watch: {
     keyword () {
       this.scrolledGroup()
-      this.restartInterval()
+      this.startEmojiLoad()
     }
   },
   computed: {
@@ -120,6 +132,20 @@ const EmojiPicker = {
       }
       return 0
     },
+    saneAmount () {
+      // for UI
+      return LOAD_EMOJI_SANE_AMOUNT
+    },
+    filteredEmoji () {
+      return filterByKeyword(
+        this.$store.state.instance.customEmoji || [],
+        this.keyword
+      )
+    },
+    askForSanity () {
+      return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
+        !this.customEmojiLoadAllConfirmed
+    },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
       const customEmojis = this.customEmojiBuffer
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index b0ed00e9..6608f393 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -6,14 +6,20 @@
   position: absolute;
   right: 0;
   left: 0;
-  height: 320px;
   margin: 0 !important;
   z-index: 1;
 
-  .keep-open {
+  .keep-open,
+  .too-many-emoji {
     padding: 7px;
     line-height: normal;
   }
+
+  .too-many-emoji {
+    display: flex;
+    flex-direction: column;
+  }
+
   .keep-open-label {
     padding: 0 7px;
     display: flex;
@@ -28,7 +34,7 @@
   .content {
     display: flex;
     flex-direction: column;
-    flex: 1 1 0;
+    flex: 1 1 auto;
     min-height: 0px;
   }
 
@@ -36,12 +42,16 @@
     flex-grow: 1;
   }
 
+  .emoji-groups {
+    min-height: 200px;
+  }
+
   .additional-tabs {
     border-left: 1px solid;
     border-left-color: $fallback--icon;
     border-left-color: var(--icon, $fallback--icon);
     padding-left: 7px;
-    flex: 0 0 0;
+    flex: 0 0 auto;
   }
 
   .additional-tabs,
@@ -72,7 +82,7 @@
   }
 
   .sticker-picker {
-    flex: 1 1 0
+    flex: 1 1 auto
   }
 
   .stickers,
@@ -80,7 +90,7 @@
     &-content {
       display: flex;
       flex-direction: column;
-      flex: 1 1 0;
+      flex: 1 1 auto;
       min-height: 0;
 
       &.hidden {
@@ -94,7 +104,7 @@
   .emoji {
     &-search {
       padding: 5px;
-      flex: 0 0 0;
+      flex: 0 0 auto;
 
       input {
         width: 100%;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 42f20130..d9daac3e 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -92,6 +92,17 @@
             </div>
           </label>
         </div>
+        <div class="too-many-emoji" v-if="askForSanity">
+          <div class="alert warning hint">
+            {{ $t('emoji.load_all_hint', { saneAmount } ) }}
+          </div>
+          <button
+            class="btn btn-default"
+            @click="loadEmojiInsane"
+            >
+            {{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
+          </button>
+        </div>
       </div>
       <div
         v-if="showingStickers"
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 8c3d4861..ef5704c1 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -73,6 +73,7 @@ export default {
       topBarLinkColorLocal: undefined,
 
       alertErrorColorLocal: undefined,
+      alertWarningColorLocal: undefined,
 
       badgeOpacityLocal: undefined,
       badgeNotificationColorLocal: undefined,
@@ -146,6 +147,7 @@ export default {
         btnText: this.btnTextColorLocal,
 
         alertError: this.alertErrorColorLocal,
+        alertWarning: this.alertWarningColorLocal,
         badgeNotification: this.badgeNotificationColorLocal,
 
         faint: this.faintColorLocal,
@@ -229,6 +231,7 @@ export default {
         topBar: hex2rgb(colors.topBar),
         input: hex2rgb(colors.input),
         alertError: hex2rgb(colors.alertError),
+        alertWarning: hex2rgb(colors.alertWarning),
         badgeNotification: hex2rgb(colors.badgeNotification)
       }
 
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index d24394a4..d32641c2 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -216,6 +216,13 @@
               :fallback="previewTheme.colors.alertError"
             />
             <ContrastRatio :contrast="previewContrast.alertError" />
+            <ColorInput
+              v-model="alertWarningColorLocal"
+              name="alertWarning"
+              :label="$t('settings.style.advanced_colors.alert_warning')"
+              :fallback="previewTheme.colors.alertWarning"
+            />
+            <ContrastRatio :contrast="previewContrast.alertWarning" />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 32c25e3e..c6c98c48 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -114,7 +114,9 @@
     "search_emoji": "Search for an emoji",
     "add_emoji": "Insert emoji",
     "custom": "Custom emoji",
-    "unicode": "Unicode emoji"
+    "unicode": "Unicode emoji",
+    "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
+    "load_all": "Loading all {emojiAmount} emoji"
   },
   "interactions": {
     "favs_repeats": "Repeats and Favorites",
@@ -387,6 +389,7 @@
         "_tab_label": "Advanced",
         "alert": "Alert background",
         "alert_error": "Error",
+        "alert_warning": "Warning",
         "badge": "Badge background",
         "badge_notification": "Notification",
         "panel_header": "Panel header",
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 1cf7edc3..eaa495c4 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -215,6 +215,10 @@ const generateColors = (input) => {
   colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text)
   colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText)
 
+  colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange)
+  colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text)
+  colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText)
+
   colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
   colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
 
@@ -222,6 +226,7 @@ const generateColors = (input) => {
     if (typeof v === 'undefined') return
     if (k === 'alert') {
       colors.alertError.a = v
+      colors.alertWarning.a = v
       return
     }
     if (k === 'faint') {
-- 
cgit v1.2.3-70-g09d2


From fa4e69ec8ca430c0b0fae6418d955580a1a727d0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 8 Oct 2019 21:34:36 +0300
Subject: eslint

---
 src/components/emoji_picker/emoji_picker.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index d9daac3e..56501467 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -92,14 +92,17 @@
             </div>
           </label>
         </div>
-        <div class="too-many-emoji" v-if="askForSanity">
+        <div
+          v-if="askForSanity"
+          class="too-many-emoji"
+        >
           <div class="alert warning hint">
             {{ $t('emoji.load_all_hint', { saneAmount } ) }}
           </div>
           <button
             class="btn btn-default"
             @click="loadEmojiInsane"
-            >
+          >
             {{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
           </button>
         </div>
-- 
cgit v1.2.3-70-g09d2


From 6ade3be5b40816f8765e657442aa2945e51ce7d4 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 27 Oct 2019 19:20:38 +0200
Subject: fix loadEmojiInsane button submitting the form

---
 src/components/emoji_picker/emoji_picker.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 56501467..e46a5ec1 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -101,7 +101,7 @@
           </div>
           <button
             class="btn btn-default"
-            @click="loadEmojiInsane"
+            @click.prevent="loadEmojiInsane"
           >
             {{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
           </button>
-- 
cgit v1.2.3-70-g09d2


From 9338c81f4a68d217a70ffeefc4faab130d965df5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 8 Nov 2019 21:25:13 +0200
Subject: replace sanity button with loading on scroll

---
 src/components/emoji_picker/emoji_picker.js  | 81 ++++++++++++++--------------
 src/components/emoji_picker/emoji_picker.vue | 17 +-----
 2 files changed, 44 insertions(+), 54 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index a31adb63..92abf7a2 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,9 +1,12 @@
 import { set } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
 
-const LOAD_EMOJI_BY = 50
-const LOAD_EMOJI_INTERVAL = 100
-const LOAD_EMOJI_SANE_AMOUNT = 500
+// At widest, approximately 20 emoji are visible in a row,
+// loading 3 rows, could be overkill for narrow picker
+const LOAD_EMOJI_BY = 60
+
+// When to start loading new batch emoji, in pixels
+const LOAD_EMOJI_MARGIN = 64
 
 const filterByKeyword = (list, keyword = '') => {
   return list.filter(x => x.displayText.includes(keyword))
@@ -34,10 +37,22 @@ const EmojiPicker = {
     Checkbox
   },
   methods: {
+    onStickerUploaded (e) {
+      this.$emit('sticker-uploaded', e)
+    },
+    onStickerUploadFailed (e) {
+      this.$emit('sticker-upload-failed', e)
+    },
     onEmoji (emoji) {
       const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
       this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
     },
+    onScroll (e) {
+      const target = (e && e.target) || this.$refs['emoji-groups']
+      this.updateScrolledClass(target)
+      this.scrolledGroup(target)
+      this.triggerLoadMore(target)
+    },
     highlight (key) {
       const ref = this.$refs['group-' + key]
       const top = ref[0].offsetTop
@@ -47,9 +62,7 @@ const EmojiPicker = {
         this.$refs['emoji-groups'].scrollTop = top + 1
       })
     },
-    scrolledGroup (e) {
-      const target = (e && e.target) || this.$refs['emoji-groups']
-      const top = target.scrollTop + 5
+    updateScrolledClass (target) {
       if (target.scrollTop <= 5) {
         this.groupsScrolledClass = 'scrolled-top'
       } else if (target.scrollTop >= target.scrollTopMax - 5) {
@@ -57,6 +70,26 @@ const EmojiPicker = {
       } else {
         this.groupsScrolledClass = 'scrolled-middle'
       }
+    },
+    triggerLoadMore (target) {
+      const ref = this.$refs['group-end-custom'][0]
+      const bottom = ref.offsetTop + ref.offsetHeight
+
+      const scrollerBottom = target.scrollTop + target.clientHeight
+      const scrollerTop = target.scrollTop
+
+      // Loads more emoji when they come into view
+      const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
+      // Always load when at the very top in case there's no scroll space yet
+      const atTop = scrollerTop < 5
+      // Don't load when looking at unicode category
+      const bottomAboveViewport = bottom < scrollerTop
+      if (!bottomAboveViewport && (approachingBottom || atTop)) {
+        this.loadEmoji()
+      }
+    },
+    scrolledGroup (target) {
+      const top = target.scrollTop + 5
       this.$nextTick(() => {
         this.emojisView.forEach(group => {
           const ref = this.$refs['group-' + group.id]
@@ -66,56 +99,34 @@ const EmojiPicker = {
         })
       })
     },
-    loadEmojiInsane () {
-      this.customEmojiLoadAllConfirmed = true
-      this.continueEmojiLoad()
-    },
     loadEmoji () {
       const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
-      const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
-            !this.customEmojiLoadAllConfirmed
 
-      if (allLoaded || saneLoaded) {
+      if (allLoaded) {
         return
       }
 
       this.customEmojiBufferSlice += LOAD_EMOJI_BY
-      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
     },
     startEmojiLoad (forceUpdate = false) {
       const bufferSize = this.customEmojiBuffer.length
-      const bufferPrefilledSane = bufferSize === LOAD_EMOJI_SANE_AMOUNT && !this.customEmojiLoadAllConfirmed
       const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
-      if ((bufferPrefilledSane || bufferPrefilledAll) && !forceUpdate) {
+      if (bufferPrefilledAll && !forceUpdate) {
         return
       }
-      if (this.customEmojiTimeout) {
-        window.clearTimeout(this.customEmojiTimeout)
-      }
-
       this.customEmojiBufferSlice = LOAD_EMOJI_BY
-      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
-    },
-    continueEmojiLoad () {
-      this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
     },
     toggleStickers () {
       this.showingStickers = !this.showingStickers
     },
     setShowStickers (value) {
       this.showingStickers = value
-    },
-    onStickerUploaded (e) {
-      this.$emit('sticker-uploaded', e)
-    },
-    onStickerUploadFailed (e) {
-      this.$emit('sticker-upload-failed', e)
     }
   },
   watch: {
     keyword () {
       this.customEmojiLoadAllConfirmed = false
-      this.scrolledGroup()
+      this.onScroll()
       this.startEmojiLoad(true)
     }
   },
@@ -129,10 +140,6 @@ const EmojiPicker = {
       }
       return 0
     },
-    saneAmount () {
-      // for UI
-      return LOAD_EMOJI_SANE_AMOUNT
-    },
     filteredEmoji () {
       return filterByKeyword(
         this.$store.state.instance.customEmoji || [],
@@ -142,10 +149,6 @@ const EmojiPicker = {
     customEmojiBuffer () {
       return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
     },
-    askForSanity () {
-      return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
-        !this.customEmojiLoadAllConfirmed
-    },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
       const customEmojis = this.customEmojiBuffer
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 43da6aa2..9998131d 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -47,7 +47,7 @@
           ref="emoji-groups"
           class="emoji-groups"
           :class="groupsScrolledClass"
-          @scroll="scrolledGroup"
+          @scroll="onScroll"
         >
           <div
             v-for="group in emojisView"
@@ -73,6 +73,7 @@
                 :src="emoji.imageUrl"
               >
             </span>
+            <span :ref="'group-end-' + group.id"/>
           </div>
         </div>
         <div class="keep-open">
@@ -80,20 +81,6 @@
             {{ $t('emoji.keep_open') }}
           </Checkbox>
         </div>
-        <div
-          v-if="askForSanity"
-          class="too-many-emoji"
-        >
-          <div class="alert warning hint">
-            {{ $t('emoji.load_all_hint', { saneAmount } ) }}
-          </div>
-          <button
-            class="btn btn-default"
-            @click.prevent="loadEmojiInsane"
-          >
-            {{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
-          </button>
-        </div>
       </div>
       <div
         v-if="showingStickers"
-- 
cgit v1.2.3-70-g09d2


From a69946923b3931a8308e44a7d1ad254e6b95855b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 8 Nov 2019 21:35:58 +0200
Subject: eslint fix

---
 src/components/checkbox/checkbox.vue                 | 2 +-
 src/components/emoji_picker/emoji_picker.js          | 1 -
 src/components/emoji_picker/emoji_picker.vue         | 2 +-
 src/components/post_status_form/post_status_form.vue | 2 +-
 4 files changed, 3 insertions(+), 4 deletions(-)

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

diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 5917598a..1113f81d 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -12,8 +12,8 @@
     >
     <i class="checkbox-indicator" />
     <span
-      class="label"
       v-if="!!$slots.default"
+      class="label"
     >
       <slot />
     </span>
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 782c4dfc..d46dbaa5 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,4 +1,3 @@
-import { set } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
 
 // At widest, approximately 20 emoji are visible in a row,
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 9998131d..191b9fa1 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -73,7 +73,7 @@
                 :src="emoji.imageUrl"
               >
             </span>
-            <span :ref="'group-end-' + group.id"/>
+            <span :ref="'group-end-' + group.id" />
           </div>
         </div>
         <div class="keep-open">
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 13e8b0aa..0094b1aa 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -163,7 +163,7 @@
       <div
         ref="bottom"
         class="form-bottom"
-        >
+      >
         <div class="form-bottom-left">
           <media-upload
             ref="mediaUpload"
-- 
cgit v1.2.3-70-g09d2

