Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9142c50
Add Hebrew translation
Y-PLONI Jan 27, 2026
552619b
feat: עדכונים מינוריים להגדרות ותרגום web-push
ClickAndGoScript Feb 4, 2026
98b9b72
chore: שינוי שם תיקיית הלוקליזציה מ-he-IL ל-he
ClickAndGoScript Apr 27, 2026
510bebb
Merge branch 'main' of github.com-otzaria:ClickAndGoScript/nodebb-plu…
ClickAndGoScript Apr 27, 2026
d1d5e14
feat: הוספת טיפול בהרשאות התראות ושגיאות הרשמה לפוש נוטיפיקיישן
ClickAndGoScript May 1, 2026
d6d11d9
Merge branch 'NodeBB:main' into main
ClickAndGoScript May 1, 2026
9f1b30f
feat: add browser support detection and service worker timeout handling
ClickAndGoScript May 3, 2026
465fe50
feat: הוספת תמיכה ב-Service Worker לדפדני Safari ו-iOS
ClickAndGoScript May 3, 2026
add9b3f
feat: הוספת תמיכה במיזוג התראות באמצעות mergeId לתאימות עם Safari
ClickAndGoScript May 3, 2026
bb0113d
Revert "feat: הוספת תמיכה במיזוג התראות באמצעות mergeId לתאימות עם Sa…
ClickAndGoScript Jun 10, 2026
fc80c60
chore: הסרת לוגים מיותרים מרישום ה-Service Worker ומדף ההגדרות
ClickAndGoScript Jun 10, 2026
6af0d29
fix: החלפת התראות בספארי באמצעות Topic header
ClickAndGoScript Jun 10, 2026
d191261
Merge branch 'NodeBB:main' into main
ClickAndGoScript Jun 10, 2026
78ee02c
feat: הוספת כפתורי פעולה להתראות פוש - "סמן כנקרא" ו"צפה בהתראות"
ClickAndGoScript Jun 10, 2026
9d4f38d
Remove nodebb-plugin-web-push-1.xml from .gitignore
barisusakli Jun 10, 2026
eccbd03
feat: הוספת באנר הצעת הרשמה להתראות דחיפה
ClickAndGoScript Jun 11, 2026
9e49182
chore: הסרת מנגנון ה-Topic header להחלפת התראות בספארי
ClickAndGoScript Jun 11, 2026
e676d3f
refactor: שימוש בתשתיות המובנות של NodeBB בקוד הלקוח
ClickAndGoScript Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ pip-log.txt

sftp-config.json
node_modules/

28 changes: 23 additions & 5 deletions library.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ plugin.init = async (params) => {
};

