bokuweb/tsukiakari

View on GitHub
src/renderer/src/components/tweet-window.js

Summary

Maintainability
B
5 hrs
Test Coverage
/* @flow */

import React, { Component } from 'react';
import Window from 'react-window-component';
import B from '../lib/bem';
import Tooltip from 'rc-tooltip';
import AccountList from './account-list';
import { isEqual } from 'lodash';
import 'twitter-text';
import Spinner from './spinner';
import UploadMedia from '../containers/upload-media';
import TweetWindowHeader from './tweet-window-header';
import TweetEditor from './tweet-editor';
import TweetWindowFooter from './tweet-window-footer';

import type { Account } from '../../../types/account';
import type { Media } from '../../../types/media';
import type { Mentions } from '../../../types/mentions';
import type { ReplyTweet } from '../../../types/reply-tweet';

const b = B.with('tweet-window');

type Props = {
  isOpen: boolean;
  media: Array<Media>;
  mentions: Mentions;
  accounts: Array<Account>;
  close: Function;
  post: Function;
  uploadMedia: Function;
  replyTweet: ReplyTweet;
};

type State = {
  status: string;
  destroyTooltip : boolean;
  selectedAccount: Account;
  path: string;
  suggestions: Mentions;
  isDragOver: boolean;
};

const styles = {
  uploadSpinner: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: '100%',
    height: '100%',
    background: '#fff',
    opacity: '0.7',
  },
  overlay: {
    position: 'absolute',
    top: '5px',
    left: '50px',
    zIndex: 9999999999,
  },
  dropOverlay: {
    width: '100%',
    height: '100%',
    background: 'rgba(255, 255, 255, 0.9)',
    position: 'absolute',
    top: 0,
    left: 0,
    boxSizing: 'border-box',
    flexDirection: 'column',
    zIndex: 9999,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    textAlign: 'center',
    color: '#888',
    padding: '16px',
  },
  dropOverlayInner: {
    width: '100%',
    height: '100%',
    border: 'dashed 3px #ccc',
    fontSize: '36px',
    pointerEvents: 'none',
    padding: '32px',
    boxSizing: 'border-box',
  },
};

export default class TweetWindow extends Component {

  constructor(props: Props) {
    super(props);
    this.state = {
      status: '',
      destroyTooltip: false,
      selectedAccount: props.accounts[0],
      path: '',
      suggestions: props.mentions,
      isDragOver: false,
    };
    this.close = this.close.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onChange = this.onChange.bind(this);
    this.onAccountSelect = this.onAccountSelect.bind(this);
    this.onSelectFile = this.onSelectFile.bind(this);
    this.onDropFile = this.onDropFile.bind(this);
    this.onDragOver = this.onDragOver.bind(this);
  }

  /* eslint-disable react/sort-comp */
  props: Props;
  state: State;
  close: Function;
  onClick: Function;
  onChange: Function;
  onAccountSelect: Function;
  onSelectFile: Function;
  onDropFile: Function;
  onDragOver: Function;
  editor: TweetEditor;

  componentWillReceiveProps(nextProps: Props) {
    const nextId = nextProps.replyTweet.id_str;
    if (nextId && nextId !== this.props.replyTweet.id_str) {
      this.editor.updateEditorState(
        `@${nextProps.replyTweet.user.screen_name} ${this.state.status}`
      );
    }
    if (nextProps.isOpen !== this.props.isOpen) {
      this.setState({ destroyTooltip: true });
    }
    if (nextProps.media.length !== this.props.media.length) {
      this.setState({ path: '', isDragOver: false });
    }
    if (!nextProps.isMediaUploading) {
      this.setState({ isDragOver: false });
    }
  }

  shouldComponentUpdate(nextProps: Props, nextState: State): bool {
    return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
  }

  onAccountSelect(account: Account) {
    this.setState({ selectedAccount: account, destroyTooltip: true });
  }

  onClick() {
    this.props.post(this.state.selectedAccount, this.state.status, this.props.replyTweet);
    // TODO: move to reducer, if post failed note delete status
    this.setState({ status: '', destroyTooltip: true });
  }

  onChange(status: string) {
    this.setState({ status });
  }

