Notifications: Grouping and fixes for Android (#1412)

This commit is contained in:
Alexander Zinchuk 2021-08-20 23:47:27 +03:00
parent 2015b1a0d9
commit 95707c5423
3 changed files with 94 additions and 149 deletions

View File

@ -32,9 +32,8 @@ type NotificationData = {
};
let lastSyncAt = new Date().valueOf();
const clickBuffer: Record<string, NotificationData> = {};
const shownNotifications = new Set();
const clickBuffer: Record<string, NotificationData> = {};
function getPushData(e: PushEvent | Notification): PushData | undefined {
try {
@ -95,17 +94,32 @@ async function showNotification({
title,
icon,
}: NotificationData) {
await self.registration.showNotification(title, {
const tag = String(chatId || 0);
const options: NotificationOptions = {
body,
data: {
chatId,
messageId,
count: 1,
},
icon: icon || 'icon-192x192.png',
badge: icon || 'icon-192x192.png',
badge: 'icon-192x192.png',
tag,
vibrate: [200, 100, 200],
});
await playNotificationSound(messageId || chatId || 0);
};
const notifications = await self.registration.getNotifications({ tag });
if (notifications.length > 0) {
const current = notifications[0];
const count = current.data.count + 1;
options.data.count = count;
options.data.messageId = current.data.messageId;
options.body = `You have ${count} new messages`;
current.close();
}
return Promise.all([
playNotificationSound(messageId || chatId || 0),
self.registration.showNotification(title, options),
]);
}
export function handlePush(e: PushEvent) {
@ -118,10 +132,6 @@ export function handlePush(e: PushEvent) {
}
}
// Do not show notifications right after sync (when browser is opened)
// To avoid stale notifications
if (new Date().valueOf() - lastSyncAt < 3000) return;
const data = getPushData(e);
// Do not show muted notifications
@ -171,17 +181,25 @@ export function handleNotificationClick(e: NotificationEvent) {
const clientsInScope = clients.filter((client) => {
return new URL(client.url).origin === appUrl;
});
e.waitUntil(Promise.all(clientsInScope.map((client) => {
await Promise.all(clientsInScope.map((client) => {
clickBuffer[client.id] = data;
return focusChatMessage(client, data);
})));
}));
if (!self.clients.openWindow || clientsInScope.length > 0) return undefined;
// Store notification data for default client (fix for android)
clickBuffer[0] = data;
// If there is no opened client we need to open one and wait until it is fully loaded
const newClient = await self.clients.openWindow(appUrl);
if (newClient) {
// Store notification data until client is fully loaded
clickBuffer[newClient.id] = data;
try {
const newClient = await self.clients.openWindow(appUrl);
if (newClient) {
// Store notification data until client is fully loaded
clickBuffer[newClient.id] = data;
}
} catch (error) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn('[SW] ', error);
}
}
return undefined;
};
@ -197,21 +215,23 @@ export function handleClientMessage(e: ExtendableMessageEvent) {
const source = e.source as WindowClient;
if (e.data.type === 'clientReady') {
// focus on chat message when client is fully ready
if (clickBuffer[source.id]) {
e.waitUntil(focusChatMessage(source, clickBuffer[source.id]));
const data = clickBuffer[source.id] || clickBuffer[0];
if (data) {
delete clickBuffer[source.id];
delete clickBuffer[0];
e.waitUntil(focusChatMessage(source, data));
}
}
if (e.data.type === 'newMessageNotification') {
// Do not show notifications right after sync (when browser is opened)
// To avoid stale notifications
if (new Date().valueOf() - lastSyncAt < 3000) return;
// store messageId for already shown notification
const notification: NotificationData = e.data.payload;
e.waitUntil(showNotification(notification));
shownNotifications.add(notification.messageId);
}
if (e.data.type === 'notificationHandled') {
const notification: NotificationData = e.data.payload;
// mark this notification as shown if it was handled locally
shownNotifications.add(notification.messageId);
e.waitUntil(showNotification(notification));
}
}

View File

@ -3,7 +3,7 @@ import {
ApiChat, ApiMediaFormat, ApiMessage, ApiUser,
} from '../api/types';
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
import { APP_NAME, DEBUG } from '../config';
import { DEBUG } from '../config';
import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn';
import {
getChatAvatarHash,
@ -313,123 +313,6 @@ async function getAvatar(chat: ApiChat) {
return mediaData;
}
type NotificationData = {
messageId?: number;
chatId?: number;
title: string;
body: string;
icon?: string;
};
const handledNotifications = new Set();
let pendingNotifications: Record<number, NotificationData[]> = {};
async function showNotifications(groupLimit: number = 2) {
const count = Object.keys(pendingNotifications).reduce<number>((result, groupId) => {
result += pendingNotifications[Number(groupId)].length;
return result;
}, 0);
// if we have more than groupLimit notification groups we send only one notification
if (Object.keys(pendingNotifications).length > groupLimit) {
await showNotification({
title: APP_NAME,
body: `You have ${count} new Telegram notifications`,
});
} else {
// Else we send a notification per group
await Promise.all(Object.keys(pendingNotifications)
// eslint-disable-next-line no-async-without-await/no-async-without-await
.map(async (groupId) => {
const group = pendingNotifications[Number(groupId)];
if (group.length > groupLimit) {
const lastMessage = group[group.length - 1];
return showNotification({
title: APP_NAME,
body: `You have ${count} notifications from ${lastMessage.title}`,
messageId: lastMessage.messageId,
chatId: Number(groupId),
});
}
return Promise.all(group.map(showNotification));
}));
}
// Clear all pending notifications
pendingNotifications = {};
}
const flushNotifications = debounce(showNotifications, 1000, false);
async function handleNotification(data: NotificationData, groupLimit?: number) {
// Dont show already triggered notification
if (handledNotifications.has(data.messageId)) {
handledNotifications.delete(data.messageId);
return;
}
const groupId = data.chatId || 0;
if (!pendingNotifications[groupId]) {
pendingNotifications[groupId] = [];
}
pendingNotifications[groupId].push(data);
await flushNotifications(groupLimit);
if (checkIfPushSupported()) {
if (navigator.serviceWorker.controller) {
// notify service worker that notification was handled locally
navigator.serviceWorker.controller.postMessage({
type: 'notificationHandled',
payload: data,
});
}
}
handledNotifications.add(data.messageId);
}
function showNotification(data: NotificationData) {
if (checkIfPushSupported()) {
if (navigator.serviceWorker.controller) {
// notify service worker about new message notification
navigator.serviceWorker.controller.postMessage({
type: 'newMessageNotification',
payload: data,
});
}
} else {
const dispatch = getDispatch();
const options: NotificationOptions = {
body: data.body,
icon: data.icon,
badge: data.icon,
tag: data.messageId ? data.messageId.toString() : undefined,
};
if ('vibrate' in navigator) {
options.vibrate = [200, 100, 200];
}
const notification = new Notification(data.title, options);
notification.onclick = () => {
notification.close();
dispatch.focusMessage({
chatId: data.chatId,
messageId: data.messageId,
});
if (window.focus) {
window.focus();
}
};
// Play sound when notification is displayed
notification.onshow = () => {
const id = data.messageId || data.chatId;
if (id) playNotificationSound(id);
};
}
}
export async function showNewMessageNotification({
chat,
message,
@ -448,13 +331,51 @@ export async function showNewMessageNotification({
const icon = await getAvatar(chat);
await handleNotification({
title,
body,
icon,
messageId: message.id,
chatId: chat.id,
});
if (checkIfPushSupported()) {
if (navigator.serviceWorker.controller) {
// notify service worker about new message notification
navigator.serviceWorker.controller.postMessage({
type: 'newMessageNotification',
payload: {
title,
body,
icon,
chatId: chat.id,
messageId: message.id,
},
});
}
} else {
const dispatch = getDispatch();
const options: NotificationOptions = {
body,
icon,
badge: icon,
tag: message.id.toString(),
};
if ('vibrate' in navigator) {
options.vibrate = [200, 100, 200];
}
const notification = new Notification(title, options);
notification.onclick = () => {
notification.close();
dispatch.focusMessage({
chatId: chat.id,
messageId: message.id,
});
if (window.focus) {
window.focus();
}
};
// Play sound when notification is displayed
notification.onshow = () => {
playNotificationSound(message.id || chat.id);
};
}
}
// Notify service worker that client is fully loaded

View File

@ -10,6 +10,10 @@ type WorkerAction = {
function handleWorkerMessage(e: MessageEvent) {
const action: WorkerAction = e.data;
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[SW] Message from worker', action);
}
if (!action.type) return;
const dispatch = getDispatch();
switch (action.type) {