plugin.appendConfig = async (config) => {
const { publicKey } = await meta.settings.get('web-push');
const { publicKey, promptEnabled, promptDelay } = await meta.settings.get('web-push');
config['web-push'] = {
vapidKey: publicKey,
// checkbox serialization varies by database and core version
promptEnabled: [true, 'true', 'on', 1, '1'].includes(promptEnabled),
promptDelay: Math.max(parseInt(promptDelay, 10) || 3, 1),
};

return config;
Expand Down Expand Up @@ -104,8 +107,8 @@ plugin.addRoutes = async ({ router, middleware, helpers }) => {
const { subscription } = req.body;
const payload = await constructPayload({
nid: utils.generateUUID(),
bodyShort: 'Test notification',
bodyLong: 'This is a test message sent from NodeBB',
bodyShort: '[[web-push:test.title]]',
bodyLong: '[[web-push:test.body]]',
path: `/me/web-push`,
}, req.uid, userLang);
await webPush.sendNotification(subscription, JSON.stringify(payload));
Expand All @@ -116,7 +119,7 @@ plugin.addAdminNavigation = (header) => {
header.plugins.push({
route: '/plugins/web-push',
icon: 'fa-tint',
name: 'Push Notifications (via Push API)',
name: '[[web-push:admin.menu-label]]',
});

return header;
Expand Down Expand Up @@ -239,12 +242,27 @@ async function constructPayload(notification, uid, lang) {
badge = `${nconf.get('url')}${meta.config['brand:maskableIcon'] || '/apple-touch-icon'}`;
}

const actions = await constructActions(lang);

return {
title,
body,
tag,
lang,
dir,
data: { url, icon, badge },
actions,
data: { url, icon, badge, nid },
};
}

async function constructActions(lang) {
const [markRead, viewNotifications] = await translator.translateKeys([
'[[web-push:action.mark-read]]',
'[[web-push:action.view-notifications]]',
], lang);

return [
{ action: 'mark-read', title: markRead },
{ action: 'view-notifications', title: viewNotifications },
];
}
4 changes: 4 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
{ "hook": "filter:service-worker.scripts", "method": "registerServiceWorker" }
],
"languages": "public/languages",
"scripts": [
"public/lib/main.js",
"public/lib/prompt.js"
],
"modules": {
"../client/account/web-push.js": "./public/lib/settings.js",
"../admin/plugins/web-push.js": "./public/lib/admin.js"
Expand Down
36 changes: 34 additions & 2 deletions public/languages/en-GB/web-push.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,37 @@
"profile.send-test": "Send Test Notification",

"toast.test_success": "Test notification sent.",
"toast.test_unavailable": "Cannot send test notification as push notifications are not enabled on this device."
}
"toast.subscribe_success": "Push notifications enabled on this device.",
"toast.test_unavailable": "Cannot send test notification as push notifications are not enabled on this device.",
"toast.permission_denied": "Notification permission was denied. Please enable notifications for this site in your browser settings.",
"toast.subscribe_failed": "Could not enable push notifications on this device. Please try again.",
"toast.unsupported": "This browser does not support push notifications.",
"toast.sw_not_registered": "Push notifications are unavailable: the background service is not registered. On iOS, install this site to your Home Screen first.",

"action.mark-read": "Mark as read",
"action.view-notifications": "View notifications",

"prompt.title": "Stay in the loop",
"prompt.body": "Enable push notifications to get alerted even when you're not on the site.",
"prompt.confirm": "Enable notifications",
"prompt.dismiss": "No thanks",

"test.title": "Test notification",
"test.body": "This is a test message sent from NodeBB",

"admin.menu-label": "Push Notifications (via Push API)",
"admin.settings": "Settings",
"admin.max-length": "Maximum length",
"admin.max-length-help": "Additional characters beyond this specified length will be truncated. Due to a software limitation, if the message body is greater than 4096 bytes, the message itself will be an attachment in the push notification.",
"admin.badge": "Badge URL",
"admin.badge-help": "Optional — overrides the badge for messages sent (usually seen in the notification bar on mobile devices). By default, the site's configured \"touch icon\" is sent.",
"admin.icon": "Icon URL",
"admin.icon-help": "Optional — overrides the icon for messages sent (can be used for branding, etc.). By default, the site's configured \"touch icon\" is sent.",
"admin.prompt-enabled": "Show opt-in prompt to logged-in users",
"admin.prompt-enabled-help": "When enabled, a small corner banner inviting users to enable push notifications appears after the configured number of page visits. It does not block the page, and is never shown again once the user dismisses it or subscribes.",
"admin.prompt-delay": "Show prompt after (page visits)",
"admin.prompt-delay-help": "Number of page visits a logged-in user must make before the opt-in prompt appears. Minimum 1.",
"admin.users": "Users",
"admin.user": "User",
"admin.devices": "Devices"
}
43 changes: 43 additions & 0 deletions public/languages/he/web-push.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"profile.label": "התראות דחיפה",
"profile.introduction": "בנוסף להתראות בתוך האפליקציה ולהתראות בדוא״ל, ניתן לבחור לקבל גם התראות דחיפה. כך תוכלו לקבל התראות גם כשהאפליקציה אינה פתוחה במכשיר.",
"profile.option": "הפעלת התראות דחיפה במכשיר זה",
"profile.devices": "כרגע נשלחות התראות ל־ <strong>%1</strong> מכשיר(ים).",
"profile.permissionBlocked": "המכשיר שלך אינו מאפשר כרגע לקבל התראות מאתר זה. יש לאשר את הרשאת ההתראות כדי להמשיך.",
"profile.send-test": "שליחת התראת בדיקה",

"toast.test_success": "התראת הבדיקה נשלחה.",
"toast.subscribe_success": "התראות דחיפה הופעלו במכשיר זה.",
"toast.test_unavailable": "לא ניתן לשלוח התראת בדיקה כי התראות דחיפה אינן מופעלות במכשיר זה.",
"toast.permission_denied": "הרשאת ההתראות נדחתה. יש לאפשר התראות עבור אתר זה בהגדרות הדפדפן.",
"toast.subscribe_failed": "לא ניתן להפעיל התראות דחיפה במכשיר זה. נסו שוב.",
"toast.unsupported": "הדפדפן הזה אינו תומך בהתראות דחיפה.",
"toast.sw_not_registered": "התראות דחיפה אינן זמינות: שירות הרקע לא נרשם. באייפון, יש להתקין את האתר תחילה למסך הבית.",

