Skip to content

Commit 76ce4ac

Browse files
committed
Remove-dismiss stale notifications from device
1 parent db06ede commit 76ce4ac

File tree

2 files changed

+174
-11
lines changed

2 files changed

+174
-11
lines changed

notification/method/webpush.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,47 @@ protected function notify_using_webpush(): void
310310
*/
311311
public function mark_notifications($notification_type_id, $item_id, $user_id, $time = false, $mark_read = true)
312312
{
313+
// Send dismiss push messages BEFORE deleting to close the browser notifications
314+
// This is called by the notification manager when phpBB marks notifications as read
315+
// (e.g., viewing a PM, viewing a topic, clicking "mark all read", etc.)
316+
if ($notification_type_id !== false && $item_id !== false && $user_id !== false)
317+
{
318+
// When item_id and user_id are specific, send dismiss for each notification
319+
// Arrays are typically same-length parallel arrays or single notification type with specific item
320+
$type_ids = is_array($notification_type_id) ? $notification_type_id : [$notification_type_id];
321+
$item_ids = is_array($item_id) ? $item_id : [$item_id];
322+
$user_ids = is_array($user_id) ? $user_id : [$user_id];
323+
324+
// Most common case: single notification (single type, item, user)
325+
if (count($type_ids) === 1 && count($item_ids) === 1 && count($user_ids) === 1)
326+
{
327+
$this->dismiss_using_webpush($type_ids[0], $item_ids[0], $user_ids[0]);
328+
}
329+
// Parallel arrays case: matching length arrays
330+
else if (count($type_ids) === count($item_ids) && count($item_ids) === count($user_ids))
331+
{
332+
for ($i = 0, $iMax = count($type_ids); $i < $iMax; $i++)
333+
{
334+
$this->dismiss_using_webpush($type_ids[$i], $item_ids[$i], $user_ids[$i]);
335+
}
336+
}
337+
// Mixed case: iterate combinations (rare but handle it)
338+
else
339+
{
340+
foreach ($type_ids as $type)
341+
{
342+
foreach ($item_ids as $iid)
343+
{
344+
foreach ($user_ids as $uid)
345+
{
346+
$this->dismiss_using_webpush($type, $iid, $uid);
347+
}
348+
}
349+
}
350+
}
351+
}
352+
353+
// Delete the notifications from our table
313354
$sql = 'DELETE FROM ' . $this->notification_webpush_table . '
314355
WHERE ' . ($notification_type_id !== false ? $this->db->sql_in_set('notification_type_id', is_array($notification_type_id) ? $notification_type_id : [$notification_type_id]) : '1=1') .
315356
($user_id !== false ? ' AND ' . $this->db->sql_in_set('user_id', $user_id) : '') .
@@ -322,6 +363,24 @@ public function mark_notifications($notification_type_id, $item_id, $user_id, $t
322363
*/
323364
public function mark_notifications_by_parent($notification_type_id, $item_parent_id, $user_id, $time = false, $mark_read = true)
324365
{
366+
// Send dismiss push messages BEFORE deleting
367+
// Query needed because service worker uses item_id (not item_parent_id) to match notification tags
368+
if ($notification_type_id !== false && $user_id !== false && $item_parent_id !== false)
369+
{
370+
$sql = 'SELECT notification_type_id, item_id, user_id
371+
FROM ' . $this->notification_webpush_table . '
372+
WHERE ' . $this->db->sql_in_set('notification_type_id', is_array($notification_type_id) ? $notification_type_id : [$notification_type_id]) .
373+
' AND ' . $this->db->sql_in_set('user_id', is_array($user_id) ? $user_id : [$user_id]) .
374+
' AND ' . $this->db->sql_in_set('item_parent_id', is_array($item_parent_id) ? $item_parent_id : [$item_parent_id], false, true);
375+
$result = $this->db->sql_query($sql);
376+
while ($row = $this->db->sql_fetchrow($result))
377+
{
378+
$this->dismiss_using_webpush($row['notification_type_id'], $row['item_id'], $row['user_id']);
379+
}
380+
$this->db->sql_freeresult($result);
381+
}
382+
383+
// Delete the notifications from our table
325384
$sql = 'DELETE FROM ' . $this->notification_webpush_table . '
326385
WHERE ' . ($notification_type_id !== false ? $this->db->sql_in_set('notification_type_id', is_array($notification_type_id) ? $notification_type_id : [$notification_type_id]) : '1=1') .
327386
($user_id !== false ? ' AND ' . $this->db->sql_in_set('user_id', $user_id) : '') .
@@ -493,4 +552,75 @@ protected function set_endpoint_padding(\Minishlink\WebPush\WebPush $web_push, s
493552
}
494553
}
495554
}
555+
556+
/**
557+
* Send dismiss message via Web Push to close a browser notification
558+
*
559+
* @param int $notification_type_id Notification type ID
560+
* @param int $item_id Item ID
561+
* @param int $user_id User ID
562+
* @return void
563+
*/
564+
protected function dismiss_using_webpush(int $notification_type_id, int $item_id, int $user_id): void
565+
{
566+
// Get user subscriptions
567+
$user_subscription_map = $this->get_user_subscription_map([$user_id]);
568+
$user_subscriptions = $user_subscription_map[$user_id] ?? [];
569+
570+
if (empty($user_subscriptions))
571+
{
572+
return;
573+
}
574+
575+
$auth = [
576+
'VAPID' => [
577+
'subject' => generate_board_url(false),
578+
'publicKey' => $this->config['wpn_webpush_vapid_public'],
579+
'privateKey' => $this->config['wpn_webpush_vapid_private'],
580+
],
581+
];
582+
583+
$web_push = new \Minishlink\WebPush\WebPush($auth);
584+
585+
// Create dismiss message
586+
$data = [
587+
'action' => 'dismiss',
588+
'notifications' => [[
589+
'type_id' => $notification_type_id,
590+
'item_id' => $item_id,
591+
]],
592+
];
593+
$json_data = json_encode($data);
594+
595+
// Send dismiss message to all user's subscriptions
596+
foreach ($user_subscriptions as $subscription)
597+
{
598+
try
599+
{
600+
$this->set_endpoint_padding($web_push, $subscription['endpoint']);
601+
$push_subscription = Subscription::create([
602+
'endpoint' => $subscription['endpoint'],
603+
'keys' => [
604+
'p256dh' => $subscription['p256dh'],
605+
'auth' => $subscription['auth'],
606+
],
607+
]);
608+
$web_push->queueNotification($push_subscription, $json_data);
609+
}
610+
catch (\ErrorException $exception)
611+
{
612+
// Ignore - dismiss is best-effort
613+
}
614+
}
615+
616+
// Flush and ignore any errors - dismiss messages are best-effort
617+
try
618+
{
619+
$web_push->flush();
620+
}
621+
catch (\ErrorException $exception)
622+
{
623+
// Ignore errors
624+
}
625+
}
496626
}

