Notify Chat Members of New Messages
Build a Group Chat shows new messages to members who have the chat open. This recipe notifies everyone else, with a push on their phone or computer.
puter.notifications.notify() only notifies the user
your app runs for, so the sender's app can't notify the other members. Instead,
each member's app subscribes to the room with a handler that Puter runs for that
member, even while your app is closed. When a message arrives, the handler calls
notify() for its member. A member who has the chat
open gets the notification in the page, and Puter doesn't push it.
This recipe uses the room, room, prefix, handle and user from
Build a Group Chat. Your site also needs to serve
/puter-sw.js, as in
Push notifications for your app.
Publish the Handler
A handler is a function your app publishes once, under a name, with
puter.events.handlers.publish(). Puter runs it in your
app's events worker, where user is the member the
subscription belongs to:
await puter.events.handlers.publish('notifyNewMessage', async ({ event, user, ctx }) => {
const message = event.value;
if (event.op !== 'set' || !message || message.from === ctx.me) return;
await user.notifications.notify({
title: `${message.from} in ${ctx.roomTitle}`.slice(0, 120),
text: String(message.text).slice(0, 240),
channel: 'chat',
group: `room:${ctx.roomId}`,
target: `/rooms/${ctx.roomId}`,
data: { roomId: ctx.roomId },
});
});
Only the account that owns the app can publish, so run this once yourself, signed in to your site as the owner. The browser console is enough. Publishing the same source again does nothing.
The handler can't use variables from around it, because Puter runs it later,
somewhere else. Everything it needs comes from event, user, and ctx, the
values each subscription is created with. The me check skips the member's own
messages, since their subscription sees those too.
Say What a Push Shows
On every page load, register a handler for the chat channel with
puter.notifications.onPush(), and open the room
when the user taps a notification with
puter.notifications.on('click'):
await puter.notifications.onPush('chat', (n) => ({
title: n.title,
body: n.text ?? '',
url: n.target ?? '/',
}));
puter.notifications.on('click', ({ data }) => {
if (data?.roomId) openRoomById(data.roomId);
});
Here openRoomById() is your app's own function that shows a room.
While the chat is open, the notification reaches this handler in the page
instead of being pushed. The page shows it only when its tab is hidden or its
window isn't focused, and the message itself arrives through the room's
watch(). While the chat is closed, the same handler runs in your service
worker to decide what the push shows.
Subscribe Each Member
Each member subscribes to the room's messages with
puter.events.onPersistent(), naming the handler. A
persistent subscription is stored on the member's account and keeps running
after they close your app. The creator watches the room by its prefix, and a
member watches it through their share's handle:
const roomSubject = handle
? `kv:${handle}:msg:*`
: `kv:${puter.appID}:${prefix}msg:*`;
Running a handler while the user is away needs the user's consent, which
puter.perms.request() asks for with the backgroundEvents
resource. Check puter.events.list() first, so a member who
already subscribed doesn't get a second subscription:
async function subscribeToRoom (subject, roomId, roomTitle) {
const subs = await puter.events.list();
const subscribed = subs.some((sub) =>
sub.subject === subject && sub.handlerName === 'notifyNewMessage' && !sub.suspendedAt);
if (subscribed) return true;
const options = {
subject,
handlerName: 'notifyNewMessage',
includeValue: true,
context: { me: user.username, roomId, roomTitle },
};
try {
await puter.events.onPersistent(options);
} catch (error) {
if (error.code !== 'events_background_consent_required') throw error;
if (!await puter.perms.request('backgroundEvents')) return false;
await puter.events.onPersistent(options);
}
return true;
}
With includeValue: true, the new message comes in event.value, so the
handler doesn't have to read it. When the creator removes a member, or the member
leaves, the member's subscription stops on its own.
Turn Notifications On
Give the room a "Notify me" button. Browsers only show their permission prompt
from a click, so call puter.notifications.enable()
first, then subscribe:
document.getElementById('notify').addEventListener('click', async () => {
const pushes = await puter.notifications.enable();
const { roomId, title } = await room.get('meta');
const subscribed = await subscribeToRoom(roomSubject, roomId, title);
console.log(pushes && subscribed ? 'You will be notified' : 'Notifications are off');
});
puter.notifications.enable() turns pushes on in this
browser only. A member who wants them on their phone too presses the button
there. On iPhone and iPad, that's from your site added to the Home Screen, as
Where pushes work explains.
Full Code
The handler, published once by the app's owner:
await puter.events.handlers.publish('notifyNewMessage', async ({ event, user, ctx }) => {
const message = event.value;
if (event.op !== 'set' || !message || message.from === ctx.me) return;
await user.notifications.notify({
title: `${message.from} in ${ctx.roomTitle}`.slice(0, 120),
text: String(message.text).slice(0, 240),
channel: 'chat',
group: `room:${ctx.roomId}`,
target: `/rooms/${ctx.roomId}`,
data: { roomId: ctx.roomId },
});
});
The page, after the room is open:
await puter.notifications.onPush('chat', (n) => ({
title: n.title,
body: n.text ?? '',
url: n.target ?? '/',
}));
puter.notifications.on('click', ({ data }) => {
if (data?.roomId) openRoomById(data.roomId);
});
const roomSubject = handle
? `kv:${handle}:msg:*`
: `kv:${puter.appID}:${prefix}msg:*`;
async function subscribeToRoom (subject, roomId, roomTitle) {
const subs = await puter.events.list();
const subscribed = subs.some((sub) =>
sub.subject === subject && sub.handlerName === 'notifyNewMessage' && !sub.suspendedAt);
if (subscribed) return true;
const options = {
subject,
handlerName: 'notifyNewMessage',
includeValue: true,
context: { me: user.username, roomId, roomTitle },
};
try {
await puter.events.onPersistent(options);
} catch (error) {
if (error.code !== 'events_background_consent_required') throw error;
if (!await puter.perms.request('backgroundEvents')) return false;
await puter.events.onPersistent(options);
}
return true;
}
document.getElementById('notify').addEventListener('click', async () => {
const pushes = await puter.notifications.enable();
const { roomId, title } = await room.get('meta');
const subscribed = await subscribeToRoom(roomSubject, roomId, title);
console.log(pushes && subscribed ? 'You will be notified' : 'Notifications are off');
});
The worker, at /puter-sw.js:
importScripts('https://js.puter.com/push-sw.js');