aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/conversation/conversation.js1
-rw-r--r--src/components/react_button/react_button.js49
-rw-r--r--src/components/react_button/react_button.vue107
-rw-r--r--src/components/status/status.js21
-rw-r--r--src/components/status/status.vue48
5 files changed, 226 insertions, 0 deletions
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 08283fff..7ff0ac08 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -150,6 +150,7 @@ const conversation = {
if (!id) return
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
+ this.$store.dispatch('fetchEmojiReactions', id)
},
getHighlight () {
return this.isExpanded ? this.highlight : null
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
new file mode 100644
index 00000000..76a49305
--- /dev/null
+++ b/src/components/react_button/react_button.js
@@ -0,0 +1,49 @@
+import { mapGetters } from 'vuex'
+
+const ReactButton = {
+ props: ['status', 'loggedIn'],
+ data () {
+ return {
+ animated: false,
+ showTooltip: false,
+ filterWord: '',
+ popperOptions: {
+ modifiers: {
+ preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
+ }
+ }
+ }
+ },
+ methods: {
+ toggleReactionSelect () {
+ this.showTooltip = !this.showTooltip
+ },
+ closeReactionSelect () {
+ this.showTooltip = false
+ },
+ addReaction (event, emoji) {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ this.closeReactionSelect()
+ }
+ },
+ computed: {
+ commonEmojis () {
+ return ['💖', '😠', '👀', '😂', '🔥']
+ },
+ emojis () {
+ if (this.filterWord !== '') {
+ return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
+ }
+ return this.$store.state.instance.emoji || []
+ },
+ classes () {
+ return {
+ 'icon-smile': true,
+ 'animate-spin': this.animated
+ }
+ },
+ ...mapGetters(['mergedConfig'])
+ }
+}
+
+export default ReactButton
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
new file mode 100644
index 00000000..f7015316
--- /dev/null
+++ b/src/components/react_button/react_button.vue
@@ -0,0 +1,107 @@
+<template>
+ <v-popover
+ :popper-options="popperOptions"
+ :open="showTooltip"
+ trigger="manual"
+ placement="top"
+ class="react-button-popover"
+ @hide="closeReactionSelect"
+ >
+ <div slot="popover">
+ <div class="reaction-picker-filter">
+ <input v-model="filterWord" placeholder="Search...">
+ </div>
+ <div class="reaction-picker">
+ <span
+ v-for="emoji in commonEmojis"
+ :key="emoji"
+ class="emoji-reaction-button"
+ @click="addReaction($event, emoji)"
+ >
+ {{ emoji }}
+ </span>
+ <div class="reaction-picker-divider" />
+ <span
+ v-for="(emoji, key) in emojis"
+ :key="key"
+ class="emoji-reaction-button"
+ @click="addReaction($event, emoji.replacement)"
+ >
+ {{ emoji.replacement }}
+ </span>
+ <div class="reaction-bottom-fader" />
+ </div>
+ </div>
+ <div
+ v-if="loggedIn"
+ @click.prevent="toggleReactionSelect"
+ >
+ <i
+ :class="classes"
+ class="button-icon favorite-button fav-active"
+ :title="$t('tool_tip.add_reaction')"
+ />
+ <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
+ </div>
+ </v-popover>
+</template>
+
+<script src="./react_button.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.reaction-picker-filter {
+ padding: 0.5em;
+}
+
+.reaction-picker-divider {
+ height: 1px;
+ width: 100%;
+ margin: 0.4em;
+ background-color: var(--border, $fallback--border);
+}
+
+.reaction-picker {
+ width: 10em;
+ height: 9em;
+ font-size: 1.5em;
+ overflow-y: scroll;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0.5em;
+ text-align: center;
+ align-content: flex-start;
+ 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);
+ 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;
+}
+
+.emoji-reaction-button {
+ flex-basis: 20%;
+ line-height: 1.5em;
+ align-content: center;
+}
+
+.fav-active {
+ cursor: pointer;
+ animation-duration: 0.6s;
+
+ &:hover {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+ }
+}
+
+.favorite-button.icon-star {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+}
+</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index c49e729c..18617938 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,5 +1,6 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
+import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
@@ -310,6 +311,9 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
+ emojiReactions () {
+ return this.status.emojiReactions
+ },
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
@@ -319,6 +323,7 @@ const Status = {
components: {
Attachment,
FavoriteButton,
+ ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
@@ -413,6 +418,22 @@ const Status = {
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
+ },
+ reactedWith (emoji) {
+ return this.status.reactedWithEmoji.includes(emoji)
+ },
+ reactWith (emoji) {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ },
+ unreact (emoji) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ },
+ emojiOnClick (emoji, event) {
+ if (this.reactedWith(emoji)) {
+ this.unreact(emoji)
+ } else {
+ this.reactWith(emoji)
+ }
}
},
watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d291e762..4ea1b74b 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -354,6 +354,19 @@
</div>
</transition>
+ <div v-if="isFocused" class="emoji-reactions">
+ <button
+ v-for="(users, emoji) in emojiReactions"
+ :key="emoji"
+ class="emoji-reaction btn btn-default"
+ :class="{ 'picked-reaction': reactedWith(emoji) }"
+ @click="emojiOnClick(emoji, $event)"
+ >
+ <span v-if="users">{{ users.length }}</span>
+ <span>{{ emoji }}</span>
+ </button>
+ </div>
+
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
@@ -382,6 +395,10 @@
:logged-in="loggedIn"
:status="status"
/>
+ <ReactButton
+ :logged-in="loggedIn"
+ :status="status"
+ />
<extra-buttons
:status="status"
@onError="showError"
@@ -772,6 +789,37 @@ $status-margin: 0.75em;
}
}
+.emoji-reactions {
+ display: flex;
+ margin-top: 0.25em;
+ flex-wrap: wrap;
+}
+
+.emoji-reaction {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ :first-child {
+ margin-right: 0.25em;
+ }
+ :last-child {
+ width: 1.5em;
+ }
+ &:focus {
+ outline: none;
+ }
+}
+
+.picked-reaction {
+ border: 1px solid var(--link, $fallback--link);
+ margin-left: -1px; // offset the border, can't use inset shadows either
+ margin-right: calc(0.5em - 1px);
+}
+
.button-icon.icon-reply {
&:not(.button-icon-disabled):hover,
&.button-icon-active {