Port modular app notifications, pargar date display, and closed-store product UX.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { notification } from 'ant-design-vue'
|
||||
import { getAppNotifications, getAppNotificationsUnreadCount } from '@/api/appNotifications'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { getToken } from '@/utils'
|
||||
import { showBrowserNotification } from '@/utils/browserNotification'
|
||||
|
||||
const unreadCount = ref(0)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let started = false
|
||||
let lastStreamNotifId = 0
|
||||
let hydratedUnread = false
|
||||
let lastShownNotifId = 0
|
||||
|
||||
async function refreshUnreadCount() {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (!getToken() || !settingsStore.isSettingTruthy('has_page_notification')) {
|
||||
unreadCount.value = 0
|
||||
hydratedUnread = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await getAppNotificationsUnreadCount()
|
||||
const next = Number(data?.unread_count ?? 0) || 0
|
||||
const prev = unreadCount.value
|
||||
|
||||
// اگر SSE نرسید ولی تعداد خواندهنشده زیاد شد → نوتیف بساز
|
||||
if (hydratedUnread && next > prev) {
|
||||
void notifyFromLatestUnread()
|
||||
}
|
||||
|
||||
unreadCount.value = next
|
||||
hydratedUnread = true
|
||||
} catch {
|
||||
// silent — badge optional
|
||||
}
|
||||
}
|
||||
|
||||
function clearUnreadLocally() {
|
||||
unreadCount.value = 0
|
||||
}
|
||||
|
||||
function showInAppToast(title: string, body: string) {
|
||||
try {
|
||||
notification.open({
|
||||
message: title || 'اعلان جدید',
|
||||
description: body || undefined,
|
||||
placement: 'topRight',
|
||||
duration: 5,
|
||||
class: 'app-notif-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',
|
||||
})
|
||||
}
|
||||
|
||||
async function notifyFromLatestUnread() {
|
||||
try {
|
||||
const rows = await getAppNotifications()
|
||||
const latest = (Array.isArray(rows) ? rows : []).find((row) => !row.is_read)
|
||||
if (!latest) return
|
||||
await presentNotification({
|
||||
id: latest.id,
|
||||
title: latest.title,
|
||||
body: latest.body,
|
||||
})
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** از SSE زنده — بدون Firebase / subscription */
|
||||
function bumpUnreadFromStream(payload: Record<string, unknown>) {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (!getToken() || !settingsStore.isSettingTruthy('has_page_notification')) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = Number(payload.id ?? 0)
|
||||
if (id > 0 && id === lastStreamNotifId) {
|
||||
return
|
||||
}
|
||||
if (id > 0) {
|
||||
lastStreamNotifId = id
|
||||
}
|
||||
|
||||
unreadCount.value = Math.max(0, unreadCount.value) + 1
|
||||
void presentNotification({
|
||||
id,
|
||||
title: String(payload.title ?? ''),
|
||||
body: String(payload.body ?? ''),
|
||||
})
|
||||
void refreshUnreadCount()
|
||||
}
|
||||
|
||||
export function useAppNotificationBadge() {
|
||||
const settingsStore = useSettingsStore()
|
||||
const enabled = computed(() => settingsStore.isSettingTruthy('has_page_notification'))
|
||||
|
||||
onMounted(() => {
|
||||
if (started) {
|
||||
void refreshUnreadCount()
|
||||
return
|
||||
}
|
||||
started = true
|
||||
void refreshUnreadCount()
|
||||
// هر ۲۰ثانیه — اگر SSE قطع بود هم نوتیف دیرتر میآید
|
||||
pollTimer = setInterval(() => {
|
||||
void refreshUnreadCount()
|
||||
}, 20000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// keep poll for other mounted headers
|
||||
})
|
||||
|
||||
watch(enabled, (on) => {
|
||||
if (!on) unreadCount.value = 0
|
||||
else void refreshUnreadCount()
|
||||
})
|
||||
|
||||
return {
|
||||
enabled,
|
||||
unreadCount,
|
||||
refreshUnreadCount,
|
||||
clearUnreadLocally,
|
||||
}
|
||||
}
|
||||
|
||||
export { refreshUnreadCount, clearUnreadLocally, bumpUnreadFromStream }
|
||||
Reference in New Issue
Block a user