"action.mark-read": "סמן כנקרא",
"action.view-notifications": "צפה בהתראות",

"prompt.title": "הישארו מעודכנים",
"prompt.body": "הפעילו התראות דחיפה וקבלו עדכונים גם כשאתם לא באתר.",
"prompt.confirm": "הפעלת התראות",
"prompt.dismiss": "לא תודה",

"test.title": "התראת בדיקה",
"test.body": "זוהי הודעת בדיקה שנשלחה מ־NodeBB",

"admin.menu-label": "התראות דחיפה (Push API)",
"admin.settings": "הגדרות",
"admin.max-length": "אורך מרבי",
"admin.max-length-help": "תווים מעבר לאורך שצוין ייחתכו. בשל מגבלת תוכנה, אם גוף ההודעה גדול מ־4096 בתים, ההודעה עצמה תישלח כקובץ מצורף בהתראת הדחיפה.",
"admin.badge": "כתובת תג (Badge)",
"admin.badge-help": "אופציונלי — מחליף את התג בהודעות הנשלחות (מוצג בדרך כלל בשורת ההתראות במכשירים ניידים). כברירת מחדל נשלח סמל ה־touch icon המוגדר באתר.",
"admin.icon": "כתובת סמל (Icon)",
"admin.icon-help": "אופציונלי — מחליף את הסמל בהודעות הנשלחות (שימושי למיתוג וכדומה). כברירת מחדל נשלח סמל ה־touch icon המוגדר באתר.",
"admin.prompt-enabled": "הצגת הצעת הרשמה למשתמשים מחוברים",
"admin.prompt-enabled-help": "כאשר מופעל, באנר קטן בפינת המסך המזמין משתמשים להפעיל התראות דחיפה יוצג לאחר מספר הביקורים שהוגדר. הבאנר אינו חוסם את הדף, ולא יוצג שוב לאחר שהמשתמש דחה אותו או נרשם.",
"admin.prompt-delay": "הצגת ההצעה לאחר (מספר ביקורים)",
"admin.prompt-delay-help": "מספר הביקורים שמשתמש מחובר צריך לבצע לפני שהצעת ההרשמה תוצג. מינימום 1.",
"admin.users": "משתמשים",
"admin.user": "משתמש",
"admin.devices": "מכשירים"
}
36 changes: 34 additions & 2 deletions public/languages/zh-CN/web-push.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,37 @@
"profile.send-test": "发送测试通知",

"toast.test_success": "测试通知已发送。",
"toast.test_unavailable": "由于此设备未启用推送通知,因此无法发送测试通知。"
}
"toast.subscribe_success": "已在此设备上启用推送通知。",
"toast.test_unavailable": "由于此设备未启用推送通知,因此无法发送测试通知。",
"toast.permission_denied": "通知权限已被拒绝。请在浏览器设置中允许此站点发送通知。",
"toast.subscribe_failed": "无法在此设备上启用推送通知。请重试。",
"toast.unsupported": "此浏览器不支持推送通知。",
"toast.sw_not_registered": "推送通知不可用:后台服务未注册。在 iOS 上,请先将本站添加到主屏幕。",

"action.mark-read": "标记为已读",
"action.view-notifications": "查看通知",

"prompt.title": "保持最新动态",
"prompt.body": "启用推送通知,即使不在网站时也能及时收到提醒。",
"prompt.confirm": "启用通知",
"prompt.dismiss": "不,谢谢",

"test.title": "测试通知",
"test.body": "这是一条来自 NodeBB 的测试消息",

