-
Matiss Janis Aboltins authored
Closes #956
Matiss Janis Aboltins authoredCloses #956
Notifications.js 6.27 KiB
import React, { useState, useEffect, useMemo } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from 'loot-core/src/client/actions';
import Loading from '../icons/AnimatedLoading';
import Delete from '../icons/v0/Delete';
import { styles, colors } from '../style';
import {
View,
Text,
Button,
ButtonWithLoading,
Stack,
ExternalLink,
} from './common';
function compileMessage(message, actions, setLoading, onRemove) {
return (
<Stack spacing={2}>
{message.split(/\n\n/).map((paragraph, idx) => {
let parts = paragraph.split(/(\[[^\]]*\]\([^)]*\))/g);
return (
<Text key={idx} style={{ lineHeight: '1.4em' }}>
{parts.map((part, idx) => {
let match = part.match(/\[([^\]]*)\]\(([^)]*)\)/);
if (match) {
let [_, text, href] = match;
if (href[0] === '#') {
let actionName = href.slice(1);
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a
href="#"
onClick={async e => {
e.preventDefault();
if (actions[actionName]) {
setLoading(true);
await actions[actionName]();
onRemove();
}
}}
>
{text}
</a>
);
}
return (
<ExternalLink key={idx} asAnchor={true} href={match[2]}>
{match[1]}
</ExternalLink>
);
}
return <Text key={idx}>{part}</Text>;
})}
</Text>
);
})}
</Stack>
);
}
function Notification({ notification, onRemove }) {
let { type, title, message, pre, messageActions, sticky, internal, button } =
notification;
let [loading, setLoading] = useState(false);
let [overlayLoading, setOverlayLoading] = useState(false);
useEffect(() => {
if (type === 'error' && internal) {
console.error('Internal error:', internal);
}
if (!sticky) {
setTimeout(onRemove, 6500);
}
}, []);
let positive = type === 'message';
let error = type === 'error';
let processedMessage = useMemo(
() => compileMessage(message, messageActions, setOverlayLoading, onRemove),
[message, messageActions],
);
return (
<View
style={{
marginTop: 10,
color: positive ? colors.g3 : error ? colors.r3 : colors.y2,
}}
>
<Stack
align="center"
direction="row"
style={{
padding: '14px 14px',
fontSize: 14,
backgroundColor: positive
? colors.g11
: error
? colors.r11
: colors.y10,
borderTop: `3px solid ${
positive ? colors.g5 : error ? colors.r5 : colors.y4
}`,
...styles.shadowLarge,
maxWidth: 550,
'& a': { color: 'currentColor' },
}}
>
<Stack align="flex-start">
{title && (
<View style={{ fontWeight: 700, marginBottom: 10 }}>{title}</View>
)}
<View>{processedMessage}</View>
{pre
? pre.split('\n\n').map((text, idx) => (
<View
key={idx}
style={{
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
fontSize: 12,
backgroundColor: 'rgba(0, 0, 0, .05)',
padding: 10,
borderRadius: 4,
}}
>
{text}
</View>
))
: null}
{button && (
<ButtonWithLoading
bare
loading={loading}
onClick={async () => {
setLoading(true);
await button.action();
onRemove();
setLoading(false);
}}
style={{
backgroundColor: 'transparent',
border: `1px solid ${
positive ? colors.g5 : error ? colors.r4 : colors.y3
}`,
color: 'currentColor',
fontSize: 14,
flexShrink: 0,
'&:hover, &:active': {
backgroundColor: positive
? colors.g9
: error
? colors.r10
: colors.y9,
},
}}
>
{button.title}
</ButtonWithLoading>
)}
</Stack>
{sticky && (
<Button
bare
style={{ flexShrink: 0, color: 'currentColor' }}
onClick={onRemove}
>
<Delete style={{ width: 9, height: 9, color: 'currentColor' }} />
</Button>
)}
</Stack>
{overlayLoading && (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(250, 250, 250, .75)',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Loading
color="currentColor"
style={{ width: 20, height: 20, color: 'currentColor' }}
/>
</View>
)}
</View>
);
}
function Notifications({ notifications, removeNotification, style }) {
return (
<View
style={[
{
position: 'fixed',
bottom: 20,
right: 13,
zIndex: 10000,
},
style,
]}
>
{notifications.map(note => (
<Notification
key={note.id}
notification={note}
onRemove={() => {
if (note.onClose) {
note.onClose();
}
removeNotification(note.id);
}}
/>
))}
</View>
);
}
export default connect(
state => ({ notifications: state.notifications.notifications }),
dispatch => bindActionCreators(actions, dispatch),
)(Notifications);