149da578fd
Removed testing banner Fixed a JS console error when assigning a user to a previously open task Fixed a potential abuse where user might be able to score someone else's task via API call Fixed an issue where finding tasks by alias could return tasks belonging to other users Fixed an issue that was appending the user's party ID to their list of Guilds Fixed an issue where group tasks were not receiving the default tag needed for filtering them on user's personal list
391 lines
11 KiB
Vue
391 lines
11 KiB
Vue
<template>
|
|
<div
|
|
class="notifications"
|
|
:class="notificationsTopPosClass"
|
|
:style="{'--current-scrollY': notificationTopY}"
|
|
>
|
|
<transition-group
|
|
name="notifications"
|
|
class="animations-holder"
|
|
appear
|
|
>
|
|
<notification
|
|
v-for="(notification, index) in visibleNotifications"
|
|
:key="notification.uuid"
|
|
:notification="notification"
|
|
class="notification-item"
|
|
:visible-amount="index"
|
|
@clicked="notificationRemoved(notification)"
|
|
@hidden="notificationRemoved($event)"
|
|
/>
|
|
</transition-group>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.notifications {
|
|
position: fixed;
|
|
right: 10px;
|
|
width: 350px;
|
|
z-index: 9999; // to keep it above modal overlays
|
|
|
|
top: var(--current-scrollY);
|
|
|
|
justify-content: flex-end;
|
|
display: flex;
|
|
}
|
|
|
|
.animations-holder {
|
|
position: relative;
|
|
display: block;
|
|
}
|
|
|
|
.notification-item {
|
|
transition: transform 0.25s ease-in, opacity 0.25s ease-in;
|
|
}
|
|
|
|
.notifications-move {
|
|
// transition: transform .5s;
|
|
}
|
|
|
|
.notifications-enter-active {
|
|
// transition: opacity .5s;
|
|
}
|
|
|
|
.notifications-leave-active {
|
|
position: absolute;
|
|
right: 0;
|
|
}
|
|
|
|
.notifications-enter,
|
|
.notifications-leave-to {
|
|
opacity: 0;
|
|
right: 0;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import debounce from 'lodash/debounce';
|
|
import find from 'lodash/find';
|
|
|
|
import { mapState } from '@/libs/store';
|
|
import notification from './notification';
|
|
import { sleepAsync } from '../../../../common/script/libs/sleepAsync';
|
|
import { getBannerHeight } from '@/libs/banner.func';
|
|
import { EVENTS } from '@/libs/events';
|
|
import { worldStateMixin } from '@/mixins/worldState';
|
|
|
|
const NOTIFICATIONS_VISIBLE_AT_ONCE = 4;
|
|
const REMOVAL_INTERVAL = 2500;
|
|
const DELAY_DELETE_AND_NEW = 60;
|
|
const DELAY_FILLING_ENTRIES = 240;
|
|
|
|
export default {
|
|
components: {
|
|
notification,
|
|
},
|
|
mixins: [
|
|
worldStateMixin,
|
|
],
|
|
props: {
|
|
preventQueue: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
debugMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
data () {
|
|
return {
|
|
visibleNotifications: [],
|
|
allowedToFillAgain: true,
|
|
removalIntervalId: null,
|
|
notificationTopY: '0px',
|
|
preventMultipleWatchExecution: false,
|
|
eventPromoBannerHeight: null,
|
|
sleepingBannerHeight: null,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState({
|
|
notificationStore: 'notificationStore',
|
|
userSleeping: 'user.data.preferences.sleep',
|
|
currentEventList: 'worldState.data.currentEventList',
|
|
}),
|
|
currentEvent () {
|
|
return find(this.currentEventList, event => Boolean(event.gemsPromo) || Boolean(event.promo));
|
|
},
|
|
isEventActive () {
|
|
return Boolean(this.currentEvent?.event);
|
|
},
|
|
notificationsTopPosClass () {
|
|
const base = 'notifications-top-pos-';
|
|
let modifier = '';
|
|
|
|
if (this.userSleeping) {
|
|
modifier = 'sleeping';
|
|
} else {
|
|
modifier = 'normal';
|
|
}
|
|
|
|
return `${base}${modifier} scroll-${this.scrollY}`;
|
|
},
|
|
notificationBannerHeight () {
|
|
let scrollPosToCheck = 56;
|
|
|
|
if (this.sleepingBannerHeight) {
|
|
scrollPosToCheck += this.sleepingBannerHeight;
|
|
}
|
|
|
|
if (this.isEventActive) {
|
|
scrollPosToCheck += this.eventPromoBannerHeight ?? 0;
|
|
}
|
|
|
|
return scrollPosToCheck;
|
|
},
|
|
visibleNotificationsWithoutErrors () {
|
|
return this.visibleNotifications.filter(n => n.type !== 'error');
|
|
},
|
|
},
|
|
watch: {
|
|
notificationStore: async function notificationStore (notifications) {
|
|
if (this.preventMultipleWatchExecution) {
|
|
return;
|
|
}
|
|
|
|
this.preventMultipleWatchExecution = true;
|
|
|
|
this.debug('notifications changed', {
|
|
notifications: notifications.length,
|
|
});
|
|
|
|
// if the timer is already running, stop it
|
|
// otherwise a newly added notification might just disappear faster
|
|
this.stopNotificationsRemovalTimer();
|
|
|
|
const fillingPromise = this.triggerFillUntilFull();
|
|
|
|
this.triggerRemovalTimerIfAllowed();
|
|
|
|
await fillingPromise;
|
|
|
|
this.preventMultipleWatchExecution = false;
|
|
},
|
|
currentEvent: function currentEventChanged () {
|
|
this.updateEventBannerHeight();
|
|
},
|
|
},
|
|
async mounted () {
|
|
window.addEventListener('scroll', this.updateScrollY, {
|
|
passive: true,
|
|
});
|
|
|
|
this.$root.$on(EVENTS.BANNER_HEIGHT_UPDATED, () => {
|
|
this.updateBannerHeightAndScrollY();
|
|
});
|
|
this.$root.$on(EVENTS.WORLD_STATE_LOADED, () => {
|
|
this.updateBannerHeightAndScrollY();
|
|
});
|
|
|
|
await this.triggerGetWorldState();
|
|
this.updateBannerHeightAndScrollY();
|
|
},
|
|
destroyed () {
|
|
window.removeEventListener('scroll', this.updateScrollY, {
|
|
passive: true,
|
|
});
|
|
|
|
this.$root.$off(EVENTS.BANNER_HEIGHT_UPDATED);
|
|
this.$root.$off(EVENTS.WORLD_STATE_LOADED);
|
|
},
|
|
methods: {
|
|
debug (...args) {
|
|
if (this.debugMode) {
|
|
console.info(...args); // eslint-disable-line no-console
|
|
}
|
|
},
|
|
notificationRemoved ($event) {
|
|
// findIndex+splice is the way to go on removing, instead of .filter
|
|
// due to the way vue handles new arrays / entries even if you use :key="uuid"
|
|
// the notification object was replaced and prevented to stop the right timer => to remove it
|
|
const foundNotification = this.visibleNotifications.findIndex(n => n.uuid === $event.uuid);
|
|
|
|
this.visibleNotifications.splice(foundNotification, 1);
|
|
|
|
this.updateAllowedToFillAgain();
|
|
this.$store.dispatch('snackbars:remove', $event);
|
|
|
|
this.debug('removed', {
|
|
allowedToFillAgain: this.allowedToFillAgain,
|
|
storeLength: this.notificationStore.length,
|
|
});
|
|
|
|
if (this.allowedToFillAgain) {
|
|
// reset the flag so that the next call (for a new notification) will be immediately
|
|
if (this.visibleNotificationsWithoutErrors.length !== 0) {
|
|
this.debug('start timeout to fill again');
|
|
setTimeout(() => {
|
|
this.debug('before fill new notifications');
|
|
this.triggerFillUntilFull();
|
|
|
|
this.triggerRemovalTimerIfAllowed();
|
|
}, DELAY_DELETE_AND_NEW);
|
|
}
|
|
}
|
|
},
|
|
fillVisibleNotifications (notifications) {
|
|
this.debug({
|
|
fillAgain: this.allowedToFillAgain,
|
|
visible: this.visibleNotifications.length,
|
|
notifications: notifications.length,
|
|
});
|
|
|
|
// the generic checks - new notification array has enough items
|
|
// is allowed to be filled and don't do anything while the visible items are 2
|
|
if (notifications.length === 0 || !this.allowedToFillAgain
|
|
|| this.visibleNotifications.length === NOTIFICATIONS_VISIBLE_AT_ONCE) {
|
|
this.debug('stop fill - 1');
|
|
return;
|
|
}
|
|
|
|
// this checks if the visible items are the current ones in the notifications array
|
|
// if so - there is no need to continue, for example on only one visible notification,
|
|
// it doesn't need to go with the loop again
|
|
if (notifications.length === this.visibleNotifications.length) {
|
|
const visibleIds = this.visibleNotifications.map(n => n.uuid);
|
|
|
|
const allTheSame = notifications.every(n => visibleIds.includes(n.uuid));
|
|
|
|
if (allTheSame) {
|
|
this.debug('stop fill - 2', {
|
|
visibleIds,
|
|
notifications: notifications.length,
|
|
notificationsStore: this.notificationStore.length,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// fill the new items that needs to be visible
|
|
if (this.visibleNotifications.length < NOTIFICATIONS_VISIBLE_AT_ONCE) {
|
|
const visibleIds = this.visibleNotifications.map(n => n.uuid);
|
|
|
|
const notAddedYet = notifications.filter(n => !visibleIds.includes(n.uuid));
|
|
|
|
this.debug({
|
|
visibleIds,
|
|
notAddedYet: notAddedYet.length,
|
|
});
|
|
|
|
if (notAddedYet.length > 0) {
|
|
this.visibleNotifications.push(notAddedYet[0]);
|
|
}
|
|
}
|
|
|
|
this.updateAllowedToFillAgain();
|
|
},
|
|
async triggerFillUntilFull () {
|
|
for (let i = 0; i < NOTIFICATIONS_VISIBLE_AT_ONCE; i += 1) {
|
|
this.debug(`fill ${i}`);
|
|
this.fillVisibleNotifications(this.notificationStore);
|
|
|
|
await sleepAsync(DELAY_FILLING_ENTRIES); // eslint-disable-line no-await-in-loop
|
|
}
|
|
},
|
|
triggerRemovalTimerIfAllowed () {
|
|
// this is only for storybook
|
|
if (this.preventQueue) {
|
|
return;
|
|
}
|
|
|
|
if (this.notificationStore.length !== 0) {
|
|
this.startNotificationRemovalTimer();
|
|
}
|
|
},
|
|
startNotificationRemovalTimer () {
|
|
if (this.removalIntervalId != null) {
|
|
// current interval still running - wait until its done
|
|
return;
|
|
}
|
|
|
|
this.debug('start removal interval');
|
|
this.removalIntervalId = setInterval(() => {
|
|
const nonErrorNotifications = this.visibleNotifications.filter(n => n.type !== 'error');
|
|
|
|
if (nonErrorNotifications.length !== 0) {
|
|
const firstEntry = nonErrorNotifications[0];
|
|
|
|
this.debug('removed entry', firstEntry);
|
|
this.notificationRemoved(firstEntry);
|
|
}
|
|
|
|
if (nonErrorNotifications.length === 0) {
|
|
this.stopNotificationsRemovalTimer();
|
|
|
|
this.updateAllowedToFillAgain();
|
|
}
|
|
}, REMOVAL_INTERVAL);
|
|
},
|
|
|
|
stopNotificationsRemovalTimer () {
|
|
if (this.removalIntervalId == null) {
|
|
// current interval still running - wait until its done
|
|
return;
|
|
}
|
|
|
|
this.debug('clear removal interval');
|
|
clearInterval(this.removalIntervalId);
|
|
this.removalIntervalId = null;
|
|
},
|
|
|
|
updateAllowedToFillAgain () {
|
|
const notificationsAmount = this.visibleNotificationsWithoutErrors.length;
|
|
this.allowedToFillAgain = notificationsAmount < NOTIFICATIONS_VISIBLE_AT_ONCE;
|
|
|
|
this.debug({
|
|
allowedToFillAgain: this.allowedToFillAgain,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* This updates the position of all notifications so that its always at the top,
|
|
* unless the header is visible then its under the header
|
|
*/
|
|
updateScrollY: debounce(function updateScrollY () {
|
|
const topY = Math.min(window.scrollY, this.notificationBannerHeight) - 10;
|
|
|
|
this.notificationTopY = `${this.notificationBannerHeight - topY}px`;
|
|
}, 16),
|
|
|
|
updateBannerHeightAndScrollY () {
|
|
this.updateEventBannerHeight();
|
|
this.sleepingBannerHeight = getBannerHeight('damage-paused');
|
|
this.updateScrollY();
|
|
},
|
|
|
|
updateEventBannerHeight () {
|
|
if (this.isEventActive) {
|
|
this.eventPromoBannerHeight = getBannerHeight(this.currentEventBannerName());
|
|
}
|
|
},
|
|
|
|
currentEventBannerName () {
|
|
// if there are any other types of promo bars
|
|
// this method needs to be updated
|
|
|
|
if (this.currentEvent?.promo) {
|
|
return 'gift-promo';
|
|
}
|
|
|
|
if (this.currentEvent?.gemsPromo) {
|
|
return 'gems-promo';
|
|
}
|
|
|
|
return '';
|
|
},
|
|
},
|
|
};
|
|
</script>
|