Kaynağa Gözat

Merge branch 'pleroma-akkoma-emoji-port' into 'develop'

Custom emoji reaction support

See merge request pleroma/pleroma-fe!1792
HJ 1 yıl önce
ebeveyn
işleme
fa532b1f00

+ 1 - 0
src/boot/after_store.js

@@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
       store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
       store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
       store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
+      store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
       store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
       store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
       store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })

+ 8 - 0
src/components/emoji_picker/emoji_picker.js

@@ -98,6 +98,11 @@ const EmojiPicker = {
       required: false,
       type: Boolean,
       default: false
+    },
+    hideCustomEmoji: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   data () {
@@ -280,6 +285,9 @@ const EmojiPicker = {
       return 0
     },
     allCustomGroups () {
+      if (this.hideCustomEmoji) {
+        return {}
+      }
       const emojis = this.$store.getters.groupedCustomEmojis
       if (emojis.unpacked) {
         emojis.unpacked.text = this.$t('emoji.unpacked')

+ 35 - 3
src/components/emoji_reactions/emoji_reactions.vue

@@ -2,7 +2,7 @@
   <div class="EmojiReactions">
     <UserListPopover
       v-for="(reaction) in emojiReactions"
-      :key="reaction.name"
+      :key="reaction.url || reaction.name"
       :users="accountsForEmoji[reaction.name]"
     >
       <button
@@ -11,7 +11,21 @@
         @click="emojiOnClick(reaction.name, $event)"
         @mouseenter="fetchEmojiReactionsByIfMissing()"
       >
-        <span class="reaction-emoji">{{ reaction.name }}</span>
+        <span
+          class="reaction-emoji"
+        >
+          <img
+            v-if="reaction.url"
+            :src="reaction.url"
+            :title="reaction.name"
+            class="reaction-emoji-content"
+            width="1em"
+          >
+          <span
+            v-else
+            class="reaction-emoji reaction-emoji-content"
+          >{{ reaction.name }}</span>
+        </span>
         <span>{{ reaction.count }}</span>
       </button>
     </UserListPopover>
@@ -35,6 +49,8 @@
   margin-top: 0.25em;
   flex-wrap: wrap;
 
+  --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
+
   .emoji-reaction {
     padding: 0 0.5em;
     margin-right: 0.5em;
@@ -45,8 +61,24 @@
     box-sizing: border-box;
 
     .reaction-emoji {
-      width: 1.25em;
+      width: var(--emoji-size);
+      height: var(--emoji-size);
       margin-right: 0.25em;
+      line-height: var(--emoji-size);
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .reaction-emoji-content {
+      max-width: 100%;
+      max-height: 100%;
+      width: auto;
+      height: auto;
+      line-height: inherit;
+      overflow: hidden;
+      font-size: calc(var(--emoji-size) * 0.8);
+      margin: 0;
     }
 
     &:focus {

+ 12 - 3
src/components/notification/notification.vue

@@ -121,7 +121,16 @@
                   scope="global"
                   keypath="notifications.reacted_with"
                 >
-                  <span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
+                  <img
+                    v-if="notification.emoji_url"
+                    class="emoji-reaction-emoji emoji-reaction-emoji-image"
+                    :src="notification.emoji_url"
+                    :name="notification.emoji"
+                  >
+                  <span
+                    v-else
+                    class="emoji-reaction-emoji"
+                  >{{ notification.emoji }}</span>
                 </i18n-t>
               </small>
             </span>
@@ -153,9 +162,9 @@
             </router-link>
             <button
               class="button-unstyled expand-icon"
-              @click.prevent="toggleStatusExpanded"
-              :title="$t('tool_tip.toggle_expand')"
               :aria-expanded="statusExpanded"
+              :title="$t('tool_tip.toggle_expand')"
+              @click.prevent="toggleStatusExpanded"
             >
               <FAIcon
                 class="fa-scale-110"

+ 7 - 0
src/components/notifications/notifications.scss

@@ -129,6 +129,13 @@
 
   .emoji-reaction-emoji {
     font-size: 1.3em;
+    max-width: 1.25em;
+    height: 1.25em;
+    width: auto;
+  }
+
+  .emoji-reaction-emoji-image {
+    vertical-align: middle;
   }
 
   .notification-details {

+ 12 - 84
src/components/react_button/react_button.js

@@ -1,9 +1,8 @@
 import Popover from '../popover/popover.vue'
-import { ensureFinalFallback } from '../../i18n/languages.js'
+import EmojiPicker from '../emoji_picker/emoji_picker.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
 import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
-import { trim } from 'lodash'
 
 library.add(
   faPlus,
@@ -20,105 +19,34 @@ const ReactButton = {
     }
   },
   components: {
-    Popover
+    Popover,
+    EmojiPicker
   },
   methods: {
-    addReaction (event, emoji, close) {
+    addReaction (event) {
+      const emoji = event.insertion
       const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
       if (existingReaction && existingReaction.me) {
         this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
       } else {
         this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
       }
-      close()
+    },
+    show () {
+      if (!this.expanded) {
+        this.$refs.picker.showPicker()
+      }
     },
     onShow () {
       this.expanded = true
-      this.focusInput()
     },
     onClose () {
       this.expanded = false
-    },
-    focusInput () {
-      this.$nextTick(() => {
-        const input = document.querySelector('.reaction-picker-filter > input')
-        if (input) input.focus()
-      })
-    },
-    // Vaguely adjusted copypaste from emoji_input and emoji_picker!
-    maybeLocalizedEmojiNamesAndKeywords (emoji) {
-      const names = [emoji.displayText]
-      const keywords = []
-
-      if (emoji.displayTextI18n) {
-        names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
-      }
-
-      if (emoji.annotations) {
-        this.languages.forEach(lang => {
-          names.push(emoji.annotations[lang]?.name)
-
-          keywords.push(...(emoji.annotations[lang]?.keywords || []))
-        })
-      }
-
-      return {
-        names: names.filter(k => k),
-        keywords: keywords.filter(k => k)
-      }
-    },
-    maybeLocalizedEmojiName (emoji) {
-      if (!emoji.annotations) {
-        return emoji.displayText
-      }
-
-      if (emoji.displayTextI18n) {
-        return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
-      }
-
-      for (const lang of this.languages) {
-        if (emoji.annotations[lang]?.name) {
-          return emoji.annotations[lang].name
-        }
-      }
-
-      return emoji.displayText
     }
   },
   computed: {
-    commonEmojis () {
-      const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥'])
-      return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement))
-    },
-    languages () {
-      return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
-    },
-    emojis () {
-      if (this.filterWord !== '') {
-        const keywordLowercase = trim(this.filterWord.toLowerCase())
-
-        const orderedEmojiList = []
-        for (const emoji of this.$store.getters.standardEmojiList) {
-          const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji)
-            .keywords
-            .map(k => k.toLowerCase().indexOf(keywordLowercase))
-            .filter(k => k > -1)
-
-          const indexOfKeyword = indices.length ? Math.min(...indices) : -1
-
-          if (indexOfKeyword > -1) {
-            if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
-              orderedEmojiList[indexOfKeyword] = []
-            }
-            orderedEmojiList[indexOfKeyword].push(emoji)
-          }
-        }
-        return orderedEmojiList.flat()
-      }
-      return this.$store.getters.standardEmojiList || []
-    },
-    mergedConfig () {
-      return this.$store.getters.mergedConfig
+    hideCustomEmoji () {
+      return !this.$store.state.instance.pleromaChatMessagesAvailable
     }
   }
 }

+ 35 - 74
src/components/react_button/react_button.vue

@@ -1,73 +1,39 @@
 <template>
-  <Popover
-    trigger="click"
-    class="ReactButton"
-    placement="top"
-    :offset="{ y: 5 }"
-    :bound-to="{ x: 'container' }"
-    remove-padding
-    popover-class="ReactButton popover-default"
-    @show="onShow"
-    @close="onClose"
-  >
-    <template #content="{close}">
-      <div class="reaction-picker-filter">
-        <input
-          v-model="filterWord"
-          size="1"
-          :placeholder="$t('emoji.search_emoji')"
-          @input="$event.target.composing = false"
-        >
-      </div>
-      <div class="reaction-picker">
-        <span
-          v-for="emoji in commonEmojis"
-          :key="emoji.replacement"
-          class="emoji-button"
-          :title="maybeLocalizedEmojiName(emoji)"
-          @click="addReaction($event, emoji.replacement, close)"
-        >
-          {{ emoji.replacement }}
-        </span>
-        <div class="reaction-picker-divider" />
-        <span
-          v-for="(emoji, key) in emojis"
-          :key="key"
-          class="emoji-button"
-          :title="maybeLocalizedEmojiName(emoji)"
-          @click="addReaction($event, emoji.replacement, close)"
-        >
-          {{ emoji.replacement }}
-        </span>
-        <div class="reaction-bottom-fader" />
-      </div>
-    </template>
-    <template #trigger>
-      <span
-        class="button-unstyled popover-trigger"
-        :title="$t('tool_tip.add_reaction')"
-      >
-        <FALayers>
-          <FAIcon
-            class="fa-scale-110 fa-old-padding"
-            :icon="['far', 'smile-beam']"
-          />
-          <FAIcon
-            v-show="!expanded"
-            class="focus-marker"
-            transform="shrink-6 up-9 right-17"
-            icon="plus"
-          />
-          <FAIcon
-            v-show="expanded"
-            class="focus-marker"
-            transform="shrink-6 up-9 right-17"
-            icon="times"
-          />
-        </FALayers>
-      </span>
-    </template>
-  </Popover>
+  <span class="ReactButton">
+    <EmojiPicker
+      ref="picker"
+      :enable-sticker-picker="enableStickerPicker"
+      :hide-custom-emoji="hideCustomEmoji"
+      class="emoji-picker-panel"
+      @emoji="addReaction"
+      @show="onShow"
+      @close="onClose"
+    />
+    <span
+      class="button-unstyled popover-trigger"
+      :title="$t('tool_tip.add_reaction')"
+      @click.stop.prevent="show"
+    >
+      <FALayers>
+        <FAIcon
+          class="fa-scale-110 fa-old-padding"
+          :icon="['far', 'smile-beam']"
+        />
+        <FAIcon
+          v-show="!expanded"
+          class="focus-marker"
+          transform="shrink-6 up-9 right-17"
+          icon="plus"
+        />
+        <FAIcon
+          v-show="expanded"
+          class="focus-marker"
+          transform="shrink-6 up-9 right-17"
+          icon="times"
+        />
+      </FALayers>
+    </span>
+  </span>
 </template>
 
 <script src="./react_button.js"></script>
@@ -135,11 +101,6 @@
       color: $fallback--text;
       color: var(--text, $fallback--text);
     }
-  }
-
-  .popover-trigger-button {
-    /* override of popover internal stuff */
-    width: auto;
 
     @include unfocused-style {
       .focus-marker {

+ 16 - 0
src/components/settings_modal/helpers/float_setting.vue

@@ -0,0 +1,16 @@
+<template>
+  <NumberSetting
+    v-bind="$attrs"
+  >
+    <slot />
+  </NumberSetting>
+</template>
+
+<script>
+import NumberSetting from './number_setting.vue'
+export default {
+  components: {
+    NumberSetting
+  }
+}
+</script>

+ 13 - 23
src/components/settings_modal/helpers/integer_setting.vue

@@ -1,27 +1,17 @@
 <template>
-  <span
-    v-if="matchesExpertLevel"
-    class="IntegerSetting"
+  <NumberSetting
+    v-bind="$attrs"
+    truncate="1"
   >
-    <label :for="path">
-      <slot />
-    </label>
-    <input
-      :id="path"
-      class="number-input"
-      type="number"
-      step="1"
-      :disabled="disabled"
-      :min="min || 0"
-      :value="state"
-      @change="update"
-    >
-    {{ ' ' }}
-    <ModifiedIndicator
-      :changed="isChanged"
-      :onclick="reset"
-    />
-  </span>
+    <slot />
+  </NumberSetting>
 </template>
 
-<script src="./integer_setting.js"></script>
+<script>
+import NumberSetting from './number_setting.vue'
+export default {
+  components: {
+    NumberSetting
+  }
+}
+</script>

+ 17 - 5
src/components/settings_modal/helpers/integer_setting.js → src/components/settings_modal/helpers/number_setting.js

@@ -8,6 +8,8 @@ export default {
     path: String,
     disabled: Boolean,
     min: Number,
+    step: Number,
+    truncate: Number,
     expert: [Number, String]
   },
   computed: {
@@ -15,8 +17,11 @@ export default {
       const [firstSegment, ...rest] = this.path.split('.')
       return [firstSegment + 'DefaultValue', ...rest].join('.')
     },
+    parent () {
+      return this.$parent.$parent
+    },
     state () {
-      const value = get(this.$parent, this.path)
+      const value = get(this.parent, this.path)
       if (value === undefined) {
         return this.defaultState
       } else {
@@ -24,21 +29,28 @@ export default {
       }
     },
     defaultState () {
-      return get(this.$parent, this.pathDefault)
+      return get(this.parent, this.pathDefault)
     },
     isChanged () {
       return this.state !== this.defaultState
     },
     matchesExpertLevel () {
-      return (this.expert || 0) <= this.$parent.expertLevel
+      return (this.expert || 0) <= this.parent.expertLevel
     }
   },
   methods: {
+    truncateValue (value) {
+      if (!this.truncate) {
+        return value
+      }
+
+      return Math.trunc(value / this.truncate) * this.truncate
+    },
     update (e) {
-      set(this.$parent, this.path, parseInt(e.target.value))
+      set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value)))
     },
     reset () {
-      set(this.$parent, this.path, this.defaultState)
+      set(this.parent, this.path, this.defaultState)
     }
   }
 }

+ 27 - 0
src/components/settings_modal/helpers/number_setting.vue

@@ -0,0 +1,27 @@
+<template>
+  <span
+    v-if="matchesExpertLevel"
+    class="NumberSetting"
+  >
+    <label :for="path">
+      <slot />
+    </label>
+    <input
+      :id="path"
+      class="number-input"
+      type="number"
+      :step="step || 1"
+      :disabled="disabled"
+      :min="min || 0"
+      :value="state"
+      @change="update"
+    >
+    {{ ' ' }}
+    <ModifiedIndicator
+      :changed="isChanged"
+      :onclick="reset"
+    />
+  </span>
+</template>
+
+<script src="./number_setting.js"></script>

+ 2 - 0
src/components/settings_modal/tabs/general_tab.js

@@ -2,6 +2,7 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
 import ChoiceSetting from '../helpers/choice_setting.vue'
 import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
 import IntegerSetting from '../helpers/integer_setting.vue'
+import FloatSetting from '../helpers/float_setting.vue'
 import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
 import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 
@@ -62,6 +63,7 @@ const GeneralTab = {
     BooleanSetting,
     ChoiceSetting,
     IntegerSetting,
+    FloatSetting,
     SizeSetting,
     InterfaceLanguageSwitcher,
     ScopeSelector,

+ 9 - 0
src/components/settings_modal/tabs/general_tab.vue

@@ -271,6 +271,15 @@
             {{ $t('settings.no_rich_text_description') }}
           </BooleanSetting>
         </li>
+        <li>
+          <FloatSetting
+            v-if="user"
+            path="emojiReactionsScale"
+            expert="1"
+          >
+            {{ $t('settings.emoji_reactions_scale') }}
+          </FloatSetting>
+        </li>
         <h3>{{ $t('settings.attachments') }}</h3>
         <li>
           <BooleanSetting

+ 1 - 0
src/i18n/en.json

@@ -467,6 +467,7 @@
     "pad_emoji": "Pad emoji with spaces when adding from picker",
     "autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
     "emoji_reactions_on_timeline": "Show emoji reactions on timeline",
+    "emoji_reactions_scale": "Reactions scale factor",
     "export_theme": "Save preset",
     "filtering": "Filtering",
     "wordfilter": "Wordfilter",

+ 2 - 0
src/modules/config.js

@@ -97,6 +97,7 @@ export const defaultState = {
   sidebarColumnWidth: '25rem',
   contentColumnWidth: '45rem',
   notifsColumnWidth: '25rem',
+  emojiReactionsScale: 1.0,
   navbarColumnStretch: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
@@ -185,6 +186,7 @@ const config = {
         case 'sidebarColumnWidth':
         case 'contentColumnWidth':
         case 'notifsColumnWidth':
+        case 'emojiReactionsScale':
           applyConfig(state)
           break
         case 'customTheme':

+ 1 - 0
src/modules/instance.js

@@ -123,6 +123,7 @@ const defaultState = {
   // Feature-set, apparently, not everything here is reported...
   shoutAvailable: false,
   pleromaChatMessagesAvailable: false,
+  pleromaCustomEmojiReactionsAvailable: false,
   gopherAvailable: false,
   mediaProxyAvailable: false,
   suggestionsEnabled: false,

+ 1 - 0
src/services/entity_normalizer/entity_normalizer.service.js

@@ -441,6 +441,7 @@ export const parseNotification = (data) => {
       : parseUser(data.target)
     output.from_profile = parseUser(data.account)
     output.emoji = data.emoji
+    output.emoji_url = data.emoji_url
     if (data.report) {
       output.report = data.report
       output.report.content = data.report.content

+ 2 - 2
src/services/style_setter/style_setter.js

@@ -21,8 +21,8 @@ export const applyTheme = (input) => {
   body.classList.remove('hidden')
 }
 
-const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth }) =>
-  ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth })
+const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) =>
+  ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale })
 
 const defaultConfigColumns = configColumns(defaultState)