styles/all/template/push_worker.js.twig

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,27 @@ self.addEventListener('push', event => {
2121
return;
2222
}
2323

24-
let itemId = 0;
25-
let typeId = 0;
26-
let userId = 0;
27-
let notificationVersion = 0;
28-
let pushToken = '';
24+
let pushData;
2925
try {
30-
const notificationData = event.data.json();
31-
itemId = notificationData.item_id;
32-
typeId = notificationData.type_id;
33-
userId = notificationData.user_id;
34-
notificationVersion = parseInt(notificationData.version, 10);
35-
pushToken = notificationData.token;
26+
pushData = event.data.json();
3627
} catch {
3728
event.waitUntil(self.registration.showNotification(event.data.text()));
3829
return;
3930
}
4031

32+
// Handle dismiss action
33+
if (pushData.action === 'dismiss') {
34+
event.waitUntil(handleDismissNotifications(pushData.notifications));
35+
return;
36+
}
37+
38+
// Handle regular notification display
39+
let itemId = pushData.item_id || 0;
40+
let typeId = pushData.type_id || 0;
41+
let userId = pushData.user_id || 0;
42+
let notificationVersion = parseInt(pushData.version, 10) || 0;
43+
let pushToken = pushData.token || '';
44+
4145
event.waitUntil((async () => {
4246
const getNotificationUrl = '{{ U_WEBPUSH_GET_NOTIFICATION }}';
4347
const assetsVersion = parseInt('{{ ASSETS_VERSION }}', 10);
@@ -65,10 +69,12 @@ self.addEventListener('push', event => {
6569
const responseData = await response.json();
6670

6771
const responseBody = responseData.title + '\n' + responseData.text;
72+
const notificationTag = typeId + '_' + itemId;
6873
const options = {
6974
body: responseBody,
7075
data: responseData,
7176
icon: responseData.avatar.src,
77+
tag: notificationTag,
7278
};
7379

7480
await self.registration.showNotification(responseData.heading, options);
@@ -87,3 +93,30 @@ self.addEventListener('notificationclick', event => {
8793
event.waitUntil(self.clients.openWindow(event.notification.data.url));
8894
}
8995
});
96+
97+
/**
98+
* Handle dismiss notifications pushed from the server
99+
*
100+
* @param {Array} notifications Array of notifications to dismiss
101+
* @returns {Promise<void>}
102+
*/
103+
async function handleDismissNotifications(notifications) {
104+
if (!notifications || !Array.isArray(notifications) || notifications.length === 0) {
105+
return;
106+
}
107+
108+
try {
109+
// Close each notification by its tag
110+
for (const dismissed of notifications) {
111+
const tag = dismissed.type_id + '_' + dismissed.item_id;
112+
113+
// Get and close notifications with this specific tag
114+
const matchingNotifications = await self.registration.getNotifications({ tag: tag });
115+
for (const notification of matchingNotifications) {
116+
notification.close();
117+
}
118+
}
119+
} catch (e) {
120+
console.error('Error dismissing notifications:', e);
121+
}
122+
}

0 commit comments

Comments
 (0)