Merge pull request #16515 from nextcloud/fix/noid/toaster

This commit is contained in:
Maksim Sukharev 2025-12-15 12:51:05 +01:00 committed by GitHub
commit fdd705c4a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 63 deletions

View file

@ -4,11 +4,7 @@
-->
<template>
<TransitionWrapper
class="toaster"
name="toast"
tag="ul"
group>
<ul class="toaster">
<li
v-for="toast in toasts"
:key="toast.seed"
@ -28,7 +24,7 @@
{{ toast.name }}
</span>
</li>
</TransitionWrapper>
</ul>
</template>
<script>
@ -38,7 +34,6 @@ import { imagePath } from '@nextcloud/router'
import { usernameToColor } from '@nextcloud/vue/functions/usernameToColor'
import Hex from 'crypto-js/enc-hex.js'
import SHA1 from 'crypto-js/sha1.js'
import TransitionWrapper from '../../UIShared/TransitionWrapper.vue'
import { useActorStore } from '../../../stores/actor.ts'
import { useGuestNameStore } from '../../../stores/guestName.ts'
@ -57,13 +52,14 @@ const reactions = {
'😥': 'Concerned.gif',
}
const ANIMATION_LENGTH = 2_000
const TOAST_INTERVAL = 500
// Timestamp of when the next reaction should be shown in UI
let nextProcessedTimestamp = 0
export default {
name: 'ReactionToaster',
components: {
TransitionWrapper,
},
props: {
/**
* The conversation token
@ -101,7 +97,6 @@ export default {
registeredModels: {},
reactionsQueue: [],
intervalId: null,
animationLength: 2000,
toasts: [],
}
},
@ -113,25 +108,29 @@ export default {
},
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)
})
callParticipantModels: {
handler(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]
})
// 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]
})
},
immediate: true,
},
},
mounted() {
this.intervalId = setInterval(this.processReactionsQueue, this.animationLength / 4)
this.intervalId = setInterval(this.processReactionsQueue, TOAST_INTERVAL)
subscribe('send-reaction', this.handleOwnReaction)
},
@ -151,8 +150,8 @@ export default {
},
handleReaction(model, reaction, isLocalModel = false) {
// prevent spamming to queue from a single account
if (this.reactionsQueue.some((item) => item.id === model.attributes.peerId)) {
// prevent spamming to queue from a single account (unless in debug mode)
if (!OC.debug && this.reactionsQueue.some((item) => item.id === model.attributes.peerId)) {
return
}
@ -169,19 +168,39 @@ export default {
? this.actorStore.displayName || t('spreed', 'Guest')
: this.getParticipantName(model),
seed: Math.random(),
timestamp: Date.now(),
})
},
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)
if (this.reactionsQueue.length === 0) {
return
}
// Prevent spamming with reactions, if tab was suspended
const now = Date.now()
if (now < nextProcessedTimestamp) {
return
}
// Discard reactions, that should have been fired long ago
if (this.reactionsQueue.at(0).timestamp < now - 30_000) {
this.reactionsQueue = this.reactionsQueue.filter((reaction) => reaction.timestamp >= now - 30_000)
if (this.reactionsQueue.length === 0) {
return
}
}
nextProcessedTimestamp = now + TOAST_INTERVAL
// Move reactions from queue to visible array
this.toasts.push(this.reactionsQueue.shift())
// Delete reactions from array after animation ends
setTimeout(() => {
this.toasts.shift()
}, ANIMATION_LENGTH)
},
getParticipantName(model) {
@ -209,7 +228,7 @@ export default {
return {
'--background-color': `rgb(${color.r}, ${color.g}, ${color.b})`,
'--animation-length': `${this.animationLength + 300}ms`,
'--animation-length': `${ANIMATION_LENGTH + 300}ms`,
'--horizontal-offset': `${10 + 20 * seed}%`,
'--vertical-offset': 30 + 5 * seed,
}
@ -268,6 +287,10 @@ export default {
@keyframes toast-floating {
0% {
transform: translateY(0);
opacity: 0;
}
5% {
transform: translateY(calc(-0.05 * var(--vertical-offset) * 1vh));
opacity: 1;
}
50% {

View file

@ -33,7 +33,6 @@ export default {
'slide-up',
'slide-right',
'slide-down',
'toast',
'zoom',
].includes(value)
},
@ -153,23 +152,6 @@ export default {
}
}
@mixin toast-rules {
&-enter,
&-leave-to {
opacity: 0;
}
&-enter-to,
&-leave {
opacity: 1;
}
&-enter-active,
&-leave-active {
transition: $transition-slow;
transition-property: opacity;
transition-timing-function: linear;
}
}
@mixin zoom-rules {
&-enter,
&-leave-to {
@ -207,10 +189,6 @@ export default {
@include slide-down-rules;
}
.toast {
@include toast-rules;
}
.zoom {
@include zoom-rules;
}
@ -240,10 +218,6 @@ export default {
@include group-rules;
@include slide-down-rules;
}
&.toast {
@include group-rules;
@include toast-rules;
}
&.zoom {
@include group-rules;
@include zoom-rules;