Notifications: Grouping and fixes for Android (#1412)
This commit is contained in:
parent
2015b1a0d9
commit
95707c5423
@ -32,9 +32,8 @@ type NotificationData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let lastSyncAt = new Date().valueOf();
|
let lastSyncAt = new Date().valueOf();
|
||||||
|
|
||||||
const clickBuffer: Record<string, NotificationData> = {};
|
|
||||||
const shownNotifications = new Set();
|
const shownNotifications = new Set();
|
||||||
|
const clickBuffer: Record<string, NotificationData> = {};
|
||||||
|
|
||||||
function getPushData(e: PushEvent | Notification): PushData | undefined {
|
function getPushData(e: PushEvent | Notification): PushData | undefined {
|
||||||
try {
|
try {
|
||||||
@ -95,17 +94,32 @@ async function showNotification({
|
|||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
}: NotificationData) {
|
}: NotificationData) {
|
||||||
await self.registration.showNotification(title, {
|
const tag = String(chatId || 0);
|
||||||
|
const options: NotificationOptions = {
|
||||||
body,
|
body,
|
||||||
data: {
|
data: {
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
icon: icon || 'icon-192x192.png',
|
icon: icon || 'icon-192x192.png',
|
||||||
badge: icon || 'icon-192x192.png',
|
badge: 'icon-192x192.png',
|
||||||
|
tag,
|
||||||
vibrate: [200, 100, 200],
|
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) {
|
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);
|
const data = getPushData(e);
|
||||||
|
|
||||||
// Do not show muted notifications
|
// Do not show muted notifications
|
||||||
@ -171,17 +181,25 @@ export function handleNotificationClick(e: NotificationEvent) {
|
|||||||
const clientsInScope = clients.filter((client) => {
|
const clientsInScope = clients.filter((client) => {
|
||||||
return new URL(client.url).origin === appUrl;
|
return new URL(client.url).origin === appUrl;
|
||||||
});
|
});
|
||||||
e.waitUntil(Promise.all(clientsInScope.map((client) => {
|
await Promise.all(clientsInScope.map((client) => {
|
||||||
clickBuffer[client.id] = data;
|
clickBuffer[client.id] = data;
|
||||||
return focusChatMessage(client, data);
|
return focusChatMessage(client, data);
|
||||||
})));
|
}));
|
||||||
if (!self.clients.openWindow || clientsInScope.length > 0) return undefined;
|
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
|
// 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);
|
try {
|
||||||
if (newClient) {
|
const newClient = await self.clients.openWindow(appUrl);
|
||||||
// Store notification data until client is fully loaded
|
if (newClient) {
|
||||||
clickBuffer[newClient.id] = data;
|
// 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;
|
return undefined;
|
||||||
};
|
};
|
||||||
@ -197,21 +215,23 @@ export function handleClientMessage(e: ExtendableMessageEvent) {
|
|||||||
const source = e.source as WindowClient;
|
const source = e.source as WindowClient;
|
||||||
if (e.data.type === 'clientReady') {
|
if (e.data.type === 'clientReady') {
|
||||||
// focus on chat message when client is fully ready
|
// focus on chat message when client is fully ready
|
||||||
if (clickBuffer[source.id]) {
|
const data = clickBuffer[source.id] || clickBuffer[0];
|
||||||
e.waitUntil(focusChatMessage(source, clickBuffer[source.id]));
|
if (data) {
|
||||||
delete clickBuffer[source.id];
|
delete clickBuffer[source.id];
|
||||||
|
delete clickBuffer[0];
|
||||||
|
e.waitUntil(focusChatMessage(source, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.data.type === 'newMessageNotification') {
|
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
|
// 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;
|
const notification: NotificationData = e.data.payload;
|
||||||
// mark this notification as shown if it was handled locally
|
// mark this notification as shown if it was handled locally
|
||||||
shownNotifications.add(notification.messageId);
|
shownNotifications.add(notification.messageId);
|
||||||
|
e.waitUntil(showNotification(notification));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
ApiChat, ApiMediaFormat, ApiMessage, ApiUser,
|
ApiChat, ApiMediaFormat, ApiMessage, ApiUser,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
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 { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn';
|
||||||
import {
|
import {
|
||||||
getChatAvatarHash,
|
getChatAvatarHash,
|
||||||
@ -313,123 +313,6 @@ async function getAvatar(chat: ApiChat) {
|
|||||||
return mediaData;
|
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({
|
export async function showNewMessageNotification({
|
||||||
chat,
|
chat,
|
||||||
message,
|
message,
|
||||||
@ -448,13 +331,51 @@ export async function showNewMessageNotification({
|
|||||||
|
|
||||||
const icon = await getAvatar(chat);
|
const icon = await getAvatar(chat);
|
||||||
|
|
||||||
await handleNotification({
|
if (checkIfPushSupported()) {
|
||||||
title,
|
if (navigator.serviceWorker.controller) {
|
||||||
body,
|
// notify service worker about new message notification
|
||||||
icon,
|
navigator.serviceWorker.controller.postMessage({
|
||||||
messageId: message.id,
|
type: 'newMessageNotification',
|
||||||
chatId: chat.id,
|
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
|
// Notify service worker that client is fully loaded
|
||||||
|
|||||||
@ -10,6 +10,10 @@ type WorkerAction = {
|
|||||||
|
|
||||||
function handleWorkerMessage(e: MessageEvent) {
|
function handleWorkerMessage(e: MessageEvent) {
|
||||||
const action: WorkerAction = e.data;
|
const action: WorkerAction = e.data;
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[SW] Message from worker', action);
|
||||||
|
}
|
||||||
if (!action.type) return;
|
if (!action.type) return;
|
||||||
const dispatch = getDispatch();
|
const dispatch = getDispatch();
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user