fix some bugs

This commit is contained in:
2026-08-03 11:52:44 +03:30
parent e41a898686
commit d6608283f6
13 changed files with 1016 additions and 53 deletions
+176 -17
View File
@@ -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,
}