diff --git a/packages/frontend/src/components/MkNotificationToast.vue b/packages/frontend/src/components/MkNotificationToast.vue deleted file mode 100644 index 39e8373e37..0000000000 --- a/packages/frontend/src/components/MkNotificationToast.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> -<div class="mk-notification-toast" :style="{ zIndex }"> - <Transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')"> - <XNotification v-if="showing" :notification="notification" class="notification _acrylic"/> - </Transition> -</div> -</template> - -<script lang="ts" setup> -import { onMounted } from 'vue'; -import XNotification from '@/components/MkNotification.vue'; -import * as os from '@/os'; - -defineProps<{ - notification: any; // TODO -}>(); - -const emit = defineEmits<{ - (ev: 'closed'): void; -}>(); - -const zIndex = os.claimZIndex('high'); -let showing = $ref(true); - -onMounted(() => { - window.setTimeout(() => { - showing = false; - }, 6000); -}); -</script> - -<style lang="scss" scoped> -.notification-toast-enter-active, .notification-toast-leave-active { - transition: opacity 0.3s, transform 0.3s !important; -} -.notification-toast-enter-from, .notification-toast-leave-to { - opacity: 0; - transform: translateX(-250px); -} - -.mk-notification-toast { - position: fixed; - left: 0; - width: 250px; - top: 32px; - padding: 0 32px; - pointer-events: none; - container-type: inline-size; - - @media (max-width: 700px) { - top: initial; - bottom: 112px; - padding: 0 16px; - } - - @media (max-width: 500px) { - bottom: calc(env(safe-area-inset-bottom, 0px) + 92px); - padding: 0 8px; - } - - > .notification { - height: 100%; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - border-radius: 8px; - overflow: hidden; - } -} -</style> diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 7f3fc0e4af..0333e20d0a 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -9,6 +9,10 @@ <XUpload v-if="uploads.length > 0"/> +<TransitionGroup :name="$store.state.animation ? 'notification' : ''" tag="div" class="notifications"> + <XNotification v-for="notification in notifications" :key="notification.id" :notification="notification" class="notification"/> +</TransitionGroup> + <XStreamIndicator/> <div v-if="pendingApiRequestsCount > 0" id="wait"></div> @@ -19,8 +23,10 @@ </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, nextTick } from 'vue'; +import * as misskey from 'misskey-js'; import { swInject } from './sw-inject'; +import XNotification from './notification.vue'; import { popup, popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; @@ -33,7 +39,9 @@ const XUpload = defineAsyncComponent(() => import('./upload.vue')); const dev = _DEV_; -const onNotification = notification => { +let notifications = $ref<misskey.entities.Notification[]>([]); + +function onNotification(notification) { if ($i.mutingNotificationTypes.includes(notification.type)) return; if (document.visibilityState === 'visible') { @@ -41,13 +49,18 @@ const onNotification = notification => { id: notification.id, }); - popup(defineAsyncComponent(() => import('@/components/MkNotificationToast.vue')), { - notification, - }, {}, 'closed'); + notifications.unshift(notification); + window.setTimeout(() => { + if (notifications.length > 3) notifications.pop(); + }, 500); + + window.setTimeout(() => { + notifications = notifications.filter(x => x.id !== notification.id); + }, 6000); } sound.play('notification'); -}; +} if ($i) { const connection = stream.useChannel('main', null, 'UI'); @@ -60,6 +73,53 @@ if ($i) { } </script> +<style lang="scss" scoped> +.notification-move, .notification-enter-active, .notification-leave-active { + transition: opacity 0.3s, transform 0.3s !important; +} +.notification-enter-from, .notification-leave-to { + opacity: 0; + transform: translateX(-250px); +} + +.notifications { + position: fixed; + z-index: 3900000; + left: 0; + width: 250px; + top: 32px; + padding: 0 32px; + pointer-events: none; + container-type: inline-size; + + > .notification { + & + .notification { + margin-top: 8px; + } + } + + @media (max-width: 700px) { + top: initial; + bottom: 112px; + padding: 0 16px; + display: flex; + flex-direction: column-reverse; + + > .notification { + & + .notification { + margin-top: 0; + margin-bottom: 8px; + } + } + } + + @media (max-width: 500px) { + bottom: calc(env(safe-area-inset-bottom, 0px) + 92px); + padding: 0 8px; + } +} +</style> + <style lang="scss"> @keyframes dev-ticker-blink { 0% { opacity: 1; } diff --git a/packages/frontend/src/ui/_common_/notification.vue b/packages/frontend/src/ui/_common_/notification.vue new file mode 100644 index 0000000000..1f9c675a15 --- /dev/null +++ b/packages/frontend/src/ui/_common_/notification.vue @@ -0,0 +1,24 @@ +<template> +<div :class="$style.root"> + <XNotification :notification="notification" class="notification _acrylic"/> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import * as misskey from 'misskey-js'; +import XNotification from '@/components/MkNotification.vue'; + +defineProps<{ + notification: misskey.entities.Notification; +}>(); +</script> + +<style lang="scss" module> +.root { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + border-radius: 8px; + overflow: clip; + contain: content; +} +</style>