diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5e295..9ee9ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 28.2.0-rc.3 + +* Added: background push notifications render the server `notification` title, body, and image +* Added: notification `data` now carries `topic` and `payload` +* Fixed: a server-sent title dedupes to one notification per topic instead of one per subscription +* Fixed: malformed push payloads no longer break notification rendering + ## 28.2.0-rc.2 * Added: `Push` service for MQTT realtime push messages diff --git a/README.md b/README.md index cac2883..a72dfb6 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ import { Client, Account } from 'appwrite'; To install with a CDN (content delivery network) add the following scripts to the bottom of your tag, but before you use any Appwrite services: ```html - + ``` ## Getting Started diff --git a/package-lock.json b/package-lock.json index 30413c1..80ed902 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite", - "version": "28.2.0-rc.2", + "version": "28.2.0-rc.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite", - "version": "28.2.0-rc.2", + "version": "28.2.0-rc.3", "license": "BSD-3-Clause", "dependencies": { "buffer": "6.0.3", diff --git a/package.json b/package.json index 1b338a8..ced404a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "appwrite", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "28.2.0-rc.2", + "version": "28.2.0-rc.3", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { diff --git a/src/client.ts b/src/client.ts index 7cb7557..6ed4147 100644 --- a/src/client.ts +++ b/src/client.ts @@ -401,7 +401,7 @@ class Client { 'x-sdk-name': 'Web', 'x-sdk-platform': 'client', 'x-sdk-language': 'web', - 'x-sdk-version': '28.2.0-rc.2', + 'x-sdk-version': '28.2.0-rc.3', 'X-Appwrite-Response-Format': '2.3.0', }; diff --git a/src/services/push.ts b/src/services/push.ts index 5b2162d..fb731dc 100644 --- a/src/services/push.ts +++ b/src/services/push.ts @@ -817,16 +817,21 @@ export class Push { payload, qos: packet.qos, }; + const content = notificationContent(message); + const titles = new Set(); for (const sub of this.subscriptions.values()) { if (matches(sub.topic, message.topic)) { await sub.callback(message); - // Notification is per-subscription: only subs that opted in post one, - // each with its own title. + // Notification is per-subscription: only subs that opted in post one, each + // with its own title. A title the server sent replaces theirs, so one posts. if (sub.background) { - this.notify(message, sub.title); + titles.add(content.title ?? sub.title ?? message.topic); } } } + for (const title of titles) { + this.notify(message, title, content); + } } private requestNotificationPermission(): void { @@ -841,17 +846,26 @@ export class Push { } /** Post a browser notification for a message a background subscription matched. */ - private notify(message: PushMessage, title?: string): void { + private notify( + message: PushMessage, + title: string, + content: NotificationContent, + ): void { if ( typeof Notification === 'undefined' || Notification.permission !== 'granted' ) { return; } + const raw = !content.present; + // `image` is not in the DOM typings, but Chromium-based browsers show it. + const options: NotificationOptions & { image?: string } = { + body: content.body ?? (raw ? message.data : undefined), + image: content.image, + data: { topic: message.topic, payload: message.data }, + }; try { - new Notification(title ?? message.topic, { - body: message.data, - }); + new Notification(title, options); } catch { // Some browsers only allow notifications from a service worker; ignore. } @@ -864,6 +878,39 @@ class SupersededError extends Error { } } +/** What a message's `notification` block asks a background notification to show. */ +interface NotificationContent { + /** Whether the message has a `notification` block at all. */ + present: boolean; + title?: string; + body?: string; + image?: string; +} + +/** The server's `notification` block in a message; empty when the payload has none or is not JSON. */ +function notificationContent(message: PushMessage): NotificationContent { + let notification: unknown; + try { + notification = (JSON.parse(message.data) as { notification?: unknown }) + ?.notification; + } catch { + return { present: false }; + } + if (typeof notification !== 'object' || notification === null) { + return { present: false }; + } + const field = (name: string): string | undefined => { + const value = (notification as Record)[name]; + return typeof value === 'string' && value !== '' ? value : undefined; + }; + return { + present: true, + title: field('title'), + body: field('body'), + image: field('image'), + }; +} + /** Normalize anything thrown or passed to an error handler into an `Error`. */ function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error));