You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
6.2 KiB
279 lines
6.2 KiB
<!--
|
|
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
-->
|
|
|
|
<template>
|
|
<TransitionWrapper class="toaster"
|
|
name="toast"
|
|
tag="ul"
|
|
group>
|
|
<li v-for="toast in toasts"
|
|
:key="toast.seed"
|
|
class="toast"
|
|
:style="styled(toast.name, toast.seed)">
|
|
<img v-if="toast.reactionURL"
|
|
class="toast__reaction-img"
|
|
:src="toast.reactionURL"
|
|
:alt="toast.reaction"
|
|
width="34"
|
|
height="34">
|
|
<span v-else class="toast__reaction">
|
|
{{ toast.reaction }}
|
|
</span>
|
|
<span class="toast__name">
|
|
{{ toast.name }}
|
|
</span>
|
|
</li>
|
|
</TransitionWrapper>
|
|
</template>
|
|
|
|
<script>
|
|
import Hex from 'crypto-js/enc-hex.js'
|
|
import SHA1 from 'crypto-js/sha1.js'
|
|
|
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
import { t } from '@nextcloud/l10n'
|
|
import { imagePath } from '@nextcloud/router'
|
|
|
|
import usernameToColor from '@nextcloud/vue/dist/Functions/usernameToColor.js'
|
|
|
|
import TransitionWrapper from '../../UIShared/TransitionWrapper.vue'
|
|
|
|
import { useGuestNameStore } from '../../../stores/guestName.js'
|
|
|
|
const reactions = {
|
|
'❤️': 'Heart.gif',
|
|
'🎉': 'Party.gif',
|
|
'👏': 'Clap.gif',
|
|
'👋': 'Wave.gif',
|
|
'👍': 'Thumbs-up.gif',
|
|
'👎': 'Thumbs-down.gif',
|
|
'🔥': 'Fire.gif',
|
|
'😂': 'Joy.gif',
|
|
'🤩': 'Star-struck.gif',
|
|
'🤔': 'Thinking-face.gif',
|
|
'😲': 'Surprised.gif',
|
|
'😥': 'Concerned.gif',
|
|
}
|
|
|
|
export default {
|
|
name: 'ReactionToaster',
|
|
|
|
components: {
|
|
TransitionWrapper,
|
|
},
|
|
|
|
props: {
|
|
/**
|
|
* The conversation token
|
|
*/
|
|
token: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
/**
|
|
* Supported reactions
|
|
*/
|
|
supportedReactions: {
|
|
type: Array,
|
|
validator: (prop) => prop.every(e => typeof e === 'string'),
|
|
required: true,
|
|
},
|
|
|
|
callParticipantModels: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
},
|
|
|
|
setup() {
|
|
const guestNameStore = useGuestNameStore()
|
|
return { guestNameStore }
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
registeredModels: {},
|
|
reactionsQueue: [],
|
|
intervalId: null,
|
|
animationLength: 2000,
|
|
toasts: [],
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
participants() {
|
|
return this.$store.getters.participantsList(this.token)
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
callParticipantModels(models) {
|
|
// subscribe connected models for reaction signals
|
|
const addedModels = models.filter(model => !this.registeredModels[model.attributes.peerId])
|
|
addedModels.forEach(addedModel => {
|
|
this.registeredModels[addedModel.attributes.peerId] = addedModel
|
|
this.registeredModels[addedModel.attributes.peerId].on('reaction', this.handleReaction)
|
|
})
|
|
|
|
// unsubscribe disconnected models
|
|
const removedModelIds = Object.keys(this.registeredModels).filter(registeredModelId => !models.find(model => model.attributes.peerId === registeredModelId))
|
|
removedModelIds.forEach(removedModelId => {
|
|
this.registeredModels[removedModelId].off('reaction', this.handleReaction)
|
|
delete this.registeredModels[removedModelId]
|
|
})
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.intervalId = setInterval(this.processReactionsQueue, this.animationLength / 4)
|
|
subscribe('send-reaction', this.handleOwnReaction)
|
|
},
|
|
|
|
beforeDestroy() {
|
|
clearInterval(this.intervalId)
|
|
unsubscribe('send-reaction', this.handleOwnReaction)
|
|
Object.keys(this.registeredModels).forEach(modelId => {
|
|
this.registeredModels[modelId].off('reaction', this.handleReaction)
|
|
delete this.registeredModels[modelId]
|
|
})
|
|
},
|
|
|
|
methods: {
|
|
t,
|
|
handleOwnReaction({ model, reaction }) {
|
|
this.handleReaction(model, reaction, true)
|
|
},
|
|
|
|
handleReaction(model, reaction, isLocalModel = false) {
|
|
// prevent spamming to queue from a single account
|
|
if (this.reactionsQueue.some(item => item.id === model.attributes.peerId)) {
|
|
return
|
|
}
|
|
|
|
// prevent receiving anything rather than defined reactions in capabilities
|
|
if (!this.supportedReactions.includes(reaction)) {
|
|
return
|
|
}
|
|
|
|
this.reactionsQueue.push({
|
|
id: model.attributes.peerId,
|
|
reaction,
|
|
reactionURL: this.getReactionURL(reaction),
|
|
name: isLocalModel
|
|
? this.$store.getters.getDisplayName() || t('spreed', 'Guest')
|
|
: this.getParticipantName(model),
|
|
seed: Math.random(),
|
|
})
|
|
},
|
|
|
|
processReactionsQueue() {
|
|
if (this.reactionsQueue.length > 0) {
|
|
// Move reactions from queue to visible array
|
|
this.toasts.push(this.reactionsQueue.shift())
|
|
|
|
// Delete reactions from array after animation ends
|
|
setTimeout(() => {
|
|
this.toasts.shift()
|
|
}, this.animationLength)
|
|
}
|
|
},
|
|
|
|
getParticipantName(model) {
|
|
const { name, nextcloudSessionId } = model.attributes
|
|
if (name) {
|
|
return name
|
|
}
|
|
|
|
const participant = this.participants.find(participant => participant.sessionIds.includes(nextcloudSessionId))
|
|
if (participant?.displayName) {
|
|
return participant.displayName
|
|
}
|
|
|
|
return this.guestNameStore.getGuestName(this.token, Hex.stringify(SHA1(nextcloudSessionId)))
|
|
},
|
|
|
|
getReactionURL(emoji) {
|
|
return reactions[emoji]
|
|
? imagePath('spreed', 'emojis/' + reactions[emoji])
|
|
: undefined
|
|
},
|
|
|
|
styled(name, seed) {
|
|
const color = usernameToColor(name)
|
|
|
|
return {
|
|
'--background-color': `rgb(${color.r}, ${color.g}, ${color.b})`,
|
|
'--animation-length': `${this.animationLength + 300}ms`,
|
|
'--horizontal-offset': `${10 + 20 * seed}%`,
|
|
'--vertical-offset': 30 + 5 * seed,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.toaster {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
width: 100%;
|
|
z-index: 1;
|
|
}
|
|
|
|
.toast {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: var(--horizontal-offset, 0);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
animation: toast-floating var(--animation-length) linear;
|
|
|
|
&__reaction {
|
|
font-size: 250%;
|
|
line-height: 100%;
|
|
|
|
@media only screen and (max-width: 1920px) {
|
|
& {
|
|
font-size: 150%;
|
|
}
|
|
&-img {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
}
|
|
}
|
|
|
|
&__name {
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
line-height: 100%;
|
|
white-space: nowrap;
|
|
color: #ffffff;
|
|
background-color: var(--background-color);
|
|
box-shadow: 1px 1px 4px var(--color-box-shadow);
|
|
}
|
|
}
|
|
|
|
@keyframes toast-floating {
|
|
0% {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: translateY(calc(-0.5 * var(--vertical-offset) * 1vh));
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translateY(calc(-1 * var(--vertical-offset) * 1vh));
|
|
opacity: 0;
|
|
}
|
|
}
|
|
</style>
|