[Notify] Add live in-app notifications over SSE (Iran-friendly)
Web Push is delivered by the browser vendor's push service (Chrome to Google FCM), which is filtered in Iran, so background push is unreliable. Add a Server-Sent Events channel over our own origin that always reaches users while the tab/PWA is open: NotificationHub (in-memory pub/sub), a /notifications/stream SSE endpoint (auth-gated, keep-alive pings, nginx no-buffer), and NotificationService now publishes each saved notification to the hub. Client updates the bell badge instantly, shows a toast, and fires a local OS notification via the service worker when permission is granted (no push server). Web Push stays as best-effort for closed-app reach. Verified end-to-end: login, open stream, broadcast, event delivered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="همکادر" />
|
||||
</head>
|
||||
<body>
|
||||
<body data-unread="@unreadCount" data-authed="@(User.Identity?.IsAuthenticated == true ? "1" : "0")">
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" asp-page="/Index">
|
||||
@@ -40,7 +40,7 @@
|
||||
@* Always-visible bell on mobile (next to the burger) so notifications stay one tap away *@
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
<a class="bell-mobile" asp-page="/Me/Notifications" title="اعلانها">🔔@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
<a class="bell-mobile js-bell" asp-page="/Me/Notifications" title="اعلانها">🔔@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
}
|
||||
|
||||
<input type="checkbox" id="nav-toggle" class="nav-toggle" hidden />
|
||||
@@ -70,7 +70,7 @@
|
||||
{
|
||||
<a class="nav-action" asp-page="/Employer/Index">پنل کارفرما</a>
|
||||
}
|
||||
<a class="nav-action bell-inline" asp-page="/Me/Notifications" title="اعلانها"><span class="bell-ico">🔔</span><span class="bell-label">اعلانها</span>@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
<a class="nav-action bell-inline js-bell" asp-page="/Me/Notifications" title="اعلانها"><span class="bell-ico">🔔</span><span class="bell-label">اعلانها</span>@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
<a class="nav-action" asp-page="/Me/Index">پنل کارجو</a>
|
||||
<form method="post" asp-page="/Account/Logout" style="display:contents;">
|
||||
<button type="submit" class="btn btn-outline btn-sm">خروج</button>
|
||||
@@ -103,12 +103,73 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div id="toast-host" class="toast-host" aria-live="polite"></div>
|
||||
|
||||
@* Register the PWA service worker (offline + push notifications). *@
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () { navigator.serviceWorker.register('/sw.js').catch(function () {}); });
|
||||
}
|
||||
</script>
|
||||
|
||||
@* Live in-app notifications over SSE (our own origin — works in Iran, no Google push).
|
||||
Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
<script>
|
||||
(function () {
|
||||
var faDigits = ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'];
|
||||
function toFa(s){ return String(s).replace(/[0-9]/g, function(d){ return faDigits[+d]; }); }
|
||||
var count = parseInt(document.body.getAttribute('data-unread') || '0', 10) || 0;
|
||||
|
||||
function paintBell() {
|
||||
document.querySelectorAll('.js-bell').forEach(function (bell) {
|
||||
var badge = bell.querySelector('.bell-badge');
|
||||
if (count > 0) {
|
||||
if (!badge) { badge = document.createElement('span'); badge.className = 'bell-badge'; bell.appendChild(badge); }
|
||||
badge.textContent = toFa(count > 99 ? '99+' : count);
|
||||
} else if (badge) { badge.remove(); }
|
||||
});
|
||||
}
|
||||
|
||||
function toast(n) {
|
||||
var host = document.getElementById('toast-host');
|
||||
if (!host) return;
|
||||
var el = document.createElement('a');
|
||||
el.className = 'toast';
|
||||
el.href = n.url || '/';
|
||||
el.innerHTML = '<span class="toast-ico">🔔</span><span class="toast-body"><strong></strong><span></span></span>';
|
||||
el.querySelector('strong').textContent = n.title || 'همکادر';
|
||||
el.querySelector('.toast-body span').textContent = n.body || '';
|
||||
host.appendChild(el);
|
||||
requestAnimationFrame(function(){ el.classList.add('show'); });
|
||||
setTimeout(function(){ el.classList.remove('show'); setTimeout(function(){ el.remove(); }, 300); }, 6000);
|
||||
}
|
||||
|
||||
function osNotify(n) {
|
||||
if (!('Notification' in window) || Notification.permission !== 'granted' || !navigator.serviceWorker) return;
|
||||
navigator.serviceWorker.ready.then(function (reg) {
|
||||
reg.showNotification(n.title || 'همکادر', {
|
||||
body: n.body || '', icon: '/icons/icon-192.png', badge: '/icons/icon-192.png',
|
||||
dir: 'rtl', lang: 'fa', tag: n.url || '/', data: { url: n.url || '/' }
|
||||
});
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
if (!('EventSource' in window)) return;
|
||||
var es;
|
||||
function connect() {
|
||||
es = new EventSource('/notifications/stream');
|
||||
es.addEventListener('notice', function (ev) {
|
||||
var n; try { n = JSON.parse(ev.data); } catch (_) { return; }
|
||||
count++; paintBell(); toast(n); osNotify(n);
|
||||
});
|
||||
// EventSource auto-reconnects on transient errors; nothing else needed.
|
||||
}
|
||||
connect();
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user