Notifications: Add web settings, sound and avatar, some fixes (#1317)
This commit is contained in:
parent
dd58fc08df
commit
d59390364c
BIN
public/notification.mp3
Normal file
BIN
public/notification.mp3
Normal file
Binary file not shown.
@ -136,6 +136,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-slider {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
&-description {
|
&-description {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import RangeSlider from '../../ui/RangeSlider';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -27,10 +28,14 @@ type StateProps = {
|
|||||||
hasBroadcastNotifications: boolean;
|
hasBroadcastNotifications: boolean;
|
||||||
hasBroadcastMessagePreview: boolean;
|
hasBroadcastMessagePreview: boolean;
|
||||||
hasContactJoinedNotifications: boolean;
|
hasContactJoinedNotifications: boolean;
|
||||||
|
hasWebNotifications: boolean;
|
||||||
|
hasPushNotifications: boolean;
|
||||||
|
notificationSoundVolume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings'
|
'loadNotificationSettings' | 'updateContactSignUpNotification' |
|
||||||
|
'updateNotificationSettings' | 'updateWebNotificationSettings'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
@ -44,9 +49,13 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
hasBroadcastNotifications,
|
hasBroadcastNotifications,
|
||||||
hasBroadcastMessagePreview,
|
hasBroadcastMessagePreview,
|
||||||
hasContactJoinedNotifications,
|
hasContactJoinedNotifications,
|
||||||
|
hasPushNotifications,
|
||||||
|
hasWebNotifications,
|
||||||
|
notificationSoundVolume,
|
||||||
loadNotificationSettings,
|
loadNotificationSettings,
|
||||||
updateContactSignUpNotification,
|
updateContactSignUpNotification,
|
||||||
updateNotificationSettings,
|
updateNotificationSettings,
|
||||||
|
updateWebNotificationSettings,
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNotificationSettings();
|
loadNotificationSettings();
|
||||||
@ -88,6 +97,44 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-content custom-scroll">
|
<div className="settings-content custom-scroll">
|
||||||
|
<div className="settings-item">
|
||||||
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
Web notifications
|
||||||
|
</h4>
|
||||||
|
<Checkbox
|
||||||
|
label="Web notifications"
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
subLabel={lang(hasWebNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
|
checked={hasWebNotifications}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateWebNotificationSettings({ hasWebNotifications: e.target.checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Offline notifications"
|
||||||
|
disabled={!hasWebNotifications}
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
subLabel={lang(hasPushNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
|
checked={hasPushNotifications}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateWebNotificationSettings({ hasPushNotifications: e.target.checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="settings-item-slider">
|
||||||
|
<RangeSlider
|
||||||
|
label="Sound"
|
||||||
|
disabled={!hasWebNotifications}
|
||||||
|
range={{
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
}}
|
||||||
|
value={notificationSoundVolume}
|
||||||
|
onChange={(volume) => {
|
||||||
|
updateWebNotificationSettings({ notificationSoundVolume: volume });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{lang('AutodownloadPrivateChats')}
|
{lang('AutodownloadPrivateChats')}
|
||||||
@ -102,6 +149,7 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={lang('MessagePreview')}
|
label={lang('MessagePreview')}
|
||||||
|
disabled={!hasPrivateChatsNotifications}
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
subLabel={lang(hasPrivateChatsMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
subLabel={lang(hasPrivateChatsMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
checked={hasPrivateChatsMessagePreview}
|
checked={hasPrivateChatsMessagePreview}
|
||||||
@ -120,6 +168,7 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={lang('MessagePreview')}
|
label={lang('MessagePreview')}
|
||||||
|
disabled={!hasGroupNotifications}
|
||||||
subLabel={lang(hasGroupMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
subLabel={lang(hasGroupMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
checked={hasGroupMessagePreview}
|
checked={hasGroupMessagePreview}
|
||||||
onChange={(e) => { handleSettingsChange(e, 'group', 'showPreviews'); }}
|
onChange={(e) => { handleSettingsChange(e, 'group', 'showPreviews'); }}
|
||||||
@ -138,6 +187,7 @@ const SettingsNotifications: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={lang('MessagePreview')}
|
label={lang('MessagePreview')}
|
||||||
|
disabled={!hasBroadcastNotifications}
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
subLabel={lang(hasBroadcastMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
subLabel={lang(hasBroadcastMessagePreview ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
checked={hasBroadcastMessagePreview}
|
checked={hasBroadcastMessagePreview}
|
||||||
@ -167,10 +217,14 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
|
|||||||
hasBroadcastNotifications: Boolean(global.settings.byKey.hasBroadcastNotifications),
|
hasBroadcastNotifications: Boolean(global.settings.byKey.hasBroadcastNotifications),
|
||||||
hasBroadcastMessagePreview: Boolean(global.settings.byKey.hasBroadcastMessagePreview),
|
hasBroadcastMessagePreview: Boolean(global.settings.byKey.hasBroadcastMessagePreview),
|
||||||
hasContactJoinedNotifications: Boolean(global.settings.byKey.hasContactJoinedNotifications),
|
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, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
'loadNotificationSettings',
|
'loadNotificationSettings',
|
||||||
'updateContactSignUpNotification',
|
'updateContactSignUpNotification',
|
||||||
'updateNotificationSettings',
|
'updateNotificationSettings',
|
||||||
|
'updateWebNotificationSettings',
|
||||||
]))(SettingsNotifications));
|
]))(SettingsNotifications));
|
||||||
|
|||||||
@ -133,6 +133,9 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
shouldAutoDownloadMediaInPrivateChats: true,
|
shouldAutoDownloadMediaInPrivateChats: true,
|
||||||
shouldAutoDownloadMediaInGroups: true,
|
shouldAutoDownloadMediaInGroups: true,
|
||||||
shouldAutoDownloadMediaInChannels: true,
|
shouldAutoDownloadMediaInChannels: true,
|
||||||
|
hasWebNotifications: true,
|
||||||
|
hasPushNotifications: true,
|
||||||
|
notificationSoundVolume: 5,
|
||||||
shouldAutoPlayGifs: true,
|
shouldAutoPlayGifs: true,
|
||||||
shouldAutoPlayVideos: true,
|
shouldAutoPlayVideos: true,
|
||||||
shouldSuggestStickers: true,
|
shouldSuggestStickers: true,
|
||||||
|
|||||||
@ -483,9 +483,9 @@ export type ActionTypes = (
|
|||||||
'loadBlockedContacts' | 'blockContact' | 'unblockContact' |
|
'loadBlockedContacts' | 'blockContact' | 'unblockContact' |
|
||||||
'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' |
|
'loadAuthorizations' | 'terminateAuthorization' | 'terminateAllAuthorizations' |
|
||||||
'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' |
|
'loadNotificationSettings' | 'updateContactSignUpNotification' | 'updateNotificationSettings' |
|
||||||
'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' | 'setPrivacySettings' |
|
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
|
||||||
'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' | 'loadContentSettings' |
|
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
|
||||||
'updateContentSettings' |
|
'loadContentSettings' | 'updateContentSettings' |
|
||||||
// Stickers & GIFs
|
// Stickers & GIFs
|
||||||
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
||||||
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
|
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
|
import { subscribe, unsubscribe } from '../../../util/notifications';
|
||||||
import { selectUser } from '../../selectors';
|
import { selectUser } from '../../selectors';
|
||||||
import {
|
import {
|
||||||
addUsers, addBlockedContact, updateChats, updateUser, removeBlockedContact, replaceSettings, updateNotifySettings,
|
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) => {
|
addReducer('updateContactSignUpNotification', (global, actions, payload) => {
|
||||||
const { isSilent } = payload!;
|
const { isSilent } = payload!;
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
selectIsChatListed,
|
selectIsChatListed,
|
||||||
selectChatListType,
|
selectChatListType,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectCountNotMutedUnread,
|
selectCountNotMutedUnread, selectNotifySettings,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
|
|
||||||
@ -135,7 +135,15 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
|||||||
|
|
||||||
const unreadCount = selectCountNotMutedUnread(getGlobal());
|
const unreadCount = selectCountNotMutedUnread(getGlobal());
|
||||||
updateAppBadge(unreadCount);
|
updateAppBadge(unreadCount);
|
||||||
showNewMessageNotification({ chat, message, isActiveChat });
|
|
||||||
|
const { hasWebNotifications } = selectNotifySettings(global);
|
||||||
|
if (hasWebNotifications) {
|
||||||
|
showNewMessageNotification({
|
||||||
|
chat,
|
||||||
|
message,
|
||||||
|
isActiveChat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { DEBUG, SESSION_USER_KEY } from '../../../config';
|
|||||||
import { subscribe } from '../../../util/notifications';
|
import { subscribe } from '../../../util/notifications';
|
||||||
import { updateUser } from '../../reducers';
|
import { updateUser } from '../../reducers';
|
||||||
import { setLanguage } from '../../../util/langProvider';
|
import { setLanguage } from '../../../util/langProvider';
|
||||||
|
import { selectNotifySettings } from '../../selectors';
|
||||||
|
|
||||||
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -68,7 +69,8 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onUpdateApiReady(global: GlobalState) {
|
function onUpdateApiReady(global: GlobalState) {
|
||||||
subscribe();
|
const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global);
|
||||||
|
if (hasWebNotifications && hasPushNotifications) subscribe();
|
||||||
setLanguage(global.settings.byKey.language);
|
setLanguage(global.settings.byKey.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -225,9 +225,14 @@ export function isChatArchived(chat: ApiChat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function selectIsChatMuted(
|
export function selectIsChatMuted(
|
||||||
chat: ApiChat, notifySettings: NotifySettings, notifyExceptions?: Record<number, NotifyException>,
|
chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record<number, NotifyException> = [],
|
||||||
) {
|
) {
|
||||||
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
|
chat.isMuted
|
||||||
|| (isChatPrivate(chat.id) && !notifySettings.hasPrivateChatsNotifications)
|
|| (isChatPrivate(chat.id) && !notifySettings.hasPrivateChatsNotifications)
|
||||||
|| (isChatChannel(chat) && !notifySettings.hasBroadcastNotifications)
|
|| (isChatChannel(chat) && !notifySettings.hasBroadcastNotifications)
|
||||||
@ -235,6 +240,24 @@ export function selectIsChatMuted(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectShouldShowMessagePreview(
|
||||||
|
chat: ApiChat, notifySettings: NotifySettings, notifyExceptions: Record<number, NotifyException> = [],
|
||||||
|
) {
|
||||||
|
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) {
|
export function getCanDeleteChat(chat: ApiChat) {
|
||||||
return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator);
|
return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ type NotificationData = {
|
|||||||
chatId?: number;
|
chatId?: number;
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastSyncAt = new Date().valueOf();
|
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,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
body,
|
body,
|
||||||
title,
|
title,
|
||||||
|
icon,
|
||||||
}: NotificationData) {
|
}: NotificationData) {
|
||||||
return self.registration.showNotification(title, {
|
await self.registration.showNotification(title, {
|
||||||
body,
|
body,
|
||||||
data: {
|
data: {
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
},
|
},
|
||||||
icon: 'icon-192x192.png',
|
icon: icon || 'icon-192x192.png',
|
||||||
badge: 'icon-192x192.png',
|
badge: icon || 'icon-192x192.png',
|
||||||
vibrate: [200, 100, 200],
|
vibrate: [200, 100, 200],
|
||||||
});
|
});
|
||||||
|
await playNotificationSound(messageId || chatId || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handlePush(e: PushEvent) {
|
export function handlePush(e: PushEvent) {
|
||||||
|
|||||||
@ -38,6 +38,9 @@ export type NotifySettings = {
|
|||||||
hasBroadcastNotifications?: boolean;
|
hasBroadcastNotifications?: boolean;
|
||||||
hasBroadcastMessagePreview?: boolean;
|
hasBroadcastMessagePreview?: boolean;
|
||||||
hasContactJoinedNotifications?: boolean;
|
hasContactJoinedNotifications?: boolean;
|
||||||
|
hasWebNotifications: boolean;
|
||||||
|
hasPushNotifications: boolean;
|
||||||
|
notificationSoundVolume: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LangCode = (
|
export type LangCode = (
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { callApi } from '../api/gramjs';
|
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 { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||||
import { 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,
|
||||||
getChatTitle,
|
getChatTitle,
|
||||||
getMessageAction,
|
getMessageAction,
|
||||||
getMessageSenderName,
|
getMessageSenderName,
|
||||||
@ -11,7 +14,7 @@ import {
|
|||||||
getPrivateChatUserId,
|
getPrivateChatUserId,
|
||||||
isActionMessage,
|
isActionMessage,
|
||||||
isChatChannel,
|
isChatChannel,
|
||||||
selectIsChatMuted,
|
selectIsChatMuted, selectShouldShowMessagePreview,
|
||||||
} from '../modules/helpers';
|
} from '../modules/helpers';
|
||||||
import { getTranslation } from './langProvider';
|
import { getTranslation } from './langProvider';
|
||||||
import { addNotifyExceptions, replaceSettings } from '../modules/reducers';
|
import { addNotifyExceptions, replaceSettings } from '../modules/reducers';
|
||||||
@ -19,6 +22,8 @@ import {
|
|||||||
selectChatMessage, selectNotifyExceptions, selectNotifySettings, selectUser,
|
selectChatMessage, selectNotifyExceptions, selectNotifySettings, selectUser,
|
||||||
} from '../modules/selectors';
|
} from '../modules/selectors';
|
||||||
import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||||
|
import * as mediaLoader from './mediaLoader';
|
||||||
|
import { debounce } from './schedulers';
|
||||||
|
|
||||||
function getDeviceToken(subscription: PushSubscription) {
|
function getDeviceToken(subscription: PushSubscription) {
|
||||||
const data = subscription.toJSON();
|
const data = subscription.toJSON();
|
||||||
@ -81,6 +86,38 @@ function checkIfNotificationsSupported() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expirationTime = 12 * 60 * 60 * 1000; // 12 hours
|
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<number>();
|
||||||
|
|
||||||
|
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) {
|
function checkIfShouldResubscribe(subscription: PushSubscription | null) {
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
@ -202,8 +239,8 @@ export async function subscribe() {
|
|||||||
function checkIfShouldNotify(chat: ApiChat, isActive: boolean) {
|
function checkIfShouldNotify(chat: ApiChat, isActive: boolean) {
|
||||||
if (!areSettingsLoaded) return false;
|
if (!areSettingsLoaded) return false;
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
if (selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)) || chat.isNotJoined
|
const isMuted = selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
||||||
|| !chat.isListed) {
|
if (isMuted || chat.isNotJoined || !chat.isListed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Dont show notification for active chat if client has focus
|
// Dont show notification for active chat if client has focus
|
||||||
@ -216,6 +253,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
senderId,
|
senderId,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
const messageSender = senderId ? selectUser(global, senderId) : undefined;
|
const messageSender = senderId ? selectUser(global, senderId) : undefined;
|
||||||
const messageAction = getMessageAction(message as ApiMessage);
|
const messageAction = getMessageAction(message as ApiMessage);
|
||||||
const actionTargetMessage = messageAction && replyToMessageId
|
const actionTargetMessage = messageAction && replyToMessageId
|
||||||
@ -227,29 +265,35 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
} = messageAction || {};
|
} = messageAction || {};
|
||||||
|
|
||||||
const actionTargetUsers = actionTargetUserIds
|
const actionTargetUsers = actionTargetUserIds
|
||||||
? actionTargetUserIds.map((userId) => selectUser(global, userId)).filter<ApiUser>(Boolean as any)
|
? actionTargetUserIds.map((userId) => selectUser(global, userId))
|
||||||
|
.filter<ApiUser>(Boolean as any)
|
||||||
: undefined;
|
: undefined;
|
||||||
const privateChatUserId = getPrivateChatUserId(chat);
|
const privateChatUserId = getPrivateChatUserId(chat);
|
||||||
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
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 {
|
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<ApiMediaFormat.BlobUrl>(imageHash);
|
||||||
|
if (!mediaData) {
|
||||||
|
await mediaLoader.fetch(imageHash, ApiMediaFormat.BlobUrl);
|
||||||
|
mediaData = mediaLoader.getFromMemory<ApiMediaFormat.BlobUrl>(imageHash);
|
||||||
|
}
|
||||||
|
return mediaData;
|
||||||
|
}
|
||||||
|
|
||||||
export async function showNewMessageNotification({
|
export async function showNewMessageNotification({
|
||||||
chat,
|
chat,
|
||||||
message,
|
message,
|
||||||
isActiveChat,
|
isActiveChat,
|
||||||
}: { chat: ApiChat; message: Partial<ApiMessage>; isActiveChat: boolean}) {
|
}: { chat: ApiChat; message: Partial<ApiMessage>; isActiveChat: boolean }) {
|
||||||
if (!checkIfNotificationsSupported()) return;
|
if (!checkIfNotificationsSupported()) return;
|
||||||
if (!message.id) return;
|
if (!message.id) return;
|
||||||
|
|
||||||
@ -274,6 +329,8 @@ export async function showNewMessageNotification({
|
|||||||
body,
|
body,
|
||||||
} = getNotificationContent(chat, message as ApiMessage);
|
} = getNotificationContent(chat, message as ApiMessage);
|
||||||
|
|
||||||
|
const icon = await getAvatar(chat);
|
||||||
|
|
||||||
if (checkIfPushSupported()) {
|
if (checkIfPushSupported()) {
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
// notify service worker about new message notification
|
// notify service worker about new message notification
|
||||||
@ -282,6 +339,7 @@ export async function showNewMessageNotification({
|
|||||||
payload: {
|
payload: {
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
|
icon,
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
},
|
},
|
||||||
@ -291,8 +349,8 @@ export async function showNewMessageNotification({
|
|||||||
const dispatch = getDispatch();
|
const dispatch = getDispatch();
|
||||||
const options: NotificationOptions = {
|
const options: NotificationOptions = {
|
||||||
body,
|
body,
|
||||||
icon: 'icon-192x192.png',
|
icon,
|
||||||
badge: 'icon-192x192.png',
|
badge: icon,
|
||||||
tag: message.id.toString(),
|
tag: message.id.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -312,6 +370,11 @@ export async function showNewMessageNotification({
|
|||||||
window.focus();
|
window.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Play sound when notification is displayed
|
||||||
|
notification.onshow = () => {
|
||||||
|
playNotificationSound(message.id || chat.id);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { scriptUrl } from 'service-worker-loader!../serviceWorker';
|
|||||||
import { DEBUG } from '../config';
|
import { DEBUG } from '../config';
|
||||||
import { getDispatch } from '../lib/teact/teactn';
|
import { getDispatch } from '../lib/teact/teactn';
|
||||||
import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment';
|
||||||
import { notifyClientReady } from './notifications';
|
import { notifyClientReady, playNotificationSound } from './notifications';
|
||||||
|
|
||||||
type WorkerAction = {
|
type WorkerAction = {
|
||||||
type: string;
|
type: string;
|
||||||
@ -16,7 +16,12 @@ function handleWorkerMessage(e: MessageEvent) {
|
|||||||
const dispatch = getDispatch();
|
const dispatch = getDispatch();
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'focusMessage':
|
case 'focusMessage':
|
||||||
dispatch.focusMessage(action.payload);
|
if (dispatch.focusMessage) {
|
||||||
|
dispatch.focusMessage(action.payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'playNotificationSound':
|
||||||
|
playNotificationSound(action.payload.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user