Skip to content
Snippets Groups Projects
Notifications.js 6.27 KiB
Newer Older
  • Learn to ignore specific revisions
  • James Long's avatar
    James Long committed
    import React, { useState, useEffect, useMemo } from 'react';
    import { connect } from 'react-redux';
    
    
    import { bindActionCreators } from 'redux';
    
    
    James Long's avatar
    James Long committed
    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';
    
    
    James Long's avatar
    James Long committed
    import {
      View,
      Text,
      Button,
      ButtonWithLoading,
      Stack,
    
    } from './common';
    
    James Long's avatar
    James Long committed
    
    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) {
    
    Matiss Janis Aboltins's avatar
    Matiss Janis Aboltins committed
                    let [_, text, href] = match;
    
    James Long's avatar
    James Long committed
    
                    if (href[0] === '#') {
                      let actionName = href.slice(1);
                      return (
    
                        // eslint-disable-next-line jsx-a11y/anchor-is-valid
    
    James Long's avatar
    James Long committed
                        <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 } =
    
    James Long's avatar
    James Long committed
    
      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],
    
    James Long's avatar
    James Long committed
      );
    
      return (
        <View
          style={{
            marginTop: 10,
    
            color: positive ? colors.g3 : error ? colors.r3 : colors.y2,
    
    James Long's avatar
    James Long committed
          }}
        >
          <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,
    
    James Long's avatar
    James Long committed
              maxWidth: 550,
    
    
              '& a': { color: 'currentColor' },
    
    James Long's avatar
    James Long committed
            }}
          >
            <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}
    
    James Long's avatar
    James Long committed
              {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
    
    James Long's avatar
    James Long committed
                  }}
                >
                  {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',
    
    James Long's avatar
    James Long committed
              }}
            >
              <Loading
                color="currentColor"
                style={{ width: 20, height: 20, color: 'currentColor' }}
              />
            </View>
          )}
        </View>
      );
    }
    
    function Notifications({ notifications, removeNotification, style }) {
      return (
        <View
          style={[
            {
    
    James Long's avatar
    James Long committed
              bottom: 20,
              right: 13,
    
    James Long's avatar
    James Long committed
            },
    
    James Long's avatar
    James Long committed
          ]}
        >
          {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),
    
    James Long's avatar
    James Long committed
    )(Notifications);