fix some bugs
This commit is contained in:
@@ -1,21 +1,66 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { notification } from 'ant-design-vue'
|
||||
import { getAppNotificationsUnreadCount } from '@/api/appNotifications'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { isToken } from '@/utils'
|
||||
import { getToken } from '@/utils'
|
||||
import { showBrowserNotification } from '@/utils/browserNotification'
|
||||
import { priceSocketConnected, priceWssEnabled } from '@/utils/priceSocketState'
|
||||
import { APP_Z_TOAST } from '@/utils/zIndex'
|
||||
|
||||
const unreadCount = ref(0)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let started = false
|
||||
let lastStreamNotifId = 0
|
||||
let hydratedUnread = false
|
||||
let lastShownNotifId = 0
|
||||
let toastConfigured = false
|
||||
let hydrateFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function refreshUnreadCount() {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (!isToken() || !settingsStore.isSettingTruthy('has_page_notification')) {
|
||||
function ensureNotifToastConfig() {
|
||||
if (toastConfigured) return
|
||||
toastConfigured = true
|
||||
notification.config({
|
||||
placement: 'topRight',
|
||||
top: 'calc(var(--app-home-header-offset, 4.25rem) + 0.5rem)',
|
||||
duration: 5,
|
||||
})
|
||||
}
|
||||
|
||||
function notificationsEnabled(): boolean {
|
||||
return Boolean(getToken()) && useSettingsStore().isSettingTruthy('has_page_notification')
|
||||
}
|
||||
|
||||
function clearHydrateFallback() {
|
||||
if (hydrateFallbackTimer) {
|
||||
clearTimeout(hydrateFallbackTimer)
|
||||
hydrateFallbackTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/** از SSE رویداد unread-count — بدون HTTP */
|
||||
function applyUnreadCountFromStream(payload: Record<string, unknown>) {
|
||||
if (!notificationsEnabled()) {
|
||||
unreadCount.value = 0
|
||||
hydratedUnread = false
|
||||
return
|
||||
}
|
||||
|
||||
const next = Number(payload.unread_count ?? 0) || 0
|
||||
unreadCount.value = Math.max(0, next)
|
||||
hydratedUnread = true
|
||||
clearHydrateFallback()
|
||||
}
|
||||
|
||||
/** فقط وقتی SSE در دسترس نیست یا برای ریفرش دستی صفحه اعلانها */
|
||||
async function refreshUnreadCount() {
|
||||
if (!notificationsEnabled()) {
|
||||
unreadCount.value = 0
|
||||
hydratedUnread = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await getAppNotificationsUnreadCount()
|
||||
unreadCount.value = Number(data?.unread_count ?? 0) || 0
|
||||
hydratedUnread = true
|
||||
} catch {
|
||||
// silent — badge optional
|
||||
}
|
||||
@@ -25,29 +70,138 @@ function clearUnreadLocally() {
|
||||
unreadCount.value = 0
|
||||
}
|
||||
|
||||
function showInAppToast(title: string, body: string) {
|
||||
try {
|
||||
ensureNotifToastConfig()
|
||||
notification.open({
|
||||
message: title || 'اعلان جدید',
|
||||
description: body || undefined,
|
||||
placement: 'topRight',
|
||||
duration: 5,
|
||||
class: 'app-notif-toast',
|
||||
style: { zIndex: APP_Z_TOAST },
|
||||
onClick: () => {
|
||||
void import('@/router').then((mod) => {
|
||||
mod.default.push({ name: 'appNotifications' }).catch(() => undefined)
|
||||
})
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function presentNotification(payload: {
|
||||
id?: number
|
||||
title?: string
|
||||
body?: string
|
||||
}) {
|
||||
const id = Number(payload.id ?? 0)
|
||||
if (id > 0 && id === lastShownNotifId) {
|
||||
return
|
||||
}
|
||||
if (id > 0) {
|
||||
lastShownNotifId = id
|
||||
}
|
||||
|
||||
const title = String(payload.title ?? '').trim() || 'اعلان جدید'
|
||||
const body = String(payload.body ?? '').trim()
|
||||
|
||||
showInAppToast(title, body)
|
||||
await showBrowserNotification({
|
||||
title,
|
||||
body,
|
||||
tag: id > 0 ? `app-notification-${id}` : undefined,
|
||||
url: '/appNotifications',
|
||||
})
|
||||
}
|
||||
|
||||
/** از SSE رویداد app_notification — بدون polling HTTP */
|
||||
function bumpUnreadFromStream(payload: Record<string, unknown>) {
|
||||
if (!notificationsEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = Number(payload.id ?? 0)
|
||||
if (id > 0 && id === lastStreamNotifId) {
|
||||
return
|
||||
}
|
||||
if (id > 0) {
|
||||
lastStreamNotifId = id
|
||||
}
|
||||
|
||||
if (payload.unread_count !== undefined && payload.unread_count !== null) {
|
||||
applyUnreadCountFromStream(payload)
|
||||
} else {
|
||||
unreadCount.value = Math.max(0, unreadCount.value) + 1
|
||||
hydratedUnread = true
|
||||
clearHydrateFallback()
|
||||
}
|
||||
|
||||
void presentNotification({
|
||||
id,
|
||||
title: String(payload.title ?? ''),
|
||||
body: String(payload.body ?? ''),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* بدون interval: اگر استریم قیمت روشن است منتظر رویداد SSE میمانیم؛
|
||||
* فقط وقتی wss خاموش است یا SSE تا ۱۲ثانیه نیامد، یکبار HTTP میزنیم.
|
||||
*/
|
||||
function scheduleHydrate() {
|
||||
if (!notificationsEnabled()) {
|
||||
unreadCount.value = 0
|
||||
hydratedUnread = false
|
||||
clearHydrateFallback()
|
||||
return
|
||||
}
|
||||
|
||||
if (!priceWssEnabled.value) {
|
||||
void refreshUnreadCount()
|
||||
return
|
||||
}
|
||||
|
||||
if (priceSocketConnected.value || hydratedUnread) {
|
||||
clearHydrateFallback()
|
||||
return
|
||||
}
|
||||
|
||||
clearHydrateFallback()
|
||||
hydrateFallbackTimer = setTimeout(() => {
|
||||
hydrateFallbackTimer = null
|
||||
if (!hydratedUnread && notificationsEnabled() && !priceSocketConnected.value) {
|
||||
void refreshUnreadCount()
|
||||
}
|
||||
}, 12000)
|
||||
}
|
||||
|
||||
export function useAppNotificationBadge() {
|
||||
const settingsStore = useSettingsStore()
|
||||
const enabled = computed(() => settingsStore.isSettingTruthy('has_page_notification'))
|
||||
|
||||
onMounted(() => {
|
||||
if (started) {
|
||||
void refreshUnreadCount()
|
||||
return
|
||||
}
|
||||
started = true
|
||||
void refreshUnreadCount()
|
||||
pollTimer = setInterval(() => {
|
||||
void refreshUnreadCount()
|
||||
}, 60000)
|
||||
scheduleHydrate()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// keep poll for other mounted headers; only clear if needed later
|
||||
// module-level badge — keep state for other headers
|
||||
})
|
||||
|
||||
watch(enabled, (on) => {
|
||||
if (!on) unreadCount.value = 0
|
||||
else void refreshUnreadCount()
|
||||
if (!on) {
|
||||
unreadCount.value = 0
|
||||
hydratedUnread = false
|
||||
clearHydrateFallback()
|
||||
} else {
|
||||
scheduleHydrate()
|
||||
}
|
||||
})
|
||||
|
||||
watch([priceSocketConnected, priceWssEnabled], () => {
|
||||
if (!started) return
|
||||
scheduleHydrate()
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -58,4 +212,9 @@ export function useAppNotificationBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
export { refreshUnreadCount, clearUnreadLocally }
|
||||
export {
|
||||
refreshUnreadCount,
|
||||
clearUnreadLocally,
|
||||
bumpUnreadFromStream,
|
||||
applyUnreadCountFromStream,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user