"admin.menu-label": "推送通知(Push API)",
"admin.settings": "设置",
"admin.max-length": "最大长度",
"admin.max-length-help": "超出指定长度的字符将被截断。由于软件限制,如果消息正文超过 4096 字节,消息本身将作为附件随推送通知发送。",
"admin.badge": "徽章 URL",
"admin.badge-help": "可选 — 覆盖所发送消息的徽章(通常显示在移动设备的通知栏中)。默认发送站点配置的“touch icon”。",
"admin.icon": "图标 URL",
"admin.icon-help": "可选 — 覆盖所发送消息的图标(可用于品牌宣传等)。默认发送站点配置的“touch icon”。",
"admin.prompt-enabled": "向已登录用户显示订阅提示",
"admin.prompt-enabled-help": "启用后,在达到设定的访问次数后,会在屏幕角落显示一个邀请用户启用推送通知的小横幅。它不会阻挡页面,用户关闭或订阅后将不再显示。",
"admin.prompt-delay": "显示提示前的访问次数",
"admin.prompt-delay-help": "已登录用户需要访问多少次页面后才会显示订阅提示。最小值为 1。",
"admin.users": "用户",
"admin.user": "用户",
"admin.devices": "设备"
}
33 changes: 30 additions & 3 deletions public/lib/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
// this file here as placeholder in case needed. Add back to plugin.json to use

'use strict';

// NodeBB core skips serviceWorker.register() on Safari (see public/src/app.js).
// That predates iOS 16.4, where Safari/PWA does support Web Push. Without an SW,
// navigator.serviceWorker.ready hangs forever and our settings page silently fails.
// This script registers the SW ourselves on Safari/iOS.

(async () => {
const [hooks] = await app.require(['hooks']);

hooks.on('action:app.load', async () => {
// ...
if (!('serviceWorker' in navigator)) {
return;
}

// Mirror core's own condition (config.useragent.isSafari) so the two
// checks can never disagree: core skips exactly when we register.
if (!config.useragent || !config.useragent.isSafari) {
return;
}

// Core already tried (and skipped) registration by this point. If something
// is registered, leave it alone.
const existing = await navigator.serviceWorker.getRegistration();
if (existing) {
return;
}

const swUrl = (config.relative_path || '') + '/service-worker.js';
const scope = (config.relative_path || '') + '/';

try {
await navigator.serviceWorker.register(swUrl, { scope });
} catch (err) {
console.error('[web-push] service worker registration failed:', err);
}
});
})();
94 changes: 94 additions & 0 deletions public/lib/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

// Invites logged-in users to enable push notifications via a corner banner,
// shown after a configurable number of page loads (see ACP settings).
// Visit count and dismissal are tracked per-device in localStorage.

(async () => {
const [hooks, api, alerts, storage] = await app.require(['hooks', 'api', 'alerts', 'storage']);

const visitsKey = 'web-push:visits';
const dismissedKey = 'web-push:prompt-dismissed';

hooks.on('action:app.load', async () => {
const { promptEnabled, promptDelay, vapidKey } = config['web-push'] || {};
if (!promptEnabled || !vapidKey || !app.user.uid || storage.getItem(dismissedKey)) {
return;
}

if (!('serviceWorker' in navigator) || !('PushManager' in window) ||
Notification.permission === 'denied') {
return;
}

const registration = await navigator.serviceWorker.getRegistration();
if (!registration || await registration.pushManager.getSubscription()) {
return;
}

const visits = (parseInt(storage.getItem(visitsKey), 10) || 0) + 1;
storage.setItem(visitsKey, visits);
if (visits < promptDelay) {
return;
}

showBanner(registration);
});

async function showBanner(registration) {
const $banner = await app.parseAndTranslate('partials/web-push/prompt', {});
const banner = $banner.get(0);
document.body.append(banner);

banner.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) {
return;
}

if (btn.dataset.action === 'subscribe') {
subscribe(registration);
}

storage.setItem(dismissedKey, '1');
banner.remove();
});
}

async function subscribe(registration) {
try {
// As in settings.js: when permission is already granted there is no
// await before subscribe(), keeping the call within the click's user
// activation (iOS Safari requires this).
if (Notification.permission !== 'granted' &&
await Notification.requestPermission() !== 'granted') {
alerts.warning('[[web-push:toast.permission_denied]]');
return;
}

const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(config['web-push'].vapidKey),
});
await api.post('/plugins/web-push/subscription', { subscription: subscription.toJSON() });
alerts.success('[[web-push:toast.subscribe_success]]');
} catch (err) {
console.error('[web-push] subscribing from prompt failed:', err);
alerts.warning('[[web-push:toast.subscribe_failed]]');
}
}

// Chrome doesn't accept a base64 string for applicationServerKey
// https://bugs.chromium.org/p/chromium/issues/detail?id=802280
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');

const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
})();
Loading