197 lines
5.2 KiB
Vue
197 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import type {IReactionPerEntity, ReactionKind} from '@/modelTypes/IReaction'
|
|
import {VuemojiPicker} from 'vuemoji-picker'
|
|
import ReactionService from '@/services/reactions'
|
|
import ReactionModel from '@/models/reaction'
|
|
import BaseButton from '@/components/base/BaseButton.vue'
|
|
import type {IUser} from '@/modelTypes/IUser'
|
|
import {getDisplayName} from '@/models/user'
|
|
import {useI18n} from 'vue-i18n'
|
|
import {nextTick, onBeforeUnmount, onMounted, ref} from 'vue'
|
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
|
import {useAuthStore} from '@/stores/auth'
|
|
import {useColorScheme} from '@/composables/useColorScheme'
|
|
|
|
const {
|
|
entityKind,
|
|
entityId,
|
|
disabled = false,
|
|
} = defineProps<{
|
|
entityKind: ReactionKind,
|
|
entityId: number,
|
|
disabled?: boolean,
|
|
}>()
|
|
|
|
const authStore = useAuthStore()
|
|
const {t} = useI18n()
|
|
const reactionService = new ReactionService()
|
|
const {isDark} = useColorScheme()
|
|
|
|
const model = defineModel<IReactionPerEntity>()
|
|
|
|
async function addReaction(value: string) {
|
|
const reaction = new ReactionModel({
|
|
id: entityId,
|
|
kind: entityKind,
|
|
value,
|
|
})
|
|
await reactionService.create(reaction)
|
|
showEmojiPicker.value = false
|
|
|
|
if (typeof model.value === 'undefined') {
|
|
model.value = {}
|
|
}
|
|
|
|
if (typeof model.value[reaction.value] === 'undefined') {
|
|
model.value[reaction.value] = [authStore.info]
|
|
} else {
|
|
model.value[reaction.value].push(authStore.info)
|
|
}
|
|
}
|
|
|
|
async function removeReaction(value: string) {
|
|
const reaction = new ReactionModel({
|
|
id: entityId,
|
|
kind: entityKind,
|
|
value,
|
|
})
|
|
await reactionService.delete(reaction)
|
|
showEmojiPicker.value = false
|
|
|
|
const userIndex = model.value[reaction.value].findIndex(u => u.id === authStore.info?.id)
|
|
if (userIndex !== -1) {
|
|
model.value[reaction.value].splice(userIndex, 1)
|
|
}
|
|
if(model.value[reaction.value].length === 0) {
|
|
delete model.value[reaction.value]
|
|
}
|
|
}
|
|
|
|
function getReactionTooltip(users: IUser[], value: string) {
|
|
const names = users.map(u => getDisplayName(u))
|
|
|
|
if (names.length === 1) {
|
|
return t('reaction.reactedWith', {user: names[0], value})
|
|
}
|
|
|
|
if (names.length > 1 && names.length < 10) {
|
|
return t('reaction.reactedWithAnd', {
|
|
users: names.slice(0, names.length - 1).join(', '),
|
|
lastUser: names[names.length - 1],
|
|
value,
|
|
})
|
|
}
|
|
|
|
return t('reaction.reactedWithAndMany', {
|
|
users: names.slice(0, 10).join(', '),
|
|
num: names.length - 10,
|
|
value,
|
|
})
|
|
}
|
|
|
|
const showEmojiPicker = ref(false)
|
|
const emojiPickerRef = ref<HTMLElement | null>(null)
|
|
|
|
function hideEmojiPicker(e: MouseEvent) {
|
|
if (showEmojiPicker.value) {
|
|
closeWhenClickedOutside(e, emojiPickerRef.value.$el, () => showEmojiPicker.value = false)
|
|
}
|
|
}
|
|
|
|
onMounted(() => document.addEventListener('click', hideEmojiPicker))
|
|
onBeforeUnmount(() => document.removeEventListener('click', hideEmojiPicker))
|
|
|
|
const emojiPickerButtonRef = ref<HTMLElement | null>(null)
|
|
const reactionContainerRef = ref<HTMLElement | null>(null)
|
|
const emojiPickerPosition = ref()
|
|
|
|
function toggleEmojiPicker() {
|
|
if (!showEmojiPicker.value) {
|
|
const rect = emojiPickerButtonRef.value?.$el.getBoundingClientRect()
|
|
const container = reactionContainerRef.value?.getBoundingClientRect()
|
|
const left = rect.left - container.left + rect.width
|
|
|
|
emojiPickerPosition.value = {
|
|
left: left === 0 ? undefined : left,
|
|
}
|
|
}
|
|
|
|
nextTick(() => showEmojiPicker.value = !showEmojiPicker.value)
|
|
}
|
|
|
|
function hasCurrentUserReactedWithEmoji(value: string): boolean {
|
|
const user = model.value[value].find(u => u.id === authStore.info.id)
|
|
return typeof user !== 'undefined'
|
|
}
|
|
|
|
async function toggleReaction(value: string) {
|
|
if (hasCurrentUserReactedWithEmoji(value)) {
|
|
return removeReaction(value)
|
|
}
|
|
|
|
return addReaction(value)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="reactionContainerRef"
|
|
class="reactions"
|
|
>
|
|
<BaseButton
|
|
v-for="(users, value) in (model as IReactionPerEntity)"
|
|
:key="'button' + value"
|
|
v-tooltip="getReactionTooltip(users, value)"
|
|
class="reaction-button"
|
|
:class="{'current-user-has-reacted': hasCurrentUserReactedWithEmoji(value)}"
|
|
:disabled
|
|
@click="toggleReaction(value)"
|
|
>
|
|
{{ value }} {{ users.length }}
|
|
</BaseButton>
|
|
<BaseButton
|
|
v-if="!disabled"
|
|
ref="emojiPickerButtonRef"
|
|
v-tooltip="$t('reaction.add')"
|
|
class="reaction-button"
|
|
@click.stop="toggleEmojiPicker"
|
|
>
|
|
<Icon :icon="['far', 'face-laugh']" />
|
|
</BaseButton>
|
|
<CustomTransition name="fade">
|
|
<VuemojiPicker
|
|
v-if="showEmojiPicker"
|
|
ref="emojiPickerRef"
|
|
class="emoji-picker"
|
|
:style="{left: emojiPickerPosition?.left + 'px'}"
|
|
data-source="/emojis.json"
|
|
:is-dark="isDark"
|
|
@emojiClick="detail => addReaction(detail.unicode)"
|
|
/>
|
|
</CustomTransition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.reaction-button {
|
|
margin-right: .25rem;
|
|
margin-bottom: .25rem;
|
|
padding: .175rem .5rem .15rem;
|
|
border: 1px solid var(--grey-400);
|
|
background: var(--grey-100);
|
|
border-radius: 100px;
|
|
font-size: .75rem;
|
|
|
|
&.current-user-has-reacted {
|
|
border-color: var(--primary);
|
|
background-color: hsla(var(--primary-h), var(--primary-s), var(--primary-light-l), 0.5);
|
|
}
|
|
}
|
|
|
|
.emoji-picker {
|
|
position: absolute;
|
|
z-index: 99;
|
|
margin-top: .5rem;
|
|
}
|
|
</style> |