From d59390364ccdfe1095cda4a7157f2faca6630b56 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 11 Aug 2021 01:27:52 +0300 Subject: [PATCH] Notifications: Add web settings, sound and avatar, some fixes (#1317) --- public/notification.mp3 | Bin 0 -> 10880 bytes src/components/left/settings/Settings.scss | 4 + .../left/settings/SettingsNotifications.tsx | 56 ++++++++- src/global/initial.ts | 3 + src/global/types.ts | 6 +- src/modules/actions/api/settings.ts | 14 +++ src/modules/actions/apiUpdaters/chats.ts | 12 +- src/modules/actions/apiUpdaters/initial.ts | 4 +- src/modules/helpers/chats.ts | 27 +++- src/serviceWorker/pushNotification.ts | 23 +++- src/types/index.ts | 3 + src/util/notifications.ts | 115 ++++++++++++++---- src/util/setupServiceWorker.ts | 9 +- 13 files changed, 235 insertions(+), 41 deletions(-) create mode 100644 public/notification.mp3 diff --git a/public/notification.mp3 b/public/notification.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0a9f875f20565807ee8258b7bd3ace34aa39230a GIT binary patch literal 10880 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 const SettingsNotifications: FC = ({ @@ -44,9 +49,13 @@ const SettingsNotifications: FC = ({ hasBroadcastNotifications, hasBroadcastMessagePreview, hasContactJoinedNotifications, + hasPushNotifications, + hasWebNotifications, + notificationSoundVolume, loadNotificationSettings, updateContactSignUpNotification, updateNotificationSettings, + updateWebNotificationSettings, }) => { useEffect(() => { loadNotificationSettings(); @@ -88,6 +97,44 @@ const SettingsNotifications: FC = ({ return (
+
+

+ Web notifications +

+ { + updateWebNotificationSettings({ hasWebNotifications: e.target.checked }); + }} + /> + { + updateWebNotificationSettings({ hasPushNotifications: e.target.checked }); + }} + /> +
+ { + updateWebNotificationSettings({ notificationSoundVolume: volume }); + }} + /> +
+

