sharetribe/sharetribe

View on GitHub
client/app/components/composites/FlashNotification/FlashNotification.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { Component } from 'react';
import PropTypes from 'prop-types';
import RTG from 'react-transition-group';
const { CSSTransitionGroup: ReactCSSTransitionGroup } = RTG;
import r, { div, p } from 'r-dom';
import classNames from 'classnames';
import Immutable from 'immutable';

import closeIcon from './images/closeIcon.svg';
import css from './FlashNotification.css';

const FLASH_INITIAL_WAIT = 1000;
const FLASH_CLEAR_TIMEOUT = 15000;
const TRANSITION_TIMEOUT = 350;

const Message = function Message({ message, closeHandler, noticeRef }) {

  const errorClass = message.type === 'error' ? css.error : null;
  return div({
    className: classNames('FlashNotification_message', css.message, errorClass),
    ref: noticeRef,
    'data-id': message.id,
  }, [
    p({
      className: css.messageContent,
      dangerouslySetInnerHTML: {
        __html: message.content,
      },
    }),
    div({
      className: css.closeIcon,
      onClick: closeHandler,
      dangerouslySetInnerHTML: {
        __html: closeIcon,
      },
    }),
  ]);
};

const delayedPromiseCurry = (timeoutRefs) => (timeMs, name) => (
  new Promise((resolve) => (
    timeoutRefs.push({ name, timeout: setTimeout(resolve, timeMs) })
  ))
);

const clearTimeouts = (timeouts, name = null) => {
  timeouts.forEach((t) => {
    if (name == null || t.name === name) {
      window.clearTimeout(t.timeout);
    }
  });
};

class FlashNotification extends Component {

  constructor(props, context) {
    super(props, context);

    this.state = {
      isMounted: false,
      isHovering: false,
    };

    this.messageRefs = new Immutable.Map();
    this.timeouts = [];
    this.delay = delayedPromiseCurry(this.timeouts);

    this.handleClose = this.handleClose.bind(this);
    this.handleMouseOver = this.handleMouseOver.bind(this);
    this.handleMouseOut = this.handleMouseOut.bind(this);
    this.clearNotices = this.clearNotices.bind(this);
  }

  componentDidMount() {
    this.setState({ isMounted: true }); // eslint-disable-line react/no-set-state
  }

  componentWillUnmount() {
    this.setState({ isMounted: false }); // eslint-disable-line react/no-set-state
    this.timeouts.forEach((t) => window.clearTimeout(t.timeout));
  }

  clearNotices(timeout) {
    const that = this;
    return that.delay(timeout, 'Clear notifications')
      .then(() => {
        if (that.state.isHovering) {
          clearTimeouts(that.timeouts, 'Clear notifications');
        } else {
          that.props.messages
            .filterNot((msg) => msg.type === 'error')
            .forEach((msg) => that.props.actions.removeFlashNotification(msg.id));
        }
      });
  }

  handleClose(event) {
    this.messageRefs.forEach((messageRef) => {
      if (messageRef && messageRef.contains(event.currentTarget)) {
        const that = this;
        const id = messageRef.dataset.id;

        // Mark message as read
        that.props.actions.removeFlashNotification(id);
      }
    });
  }

  handleMouseOver() {
    this.setState({ isHovering: true }); // eslint-disable-line react/no-set-state
  }

  handleMouseOut() {
    this.setState({ isHovering: false }); // eslint-disable-line react/no-set-state
  }

  render() {
    if (this.state.isMounted) {
      const that = this;

      // Show Flash notifications after a short timeout.
      // Animations are more effective when page doesn't have too many loading images
      // Clear unimportant messages after a 15s break;
      this.delay(FLASH_INITIAL_WAIT, 'Add notifications')
        .then(() => that.clearNotices(FLASH_CLEAR_TIMEOUT));
    }

    return r(ReactCSSTransitionGroup,
      {
        className: classNames('FlashNotification', css.notifications, css.cssTransitionGroup),
        component: 'div',
        onMouseOver: this.handleMouseOver,
        onMouseOut: this.handleMouseOut,
        transitionName: {
          enter: css.enterRight,
          enterActive: css.enterRightActive,
          leave: css.leaveRight,
          leaveActive: css.leaveRightActive,
        },
        transitionEnterTimeout: TRANSITION_TIMEOUT,
        transitionLeaveTimeout: TRANSITION_TIMEOUT,
      },
      this.state.isMounted && this.props.messages.size > 0 ?
        this.props.messages.map((msg) => (
          msg.isRead ?
            null :
            r(Message, {
              key: msg.content,
              message: msg,
              closeHandler: this.handleClose,
              noticeRef: (c) => {
                this.messageRefs = this.messageRefs.set(msg.id, c);
              },
            })
        )).toArray() :
        null
      );
  }
}

const { func, instanceOf, shape } = PropTypes;
FlashNotification.propTypes = {
  actions: shape({
    removeFlashNotification: func.isRequired,
  }).isRequired,
  messages: instanceOf(Immutable.List).isRequired,
};

export default FlashNotification;