Notifications: Request permission after user gesture and better settings (#3155)
This commit is contained in:
parent
227f706a0c
commit
9d4a62e839
@ -6,7 +6,11 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import { playNotifySound } from '../../../util/notifications';
|
import {
|
||||||
|
playNotifySound,
|
||||||
|
checkIfNotificationsSupported,
|
||||||
|
checkIfOfflinePushFailed,
|
||||||
|
} from '../../../util/notifications';
|
||||||
|
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
import RangeSlider from '../../ui/RangeSlider';
|
import RangeSlider from '../../ui/RangeSlider';
|
||||||
@ -56,6 +60,9 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const runDebounced = useRunDebounced(500, true);
|
const runDebounced = useRunDebounced(500, true);
|
||||||
|
|
||||||
|
const areNotificationsSupported = checkIfNotificationsSupported();
|
||||||
|
const areOfflineNotificationsSupported = areNotificationsSupported && !checkIfOfflinePushFailed();
|
||||||
|
|
||||||
const handleSettingsChange = useCallback((
|
const handleSettingsChange = useCallback((
|
||||||
e: ChangeEvent<HTMLInputElement>,
|
e: ChangeEvent<HTMLInputElement>,
|
||||||
peerType: 'contact' | 'group' | 'broadcast',
|
peerType: 'contact' | 'group' | 'broadcast',
|
||||||
@ -81,8 +88,10 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleWebNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const handleWebNotificationsChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const isEnabled = e.target.checked;
|
||||||
updateWebNotificationSettings({
|
updateWebNotificationSettings({
|
||||||
hasWebNotifications: e.target.checked,
|
hasWebNotifications: isEnabled,
|
||||||
|
...(!isEnabled && { hasPushNotifications: false }),
|
||||||
});
|
});
|
||||||
}, [updateWebNotificationSettings]);
|
}, [updateWebNotificationSettings]);
|
||||||
|
|
||||||
@ -147,13 +156,14 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
subLabel={lang(hasWebNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
subLabel={lang(hasWebNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
||||||
checked={hasWebNotifications}
|
checked={hasWebNotifications}
|
||||||
|
disabled={!areNotificationsSupported}
|
||||||
onChange={handleWebNotificationsChange}
|
onChange={handleWebNotificationsChange}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Offline notifications"
|
label="Offline notifications"
|
||||||
disabled={!hasWebNotifications}
|
disabled={!hasWebNotifications || !areOfflineNotificationsSupported}
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
subLabel={lang(hasPushNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled')}
|
subLabel={areOfflineNotificationsSupported ? lang(hasPushNotifications ? 'UserInfo.NotificationsEnabled' : 'UserInfo.NotificationsDisabled') : 'Not supported'}
|
||||||
checked={hasPushNotifications}
|
checked={hasPushNotifications}
|
||||||
onChange={handlePushNotificationsChange}
|
onChange={handlePushNotificationsChange}
|
||||||
/>
|
/>
|
||||||
@ -162,6 +172,7 @@ const SettingsNotifications: FC<OwnProps & StateProps> = ({
|
|||||||
label="Sound"
|
label="Sound"
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
|
disabled={!areNotificationsSupported}
|
||||||
value={notificationSoundVolume}
|
value={notificationSoundVolume}
|
||||||
onChange={handleVolumeChange}
|
onChange={handleVolumeChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { APP_CONFIG_REFETCH_INTERVAL, COUNTRIES_WITH_12H_TIME_FORMAT } from '../../../config';
|
import { APP_CONFIG_REFETCH_INTERVAL, COUNTRIES_WITH_12H_TIME_FORMAT } from '../../../config';
|
||||||
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 { subscribe, unsubscribe, requestPermission } from '../../../util/notifications';
|
||||||
import { setTimeFormat } from '../../../util/langProvider';
|
import { setTimeFormat } from '../../../util/langProvider';
|
||||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
@ -353,15 +353,24 @@ addActionHandler('updateNotificationSettings', async (global, actions, payload):
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('updateWebNotificationSettings', (global, actions, payload): ActionReturnType => {
|
addActionHandler('updateWebNotificationSettings', async (global, actions, payload): Promise<void> => {
|
||||||
|
const oldSettings = global.settings.byKey;
|
||||||
global = replaceSettings(global, payload);
|
global = replaceSettings(global, payload);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
const { hasWebNotifications, hasPushNotifications } = global.settings.byKey;
|
||||||
const { hasPushNotifications, hasWebNotifications } = global.settings.byKey;
|
if (!oldSettings.hasPushNotifications && hasPushNotifications) {
|
||||||
if (hasWebNotifications && hasPushNotifications) {
|
await subscribe();
|
||||||
void subscribe();
|
}
|
||||||
} else {
|
if (oldSettings.hasPushNotifications && !hasPushNotifications) {
|
||||||
void unsubscribe();
|
await unsubscribe();
|
||||||
|
}
|
||||||
|
if (!oldSettings.hasWebNotifications && hasWebNotifications) {
|
||||||
|
const isGranted = await requestPermission();
|
||||||
|
if (!isGranted) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = replaceSettings(global, { hasWebNotifications: false });
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,17 @@ addActionHandler('initShared', (): ActionReturnType => {
|
|||||||
addActionHandler('initMain', (global): ActionReturnType => {
|
addActionHandler('initMain', (global): ActionReturnType => {
|
||||||
const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global);
|
const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global);
|
||||||
if (hasWebNotifications && hasPushNotifications) {
|
if (hasWebNotifications && hasPushNotifications) {
|
||||||
void subscribe();
|
// Most of the browsers only show the notifications permission prompt after the first user gesture.
|
||||||
|
const events = ['click', 'keypress'];
|
||||||
|
const subscribeAfterUserGesture = () => {
|
||||||
|
void subscribe();
|
||||||
|
events.forEach((event) => {
|
||||||
|
document.removeEventListener(event, subscribeAfterUserGesture);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
events.forEach((event) => {
|
||||||
|
document.addEventListener(event, subscribeAfterUserGesture, { once: true });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
getMessageRecentReaction,
|
getMessageRecentReaction,
|
||||||
getMessageSenderName,
|
getMessageSenderName,
|
||||||
getMessageSummaryText,
|
getMessageSummaryText,
|
||||||
getPrivateChatUserId, getUserFullName,
|
getPrivateChatUserId,
|
||||||
|
getUserFullName,
|
||||||
isActionMessage,
|
isActionMessage,
|
||||||
isChatChannel,
|
isChatChannel,
|
||||||
selectIsChatMuted,
|
selectIsChatMuted,
|
||||||
@ -28,7 +29,7 @@ import {
|
|||||||
selectNotifySettings,
|
selectNotifySettings,
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../global/selectors';
|
} from '../global/selectors';
|
||||||
import { IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV, IS_SAFARI } from './windowEnvironment';
|
import { IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './windowEnvironment';
|
||||||
import { translate } from './langProvider';
|
import { translate } from './langProvider';
|
||||||
import * as mediaLoader from './mediaLoader';
|
import * as mediaLoader from './mediaLoader';
|
||||||
import { debounce } from './schedulers';
|
import { debounce } from './schedulers';
|
||||||
@ -43,8 +44,7 @@ function getDeviceToken(subscription: PushSubscription) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIfPushSupported() {
|
function checkIfPushSupported() {
|
||||||
// Disable push notifications in Safari until VAPID keys are implemented on the server
|
if (!IS_SERVICE_WORKER_SUPPORTED) return false;
|
||||||
if (!IS_SERVICE_WORKER_SUPPORTED || IS_SAFARI) return false;
|
|
||||||
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -73,7 +73,7 @@ function checkIfPushSupported() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIfNotificationsSupported() {
|
export function checkIfNotificationsSupported() {
|
||||||
// Let's check if the browser supports notifications
|
// Let's check if the browser supports notifications
|
||||||
if (!('Notification' in window)) {
|
if (!('Notification' in window)) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -135,22 +135,26 @@ function checkIfShouldResubscribe(subscription: PushSubscription | null) {
|
|||||||
return Date.now() - global.push.subscribedAt > expirationTime;
|
return Date.now() - global.push.subscribedAt > expirationTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestPermission() {
|
export async function requestPermission() {
|
||||||
if (!('Notification' in window)) return;
|
if (!('Notification' in window)) {
|
||||||
if (!['granted', 'denied'].includes(Notification.permission)) {
|
return false;
|
||||||
await Notification.requestPermission();
|
|
||||||
}
|
}
|
||||||
|
let permission = Notification.permission;
|
||||||
|
if (!['granted', 'denied'].includes(permission)) {
|
||||||
|
permission = await Notification.requestPermission();
|
||||||
|
}
|
||||||
|
return permission === 'granted';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribeFromPush(subscription: PushSubscription | null) {
|
async function unsubscribeFromPush(subscription: PushSubscription | null) {
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
const dispatch = getActions();
|
const { deleteDeviceToken } = getActions();
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
try {
|
try {
|
||||||
const deviceToken = getDeviceToken(subscription);
|
const deviceToken = getDeviceToken(subscription);
|
||||||
await callApi('unregisterDevice', deviceToken);
|
await callApi('unregisterDevice', deviceToken);
|
||||||
await subscription.unsubscribe();
|
await subscription.unsubscribe();
|
||||||
dispatch.deleteDeviceToken();
|
deleteDeviceToken();
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -161,7 +165,7 @@ async function unsubscribeFromPush(subscription: PushSubscription | null) {
|
|||||||
}
|
}
|
||||||
if (global.push) {
|
if (global.push) {
|
||||||
await callApi('unregisterDevice', global.push.deviceToken);
|
await callApi('unregisterDevice', global.push.deviceToken);
|
||||||
dispatch.deleteDeviceToken();
|
deleteDeviceToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,11 +219,23 @@ async function loadCustomEmoji(id: string) {
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isSubscriptionFailed = false;
|
||||||
|
export function checkIfOfflinePushFailed() {
|
||||||
|
return isSubscriptionFailed;
|
||||||
|
}
|
||||||
|
|
||||||
export async function subscribe() {
|
export async function subscribe() {
|
||||||
|
const { setDeviceToken, updateWebNotificationSettings } = getActions();
|
||||||
|
let hasWebNotifications = false;
|
||||||
|
let hasPushNotifications = false;
|
||||||
if (!checkIfPushSupported()) {
|
if (!checkIfPushSupported()) {
|
||||||
// Ask for notification permissions only if service worker notifications are not supported
|
// Ask for notification permissions only if service worker notifications are not supported
|
||||||
// As pushManager.subscribe automatically triggers permission popup
|
// As pushManager.subscribe automatically triggers permission popup
|
||||||
await requestPermission();
|
hasWebNotifications = await requestPermission();
|
||||||
|
updateWebNotificationSettings({
|
||||||
|
hasWebNotifications,
|
||||||
|
hasPushNotifications,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
const serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||||
@ -236,10 +252,11 @@ export async function subscribe() {
|
|||||||
console.log('[PUSH] Received push subscription: ', deviceToken);
|
console.log('[PUSH] Received push subscription: ', deviceToken);
|
||||||
}
|
}
|
||||||
await callApi('registerDevice', deviceToken);
|
await callApi('registerDevice', deviceToken);
|
||||||
getActions()
|
setDeviceToken(deviceToken);
|
||||||
.setDeviceToken(deviceToken);
|
hasPushNotifications = true;
|
||||||
|
hasWebNotifications = true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (Notification.permission === 'denied' as NotificationPermission) {
|
if (Notification.permission === 'denied') {
|
||||||
// The user denied the notification permission which
|
// The user denied the notification permission which
|
||||||
// means we failed to subscribe and the user will need
|
// means we failed to subscribe and the user will need
|
||||||
// to manually change the notification permission to
|
// to manually change the notification permission to
|
||||||
@ -248,20 +265,26 @@ export async function subscribe() {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[PUSH] The user has blocked push notifications.');
|
console.warn('[PUSH] The user has blocked push notifications.');
|
||||||
}
|
}
|
||||||
} else if (DEBUG) {
|
} else {
|
||||||
// A problem occurred with the subscription, this can
|
// A problem occurred with the subscription, this can
|
||||||
// often be down to an issue or lack of the gcm_sender_id
|
// often be down to an issue or lack of the gcm_sender_id
|
||||||
// and / or gcm_user_visible_only
|
// and / or gcm_user_visible_only
|
||||||
// eslint-disable-next-line no-console
|
if (DEBUG) {
|
||||||
console.log('[PUSH] Unable to subscribe to push.', error);
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[PUSH] Unable to subscribe to push.', error);
|
||||||
|
}
|
||||||
// Request permissions and fall back to local notifications
|
// Request permissions and fall back to local notifications
|
||||||
// if pushManager.subscribe was aborted due to invalid VAPID key.
|
// if pushManager.subscribe was aborted due to invalid VAPID key.
|
||||||
if (error.code === DOMException.ABORT_ERR) {
|
if ([DOMException.ABORT_ERR, DOMException.NOT_SUPPORTED_ERR].includes(error.code)) {
|
||||||
await requestPermission();
|
isSubscriptionFailed = true;
|
||||||
|
hasWebNotifications = await requestPermission();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateWebNotificationSettings({
|
||||||
|
hasWebNotifications,
|
||||||
|
hasPushNotifications,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIfShouldNotify(chat: ApiChat, message: Partial<ApiMessage>) {
|
function checkIfShouldNotify(chat: ApiChat, message: Partial<ApiMessage>) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user