{lang('AutodownloadPrivateChats')} @@ -102,6 +149,7 @@ const SettingsNotifications: FC = ({ /> = ({ /> { handleSettingsChange(e, 'group', 'showPreviews'); }} @@ -138,6 +187,7 @@ const SettingsNotifications: FC = ({ /> ((global): StateProps => { hasBroadcastNotifications: Boolean(global.settings.byKey.hasBroadcastNotifications), hasBroadcastMessagePreview: Boolean(global.settings.byKey.hasBroadcastMessagePreview), hasContactJoinedNotifications: Boolean(global.settings.byKey.hasContactJoinedNotifications), + hasWebNotifications: global.settings.byKey.hasWebNotifications, + hasPushNotifications: global.settings.byKey.hasPushNotifications, + notificationSoundVolume: global.settings.byKey.notificationSoundVolume, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadNotificationSettings', 'updateContactSignUpNotification', 'updateNotificationSettings', + 'updateWebNotificationSettings', ]))(SettingsNotifications)); diff --git a/src/global/initial.ts b/src/global/initial.ts index 96d2f8d31..e39852aa6 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -133,6 +133,9 @@ export const INITIAL_STATE: GlobalState = { shouldAutoDownloadMediaInPrivateChats: true, shouldAutoDownloadMediaInGroups: true, shouldAutoDownloadMediaInChannels: true, + hasWebNotifications: true, + hasPushNotifications: true, + notificationSoundVolume: 5, shouldAutoPlayGifs: true, shouldAutoPlayVideos: true, shouldSuggestStickers: true, diff --git a/src/global/types.ts b/src/global/types.ts index ed1442d7c..bc48da9a7 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -483,9 +483,9 @@ export type ActionTypes = ( 'loadBlockedContacts' | 'blockContact' | 'unblockContact' | 'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' | 'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' | - 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | 'setPrivacySettings' | - 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' | 'loadContentSettings' | - 'updateContentSettings' | + 'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | + 'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' | + 'loadContentSettings' | 'updateContentSettings' | // Stickers & GIFs 'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' | 'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' | diff --git a/src/modules/actions/api/settings.ts b/src/modules/actions/api/settings.ts index 82fded80d..b6ad8937b 100644 --- a/src/modules/actions/api/settings.ts +++ b/src/modules/actions/api/settings.ts @@ -8,6 +8,7 @@ import { import { callApi } from '../../../api/gramjs'; import { buildCollectionByKey } from '../../../util/iteratees'; +import { subscribe, unsubscribe } from '../../../util/notifications'; import { selectUser } from '../../selectors'; import { addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings, @@ -346,6 +347,19 @@ addReducer('updateNotificationSettings', (global, actions, payload) => { })(); }); +addReducer('updateWebNotificationSettings', (global, actions, payload) => { + (async () => { + setGlobal(replaceSettings(getGlobal(), payload)); + const newGlobal = getGlobal(); + const { hasPushNotifications, hasWebNotifications } = newGlobal.settings.byKey; + if (hasWebNotifications && hasPushNotifications) { + await subscribe(); + } else { + await unsubscribe(); + } + })(); +}); + addReducer('updateContactSignUpNotification', (global, actions, payload) => { const { isSilent } = payload!; diff --git a/src/modules/actions/apiUpdaters/chats.ts b/src/modules/actions/apiUpdaters/chats.ts index 7a613a70c..09ea73a6c 100644 --- a/src/modules/actions/apiUpdaters/chats.ts +++ b/src/modules/actions/apiUpdaters/chats.ts @@ -19,7 +19,7 @@ import { selectIsChatListed, selectChatListType, selectCurrentMessageList, - selectCountNotMutedUnread, + selectCountNotMutedUnread, selectNotifySettings, } from '../../selectors'; import { throttle } from '../../../util/schedulers'; @@ -135,7 +135,15 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { const unreadCount = selectCountNotMutedUnread(getGlobal()); updateAppBadge(unreadCount); - showNewMessageNotification({ chat, message, isActiveChat }); + + const { hasWebNotifications } = selectNotifySettings(global); + if (hasWebNotifications) { + showNewMessageNotification({ + chat, + message, + isActiveChat, + }); + } break; } diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index 6c38a8264..6c83307ca 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -16,6 +16,7 @@ import { DEBUG, SESSION_USER_KEY } from '../../../config'; import { subscribe } from '../../../util/notifications'; import { updateUser } from '../../reducers'; import { setLanguage } from '../../../util/langProvider'; +import { selectNotifySettings } from '../../selectors'; addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { if (DEBUG) { @@ -68,7 +69,8 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { }); function onUpdateApiReady(global: GlobalState) { - subscribe(); + const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global); + if (hasWebNotifications && hasPushNotifications) subscribe(); setLanguage(global.settings.byKey.language); } diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index 3150ec442..e2a05110b 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -225,9 +225,14 @@ export function isChatArchived(chat: ApiChat) { } export function selectIsChatMuted( - chat: ApiChat, notifySettings: NotifySettings, notifyExceptions?: Record, + chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record = [], ) { - return !(notifyExceptions && notifyExceptions[chat.id] && !notifyExceptions[chat.id].isMuted) && ( + // If this chat is in exceptions they take precedence + if (notifyExceptions[chat.id] && notifyExceptions[chat.id].isMuted !== undefined) { + return notifyExceptions[chat.id].isMuted; + } + + return ( chat.isMuted || (isChatPrivate(chat.id) && !notifySettings.hasPrivateChatsNotifications) || (isChatChannel(chat) && !notifySettings.hasBroadcastNotifications) @@ -235,6 +240,24 @@ export function selectIsChatMuted( ); } +export function selectShouldShowMessagePreview( + chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record = [], +) { + const { + hasPrivateChatsMessagePreview = true, + hasBroadcastMessagePreview = true, + hasGroupMessagePreview = true, + } = notifySettings; + // If this chat is in exceptions they take precedence + if (notifyExceptions[chat.id] && notifyExceptions[chat.id].shouldShowPreviews !== undefined) { + return notifyExceptions[chat.id].shouldShowPreviews; + } + + return (isChatPrivate(chat.id) && hasPrivateChatsMessagePreview) + || (isChatChannel(chat) && hasBroadcastMessagePreview) + || (isChatGroup(chat) && hasGroupMessagePreview); +} + export function getCanDeleteChat(chat: ApiChat) { return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); } diff --git a/src/serviceWorker/pushNotification.ts b/src/serviceWorker/pushNotification.ts index a446a0803..97863e71d 100644 --- a/src/serviceWorker/pushNotification.ts +++ b/src/serviceWorker/pushNotification.ts @@ -28,6 +28,7 @@ type NotificationData = { chatId?: number; title: string; body: string; + icon?: string; }; let lastSyncAt = new Date().valueOf(); @@ -75,22 +76,36 @@ function getNotificationData(data: PushData): NotificationData { }; } -function showNotification({ +async function playNotificationSound(id: number) { + const clients = await self.clients.matchAll({ type: 'window' }) as WindowClient[]; + const clientsInScope = clients.filter((client) => client.url === self.registration.scope); + const client = clientsInScope[0]; + if (!client) return; + if (clientsInScope.length === 0) return; + client.postMessage({ + type: 'playNotificationSound', + payload: { id }, + }); +} + +async function showNotification({ chatId, messageId, body, title, + icon, }: NotificationData) { - return self.registration.showNotification(title, { + await self.registration.showNotification(title, { body, data: { chatId, messageId, }, - icon: 'icon-192x192.png', - badge: 'icon-192x192.png', + icon: icon || 'icon-192x192.png', + badge: icon || 'icon-192x192.png', vibrate: [200, 100, 200], }); + await playNotificationSound(messageId || chatId || 0); } export function handlePush(e: PushEvent) { diff --git a/src/types/index.ts b/src/types/index.ts index 034d3acdf..52358bc76 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -38,6 +38,9 @@ export type NotifySettings = { hasBroadcastNotifications?: boolean; hasBroadcastMessagePreview?: boolean; hasContactJoinedNotifications?: boolean; + hasWebNotifications: boolean; + hasPushNotifications: boolean; + notificationSoundVolume: number; }; export type LangCode = ( diff --git a/src/util/notifications.ts b/src/util/notifications.ts index 81696f6b8..fb987c423 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -1,9 +1,12 @@ import { callApi } from '../api/gramjs'; -import { ApiChat, ApiMessage, ApiUser } from '../api/types'; +import { + ApiChat, ApiMediaFormat, ApiMessage, ApiUser, +} from '../api/types'; import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText'; import { DEBUG } from '../config'; import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn'; import { + getChatAvatarHash, getChatTitle, getMessageAction, getMessageSenderName, @@ -11,7 +14,7 @@ import { getPrivateChatUserId, isActionMessage, isChatChannel, - selectIsChatMuted, + selectIsChatMuted, selectShouldShowMessagePreview, } from '../modules/helpers'; import { getTranslation } from './langProvider'; import { addNotifyExceptions, replaceSettings } from '../modules/reducers'; @@ -19,6 +22,8 @@ import { selectChatMessage, selectNotifyExceptions, selectNotifySettings, selectUser, } from '../modules/selectors'; import { IS_SERVICE_WORKER_SUPPORTED } from './environment'; +import * as mediaLoader from './mediaLoader'; +import { debounce } from './schedulers'; function getDeviceToken(subscription: PushSubscription) { const data = subscription.toJSON(); @@ -81,6 +86,38 @@ function checkIfNotificationsSupported() { } const expirationTime = 12 * 60 * 60 * 1000; // 12 hours +// Notification id is removed from soundPlayed cache after 3 seconds +const soundPlayedDelay = 3 * 1000; +const soundPlayed = new Set(); + +async function playSound(id: number) { + if (soundPlayed.has(id)) return; + const { notificationSoundVolume } = selectNotifySettings(getGlobal()); + const volume = notificationSoundVolume / 10; + if (volume === 0) return; + + const audio = new Audio('/notification.mp3'); + audio.volume = volume; + audio.setAttribute('mozaudiochannel', 'notification'); + audio.addEventListener('ended', () => { + soundPlayed.add(id); + }, { once: true }); + + setTimeout(() => { + soundPlayed.delete(id); + }, soundPlayedDelay); + + try { + await audio.play(); + } catch (error) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('[PUSH] Unable to play notification sound'); + } + } +} + +export const playNotificationSound = debounce(playSound, 1000, true, false); function checkIfShouldResubscribe(subscription: PushSubscription | null) { const global = getGlobal(); @@ -202,8 +239,8 @@ export async function subscribe() { function checkIfShouldNotify(chat: ApiChat, isActive: boolean) { if (!areSettingsLoaded) return false; const global = getGlobal(); - if (selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)) || chat.isNotJoined - || !chat.isListed) { + const isMuted = selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); + if (isMuted || chat.isNotJoined || !chat.isListed) { return false; } // Dont show notification for active chat if client has focus @@ -216,6 +253,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) { senderId, replyToMessageId, } = message; + const messageSender = senderId ? selectUser(global, senderId) : undefined; const messageAction = getMessageAction(message as ApiMessage); const actionTargetMessage = messageAction && replyToMessageId @@ -227,29 +265,35 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) { } = messageAction || {}; const actionTargetUsers = actionTargetUserIds - ? actionTargetUserIds.map((userId) => selectUser(global, userId)).filter(Boolean as any) + ? actionTargetUserIds.map((userId) => selectUser(global, userId)) + .filter(Boolean as any) : undefined; const privateChatUserId = getPrivateChatUserId(chat); const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; - let body: string; - if (isActionMessage(message)) { - const actionOrigin = chat && (isChatChannel(chat) || message.senderId === message.chatId) - ? chat - : messageSender; - body = renderActionMessageText( - getTranslation, - message, - actionOrigin, - actionTargetUsers, - actionTargetMessage, - actionTargetChatId, - { asPlain: true }, - ) as string; - } else { - const senderName = getMessageSenderName(getTranslation, chat.id, messageSender); - const summary = getMessageSummaryText(getTranslation, message); - body = senderName ? `${senderName}: ${summary}` : summary; + let body: string; + if (selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))) { + if (isActionMessage(message)) { + const actionOrigin = chat && (isChatChannel(chat) || message.senderId === message.chatId) + ? chat + : messageSender; + body = renderActionMessageText( + getTranslation, + message, + actionOrigin, + actionTargetUsers, + actionTargetMessage, + actionTargetChatId, + { asPlain: true }, + ) as string; + } else { + const senderName = getMessageSenderName(getTranslation, chat.id, messageSender); + const summary = getMessageSummaryText(getTranslation, message); + + body = senderName ? `${senderName}: ${summary}` : summary; + } + } else { + body = 'New message'; } return { @@ -258,11 +302,22 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) { }; } +async function getAvatar(chat: ApiChat) { + const imageHash = getChatAvatarHash(chat); + if (!imageHash) return undefined; + let mediaData = mediaLoader.getFromMemory(imageHash); + if (!mediaData) { + await mediaLoader.fetch(imageHash, ApiMediaFormat.BlobUrl); + mediaData = mediaLoader.getFromMemory(imageHash); + } + return mediaData; +} + export async function showNewMessageNotification({ chat, message, isActiveChat, -}: { chat: ApiChat; message: Partial; isActiveChat: boolean}) { +}: { chat: ApiChat; message: Partial; isActiveChat: boolean }) { if (!checkIfNotificationsSupported()) return; if (!message.id) return; @@ -274,6 +329,8 @@ export async function showNewMessageNotification({ body, } = getNotificationContent(chat, message as ApiMessage); + const icon = await getAvatar(chat); + if (checkIfPushSupported()) { if (navigator.serviceWorker.controller) { // notify service worker about new message notification @@ -282,6 +339,7 @@ export async function showNewMessageNotification({ payload: { title, body, + icon, chatId: chat.id, messageId: message.id, }, @@ -291,8 +349,8 @@ export async function showNewMessageNotification({ const dispatch = getDispatch(); const options: NotificationOptions = { body, - icon: 'icon-192x192.png', - badge: 'icon-192x192.png', + icon, + badge: icon, tag: message.id.toString(), }; @@ -312,6 +370,11 @@ export async function showNewMessageNotification({ window.focus(); } }; + + // Play sound when notification is displayed + notification.onshow = () => { + playNotificationSound(message.id || chat.id); + }; } } diff --git a/src/util/setupServiceWorker.ts b/src/util/setupServiceWorker.ts index e2d859407..c1ef15197 100644 --- a/src/util/setupServiceWorker.ts +++ b/src/util/setupServiceWorker.ts @@ -3,7 +3,7 @@ import { scriptUrl } from 'service-worker-loader!../serviceWorker'; import { DEBUG } from '../config'; import { getDispatch } from '../lib/teact/teactn'; import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment'; -import { notifyClientReady } from './notifications'; +import { notifyClientReady, playNotificationSound } from './notifications'; type WorkerAction = { type: string; @@ -16,7 +16,12 @@ function handleWorkerMessage(e: MessageEvent) { const dispatch = getDispatch(); switch (action.type) { case 'focusMessage': - dispatch.focusMessage(action.payload); + if (dispatch.focusMessage) { + dispatch.focusMessage(action.payload); + } + break; + case 'playNotificationSound': + playNotificationSound(action.payload.id); break; } }