Added sockets for notifications
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useNotificationStore } from '@/stores/notificationStore';
|
||||
import { Notification } from '@/Types/types';
|
||||
|
||||
const props = defineProps<{
|
||||
userId: number;
|
||||
}>();
|
||||
|
||||
interface NotificationPayload {
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
const store = useNotificationStore();
|
||||
let channel: ReturnType<typeof window.Echo.private> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
channel = window.Echo.private(`App.Models.User.${props.userId}`)
|
||||
.listen('.notification.created', (e: NotificationPayload) => {
|
||||
store.push(e.notification);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
channel?.stopListening('.notification.created');
|
||||
window.Echo.leave(`App.Models.User.${props.userId}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from 'vue'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import axios from "axios";
|
||||
import {Notification} from "@/Types/types";
|
||||
import {Link} from "@inertiajs/vue3";
|
||||
import {local} from "laravel-vite-plugin/fonts";
|
||||
import { Notification } from "@/Types/types";
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import { useNotificationStore } from '@/stores/notificationStore';
|
||||
|
||||
const props = defineProps<{
|
||||
unreadCount: number
|
||||
}>()
|
||||
|
||||
const localUnreadCount = ref(props.unreadCount)
|
||||
watch(() => props.unreadCount, (val) => {
|
||||
localUnreadCount.value = val
|
||||
})
|
||||
const store = useNotificationStore();
|
||||
|
||||
onMounted(() => {
|
||||
store.setInitialUnreadCount(props.unreadCount);
|
||||
});
|
||||
|
||||
const open = ref(false)
|
||||
const notifications = ref<Notification[]>([])
|
||||
@@ -27,15 +27,10 @@ const markAllRead = async (notifications: Notification[]) => {
|
||||
unread.forEach(n => n.read_at = new Date().toISOString())
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:unreadCount', value: number): void
|
||||
}>()
|
||||
|
||||
watch(open, async (isOpen) => {
|
||||
if (!isOpen) return
|
||||
|
||||
localUnreadCount.value = 0
|
||||
emit('update:unreadCount', 0)
|
||||
store.unreadCount = 0
|
||||
|
||||
if (notifications.value.length) return
|
||||
|
||||
@@ -50,7 +45,7 @@ watch(open, async (isOpen) => {
|
||||
<template>
|
||||
<div class="notif-wrapper">
|
||||
<v-btn icon variant="text" @click="open = !open" aria-label="Notifications">
|
||||
<v-badge :content="localUnreadCount" :model-value="localUnreadCount > 0" color="primary">
|
||||
<v-badge :content="store.unreadCount" :model-value="store.unreadCount > 0" color="primary">
|
||||
<v-icon>mdi-bell-outline</v-icon>
|
||||
</v-badge>
|
||||
</v-btn>
|
||||
|
||||
@@ -1,54 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import { Notification } from "@/Types/types";
|
||||
import axios from "axios";
|
||||
import { useNotificationStore } from '@/stores/notificationStore';
|
||||
|
||||
const activeToasts = ref<Notification[]>([])
|
||||
const seenNotificationIds = ref<Set<number>>(new Set())
|
||||
const achievementSound = new Audio('/sounds/seatBelt.wav')
|
||||
|
||||
function handleNewNotifications(notifications: Notification[]) {
|
||||
if (!notifications?.length) return
|
||||
|
||||
const newToasts: Notification[] = []
|
||||
for (const n of notifications) {
|
||||
if (!seenNotificationIds.value.has(n.id)) {
|
||||
seenNotificationIds.value.add(n.id)
|
||||
newToasts.push(n)
|
||||
}
|
||||
}
|
||||
|
||||
if (!newToasts.length) return
|
||||
activeToasts.value.push(...newToasts)
|
||||
achievementSound.play().catch(() => {})
|
||||
}
|
||||
|
||||
async function dismissToast(notification: Notification) {
|
||||
activeToasts.value = activeToasts.value.filter(n => n.id !== notification.id)
|
||||
await axios.patch(`/notifications/${notification.id}/read`)
|
||||
}
|
||||
|
||||
router.on('success', (event) => {
|
||||
handleNewNotifications(event.detail.page.props.achievement_notifications as Notification[] ?? [])
|
||||
})
|
||||
|
||||
// Handle initial page load notifications
|
||||
import { usePage } from "@inertiajs/vue3";
|
||||
import { SharedProps } from "@/Types/types";
|
||||
const page = usePage<SharedProps>().props;
|
||||
watch(
|
||||
() => page.achievement_notifications,
|
||||
handleNewNotifications,
|
||||
{ immediate: true }
|
||||
)
|
||||
const store = useNotificationStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toast-stack">
|
||||
<TransitionGroup name="toast">
|
||||
<v-card
|
||||
v-for="notification in activeToasts"
|
||||
v-for="notification in store.toasts"
|
||||
:key="notification.id"
|
||||
class="toast-card glass"
|
||||
rounded="lg"
|
||||
@@ -66,7 +26,7 @@ watch(
|
||||
variant="text"
|
||||
size="small"
|
||||
class="ml-auto"
|
||||
@click="dismissToast(notification)"
|
||||
@click="store.dismiss(notification)"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -4,11 +4,14 @@ import MainFooter from "@/Components/FlightsGoneBy/MainFooter.vue";
|
||||
import Radar from "@/Components/FlightsGoneBy/Radar.vue";
|
||||
import StatusNotifications from "@/Components/FlightsGoneBy/StatusNotifications.vue";
|
||||
import ToastNotifications from "@/Components/FlightsGoneBy/ToastNotifications.vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import {router, usePage} from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
|
||||
import NotificationListener from "@/Components/FlightsGoneBy/NotificationListener.vue";
|
||||
import {SharedProps} from "@/Types/types";
|
||||
const transitionKey = ref(0);
|
||||
|
||||
const page = usePage<SharedProps>().props
|
||||
|
||||
router.on('success', () => {
|
||||
transitionKey.value++;
|
||||
});
|
||||
@@ -25,6 +28,7 @@ router.on('success', () => {
|
||||
</Transition>
|
||||
<MainFooter :key="`footer-${transitionKey}`" />
|
||||
</div>
|
||||
<NotificationListener v-if="page.auth.user?.id" :user-id="page.auth.user?.id" />
|
||||
<StatusNotifications />
|
||||
<ToastNotifications />
|
||||
</Radar>
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import Echo from 'laravel-echo';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Echo: Echo<'reverb'>;
|
||||
Pusher: unknown;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,15 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||
import { createApp, h, DefineComponent } from 'vue';
|
||||
import vuetify from './plugins/vuetify';
|
||||
import { configureEcho } from '@laravel/echo-vue';
|
||||
const pinia = createPinia();
|
||||
configureEcho({
|
||||
broadcaster: 'reverb',
|
||||
});
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'flag-icons/css/flag-icons.min.css'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import {createPinia} from "pinia";
|
||||
|
||||
|
||||
const appName = document.querySelector('meta[name="app-name"]')?.getAttribute('content') || 'Laravel';
|
||||
@@ -25,6 +31,7 @@ createInertiaApp({
|
||||
.use(plugin)
|
||||
.use(ZiggyVue)
|
||||
.use(vuetify)
|
||||
.use(pinia)
|
||||
.use(VueApexCharts)
|
||||
.mount(el);
|
||||
},
|
||||
|
||||
Vendored
+14
@@ -2,3 +2,17 @@ import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { Notification } from '@/Types/types';
|
||||
import axios from 'axios';
|
||||
|
||||
export const useNotificationStore = defineStore('notifications', () => {
|
||||
const toasts = ref<Notification[]>([]);
|
||||
const unreadCount = ref(0);
|
||||
const latestNotification = ref<Notification | null>(null);
|
||||
const achievementSound = new Audio('/sounds/seatBelt.wav');
|
||||
|
||||
function push(notification: Notification) {
|
||||
toasts.value.push(notification);
|
||||
latestNotification.value = notification;
|
||||
unreadCount.value++;
|
||||
|
||||
if (notification.is_achievement) {
|
||||
achievementSound.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function dismiss(notification: Notification) {
|
||||
toasts.value = toasts.value.filter(n => n.id !== notification.id);
|
||||
await axios.patch(`/notifications/${notification.id}/read`);
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||
}
|
||||
|
||||
function setInitialUnreadCount(count: number) {
|
||||
unreadCount.value = count;
|
||||
}
|
||||
|
||||
return { toasts, unreadCount, latestNotification, push, dismiss, setInitialUnreadCount};
|
||||
});
|
||||
Reference in New Issue
Block a user