Add hints if an error occurs subscribing to notifications (#143)
* Add hints if an error occurs subscribing to notifications * Update import (type/linting issue) * disable optimization for debugging * Revert "disable optimization for debugging" This reverts commit 5b837f94e97b7488a7dc565e7f74d399e19c286f. * improve detection of notification support + improve ux / ui surrounding that * update translations
This commit is contained in:
parent
fa029e0c09
commit
704452732a
14 changed files with 249 additions and 117 deletions
|
@ -2200,6 +2200,10 @@
|
|||
"Dismiss": "Dismiss",
|
||||
"Heads up: browser notifications are currently blocked in this browser.": "Heads up: browser notifications are currently blocked in this browser.",
|
||||
"To enable push notifications please configure your browser to allow notifications on odysee.com.": "To enable push notifications please configure your browser to allow notifications on odysee.com.",
|
||||
"There was an error enabling browser notifications. Please make sure your browser settings allow you to subscribe to notifications.": "There was an error enabling browser notifications. Please make sure your browser settings allow you to subscribe to notifications.",
|
||||
"Browser notifications aren't supported. Here's a few tips:": "Browser notifications aren't supported. Here's a few tips:",
|
||||
"Notifications aren't available when in incognito or private mode.": "Notifications aren't available when in incognito or private mode.",
|
||||
"On Firefox, notifications won't function if cookies are set to clear on browser close. Please disable or add an exception for Odysee, then refresh.": "On Firefox, notifications won't function if cookies are set to clear on browser close. Please disable or add an exception for Odysee, then refresh.",
|
||||
"For Brave, enable google push notifications in settings.": "For Brave, enable google push notifications in settings.",
|
||||
"Check browser settings to see if notifications are disabled or otherwise restricted.": "Check browser settings to see if notifications are disabled or otherwise restricted.",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -7,16 +7,17 @@ type Props = {
|
|||
subtitle?: string,
|
||||
multirow?: boolean, // Displays the Value widget(s) below the Label instead of on the right.
|
||||
useVerticalSeparator?: boolean, // Show a separator line between Label and Value. Useful when there are multiple Values.
|
||||
disabled?: boolean,
|
||||
children?: React$Node,
|
||||
};
|
||||
|
||||
export default function SettingsRow(props: Props) {
|
||||
const { title, subtitle, multirow, useVerticalSeparator, children } = props;
|
||||
|
||||
const { title, subtitle, multirow, useVerticalSeparator, disabled, children } = props;
|
||||
return (
|
||||
<div
|
||||
className={classnames('card__main-actions settings__row', {
|
||||
'section__actions--between': !multirow,
|
||||
'opacity-40': disabled,
|
||||
})}
|
||||
>
|
||||
<div className="settings__row--title">
|
||||
|
|
|
@ -59,7 +59,7 @@ export default function SubscribeButton(props: Props) {
|
|||
}
|
||||
const claimName = channelName && '@' + channelName;
|
||||
|
||||
const { pushSupported, pushEnabled, pushRequest } = useBrowserNotifications();
|
||||
const { pushSupported, pushEnabled, pushRequest, pushErrorModal } = useBrowserNotifications();
|
||||
|
||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||
|
||||
|
@ -125,6 +125,7 @@ export default function SubscribeButton(props: Props) {
|
|||
}}
|
||||
/>
|
||||
{isSubscribed && uiNotificationsEnabled && (
|
||||
<>
|
||||
<Button
|
||||
button="alt"
|
||||
icon={notificationsDisabled ? ICONS.BELL : ICONS.BELL_ON}
|
||||
|
@ -155,6 +156,8 @@ export default function SubscribeButton(props: Props) {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
{pushErrorModal()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { doClearPublish } from 'redux/actions/publish';
|
|||
import { Lbryio } from 'lbryinc';
|
||||
import { selectFollowedTagsList } from 'redux/selectors/tags';
|
||||
import { doToast, doError, doNotificationList } from 'redux/actions/notifications';
|
||||
import { pushReconnect, pushDisconnect, pushValidate } from '$web/src/push-notifications';
|
||||
import pushNotifications from '$web/src/push-notifications';
|
||||
|
||||
import Native from 'native';
|
||||
import {
|
||||
|
@ -537,8 +537,10 @@ export function doSignIn() {
|
|||
const state = getState();
|
||||
const user = selectUser(state);
|
||||
|
||||
pushReconnect(user.id);
|
||||
pushValidate(user.id);
|
||||
if (pushNotifications.supported) {
|
||||
pushNotifications.reconnect(user.id);
|
||||
pushNotifications.validate(user.id);
|
||||
}
|
||||
|
||||
const notificationsEnabled = SIMPLE_SITE || user.experimental_ui;
|
||||
|
||||
|
@ -562,7 +564,9 @@ export function doSignOut() {
|
|||
const state = getState();
|
||||
const user = selectUser(state);
|
||||
try {
|
||||
await pushDisconnect(user.id);
|
||||
if (pushNotifications.supported) {
|
||||
await pushNotifications.disconnect(user.id);
|
||||
}
|
||||
} finally {
|
||||
Lbryio.call('user', 'signout')
|
||||
.then(doSignOutCleanup)
|
||||
|
|
3
ui/scss/component/_utils.scss
Normal file
3
ui/scss/component/_utils.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.opacity-40 {
|
||||
opacity: 0.4;
|
||||
}
|
|
@ -15,3 +15,9 @@
|
|||
color: var(--color-text-subtitle);
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.notificationsBlocked__subTextList {
|
||||
li {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import Button from 'component/button';
|
|||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
export const BrowserNotificationBanner = () => {
|
||||
const { pushSupported, pushEnabled, pushPermission, pushToggle } = useBrowserNotifications();
|
||||
const { pushSupported, pushEnabled, pushPermission, pushToggle, pushErrorModal } = useBrowserNotifications();
|
||||
const [hasAcknowledgedPush, setHasAcknowledgedPush] = usePersistedState('push-nag', false);
|
||||
|
||||
if (!pushSupported || pushEnabled || pushPermission === 'denied' || hasAcknowledgedPush) return null;
|
||||
|
@ -16,6 +16,7 @@ export const BrowserNotificationBanner = () => {
|
|||
const handleClose = () => setHasAcknowledgedPush(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="browserNotificationsBanner notice-message">
|
||||
<div className="browserNotificationsBanner__overview">
|
||||
<Icon className="browserNotificationsBanner__icon" icon={ICONS.NOTIFICATION} size={32} />
|
||||
|
@ -36,6 +37,8 @@ export const BrowserNotificationBanner = () => {
|
|||
<Button button="close" title={__('Dismiss')} icon={ICONS.REMOVE} onClick={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
{pushErrorModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
61
web/component/browserNotificationHints/index.jsx
Normal file
61
web/component/browserNotificationHints/index.jsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Icon from 'component/common/icon';
|
||||
import { Modal } from 'modal/modal';
|
||||
import 'scss/component/notifications-blocked.scss';
|
||||
|
||||
type InlineMessageProps = {
|
||||
title: string,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
const InlineMessage = (props: InlineMessageProps) => {
|
||||
const { title, children } = props;
|
||||
return (
|
||||
<div className="notificationsBlocked">
|
||||
<Icon className="notificationsBlocked__icon" color="#E50054" icon={ICONS.ALERT} size={32} />
|
||||
<div>
|
||||
<span>{title}</span>
|
||||
<span className={'notificationsBlocked__subText'}>{children}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BrowserNotificationsBlocked = () => {
|
||||
return (
|
||||
<InlineMessage title={__('Heads up: browser notifications are currently blocked in this browser.')}>
|
||||
{__('To enable push notifications please configure your browser to allow notifications on odysee.com.')}
|
||||
</InlineMessage>
|
||||
);
|
||||
};
|
||||
|
||||
export const BrowserNotificationHints = () => {
|
||||
return (
|
||||
<InlineMessage title={__("Browser notifications aren't supported. Here's a few tips:")}>
|
||||
<ul className={'notificationsBlocked__subText notificationsBlocked__subTextList'}>
|
||||
<li>{__("Notifications aren't available when in incognito or private mode.")}</li>
|
||||
<li>
|
||||
{__(
|
||||
"On Firefox, notifications won't function if cookies are set to clear on browser close. Please disable or add an exception for Odysee, then refresh."
|
||||
)}
|
||||
</li>
|
||||
<li>{__('For Brave, enable google push notifications in settings.')}</li>
|
||||
<li>{__('Check browser settings to see if notifications are disabled or otherwise restricted.')}</li>
|
||||
</ul>
|
||||
</InlineMessage>
|
||||
);
|
||||
};
|
||||
|
||||
type ModalProps = {
|
||||
doHideModal: () => void,
|
||||
};
|
||||
export const BrowserNotificationErrorModal = (props: ModalProps) => {
|
||||
const { doHideModal } = props;
|
||||
return (
|
||||
<Modal type="card" isOpen onAborted={doHideModal}>
|
||||
<BrowserNotificationHints />
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,39 +1,47 @@
|
|||
// @flow
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { pushSubscribe, pushUnsubscribe, pushIsSubscribed } from '$web/src/push-notifications';
|
||||
import { isSupported } from 'firebase/messaging';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import pushNotifications from '$web/src/push-notifications';
|
||||
import { BrowserNotificationErrorModal } from '$web/component/browserNotificationHints';
|
||||
|
||||
// @todo: Once we are on Redux 7 we should have proper hooks we can use here for store access.
|
||||
import { store } from '$ui/store';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
|
||||
export default () => {
|
||||
const [pushPermission, setPushPermission] = useState(window.Notification?.permission);
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
const [pushEnabled, setPushEnabled] = useState(false);
|
||||
const [pushSupported, setPushSupported] = useState(false);
|
||||
const [encounteredError, setEncounteredError] = useState(false);
|
||||
|
||||
const [user] = useState(selectUser(store.getState()));
|
||||
|
||||
useEffect(() => {
|
||||
pushIsSubscribed(user.id).then((isSubscribed: boolean) => setSubscribed(isSubscribed));
|
||||
isSupported().then((supported: boolean) => setPushSupported(supported));
|
||||
setPushSupported(pushNotifications.supported);
|
||||
if (pushNotifications.supported) {
|
||||
pushNotifications.subscribed(user.id).then((isSubscribed: boolean) => setSubscribed(isSubscribed));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useMemo(() => setPushEnabled(pushPermission === 'granted' && subscribed), [pushPermission, subscribed]);
|
||||
|
||||
const subscribe = async () => {
|
||||
if (await pushSubscribe(user.id)) {
|
||||
setEncounteredError(false);
|
||||
try {
|
||||
if (await pushNotifications.subscribe(user.id)) {
|
||||
setSubscribed(true);
|
||||
setPushPermission(window.Notification?.permission);
|
||||
return true;
|
||||
} else {
|
||||
showError();
|
||||
setEncounteredError(true);
|
||||
}
|
||||
} catch {
|
||||
setEncounteredError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = async () => {
|
||||
if (await pushUnsubscribe(user.id)) {
|
||||
if (await pushNotifications.unsubscribe(user.id)) {
|
||||
setSubscribed(false);
|
||||
}
|
||||
};
|
||||
|
@ -46,15 +54,8 @@ export default () => {
|
|||
return window.Notification?.permission !== 'granted' ? subscribe() : null;
|
||||
};
|
||||
|
||||
const showError = () => {
|
||||
store.dispatch(
|
||||
doToast({
|
||||
isError: true,
|
||||
message: __(
|
||||
'There was an error enabling browser notifications. Please make sure your browser settings allow you to subscribe to notifications.'
|
||||
),
|
||||
})
|
||||
);
|
||||
const pushErrorModal = () => {
|
||||
return <>{encounteredError && <BrowserNotificationErrorModal doHideModal={() => setEncounteredError(false)} />}</>;
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -63,5 +64,6 @@ export default () => {
|
|||
pushPermission,
|
||||
pushToggle,
|
||||
pushRequest,
|
||||
pushErrorModal,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import SettingsRow from 'component/settingsRow';
|
||||
import { FormField } from 'component/common/form';
|
||||
import useBrowserNotifications from '$web/component/browserNotificationSettings/use-browser-notifications';
|
||||
import 'scss/component/notifications-blocked.scss';
|
||||
import Icon from 'component/common/icon';
|
||||
|
||||
const BrowserNotificationsBlocked = () => {
|
||||
return (
|
||||
<div className="notificationsBlocked">
|
||||
<Icon className="notificationsBlocked__icon" color="#E50054" icon={ICONS.ALERT} size={32} />
|
||||
<div>
|
||||
<span>{__('Heads up: browser notifications are currently blocked in this browser.')}</span>
|
||||
<span className={'notificationsBlocked__subText'}>
|
||||
{__('To enable push notifications please configure your browser to allow notifications on odysee.com.')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { BrowserNotificationHints, BrowserNotificationsBlocked } from '$web/component/browserNotificationHints';
|
||||
|
||||
const BrowserNotificationSettings = () => {
|
||||
const { pushSupported, pushEnabled, pushPermission, pushToggle } = useBrowserNotifications();
|
||||
const { pushSupported, pushEnabled, pushPermission, pushToggle, pushErrorModal } = useBrowserNotifications();
|
||||
|
||||
if (!pushSupported) return null;
|
||||
if (pushPermission === 'denied') return <BrowserNotificationsBlocked />;
|
||||
const pushBlocked = pushPermission === 'denied';
|
||||
|
||||
const renderHints = () => (!pushSupported ? <BrowserNotificationHints /> : null);
|
||||
const renderBlocked = () => (pushBlocked ? <BrowserNotificationsBlocked /> : null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRow
|
||||
title={__('Browser Notifications')}
|
||||
subtitle={__("Receive push notifications in this browser, even when you're not on odysee.com")}
|
||||
disabled={!pushSupported || pushBlocked}
|
||||
>
|
||||
<FormField type="checkbox" name="browserNotification" onChange={pushToggle} checked={pushEnabled} />
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="browserNotification"
|
||||
disabled={!pushSupported || pushBlocked}
|
||||
onChange={pushToggle}
|
||||
checked={pushEnabled}
|
||||
/>
|
||||
</SettingsRow>
|
||||
{renderHints()}
|
||||
{renderBlocked()}
|
||||
{pushErrorModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -69,3 +69,4 @@
|
|||
@import '../../ui/scss/component/empty';
|
||||
@import '../../ui/scss/component/stripe-card';
|
||||
@import '../../ui/scss/component/wallet-tip-send';
|
||||
@import '../../ui/scss/component/utils';
|
||||
|
|
|
@ -8,15 +8,45 @@ import { Lbryio } from 'lbryinc';
|
|||
import { initializeApp } from 'firebase/app';
|
||||
import { getMessaging, getToken, deleteToken } from 'firebase/messaging';
|
||||
import { firebaseConfig, vapidKey } from '$web/src/firebase-config';
|
||||
import { addRegistration, removeRegistration, hasRegistration } from '$web/src/fcm-management';
|
||||
import { addRegistration, removeRegistration, hasRegistration } from '$web/src/push-notifications/fcm-management';
|
||||
import { browserData } from '$web/src/ua';
|
||||
import { isPushSupported } from '$web/src/push-notifications/push-supported';
|
||||
|
||||
let messaging = null;
|
||||
let pushSystem = null;
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
(async () => {
|
||||
const supported = await isPushSupported();
|
||||
if (supported) {
|
||||
const app = initializeApp(firebaseConfig);
|
||||
messaging = getMessaging(app);
|
||||
}
|
||||
pushSystem = {
|
||||
supported: true,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
subscribed,
|
||||
reconnect,
|
||||
disconnect,
|
||||
validate,
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
// Proxy will forward to push system if it's supported.
|
||||
// $FlowIssue[incompatible-type]
|
||||
export default new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target, prop) {
|
||||
if (pushSystem) {
|
||||
return pushSystem[prop];
|
||||
} else {
|
||||
if (prop === 'supported') return false;
|
||||
throw new Error('Push notifications are not supported in this browser environment.');
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const subscriptionMetaData = () => {
|
||||
const isMobile = window.navigator.userAgentData?.mobile || false;
|
||||
|
@ -31,7 +61,7 @@ const getFcmToken = async (): Promise<string | void> => {
|
|||
return getToken(messaging, { serviceWorkerRegistration: swRegistration, vapidKey });
|
||||
};
|
||||
|
||||
export const pushSubscribe = async (userId: number, permanent: boolean = true): Promise<boolean> => {
|
||||
const subscribe = async (userId: number, permanent: boolean = true): Promise<boolean> => {
|
||||
try {
|
||||
const fcmToken = await getFcmToken();
|
||||
if (!fcmToken) return false;
|
||||
|
@ -43,7 +73,7 @@ export const pushSubscribe = async (userId: number, permanent: boolean = true):
|
|||
}
|
||||
};
|
||||
|
||||
export const pushUnsubscribe = async (userId: number, permanent: boolean = true): Promise<boolean> => {
|
||||
const unsubscribe = async (userId: number, permanent: boolean = true): Promise<boolean> => {
|
||||
try {
|
||||
const fcmToken = await getFcmToken();
|
||||
if (!fcmToken) return false;
|
||||
|
@ -56,7 +86,7 @@ export const pushUnsubscribe = async (userId: number, permanent: boolean = true)
|
|||
}
|
||||
};
|
||||
|
||||
export const pushIsSubscribed = async (userId: number): Promise<boolean> => {
|
||||
const subscribed = async (userId: number): Promise<boolean> => {
|
||||
const swRegistration = await navigator.serviceWorker?.ready;
|
||||
if (!swRegistration || !swRegistration.pushManager) return false;
|
||||
const browserSubscriptionExists = (await swRegistration.pushManager.getSubscription()) !== null;
|
||||
|
@ -64,17 +94,17 @@ export const pushIsSubscribed = async (userId: number): Promise<boolean> => {
|
|||
return browserSubscriptionExists && userRecordExists;
|
||||
};
|
||||
|
||||
export const pushReconnect = async (userId: number): Promise<boolean> => {
|
||||
if (hasRegistration(userId)) return pushSubscribe(userId, false);
|
||||
const reconnect = async (userId: number): Promise<boolean> => {
|
||||
if (hasRegistration(userId)) return subscribe(userId, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const pushDisconnect = async (userId: number): Promise<boolean> => {
|
||||
if (hasRegistration(userId)) return pushUnsubscribe(userId, false);
|
||||
const disconnect = async (userId: number): Promise<boolean> => {
|
||||
if (hasRegistration(userId)) return unsubscribe(userId, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const pushValidate = async (userId: number) => {
|
||||
const validate = async (userId: number) => {
|
||||
if (!hasRegistration(userId)) return;
|
||||
window.requestIdleCallback(async () => {
|
||||
const serverTokens = await Lbryio.call('cfm', 'list');
|
||||
|
@ -82,7 +112,7 @@ export const pushValidate = async (userId: number) => {
|
|||
if (!fcmToken) return;
|
||||
const exists = serverTokens.find((item) => item.value === fcmToken);
|
||||
if (!exists) {
|
||||
await pushSubscribe(userId, false);
|
||||
await subscribe(userId, false);
|
||||
}
|
||||
});
|
||||
};
|
16
web/src/push-notifications/push-supported.js
Normal file
16
web/src/push-notifications/push-supported.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// @flow
|
||||
|
||||
import { isSupported } from 'firebase/messaging';
|
||||
|
||||
export const isPushSupported = async (): Promise<boolean> => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Some browsers incognito expose sw but not the registration, while other don't expose sw at all.
|
||||
// $FlowIssue[incompatible-type]
|
||||
const activeRegistrations: Array<ServiceWorkerRegistration> = await navigator.serviceWorker.getRegistrations();
|
||||
const swRegistered = activeRegistrations.length > 0;
|
||||
const firebaseSupported = await isSupported();
|
||||
const notificationFeature = 'Notification' in window;
|
||||
return swRegistered && firebaseSupported && notificationFeature;
|
||||
}
|
||||
return false;
|
||||
};
|
Loading…
Reference in a new issue