Skip to content
This repository was archived by the owner on Aug 11, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 61 additions & 8 deletions containers/api/ApiProvider.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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,
CancelVerificationError
} 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';

Expand All @@ -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)) {
Expand All @@ -25,29 +29,74 @@ 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;
}

if (e.name === 'CancelUnlock') {
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: (
<OfflineNotification
onRetry={() => {
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;
}

Expand Down Expand Up @@ -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]();
});
};
Expand Down
24 changes: 24 additions & 0 deletions containers/api/OfflineNotification.js
Original file line number Diff line number Diff line change
@@ -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 = (
<LinkButton className="alignbaseline p0" disabled={loading} onClick={() => withLoading(onRetry())}>
{c('Action').t`Retry now`}
</LinkButton>
);
return (
<>
{c('Error').t`Servers are unreachable.`} {retryNow}
</>
);
};

OfflineNotification.propTypes = {
onRetry: PropTypes.func
};

export default OfflineNotification;
4 changes: 2 additions & 2 deletions containers/notifications/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Notification
key={id}
isClosing={isClosing}
type={type}
onClick={() => hideNotification(id)}
onClick={disableAutoClose ? undefined : () => hideNotification(id)}
onExit={() => removeNotification(id)}
>
{text}
Expand Down
14 changes: 11 additions & 3 deletions containers/notifications/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
{
Expand All @@ -54,6 +58,10 @@ export default (setNotifications) => {
}
];
});

intervalIds[id] = expiration === -1 ? -1 : setTimeout(() => hideNotification(id), expiration);

return id;
};

const clearNotifications = () => {
Expand Down