  onSelectFile(e: SyntheticEvent): void { // eslint-disable-line flowtype/require-return-type
    // TODO: announce
    if (this.props.media.length >= 4) return;
    if (e.target instanceof HTMLInputElement) {
      this.setState({ path: e.target.value });
      if (e.target instanceof DataTransfer) {
        this.props.uploadMedia({ account: this.state.selectedAccount, files: e.target.files });
      }
    }
  }

  onDropFile(e: SyntheticEvent) {
    // TODO: announce
    if (this.props.media.length >= 4) return;
    if (e.dataTransfer instanceof DataTransfer) {
      // this.setState({ path: dataTransfer.path });
      this.props.uploadMedia({ account: this.state.selectedAccount, files: e.dataTransfer.files });
    }
  }

  // eslint-disable-next-line flowtype/require-return-type
  onDragOver(): void {
    if (this.props.media.length >= 4) return;
    this.setState({ isDragOver: true });
  }

  close() {
    this.props.close();
    this.setState({ destroyTooltip: true });
  }

  renderAvatar(): ?React$Element<*> {
    // TODO:
    if (!this.state.selectedAccount) return null;
    return (
      <img
        src={this.state.selectedAccount.profile_image_url}
        className={b('avatar')}
        alt="avatar"
      />
    );
  }

  renderTooltip(): ?React$Element<*> {
    return (
      <div className={b('tooltip')}>
        <AccountList
          accounts={this.props.accounts}
          selectedAccount={this.state.selectedAccount}
          onSelect={this.onAccountSelect}
        />
      </div>
    );
  }

  renderAccount(): ?React$Element<*> {
    if (this.props.accounts.length === 0) {
      return <i className="fa fa-spin fa-spinner" />;
    }
    return (
      <Tooltip
        trigger="click"
        overlay={this.props.accounts.length > 1
                 ? this.renderTooltip()
                 : null}
        destroyTooltipOnHide={this.state.destroyTooltip}
        placement="bottom"
        mouseLeaveDelay={0}
        overlayStyle={styles.overlay}
      >
        {this.renderAvatar()}
      </Tooltip>
    );
  }

  renderDropOverlay(): ?React$Element<*> {
    if (!this.state.isDragOver || this.props.isMediaUploading) return null;
    return (
      <div
        style={styles.dropOverlay}
        onDragLeave={() => {
          this.setState({ isDragOver: false });
        }}
      >
        <div style={styles.dropOverlayInner} >
          <div style={{ height: '60px' }}>
            <i className="lnr lnr-picture" />
          </div>
          <div
            style={{
              fontSize: '20px',
              pointerEvents: 'none',
            }}
          >
            Please drop photo here
          </div>
        </div>
      </div>
    );
  }

  renderUploadSpinner(): ?React$Element<*> {
    if (!this.props.isMediaUploading) return null;
    return (
      <div style={styles.uploadSpinner}>
        <Spinner style={{ padding: '10% 0 0 80px' }} />
      </div>
    );
  }

  render(): ?React$Element<*> {
    // eslint-disable-next-line no-undef
    const remain = 140 - twttr.txt.getTweetLength(this.state.status);
    let buttonState;
    if (remain < 0 || remain === 140) buttonState = 'isDisabled';
    else if (this.props.isPosting) buttonState = 'isLoading';
    return (
      <Window
        isOpen={this.props.isOpen}
        x={100}
        y={300}
        width={380}
        height={210}
        minWidth={340}
        minHeight={210}
        maxWidth={800}
        maxHeight={800}
        style={{
          backgroundColor: '#fff',
          pointerEvents: 'auto',
          padding: this.props.media.length === 0 ? '0 0 4px 0' : '0 0 64px 0',
        }}
        className={b()}
      >
        <TweetWindowHeader close={this.close} />
        <div
          className={b('body')}
          onDragOver={this.onDragOver}
          onDrop={this.onDropFile}
        >
          {this.renderAccount()}
          <div className={b('textarea-wrapper')}>
            <TweetEditor
              ref={c => { this.editor = c; }}
              onChange={this.onChange}
              mentions={this.props.mentions}
            />
            {this.renderDropOverlay()}
            <UploadMedia media={this.props.media} />
            <TweetWindowFooter
              remain={remain}
              onClick={this.onClick}
              onSelectFile={this.onSelectFile}
              buttonState={buttonState}
            />
          </div>
        </div>
        {this.renderUploadSpinner()}
      </Window>
    );
  }
}