Allow custom emoji reactions

This commit is contained in:
Alexander Tumin 2022-12-18 22:04:58 +03:00
parent 8abaf8fa37
commit 998aa8f732
6 changed files with 89 additions and 161 deletions

View File

@ -2,7 +2,7 @@
<div class="EmojiReactions"> <div class="EmojiReactions">
<UserListPopover <UserListPopover
v-for="(reaction) in emojiReactions" v-for="(reaction) in emojiReactions"
:key="reaction.name" :key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.name]" :users="accountsForEmoji[reaction.name]"
> >
<button <button
@ -11,7 +11,21 @@
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >
<span class="reaction-emoji">{{ reaction.name }}</span> <span
v-if="reaction.url"
class="reaction-emoji"
>
<img
:src="reaction.url"
:title="reaction.name"
class="reaction-emoji-content"
width="1em"
>
</span>
<span
v-else
class="reaction-emoji reaction-emoji-content"
>{{ reaction.name }}</span>
<span>{{ reaction.count }}</span> <span>{{ reaction.count }}</span>
</button> </button>
</UserListPopover> </UserListPopover>
@ -46,9 +60,18 @@
.reaction-emoji { .reaction-emoji {
width: 1.25em; width: 1.25em;
height: 1.25em;
margin-right: 0.25em; margin-right: 0.25em;
} }
.reaction-emoji-content {
max-width: 1.25em;
max-height: 1.25em;
width: auto;
height: auto;
overflow: hidden;
}
&:focus { &:focus {
outline: none; outline: none;
} }

View File

@ -121,7 +121,16 @@
scope="global" scope="global"
keypath="notifications.reacted_with" 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> </i18n-t>
</small> </small>
</span> </span>
@ -153,9 +162,9 @@
</router-link> </router-link>
<button <button
class="button-unstyled expand-icon" class="button-unstyled expand-icon"
@click.prevent="toggleStatusExpanded"
:title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded" :aria-expanded="statusExpanded"
:title="$t('tool_tip.toggle_expand')"
@click.prevent="toggleStatusExpanded"
> >
<FAIcon <FAIcon
class="fa-scale-110" class="fa-scale-110"

View File

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

View File

@ -1,9 +1,8 @@
import Popover from '../popover/popover.vue' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
import { trim } from 'lodash'
library.add( library.add(
faPlus, faPlus,
@ -20,103 +19,32 @@ const ReactButton = {
} }
}, },
components: { components: {
Popover Popover,
EmojiPicker
}, },
methods: { methods: {
addReaction (event, emoji, close) { addReaction (event) {
const emoji = event.insertion
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji) const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
if (existingReaction && existingReaction.me) { if (existingReaction && existingReaction.me) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
} else { } else {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
} }
close() },
show () {
if (!this.expanded) {
this.$refs.picker.showPicker()
}
}, },
onShow () { onShow () {
this.expanded = true this.expanded = true
this.focusInput()
}, },
onClose () { onClose () {
this.expanded = false 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: { 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 () { mergedConfig () {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
} }

View File

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

View File

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