diff --git a/README.md b/README.md index 60ceeed2a..776321514 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,22 @@ const { UID, login, logout, ...} = useAuthentication(); Create notifications to be displayed in the app. ``` js -const { createNotification, removeNotification } = useNotifications(); +const { createNotification, hideNotification } = useNotifications(); const handleClick = () => { createNotification({ type: 'error', text: 'Failed to update' }); } + +const handleClickPersistent = () => { + const id = createNotification({ + expiration: -1, // does not expire + type: 'error', + text: 'Failed to update' + }); + setTimeout(() => { + hideNotification(id); + }, 1000); +} ``` ### useModals diff --git a/containers/api/ApiProvider.js b/containers/api/ApiProvider.js index 64bdfda53..332493152 100644 --- a/containers/api/ApiProvider.js +++ b/containers/api/ApiProvider.js @@ -1,6 +1,8 @@ import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import xhr from 'proton-shared/lib/fetch/fetch'; +import { getUser } from 'proton-shared/lib/api/user'; +import { ping } from 'proton-shared/lib/api/tests'; import configureApi from 'proton-shared/lib/api'; import withApiHandlers, { CancelUnlockError, @@ -8,6 +10,7 @@ import withApiHandlers, { } from 'proton-shared/lib/api/helpers/withApiHandlers'; import { getError } from 'proton-shared/lib/apiHandlers'; import { getDateHeader } from 'proton-shared/lib/fetch/helpers'; +import { API_CUSTOM_ERROR_CODES } from 'proton-shared/lib/errors'; import { updateServerTime } from 'pmcrypto'; import { c } from 'ttag'; @@ -16,6 +19,7 @@ import useNotifications from '../notifications/useNotifications'; import useModals from '../modals/useModals'; import UnlockModal from '../login/UnlockModal'; import HumanVerificationModal from './HumanVerificationModal'; +import OfflineNotification from './OfflineNotification'; const getSilenced = ({ silence } = {}, code) => { if (Array.isArray(silence)) { @@ -25,16 +29,25 @@ const getSilenced = ({ silence } = {}, code) => { }; const ApiProvider = ({ config, onLogout, children, UID }) => { - const { createNotification } = useNotifications(); + const { createNotification, hideNotification } = useNotifications(); const { createModal } = useModals(); const apiRef = useRef(); + const offlineRef = useRef(); + const appVersionBad = useRef(); + + const hideOfflineNotification = () => { + if (!offlineRef.current) { + return; + } + hideNotification(offlineRef.current.id); + offlineRef.current = undefined; + }; if (!apiRef.current) { const handleError = (e) => { const { message, code } = getError(e); - if (e.name === 'InactiveSession') { - onLogout(); + if (appVersionBad.current) { throw e; } @@ -42,12 +55,48 @@ const ApiProvider = ({ config, onLogout, children, UID }) => { throw e; } + // If the client knows it's offline and it's another offline error, just ignore it + if (offlineRef.current && e.name === 'OfflineError') { + throw e; + } + if (offlineRef.current && e.name !== 'OfflineError') { + hideOfflineNotification(); + } + + if (code === API_CUSTOM_ERROR_CODES.APP_VERSION_BAD) { + appVersionBad.current = true; + // The only way to get out of this one is to refresh. + createNotification({ + type: 'error', + text: message || c('Info').t`Application upgrade required`, + expiration: -1, + disableAutoClose: true + }); + throw e; + } + + if (e.name === 'InactiveSession') { + onLogout(); + throw e; + } + if (e.name === 'OfflineError') { - const text = navigator.onLine - ? c('Error').t`Could not connect to server.` - : c('Error').t`No internet connection found`; - const isSilenced = getSilenced(e.config, code); - !isSilenced && createNotification({ type: 'error', text }); + const id = createNotification({ + type: 'warning', + text: ( + { + hideOfflineNotification(); + // If there is a session, get user to validate it's still active after coming back online + // otherwise if it's not logged in, call ping + apiRef.current(UID ? getUser() : ping()); + }} + /> + ), + expiration: -1, + disableAutoClose: true + }); + offlineRef.current = { id }; throw e; } @@ -99,11 +148,15 @@ const ApiProvider = ({ config, onLogout, children, UID }) => { }); apiRef.current = ({ output = 'json', ...rest }) => { + if (appVersionBad.current) { + return Promise.reject(new Error(c('Error').t`Bad app version`)); + } return callWithApiHandlers(rest).then((response) => { const serverTime = getDateHeader(response.headers); if (serverTime) { updateServerTime(serverTime); } + hideOfflineNotification(); return output === 'stream' ? response.body : response[output](); }); }; diff --git a/containers/api/OfflineNotification.js b/containers/api/OfflineNotification.js new file mode 100644 index 000000000..32ebf5764 --- /dev/null +++ b/containers/api/OfflineNotification.js @@ -0,0 +1,24 @@ +import { c } from 'ttag'; +import React from 'react'; +import { LinkButton, useLoading } from 'react-components'; +import PropTypes from 'prop-types'; + +const OfflineNotification = ({ onRetry }) => { + const [loading, withLoading] = useLoading(); + const retryNow = ( + withLoading(onRetry())}> + {c('Action').t`Retry now`} + + ); + return ( + <> + {c('Error').t`Servers are unreachable.`} {retryNow} + + ); +}; + +OfflineNotification.propTypes = { + onRetry: PropTypes.func +}; + +export default OfflineNotification; diff --git a/containers/notifications/Container.js b/containers/notifications/Container.js index 075600fd7..4ea21d2ec 100644 --- a/containers/notifications/Container.js +++ b/containers/notifications/Container.js @@ -4,13 +4,13 @@ import PropTypes from 'prop-types'; import Notification from './Notification'; const NotificationsContainer = ({ notifications, removeNotification, hideNotification }) => { - const list = notifications.map(({ id, type, text, isClosing }) => { + const list = notifications.map(({ id, type, text, isClosing, disableAutoClose }) => { return ( hideNotification(id)} + onClick={disableAutoClose ? undefined : () => hideNotification(id)} onExit={() => removeNotification(id)} > {text} diff --git a/containers/notifications/manager.js b/containers/notifications/manager.js index d41bd8891..79f245472 100644 --- a/containers/notifications/manager.js +++ b/containers/notifications/manager.js @@ -19,6 +19,12 @@ export default (setNotifications) => { }; const hideNotification = (id) => { + // If the page is hidden, don't hide the notification with an animation because they get stacked. + // This is to solve e.g. offline notifications appearing when the page is hidden, and when you focus + // the tab again, they would be visible for the animation out even if they happened a while ago. + if (document.hidden) { + return removeNotification(id); + } return setNotifications((oldNotifications) => { return oldNotifications.map((oldNotification) => { if (oldNotification.id !== id) { @@ -40,9 +46,7 @@ export default (setNotifications) => { idx = 0; } - intervalIds[id] = expiration === -1 ? -1 : setTimeout(() => hideNotification(id), expiration); - - return setNotifications((oldNotifications) => { + setNotifications((oldNotifications) => { return [ ...oldNotifications, { @@ -54,6 +58,10 @@ export default (setNotifications) => { } ]; }); + + intervalIds[id] = expiration === -1 ? -1 : setTimeout(() => hideNotification(id), expiration); + + return id; }; const clearNotifications = () => {