<commit_before>import Immutable from 'immutable'
import React, { PropTypes, PureComponent } from 'react'
import { connect } from 'react-redux'
import classNames from 'classnames'
import { selectIsMobileGridStream, selectIsNavbarHidden } from '../../selectors/gui'
import { selectPropsPostId } from '../../selectors/post'
import { selectAvatar } from '../../selectors/profile'
import { selectIsPostDetail, selectPathname } from '../../selectors/routing'
import Avatar from '../assets/Avatar'
import Block from './Block'
import EmbedBlock from './EmbedBlock'
import ImageBlock from './ImageBlock'
import QuickEmoji from './QuickEmoji'
import RepostBlock from './RepostBlock'
import TextBlock from './TextBlock'
import PostActionBar from './PostActionBar'
import {
  addBlock,
  addDragBlock,
  addEmptyTextBlock,
  loadReplyAll,
  removeBlock,
  removeDragBlock,
  reorderBlocks,
  saveAsset,
  updateBlock,
} from '../../actions/editor'
import { closeOmnibar } from '../../actions/omnibar'
import { scrollToLastTextBlock } from '../../lib/jello'
import * as ACTION_TYPES from '../../constants/action_types'
import { addDragObject, removeDragObject } from './DragComponent'
import { addInputObject, removeInputObject } from './InputComponent'

function mapStateToProps(state, props) {
  const editor = state.editor.get(props.editorId, Immutable.Map())
  const collection = editor.get('collection')
  const order = editor.get('order')
  let buyLink
  const firstBlock = collection && order ? collection.get(`${order.first()}`) : null
  if (firstBlock) {
    buyLink = firstBlock.get('linkUrl')
  }
  return {
    buyLink,
    avatar: selectAvatar(state),
    collection,
    dragBlock: editor.get('dragBlock'),
    firstBlock,
    hasContent: editor.get('hasContent'),
    hasMedia: editor.get('hasMedia'),
    hasMention: editor.get('hasMention'),
    isLoading: editor.get('isLoading'),
    isPostDetail: selectIsPostDetail(state, props),
    isPosting: editor.get('isPosting'),
    isMobileGridStream: selectIsMobileGridStream(state),
    isNavbarHidden: selectIsNavbarHidden(state),
    order,
    orderLength: order ? order.size : 0,
    pathname: selectPathname(state),
    postId: selectPropsPostId(state, props),
  }
}

class BlockCollection extends PureComponent {

  static propTypes = {
    avatar: PropTypes.object,
    blocks: PropTypes.object,
    buyLink: PropTypes.string,
    cancelAction: PropTypes.func.isRequired,
    collection: PropTypes.object.isRequired,
    dispatch: PropTypes.func.isRequired,
    dragBlock: PropTypes.object,
    editorId: PropTypes.string.isRequired,
    firstBlock: PropTypes.object,
    hasContent: PropTypes.bool,
    hasMedia: PropTypes.bool,
    hasMention: PropTypes.bool,
    isComment: PropTypes.bool,
    isLoading: PropTypes.bool,
    isMobileGridStream: PropTypes.bool,
    isNavbarHidden: PropTypes.bool,
    isOwnPost: PropTypes.bool,
    isPostDetail: PropTypes.bool.isRequired,
    isPostEditing: PropTypes.bool.isRequired,
    isPostReposting: PropTypes.bool.isRequired,
    isPosting: PropTypes.bool,
    order: PropTypes.object.isRequired,
    orderLength: PropTypes.number.isRequired,
    pathname: PropTypes.string.isRequired,
    postId: PropTypes.string,
    repostContent: PropTypes.object,
    submitAction: PropTypes.func.isRequired,
    submitText: PropTypes.string,
  }

  static defaultProps = {
    avatar: null,
    buyLink: null,
    blocks: Immutable.List(),
    dragBlock: null,
    firstBlock: null,
    hasContent: false,
    hasMedia: false,
    hasMention: false,
    isComment: false,
    isLoading: false,
    isMobileGridStream: false,
    isNavbarHidden: false,
    isOwnPost: false,
    isPosting: false,
    postId: null,
    repostContent: Immutable.List(),
    submitText: 'Post',
  }

  componentWillMount() {
    const { blocks, dispatch, editorId, repostContent } = this.props
    this.state = { hasDragOver: false }
    if (repostContent.size) {
      dispatch(addBlock({ kind: 'repost', data: repostContent }, editorId))
    }
    if (blocks.size) {
      blocks.forEach((block) => {
        dispatch(addBlock(block.toJS(), editorId, false))
      })
    }
  }

  componentDidMount() {
    const { dispatch, editorId } = this.props
    this.dragObject = { component: this, dragId: editorId }
    dispatch(addEmptyTextBlock(editorId))
    addDragObject(this.dragObject)
    addInputObject(this)
  }

  componentDidUpdate(prevProps) {
    const { editorId, isNavbarHidden, isPostDetail,
      isPostEditing, isPostReposting, order } = this.props
    const isDragging = document.body.classList.contains('isDragging')
    const switchedToEditing = prevProps.isPostEditing !== isPostEditing ||
      prevProps.isPostReposting !== isPostReposting
    const orderWasModified = prevProps.order && order && prevProps.order.size !== order.size
    if (switchedToEditing || (!isPostDetail && !isDragging && orderWasModified)) {
      scrollToLastTextBlock(editorId, isNavbarHidden)
    }
  }

  componentWillUnmount() {
    if (this.dragObject) {
      removeDragObject(this.dragObject)
    }
    removeInputObject(this)
  }

  onCloseOmnibar() {
    const { dispatch, isComment } = this.props
    if (!isComment) {
      dispatch(closeOmnibar())
    }
  }

  onDragStart(props) {
    const { collection, dispatch, editorId } = this.props
    this.blockNode = props.target.parentNode.parentNode
    this.startOffset = this.blockNode.offsetTop
    this.startHeight = this.blockNode.offsetHeight
    this.prevBlock = this.blockNode.previousSibling
    this.nextBlock = this.blockNode.nextSibling
    const dragUid = this.blockNode.dataset.collectionId
    dispatch(addDragBlock(collection.get(`${dragUid}`), editorId))
    // swap the dragging block for a
    // normal block and set the height/width
    const block = {
      data: {
        width: this.blockNode.offsetWidth,
        height: this.blockNode.offsetHeight,
      },
      kind: 'block',
      uid: collection.getIn([`${dragUid}`, 'uid']),
    }
    dispatch(updateBlock(block, dragUid, editorId, true))
    this.onDragMove(props)
    document.body.classList.add('isDragging')
  }

  onDragMove(props) {
    // move the block we are currently dragging
    this.setState({ dragBlockTop: props.dragY + this.startOffset })
    // determine if we should change order
    if (props.dragY < props.lastDragY) {
      this.onDragUp(props)
    } else if (props.dragY > props.lastDragY) {
      this.onDragDown(props)
    }
  }

  onDragUp(props) {
    if (this.prevBlock &&
        !this.prevBlock.classList.contains('readonly') &&
        (props.dragY + this.startOffset) <
        (this.prevBlock.offsetTop + (this.prevBlock.offsetHeight * 0.5))) {
      this.onMoveBlock(-1)
    }
  }

  onDragDown(props) {
    if (this.nextBlock &&
        (props.dragY + this.startOffset + this.startHeight) >
        (this.nextBlock.offsetTop + (this.nextBlock.offsetHeight * 0.5))) {
      this.onMoveBlock(1)
    }
  }

  onMoveBlock(delta) {
    if (!this.blockPlaceholder) return
    const { dispatch, dragBlock, editorId } = this.props
    dispatch(reorderBlocks(dragBlock.get('uid'), delta, editorId))
    const placeholder = this.blockPlaceholder.editorBlock
    this.prevBlock = placeholder.previousSibling
    this.nextBlock = placeholder.nextSibling
  }

  onDragEnd() {
    const { dispatch, dragBlock, editorId } = this.props
    // swap the normal block out for
    // the one that was removed initially
    const dragUid = dragBlock.get('uid')
    dispatch(updateBlock(dragBlock, dragUid, editorId))
    dispatch(removeDragBlock(editorId))
    document.body.classList.remove('isDragging')
    this.setState({ dragBlockTop: null })
    dispatch(addEmptyTextBlock(editorId))
  }

  onDragOver = (e) => {
    e.preventDefault()
    if (!this.state.hasDragOver) {
      this.setState({ hasDragOver: true })
    }
  }

  onDragLeave = () => {
    if (this.state.hasDragOver) {
      this.setState({ hasDragOver: false })
    }
  }

  onDrop = (e) => {
    e.preventDefault()
    e.stopPropagation()
    this.setState({ hasDragOver: false })
    if (e.dataTransfer.files.length) { this.acceptFiles(e.dataTransfer.files) }
    if (e.dataTransfer.types.indexOf('application/json') > -1) {
      const data = JSON.parse(e.dataTransfer.getData('application/json'))
      if (data.username) {
        this.appendText(`@${data.username} `)
      }
      if (data.emojiCode) {
        this.appendText(`${data.emojiCode} `)
      }
      if (data.imgSrc) {
        this.appendText(`![img-drop](${data.imgSrc})\n\n`)
      }
      if (data.href) {
        if (data.href === data.linkText) {
          this.appendText(`${data.href}`)
        } else {
          this.appendText(`[${data.linkText}](${data.href}) `)
        }
      }
    }
  }

  onSubmitPost() {
    const { collection, editorId, hasContent } = this.props
    if (document.activeElement.parentNode.dataset.editorId === editorId &&
        collection.every(block => !block.get('isLoading')) && hasContent) {
      this.submit()
    }
  }

  onInsertEmoji = ({ value }) => {
    this.appendText(value)
  }

  getBlockElement(block) {
    const { editorId } = this.props
    const blockProps = {
      data: block.get('data'),
      editorId,
      key: block.get('uid'),
      kind: block.get('kind'),
      linkURL: block.get('linkUrl'),
      onRemoveBlock: this.remove,
      uid: block.get('uid'),
    }
    switch (block.get('kind')) {
      case 'block':
        return (
          <Block
            {...blockProps}
            className={classNames('BlockPlaceholder', { isUploading: block.get('isLoading') })}
            ref={(comp) => { this.blockPlaceholder = comp }}
          >

          </Block>
        )
      case 'embed':
        return (
          <EmbedBlock {...blockProps} />
        )
      case 'image':
        return (
          <ImageBlock blob={block.get('blob')} {...blockProps} isUploading={block.get('isLoading')} />
        )
      case 'repost':
        return (
          <RepostBlock {...blockProps} onRemoveBlock={null} />
        )
      case 'text':
        return (
          <TextBlock
            {...blockProps}
            onInput={this.handleTextBlockInput}
            shouldAutofocus={this.shouldAutofocus()}
          />
        )
      default:
        return null
    }
  }

  shouldAutofocus() {
    const { pathname, isComment } = this.props
    const postRegex = /^\/[\w-]+\/post\/.+/
    return !(isComment && postRegex.test(pathname))
  }

  appendText = (content) => {
    const { dispatch, editorId, isNavbarHidden } = this.props
    dispatch({ type: ACTION_TYPES.EDITOR.APPEND_TEXT, payload: { editorId, text: content } })
    scrollToLastTextBlock(editorId, isNavbarHidden)
  }

  remove = (uid) => {
    const { dispatch, editorId } = this.props
    dispatch(removeBlock(uid, editorId))
  }

  handleTextBlockInput = (vo) => {
    const { dragBlock, collection, dispatch, editorId } = this.props
    if (!dragBlock && collection.getIn([`${vo.uid}`, 'data']) !== vo.data) {
      dispatch(updateBlock(vo, vo.uid, editorId))
    }
  }

  replyAll = () => {
    const { dispatch, postId, editorId } = this.props
    dispatch(loadReplyAll(postId, editorId))
  }

  submit = () => {
    const { submitAction } = this.props
    const data = this.serialize()
    submitAction(data)
  }

  serialize() {
    const { collection, order } = this.props
    const results = []
    order.forEach((uid) => {
      const block = collection.get(`${uid}`)
      const kind = block.get('kind')
      const data = block.get('data')
      const linkUrl = block.get('linkUrl')
      switch (kind) {
        case 'text':
          if (data && data.length) {
            if (linkUrl && linkUrl.length) {
              results.push({ kind, data, link_url: linkUrl })
            } else {
              results.push({ kind, data })
            }
          }
          break
        case 'repost':
          break
        default:
          if (linkUrl && linkUrl.length) {
            results.push({ kind, data, link_url: linkUrl })
          } else {
            results.push({ kind, data })
          }
          break
      }
    })
    return results
  }

  handleFiles = (e) => {
    if (e.target.files.length) { this.acceptFiles(e.target.files) }
  }

  acceptFiles(files) {
    const { dispatch, editorId } = this.props
    for (let index = 0, len = files.length; index < len; index += 1) {
      if (files.item(index)) {
        dispatch(saveAsset(files[index], editorId))
      }
    }
  }

  render() {
    const {
      buyLink, avatar, cancelAction, collection, dragBlock, editorId, firstBlock,
      hasContent, hasMedia, hasMention, isComment, isLoading, isMobileGridStream,
      isOwnPost, isPosting, order, orderLength, submitText,
    } = this.props
    const { dragBlockTop, hasDragOver } = this.state
    const firstBlockIsText = firstBlock ? /text/.test(firstBlock.get('kind')) : true
    const showQuickEmoji = isComment && firstBlockIsText
    const editorClassNames = classNames('editor', {
      withQuickEmoji: showQuickEmoji,
      hasDragOver,
      hasContent,
      hasMedia,
      hasMention,
      isComment,
      isLoading,
      isPosting,
    })
    return (

        {isComment ? <Avatar sources={avatar} /> : null}

          {order ? order.map(uid => this.getBlockElement(collection.get(`${uid}`))) : null}
          {dragBlock ?

              {this.getBlockElement(dragBlock)}

 :
            null
          }

        {showQuickEmoji ? <QuickEmoji onAddEmoji={this.onInsertEmoji} /> : null}
        <PostActionBar
          buyLink={buyLink}
          cancelAction={cancelAction}
          disableSubmitAction={isPosting || isLoading || !hasContent}
          editorId={editorId}
          handleFileAction={this.handleFiles}
          hasMedia={hasMedia}
          replyAllAction={isComment && isOwnPost && !isMobileGridStream ? this.replyAll : null}
          submitAction={this.submit}
          submitText={submitText}
        />

    )
  }
}

export default connect(mapStateToProps)(BlockCollection)

<commit_msg>Track event when post is submitted with a buy link<commit_after>import Immutable from 'immutable'
import React, { PropTypes, PureComponent } from 'react'
import { connect } from 'react-redux'
import classNames from 'classnames'
import { selectIsMobileGridStream, selectIsNavbarHidden } from '../../selectors/gui'
import { selectPropsPostId } from '../../selectors/post'
import { selectAvatar } from '../../selectors/profile'
import { selectIsPostDetail, selectPathname } from '../../selectors/routing'
import Avatar from '../assets/Avatar'
import Block from './Block'
import EmbedBlock from './EmbedBlock'
import ImageBlock from './ImageBlock'
import QuickEmoji from './QuickEmoji'
import RepostBlock from './RepostBlock'
import TextBlock from './TextBlock'
import PostActionBar from './PostActionBar'
import { trackEvent } from '../../actions/analytics'
import {
  addBlock,
  addDragBlock,
  addEmptyTextBlock,
  loadReplyAll,
  removeBlock,
  removeDragBlock,
  reorderBlocks,
  saveAsset,
  updateBlock,
} from '../../actions/editor'
import { closeOmnibar } from '../../actions/omnibar'
import { scrollToLastTextBlock } from '../../lib/jello'
import * as ACTION_TYPES from '../../constants/action_types'
import { addDragObject, removeDragObject } from './DragComponent'
import { addInputObject, removeInputObject } from './InputComponent'

function mapStateToProps(state, props) {
  const editor = state.editor.get(props.editorId, Immutable.Map())
  const collection = editor.get('collection')
  const order = editor.get('order')
  let buyLink
  const firstBlock = collection && order ? collection.get(`${order.first()}`) : null
  if (firstBlock) {
    buyLink = firstBlock.get('linkUrl')
  }
  return {
    buyLink,
    avatar: selectAvatar(state),
    collection,
    dragBlock: editor.get('dragBlock'),
    firstBlock,
    hasContent: editor.get('hasContent'),
    hasMedia: editor.get('hasMedia'),
    hasMention: editor.get('hasMention'),
    isLoading: editor.get('isLoading'),
    isPostDetail: selectIsPostDetail(state, props),
    isPosting: editor.get('isPosting'),
    isMobileGridStream: selectIsMobileGridStream(state),
    isNavbarHidden: selectIsNavbarHidden(state),
    order,
    orderLength: order ? order.size : 0,
    pathname: selectPathname(state),
    postId: selectPropsPostId(state, props),
  }
}

class BlockCollection extends PureComponent {

  static propTypes = {
    avatar: PropTypes.object,
    blocks: PropTypes.object,
    buyLink: PropTypes.string,
    cancelAction: PropTypes.func.isRequired,
    collection: PropTypes.object.isRequired,
    dispatch: PropTypes.func.isRequired,
    dragBlock: PropTypes.object,
    editorId: PropTypes.string.isRequired,
    firstBlock: PropTypes.object,
    hasContent: PropTypes.bool,
    hasMedia: PropTypes.bool,
    hasMention: PropTypes.bool,
    isComment: PropTypes.bool,
    isLoading: PropTypes.bool,
    isMobileGridStream: PropTypes.bool,
    isNavbarHidden: PropTypes.bool,
    isOwnPost: PropTypes.bool,
    isPostDetail: PropTypes.bool.isRequired,
    isPostEditing: PropTypes.bool.isRequired,
    isPostReposting: PropTypes.bool.isRequired,
    isPosting: PropTypes.bool,
    order: PropTypes.object.isRequired,
    orderLength: PropTypes.number.isRequired,
    pathname: PropTypes.string.isRequired,
    postId: PropTypes.string,
    repostContent: PropTypes.object,
    submitAction: PropTypes.func.isRequired,
    submitText: PropTypes.string,
  }

  static defaultProps = {
    avatar: null,
    buyLink: null,
    blocks: Immutable.List(),
    dragBlock: null,
    firstBlock: null,
    hasContent: false,
    hasMedia: false,
    hasMention: false,
    isComment: false,
    isLoading: false,
    isMobileGridStream: false,
    isNavbarHidden: false,
    isOwnPost: false,
    isPosting: false,
    postId: null,
    repostContent: Immutable.List(),
    submitText: 'Post',
  }

  componentWillMount() {
    const { blocks, dispatch, editorId, repostContent } = this.props
    this.state = { hasDragOver: false }
    if (repostContent.size) {
      dispatch(addBlock({ kind: 'repost', data: repostContent }, editorId))
    }
    if (blocks.size) {
      blocks.forEach((block) => {
        dispatch(addBlock(block.toJS(), editorId, false))
      })
    }
  }

  componentDidMount() {
    const { dispatch, editorId } = this.props
    this.dragObject = { component: this, dragId: editorId }
    dispatch(addEmptyTextBlock(editorId))
    addDragObject(this.dragObject)
    addInputObject(this)
  }

  componentDidUpdate(prevProps) {
    const { editorId, isNavbarHidden, isPostDetail,
      isPostEditing, isPostReposting, order } = this.props
    const isDragging = document.body.classList.contains('isDragging')
    const switchedToEditing = prevProps.isPostEditing !== isPostEditing ||
      prevProps.isPostReposting !== isPostReposting
    const orderWasModified = prevProps.order && order && prevProps.order.size !== order.size
    if (switchedToEditing || (!isPostDetail && !isDragging && orderWasModified)) {
      scrollToLastTextBlock(editorId, isNavbarHidden)
    }
  }

  componentWillUnmount() {
    if (this.dragObject) {
      removeDragObject(this.dragObject)
    }
    removeInputObject(this)
  }

  onCloseOmnibar() {
    const { dispatch, isComment } = this.props
    if (!isComment) {
      dispatch(closeOmnibar())
    }
  }

  onDragStart(props) {
    const { collection, dispatch, editorId } = this.props
    this.blockNode = props.target.parentNode.parentNode
    this.startOffset = this.blockNode.offsetTop
    this.startHeight = this.blockNode.offsetHeight
    this.prevBlock = this.blockNode.previousSibling
    this.nextBlock = this.blockNode.nextSibling
    const dragUid = this.blockNode.dataset.collectionId
    dispatch(addDragBlock(collection.get(`${dragUid}`), editorId))
    // swap the dragging block for a
    // normal block and set the height/width
    const block = {
      data: {
        width: this.blockNode.offsetWidth,
        height: this.blockNode.offsetHeight,
      },
      kind: 'block',
      uid: collection.getIn([`${dragUid}`, 'uid']),
    }
    dispatch(updateBlock(block, dragUid, editorId, true))
    this.onDragMove(props)
    document.body.classList.add('isDragging')
  }

  onDragMove(props) {
    // move the block we are currently dragging
    this.setState({ dragBlockTop: props.dragY + this.startOffset })
    // determine if we should change order
    if (props.dragY < props.lastDragY) {
      this.onDragUp(props)
    } else if (props.dragY > props.lastDragY) {
      this.onDragDown(props)
    }
  }

  onDragUp(props) {
    if (this.prevBlock &&
        !this.prevBlock.classList.contains('readonly') &&
        (props.dragY + this.startOffset) <
        (this.prevBlock.offsetTop + (this.prevBlock.offsetHeight * 0.5))) {
      this.onMoveBlock(-1)
    }
  }

  onDragDown(props) {
    if (this.nextBlock &&
        (props.dragY + this.startOffset + this.startHeight) >
        (this.nextBlock.offsetTop + (this.nextBlock.offsetHeight * 0.5))) {
      this.onMoveBlock(1)
    }
  }

  onMoveBlock(delta) {
    if (!this.blockPlaceholder) return
    const { dispatch, dragBlock, editorId } = this.props
    dispatch(reorderBlocks(dragBlock.get('uid'), delta, editorId))
    const placeholder = this.blockPlaceholder.editorBlock
    this.prevBlock = placeholder.previousSibling
    this.nextBlock = placeholder.nextSibling
  }

  onDragEnd() {
    const { dispatch, dragBlock, editorId } = this.props
    // swap the normal block out for
    // the one that was removed initially
    const dragUid = dragBlock.get('uid')
    dispatch(updateBlock(dragBlock, dragUid, editorId))
    dispatch(removeDragBlock(editorId))
    document.body.classList.remove('isDragging')
    this.setState({ dragBlockTop: null })
    dispatch(addEmptyTextBlock(editorId))
  }

  onDragOver = (e) => {
    e.preventDefault()
    if (!this.state.hasDragOver) {
      this.setState({ hasDragOver: true })
    }
  }

  onDragLeave = () => {
    if (this.state.hasDragOver) {
      this.setState({ hasDragOver: false })
    }
  }

  onDrop = (e) => {
    e.preventDefault()
    e.stopPropagation()
    this.setState({ hasDragOver: false })
    if (e.dataTransfer.files.length) { this.acceptFiles(e.dataTransfer.files) }
    if (e.dataTransfer.types.indexOf('application/json') > -1) {
      const data = JSON.parse(e.dataTransfer.getData('application/json'))
      if (data.username) {
        this.appendText(`@${data.username} `)
      }
      if (data.emojiCode) {
        this.appendText(`${data.emojiCode} `)
      }
      if (data.imgSrc) {
        this.appendText(`![img-drop](${data.imgSrc})\n\n`)
      }
      if (data.href) {
        if (data.href === data.linkText) {
          this.appendText(`${data.href}`)
        } else {
          this.appendText(`[${data.linkText}](${data.href}) `)
        }
      }
    }
  }

  onSubmitPost() {
    const { collection, editorId, hasContent } = this.props
    if (document.activeElement.parentNode.dataset.editorId === editorId &&
        collection.every(block => !block.get('isLoading')) && hasContent) {
      this.submit()
    }
  }

  onInsertEmoji = ({ value }) => {
    this.appendText(value)
  }

  getBlockElement(block) {
    const { editorId } = this.props
    const blockProps = {
      data: block.get('data'),
      editorId,
      key: block.get('uid'),
      kind: block.get('kind'),
      linkURL: block.get('linkUrl'),
      onRemoveBlock: this.remove,
      uid: block.get('uid'),
    }
    switch (block.get('kind')) {
      case 'block':
        return (
          <Block
            {...blockProps}
            className={classNames('BlockPlaceholder', { isUploading: block.get('isLoading') })}
            ref={(comp) => { this.blockPlaceholder = comp }}
          >

          </Block>
        )
      case 'embed':
        return (
          <EmbedBlock {...blockProps} />
        )
      case 'image':
        return (
          <ImageBlock blob={block.get('blob')} {...blockProps} isUploading={block.get('isLoading')} />
        )
      case 'repost':
        return (
          <RepostBlock {...blockProps} onRemoveBlock={null} />
        )
      case 'text':
        return (
          <TextBlock
            {...blockProps}
            onInput={this.handleTextBlockInput}
            shouldAutofocus={this.shouldAutofocus()}
          />
        )
      default:
        return null
    }
  }

  shouldAutofocus() {
    const { pathname, isComment } = this.props
    const postRegex = /^\/[\w-]+\/post\/.+/
    return !(isComment && postRegex.test(pathname))
  }

  appendText = (content) => {
    const { dispatch, editorId, isNavbarHidden } = this.props
    dispatch({ type: ACTION_TYPES.EDITOR.APPEND_TEXT, payload: { editorId, text: content } })
    scrollToLastTextBlock(editorId, isNavbarHidden)
  }

  remove = (uid) => {
    const { dispatch, editorId } = this.props
    dispatch(removeBlock(uid, editorId))
  }

  handleTextBlockInput = (vo) => {
    const { dragBlock, collection, dispatch, editorId } = this.props
    if (!dragBlock && collection.getIn([`${vo.uid}`, 'data']) !== vo.data) {
      dispatch(updateBlock(vo, vo.uid, editorId))
    }
  }

  replyAll = () => {
    const { dispatch, postId, editorId } = this.props
    dispatch(loadReplyAll(postId, editorId))
  }

  submit = () => {
    const { buyLink, dispatch, submitAction } = this.props
    const data = this.serialize()
    if (buyLink && buyLink.length) {
      dispatch(trackEvent('added_buy_button'))
    }
    submitAction(data)
  }

  serialize() {
    const { collection, order } = this.props
    const results = []
    order.forEach((uid) => {
      const block = collection.get(`${uid}`)
      const kind = block.get('kind')
      const data = block.get('data')
      const linkUrl = block.get('linkUrl')
      switch (kind) {
        case 'text':
          if (data && data.length) {
            if (linkUrl && linkUrl.length) {
              results.push({ kind, data, link_url: linkUrl })
            } else {
              results.push({ kind, data })
            }
          }
          break
        case 'repost':
          break
        default:
          if (linkUrl && linkUrl.length) {
            results.push({ kind, data, link_url: linkUrl })
          } else {
            results.push({ kind, data })
          }
          break
      }
    })
    return results
  }

  handleFiles = (e) => {
    if (e.target.files.length) { this.acceptFiles(e.target.files) }
  }

  acceptFiles(files) {
    const { dispatch, editorId } = this.props
    for (let index = 0, len = files.length; index < len; index += 1) {
      if (files.item(index)) {
        dispatch(saveAsset(files[index], editorId))
      }
    }
  }

  render() {
    const {
      buyLink, avatar, cancelAction, collection, dragBlock, editorId, firstBlock,
      hasContent, hasMedia, hasMention, isComment, isLoading, isMobileGridStream,
      isOwnPost, isPosting, order, orderLength, submitText,
    } = this.props
    const { dragBlockTop, hasDragOver } = this.state
    const firstBlockIsText = firstBlock ? /text/.test(firstBlock.get('kind')) : true
    const showQuickEmoji = isComment && firstBlockIsText
    const editorClassNames = classNames('editor', {
      withQuickEmoji: showQuickEmoji,
      hasDragOver,
      hasContent,
      hasMedia,
      hasMention,
      isComment,
      isLoading,
      isPosting,
    })
    return (

        {isComment ? <Avatar sources={avatar} /> : null}

          {order ? order.map(uid => this.getBlockElement(collection.get(`${uid}`))) : null}
          {dragBlock ?

              {this.getBlockElement(dragBlock)}

 :
            null
          }

        {showQuickEmoji ? <QuickEmoji onAddEmoji={this.onInsertEmoji} /> : null}
        <PostActionBar
          buyLink={buyLink}
          cancelAction={cancelAction}
          disableSubmitAction={isPosting || isLoading || !hasContent}
          editorId={editorId}
          handleFileAction={this.handleFiles}
          hasMedia={hasMedia}
          replyAllAction={isComment && isOwnPost && !isMobileGridStream ? this.replyAll : null}
          submitAction={this.submit}
          submitText={submitText}
        />

    )
  }
}

export default connect(mapStateToProps)(BlockCollection)

<|endoftext|>


<commit_before>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class mercado extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'mercado',
            'name': 'Mercado Bitcoin',
            'countries': [ 'BR' ], // Brazil
            'rateLimit': 1000,
            'version': 'v3',
            'has': {
                'CORS': true,
                'createMarketOrder': false,
                'fetchOrder': true,
                'withdraw': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg',
                'api': {
                    'public': 'https://www.mercadobitcoin.net/api',
                    'private': 'https://www.mercadobitcoin.net/tapi',
                },
                'www': 'https://www.mercadobitcoin.com.br',
                'doc': [
                    'https://www.mercadobitcoin.com.br/api-doc',
                    'https://www.mercadobitcoin.com.br/trade-api',
                ],
            },
            'api': {
                'public': {
                    'get': [
                        '{coin}/orderbook/', // last slash critical
                        '{coin}/ticker/',
                        '{coin}/trades/',
                        '{coin}/trades/{from}/',
                        '{coin}/trades/{from}/{to}',
                        '{coin}/day-summary/{year}/{month}/{day}/',
                    ],
                },
                'private': {
                    'post': [
                        'cancel_order',
                        'get_account_info',
                        'get_order',
                        'get_withdrawal',
                        'list_system_messages',
                        'list_orders',
                        'list_orderbook',
                        'place_buy_order',
                        'place_sell_order',
                        'withdraw_coin',
                    ],
                },
            },
            'markets': {
                'BTC/BRL': { 'id': 'BRLBTC', 'symbol': 'BTC/BRL', 'base': 'BTC', 'quote': 'BRL', 'suffix': 'Bitcoin' },
                'LTC/BRL': { 'id': 'BRLLTC', 'symbol': 'LTC/BRL', 'base': 'LTC', 'quote': 'BRL', 'suffix': 'Litecoin' },
                'BCH/BRL': { 'id': 'BRLBCH', 'symbol': 'BCH/BRL', 'base': 'BCH', 'quote': 'BRL', 'suffix': 'BCash' },
            },
            'fees': {
                'trading': {
                    'maker': 0.3 / 100,
                    'taker': 0.7 / 100,
                },
            },
        });
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let orderbook = await this.publicGetCoinOrderbook (this.extend ({
            'coin': market['base'],
        }, params));
        return this.parseOrderBook (orderbook);
    }

    async fetchTicker (symbol, params = {}) {
        let market = this.market (symbol);
        let response = await this.publicGetCoinTicker (this.extend ({
            'coin': market['base'],
        }, params));
        let ticker = response['ticker'];
        let timestamp = parseInt (ticker['date']) * 1000;
        let last = this.safeFloat (ticker, 'last');
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeFloat (ticker, 'high'),
            'low': this.safeFloat (ticker, 'low'),
            'bid': this.safeFloat (ticker, 'buy'),
            'bidVolume': undefined,
            'ask': this.safeFloat (ticker, 'sell'),
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': undefined,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': this.safeFloat (ticker, 'vol'),
            'quoteVolume': undefined,
            'info': ticker,
        };
    }

    parseTrade (trade, market) {
        let timestamp = trade['date'] * 1000;
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': market['symbol'],
            'id': trade['tid'].toString (),
            'order': undefined,
            'type': undefined,
            'side': trade['type'],
            'price': trade['price'],
            'amount': trade['amount'],
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let method = 'publicGetCoinTrades';
        let request = {
            'coin': market['base'],
        };
        if (since !== undefined) {
            method += 'From';
            request['from'] = parseInt (since / 1000);
        }
        let to = this.safeInteger (params, 'to');
        if (to !== undefined)
            method += 'To';
        let response = await this[method] (this.extend (request, params));
        return this.parseTrades (response, market, since, limit);
    }

    async fetchBalance (params = {}) {
        let response = await this.privatePostGetAccountInfo ();
        let balances = response['response_data']['balance'];
        let result = { 'info': response };
        let currencies = Object.keys (this.currencies);
        for (let i = 0; i < currencies.length; i++) {
            let currency = currencies[i];
            let lowercase = currency.toLowerCase ();
            let account = this.account ();
            if (lowercase in balances) {
                account['free'] = parseFloat (balances[lowercase]['available']);
                account['total'] = parseFloat (balances[lowercase]['total']);
                account['used'] = account['total'] - account['free'];
            }
            result[currency] = account;
        }
        return this.parseBalance (result);
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        if (type === 'market')
            throw new ExchangeError (this.id + ' allows limit orders only');
        let method = 'privatePostPlace' + this.capitalize (side) + 'Order';
        let order = {
            'coin_pair': this.marketId (symbol),
            'quantity': amount,
            'limit_price': price,
        };
        let response = await this[method] (this.extend (order, params));
        return {
            'info': response,
            'id': response['response_data']['order']['order_id'].toString (),
        };
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        if (symbol === undefined)
            throw new ExchangeError (this.id + ' cancelOrder() requires a symbol argument');
        await this.loadMarkets ();
        let market = this.market (symbol);
        let response = await this.privatePostCancelOrder (this.extend ({
            'coin_pair': market['id'],
            'order_id': id,
        }, params));
        //
        //     {         response_data: { order: {           order_id:    2176769,
        //                                                  coin_pair:   "BRLBCH",
        //                                                 order_type:    2,
        //                                                     status:    3,
        //                                                  has_fills:    false,
        //                                                   quantity:   "0.10000000",
        //                                                limit_price:   "1996.15999",
        //                                          executed_quantity:   "0.00000000",
        //                                         executed_price_avg:   "0.00000",
        //                                                        fee:   "0.00000000",
        //                                          created_timestamp:   "1536956488",
        //                                          updated_timestamp:   "1536956499",
        //                                                 operations: []              } },
        //                 status_code:    100,
        //       server_unix_timestamp:   "1536956499"                                      }
        //
        return this.parseOrder (response['response_data']['order'], market);
    }

    parseOrderStatus (status) {
        const statuses = {
            '2': 'open',
            '3': 'canceled',
            '4': 'closed',
        };
        return this.safeString (statuses, status, status);
    }

    parseOrder (order, market = undefined) {
        //
        //     {
        //         "order_id": 4,
        //         "coin_pair": "BRLBTC",
        //         "order_type": 1,
        //         "status": 2,
        //         "has_fills": true,
        //         "quantity": "2.00000000",
        //         "limit_price": "900.00000",
        //         "executed_quantity": "1.00000000",
        //         "executed_price_avg": "900.00000",
        //         "fee": "0.00300000",
        //         "created_timestamp": "1453838494",
        //         "updated_timestamp": "1453838494",
        //         "operations": [
        //             {
        //                 "operation_id": 1,
        //                 "quantity": "1.00000000",
        //                 "price": "900.00000",
        //                 "fee_rate": "0.30",
        //                 "executed_timestamp": "1453838494",
        //             },
        //         ],
        //     }
        //
        const id = this.safeString (order, 'order_id');
        let side = undefined;
        if ('order_type' in order)
            side = (order['order_type'] === 1) ? 'buy' : 'sell';
        let status = this.parseOrderStatus (this.safeString (order, 'status'));
        let symbol = undefined;
        if (market === undefined) {
            let marketId = this.safeString (order, 'coin_pair');
            market = this.safeValue (this.markets_by_id, marketId);
        }
        if (market !== undefined) {
            symbol = market['symbol'];
        }
        let timestamp = this.safeIneteger (order, 'created_timestamp');
        if (timestamp !== undefined) {
            timestamp = timestamp * 1000;
        }
        let fee = {
            'cost': this.safeFloat (order, 'fee'),
            'currency': market['quote'],
        };
        let price = this.safeFloat (order, 'limit_price');
        // price = this.safeFloat (order, 'executed_price_avg', price);
        let average = this.safeFloat (order, 'executed_price_avg');
        let amount = this.safeFloat (order, 'quantity');
        let filled = this.safeFloat (order, 'executed_quantity');
        let remaining = amount - filled;
        let cost = amount * average;
        let lastTradeTimestamp = this.safeInteger (order, 'updated_timestamp');
        if (lastTradeTimestamp !== undefined) {
            lastTradeTimestamp = lastTradeTimestamp * 1000;
        }
        let result = {
            'info': order,
            'id': id,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': 'limit',
            'side': side,
            'price': price,
            'cost': cost,
            'average': average,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': fee,
            'trades': undefined, // todo parse trades (operations)
        };
        return result;
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        if (symbol === undefined)
            throw new ExchangeError (this.id + ' cancelOrder() requires a symbol argument');
        await this.loadMarkets ();
        let market = this.market (symbol);
        let response = undefined;
        response = await this.privatePostGetOrder (this.extend ({
            'coin_pair': market['id'],
            'order_id': parseInt (id),
        }, params));
        return this.parseOrder (response['response_data']['order']);
    }

    async withdraw (currency, amount, address, tag = undefined, params = {}) {
        this.checkAddress (address);
        await this.loadMarkets ();
        let request = {
            'coin': currency,
            'quantity': amount.toFixed (10),
            'address': address,
        };
        if (currency === 'BRL') {
            let account_ref = ('account_ref' in params);
            if (!account_ref)
                throw new ExchangeError (this.id + ' requires account_ref parameter to withdraw ' + currency);
        } else if (currency !== 'LTC') {
            let tx_fee = ('tx_fee' in params);
            if (!tx_fee)
                throw new ExchangeError (this.id + ' requires tx_fee parameter to withdraw ' + currency);
        }
        let response = await this.privatePostWithdrawCoin (this.extend (request, params));
        return {
            'info': response,
            'id': response['response_data']['withdrawal']['id'],
        };
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let url = this.urls['api'][api] + '/';
        let query = this.omit (params, this.extractParams (path));
        if (api === 'public') {
            url += this.implodeParams (path, params);
            if (Object.keys (query).length)
                url += '?' + this.urlencode (query);
        } else {
            this.checkRequiredCredentials ();
            url += this.version + '/';
            let nonce = this.nonce ();
            body = this.urlencode (this.extend ({
                'tapi_method': path,
                'tapi_nonce': nonce,
            }, params));
            let auth = '/tapi/' + this.version + '/' + '?' + body;
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded',
                'TAPI-ID': this.apiKey,
                'TAPI-MAC': this.hmac (this.encode (auth), this.encode (this.secret), 'sha512'),
            };
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let response = await this.fetch2 (path, api, method, params, headers, body);
        if ('error_message' in response)
            throw new ExchangeError (this.id + ' ' + this.json (response));
        return response;
    }
};
<commit_msg>minor mercado typo fix #818<commit_after>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class mercado extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'mercado',
            'name': 'Mercado Bitcoin',
            'countries': [ 'BR' ], // Brazil
            'rateLimit': 1000,
            'version': 'v3',
            'has': {
                'CORS': true,
                'createMarketOrder': false,
                'fetchOrder': true,
                'withdraw': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg',
                'api': {
                    'public': 'https://www.mercadobitcoin.net/api',
                    'private': 'https://www.mercadobitcoin.net/tapi',
                },
                'www': 'https://www.mercadobitcoin.com.br',
                'doc': [
                    'https://www.mercadobitcoin.com.br/api-doc',
                    'https://www.mercadobitcoin.com.br/trade-api',
                ],
            },
            'api': {
                'public': {
                    'get': [
                        '{coin}/orderbook/', // last slash critical
                        '{coin}/ticker/',
                        '{coin}/trades/',
                        '{coin}/trades/{from}/',
                        '{coin}/trades/{from}/{to}',
                        '{coin}/day-summary/{year}/{month}/{day}/',
                    ],
                },
                'private': {
                    'post': [
                        'cancel_order',
                        'get_account_info',
                        'get_order',
                        'get_withdrawal',
                        'list_system_messages',
                        'list_orders',
                        'list_orderbook',
                        'place_buy_order',
                        'place_sell_order',
                        'withdraw_coin',
                    ],
                },
            },
            'markets': {
                'BTC/BRL': { 'id': 'BRLBTC', 'symbol': 'BTC/BRL', 'base': 'BTC', 'quote': 'BRL', 'suffix': 'Bitcoin' },
                'LTC/BRL': { 'id': 'BRLLTC', 'symbol': 'LTC/BRL', 'base': 'LTC', 'quote': 'BRL', 'suffix': 'Litecoin' },
                'BCH/BRL': { 'id': 'BRLBCH', 'symbol': 'BCH/BRL', 'base': 'BCH', 'quote': 'BRL', 'suffix': 'BCash' },
            },
            'fees': {
                'trading': {
                    'maker': 0.3 / 100,
                    'taker': 0.7 / 100,
                },
            },
        });
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let orderbook = await this.publicGetCoinOrderbook (this.extend ({
            'coin': market['base'],
        }, params));
        return this.parseOrderBook (orderbook);
    }

    async fetchTicker (symbol, params = {}) {
        let market = this.market (symbol);
        let response = await this.publicGetCoinTicker (this.extend ({
            'coin': market['base'],
        }, params));
        let ticker = response['ticker'];
        let timestamp = parseInt (ticker['date']) * 1000;
        let last = this.safeFloat (ticker, 'last');
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeFloat (ticker, 'high'),
            'low': this.safeFloat (ticker, 'low'),
            'bid': this.safeFloat (ticker, 'buy'),
            'bidVolume': undefined,
            'ask': this.safeFloat (ticker, 'sell'),
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': undefined,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': this.safeFloat (ticker, 'vol'),
            'quoteVolume': undefined,
            'info': ticker,
        };
    }

    parseTrade (trade, market) {
        let timestamp = trade['date'] * 1000;
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': market['symbol'],
            'id': trade['tid'].toString (),
            'order': undefined,
            'type': undefined,
            'side': trade['type'],
            'price': trade['price'],
            'amount': trade['amount'],
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        let market = this.market (symbol);
        let method = 'publicGetCoinTrades';
        let request = {
            'coin': market['base'],
        };
        if (since !== undefined) {
            method += 'From';
            request['from'] = parseInt (since / 1000);
        }
        let to = this.safeInteger (params, 'to');
        if (to !== undefined)
            method += 'To';
        let response = await this[method] (this.extend (request, params));
        return this.parseTrades (response, market, since, limit);
    }

    async fetchBalance (params = {}) {
        let response = await this.privatePostGetAccountInfo ();
        let balances = response['response_data']['balance'];
        let result = { 'info': response };
        let currencies = Object.keys (this.currencies);
        for (let i = 0; i < currencies.length; i++) {
            let currency = currencies[i];
            let lowercase = currency.toLowerCase ();
            let account = this.account ();
            if (lowercase in balances) {
                account['free'] = parseFloat (balances[lowercase]['available']);
                account['total'] = parseFloat (balances[lowercase]['total']);
                account['used'] = account['total'] - account['free'];
            }
            result[currency] = account;
        }
        return this.parseBalance (result);
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        if (type === 'market')
            throw new ExchangeError (this.id + ' allows limit orders only');
        let method = 'privatePostPlace' + this.capitalize (side) + 'Order';
        let order = {
            'coin_pair': this.marketId (symbol),
            'quantity': amount,
            'limit_price': price,
        };
        let response = await this[method] (this.extend (order, params));
        return {
            'info': response,
            'id': response['response_data']['order']['order_id'].toString (),
        };
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        if (symbol === undefined)
            throw new ExchangeError (this.id + ' cancelOrder() requires a symbol argument');
        await this.loadMarkets ();
        let market = this.market (symbol);
        let response = await this.privatePostCancelOrder (this.extend ({
            'coin_pair': market['id'],
            'order_id': id,
        }, params));
        //
        //     {         response_data: { order: {           order_id:    2176769,
        //                                                  coin_pair:   "BRLBCH",
        //                                                 order_type:    2,
        //                                                     status:    3,
        //                                                  has_fills:    false,
        //                                                   quantity:   "0.10000000",
        //                                                limit_price:   "1996.15999",
        //                                          executed_quantity:   "0.00000000",
        //                                         executed_price_avg:   "0.00000",
        //                                                        fee:   "0.00000000",
        //                                          created_timestamp:   "1536956488",
        //                                          updated_timestamp:   "1536956499",
        //                                                 operations: []              } },
        //                 status_code:    100,
        //       server_unix_timestamp:   "1536956499"                                      }
        //
        return this.parseOrder (response['response_data']['order'], market);
    }

    parseOrderStatus (status) {
        const statuses = {
            '2': 'open',
            '3': 'canceled',
            '4': 'closed',
        };
        return this.safeString (statuses, status, status);
    }

    parseOrder (order, market = undefined) {
        //
        //     {
        //         "order_id": 4,
        //         "coin_pair": "BRLBTC",
        //         "order_type": 1,
        //         "status": 2,
        //         "has_fills": true,
        //         "quantity": "2.00000000",
        //         "limit_price": "900.00000",
        //         "executed_quantity": "1.00000000",
        //         "executed_price_avg": "900.00000",
        //         "fee": "0.00300000",
        //         "created_timestamp": "1453838494",
        //         "updated_timestamp": "1453838494",
        //         "operations": [
        //             {
        //                 "operation_id": 1,
        //                 "quantity": "1.00000000",
        //                 "price": "900.00000",
        //                 "fee_rate": "0.30",
        //                 "executed_timestamp": "1453838494",
        //             },
        //         ],
        //     }
        //
        const id = this.safeString (order, 'order_id');
        let side = undefined;
        if ('order_type' in order)
            side = (order['order_type'] === 1) ? 'buy' : 'sell';
        let status = this.parseOrderStatus (this.safeString (order, 'status'));
        let symbol = undefined;
        if (market === undefined) {
            let marketId = this.safeString (order, 'coin_pair');
            market = this.safeValue (this.markets_by_id, marketId);
        }
        if (market !== undefined) {
            symbol = market['symbol'];
        }
        let timestamp = this.safeInteger (order, 'created_timestamp');
        if (timestamp !== undefined) {
            timestamp = timestamp * 1000;
        }
        let fee = {
            'cost': this.safeFloat (order, 'fee'),
            'currency': market['quote'],
        };
        let price = this.safeFloat (order, 'limit_price');
        // price = this.safeFloat (order, 'executed_price_avg', price);
        let average = this.safeFloat (order, 'executed_price_avg');
        let amount = this.safeFloat (order, 'quantity');
        let filled = this.safeFloat (order, 'executed_quantity');
        let remaining = amount - filled;
        let cost = amount * average;
        let lastTradeTimestamp = this.safeInteger (order, 'updated_timestamp');
        if (lastTradeTimestamp !== undefined) {
            lastTradeTimestamp = lastTradeTimestamp * 1000;
        }
        let result = {
            'info': order,
            'id': id,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': 'limit',
            'side': side,
            'price': price,
            'cost': cost,
            'average': average,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': fee,
            'trades': undefined, // todo parse trades (operations)
        };
        return result;
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        if (symbol === undefined)
            throw new ExchangeError (this.id + ' cancelOrder() requires a symbol argument');
        await this.loadMarkets ();
        let market = this.market (symbol);
        let response = undefined;
        response = await this.privatePostGetOrder (this.extend ({
            'coin_pair': market['id'],
            'order_id': parseInt (id),
        }, params));
        return this.parseOrder (response['response_data']['order']);
    }

    async withdraw (currency, amount, address, tag = undefined, params = {}) {
        this.checkAddress (address);
        await this.loadMarkets ();
        let request = {
            'coin': currency,
            'quantity': amount.toFixed (10),
            'address': address,
        };
        if (currency === 'BRL') {
            let account_ref = ('account_ref' in params);
            if (!account_ref)
                throw new ExchangeError (this.id + ' requires account_ref parameter to withdraw ' + currency);
        } else if (currency !== 'LTC') {
            let tx_fee = ('tx_fee' in params);
            if (!tx_fee)
                throw new ExchangeError (this.id + ' requires tx_fee parameter to withdraw ' + currency);
        }
        let response = await this.privatePostWithdrawCoin (this.extend (request, params));
        return {
            'info': response,
            'id': response['response_data']['withdrawal']['id'],
        };
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let url = this.urls['api'][api] + '/';
        let query = this.omit (params, this.extractParams (path));
        if (api === 'public') {
            url += this.implodeParams (path, params);
            if (Object.keys (query).length)
                url += '?' + this.urlencode (query);
        } else {
            this.checkRequiredCredentials ();
            url += this.version + '/';
            let nonce = this.nonce ();
            body = this.urlencode (this.extend ({
                'tapi_method': path,
                'tapi_nonce': nonce,
            }, params));
            let auth = '/tapi/' + this.version + '/' + '?' + body;
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded',
                'TAPI-ID': this.apiKey,
                'TAPI-MAC': this.hmac (this.encode (auth), this.encode (this.secret), 'sha512'),
            };
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let response = await this.fetch2 (path, api, method, params, headers, body);
        if ('error_message' in response)
            throw new ExchangeError (this.id + ' ' + this.json (response));
        return response;
    }
};
<|endoftext|>


<commit_before>(function ($) {

	tinymce.PluginManager.add('metapic', function (editor, url) {
		var dom, currentImage;

		editor.on("init", function () {
			dom = editor.dom;

        });

		// Register a command so that it can be invoked by using tinyMCE.activeEditor.execCommand( 'WP_Link' );
		editor.addCommand('Metapic', function () {
            editor.insertContent('Metapic is awesome!');
		});
		editor.addShortcut('ctrl+m', '', 'Metapic');

		/*$(document).on('metapic.save-success', function (e, data) {
			if (currentImage) {
				currentImage.attr("data-metapic-id", data.image.id);
				currentImage.attr("data-metapic-tags", JSON.stringify(data.tags));
			}
		});
        */
        $(document).on("metapicReturn", function(data){
            console.log("metapicReturn");
            console.log(data.text);
            editor.insertContent(data.text);
        });

		editor.on('NodeChange', function (e) {
			var editorToolbar = dom.select("#mceu_6");
			var metapicTagIcon = dom.select(".metapic");
			console.log(editor);
			if (editorToolbar.length > 0 && metapicTagIcon.length === 0) {
				var elementType = $(editorToolbar).find(">:first-child").prop('tagName');
				var element = dom.create(elementType, {
					'data-mce-bogus': '1',
					'contenteditable': false,
					'class': 'dashicons dashicons-tag metapic'
				});

				var deleteElement = dom.create(elementType, {
					'data-mce-bogus': '1',
					'contenteditable': false,
					'class': 'dashicons dashicons-trash metapic'
				});

				$(dom.select(".dashicons-edit")).after(element);
				$(dom.select(".dashicons-tag")).after(deleteElement);
				$(element).on("click", function (e) {
					var metapicEditor = currentImage.MetapicEditor(editorSettings);
					metapicEditor.fireModal();
				});

				$(deleteElement).on("click", function (e) {
					var imageId = parseInt(currentImage.attr("data-metapic-id"), 10);
					if (imageId > 0) {
						var metapicEditor = currentImage.MetapicEditor(editorSettings);
						metapicEditor.deleteImage(imageId);
					}
				});
			}
		});

		editor.on('click', function (event) {
			if (event.target.nodeName === 'IMG') {
				currentImage = $(event.target);
			}
		});
		var metapicButton;
		editor.addButton('metapic', {
			text: 'Metapic text',
			icon: false,
			onclick: function () {
                $.get( "/?metapic_randomNummber", function( data ) {
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        text:editor.selection.getContent(),
                        baseUrl:data['metapicApi'],
                        startPage: "find",
                        hideSidebar:true,
                        randomKey:data['random_token']
                    })
                });
			},
			onPostRender: function () {

				editor.on('nodechange', function (event) {
					setState(metapicButton, event);
				});
			}
		});

        editor.addButton('metapicimg', {
            text: 'Metapic image',
            icon: false,
            onclick: function () {
                $.get( "/?metapic_randomNummber", function( data ) {
                    var src=$(editor.selection.getNode()).attr("src")
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        baseUrl:data['metapicApi'],
                        startPage: "tag-editor",
                        hideSidebar:true,
                        imgSrc:src,
                        text:editor.selection.getNode(),
                        randomKey:data['random_token']
                    });
                });
            },
            onPostRender: function () {
                metapicButton = this;

                editor.on('nodechange', function (event) {
                    setState(metapicButton, event);
                });
            }
        });
        editor.addButton('metapicCollage', {
            text: 'Collage',
            icon: false,
            onclick: function () {

               // editor.insertContent('metapicCollage');

                $.get( "/?metapic_randomNummber", function( data ) {
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        baseUrl:data['metapicApi'],
                        startPage: "collage",
                        hideSidebar:true,
                        randomKey:data['random_token']
                    })
                })
            },
            onPostRender: function () {
                metapicButton = this;

                editor.on('nodechange', function (event) {
                    setState(metapicButton, event);
                });
            }
        });

		function setState(button, event) {
			var editorContent = editor.selection.getContent();
			var c = editor.selection.getNode().nodeName;
			console.log(c);

			button.removeClass("metapic-new").removeClass("metapic-image").removeClass("metapic-text");
			if (editor.selection.isCollapsed() && !button.hasClass("metapic-new")) {
				removeButtonClasses(button).addClass("metapic-new");
				console.log("BRAND NEW");
			}
			else if (c == "IMG") {
				removeButtonClasses(button).addClass("metapic-image");
				console.log("IT'S AN IMAGE");
			}
			else if (editorContent.length > 0) {
				removeButtonClasses(button).addClass("metapic-text");
				console.log("IT'S TEXT");
			}
			else {
			}
			//console.log(editor.selection.isCollapsed(), editor.selection.getContent());
		}

		function removeButtonClasses(button) {
			button.removeClass("metapic-new").removeClass("metapic-image").removeClass("metapic-text");
			return button;
		}
	});

})(jQuery);<commit_msg>change to find/default<commit_after>(function ($) {

	tinymce.PluginManager.add('metapic', function (editor, url) {
		var dom, currentImage;

		editor.on("init", function () {
			dom = editor.dom;

        });

		// Register a command so that it can be invoked by using tinyMCE.activeEditor.execCommand( 'WP_Link' );
		editor.addCommand('Metapic', function () {
            editor.insertContent('Metapic is awesome!');
		});
		editor.addShortcut('ctrl+m', '', 'Metapic');

		/*$(document).on('metapic.save-success', function (e, data) {
			if (currentImage) {
				currentImage.attr("data-metapic-id", data.image.id);
				currentImage.attr("data-metapic-tags", JSON.stringify(data.tags));
			}
		});
        */
        $(document).on("metapicReturn", function(data){
            console.log("metapicReturn");
            console.log(data.text);
            editor.insertContent(data.text);
        });

		editor.on('NodeChange', function (e) {
			var editorToolbar = dom.select("#mceu_6");
			var metapicTagIcon = dom.select(".metapic");
			console.log(editor);
			if (editorToolbar.length > 0 && metapicTagIcon.length === 0) {
				var elementType = $(editorToolbar).find(">:first-child").prop('tagName');
				var element = dom.create(elementType, {
					'data-mce-bogus': '1',
					'contenteditable': false,
					'class': 'dashicons dashicons-tag metapic'
				});

				var deleteElement = dom.create(elementType, {
					'data-mce-bogus': '1',
					'contenteditable': false,
					'class': 'dashicons dashicons-trash metapic'
				});

				$(dom.select(".dashicons-edit")).after(element);
				$(dom.select(".dashicons-tag")).after(deleteElement);
				$(element).on("click", function (e) {
					var metapicEditor = currentImage.MetapicEditor(editorSettings);
					metapicEditor.fireModal();
				});

				$(deleteElement).on("click", function (e) {
					var imageId = parseInt(currentImage.attr("data-metapic-id"), 10);
					if (imageId > 0) {
						var metapicEditor = currentImage.MetapicEditor(editorSettings);
						metapicEditor.deleteImage(imageId);
					}
				});
			}
		});

		editor.on('click', function (event) {
			if (event.target.nodeName === 'IMG') {
				currentImage = $(event.target);
			}
		});
		var metapicButton;
		editor.addButton('metapic', {
			text: 'Metapic text',
			icon: false,
			onclick: function () {
                $.get( "/?metapic_randomNummber", function( data ) {
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        text:editor.selection.getContent(),
                        baseUrl:data['metapicApi'],
                        startPage: "find/default",
                        hideSidebar:true,
                        randomKey:data['random_token']
                    })
                });
			},
			onPostRender: function () {

				editor.on('nodechange', function (event) {
					setState(metapicButton, event);
				});
			}
		});

        editor.addButton('metapicimg', {
            text: 'Metapic image',
            icon: false,
            onclick: function () {
                $.get( "/?metapic_randomNummber", function( data ) {
                    var src=$(editor.selection.getNode()).attr("src")
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        baseUrl:data['metapicApi'],
                        startPage: "tag-editor",
                        hideSidebar:true,
                        imgSrc:src,
                        text:editor.selection.getNode(),
                        randomKey:data['random_token']
                    });
                });
            },
            onPostRender: function () {
                metapicButton = this;

                editor.on('nodechange', function (event) {
                    setState(metapicButton, event);
                });
            }
        });
        editor.addButton('metapicCollage', {
            text: 'Collage',
            icon: false,
            onclick: function () {

               // editor.insertContent('metapicCollage');

                $.get( "/?metapic_randomNummber", function( data ) {
                    data=JSON.parse( data);
                    $.event.trigger({
                        type: "metapic",
                        baseUrl:data['metapicApi'],
                        startPage: "collage",
                        hideSidebar:true,
                        randomKey:data['random_token']
                    })
                })
            },
            onPostRender: function () {
                metapicButton = this;

                editor.on('nodechange', function (event) {
                    setState(metapicButton, event);
                });
            }
        });

		function setState(button, event) {
			var editorContent = editor.selection.getContent();
			var c = editor.selection.getNode().nodeName;
			console.log(c);

			button.removeClass("metapic-new").removeClass("metapic-image").removeClass("metapic-text");
			if (editor.selection.isCollapsed() && !button.hasClass("metapic-new")) {
				removeButtonClasses(button).addClass("metapic-new");
				console.log("BRAND NEW");
			}
			else if (c == "IMG") {
				removeButtonClasses(button).addClass("metapic-image");
				console.log("IT'S AN IMAGE");
			}
			else if (editorContent.length > 0) {
				removeButtonClasses(button).addClass("metapic-text");
				console.log("IT'S TEXT");
			}
			else {
			}
			//console.log(editor.selection.isCollapsed(), editor.selection.getContent());
		}

		function removeButtonClasses(button) {
			button.removeClass("metapic-new").removeClass("metapic-image").removeClass("metapic-text");
			return button;
		}
	});

})(jQuery);<|endoftext|>


<commit_before>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

var React = require('react');
var DropTarget = require('react-dnd').DropTarget;
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var UnreadStatus = require('matrix-react-sdk/lib/UnreadStatus');

// turn this on for drop & drag console debugging galore
var debug = false;

var roomListTarget = {
    canDrop: function() {
        return true;
    },

    drop: function(props, monitor, component) {
        if (debug) console.log("dropped on sublist")
    },

    hover: function(props, monitor, component) {
        var item = monitor.getItem();

        if (component.state.sortedList.length == 0 && props.editable) {
            if (debug) console.log("hovering on sublist " + props.label + ", isOver=" + monitor.isOver());

            if (item.targetList !== component) {
                 item.targetList.removeRoomTile(item.room);
                 item.targetList = component;
            }

            component.moveRoomTile(item.room, 0);
        }
    },
};

var RoomSubList = React.createClass({
    displayName: 'RoomSubList',

    debug: debug,

    propTypes: {
        list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
        label: React.PropTypes.string.isRequired,
        tagName: React.PropTypes.string,
        editable: React.PropTypes.bool,
        order: React.PropTypes.string.isRequired,
        selectedRoom: React.PropTypes.string.isRequired,
        activityMap: React.PropTypes.object.isRequired,
        startAsHidden: React.PropTypes.bool,
        showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded

        // TODO: Fix the name of this. This is too easily confused with the
        // "hidden" state which is the expanded (or not) view of the list of rooms.
        // What this prop *really* does is control whether the room name is displayed
        // so it should be named as such.
        collapsed: React.PropTypes.bool.isRequired,
        onHeaderClick: React.PropTypes.func,
        alwaysShowHeader: React.PropTypes.bool,
        incomingCall: React.PropTypes.object
    },

    getInitialState: function() {
        return {
            hidden: this.props.startAsHidden || false,
            sortedList: [],
        };
    },

    getDefaultProps: function() {
        return {
            onHeaderClick: function() {} // NOP
        };
    },

    componentWillMount: function() {
        this.sortList(this.props.list, this.props.order);
    },

    componentWillReceiveProps: function(newProps) {
        // order the room list appropriately before we re-render
        //if (debug) console.log("received new props, list = " + newProps.list);
        this.sortList(newProps.list, newProps.order);
    },

    onClick: function(ev) {
        var isHidden = !this.state.hidden;
        this.setState({ hidden : isHidden });
        this.props.onHeaderClick(isHidden);
    },

    tsOfNewestEvent: function(room) {
        for (var i = room.timeline.length - 1; i >= 0; --i) {
            var ev = room.timeline[i];
            // logic copied from RoomList.js for when we do/don't highlight
            if (UnreadStatus.eventTriggersUnreadCount(ev)) {
                return ev.getTs();
            }
        }

        // we might only have events that don't trigger the unread indicator,
        // in which case use the oldest event even if normally it wouldn't count.
        // This is better than just assuming the last event was forever ago.
        if (room.timeline.length) {
            return room.timeline[0].getTs();
        } else {
            return Number.MAX_SAFE_INTEGER;
        }
    },

    // TODO: factor the comparators back out into a generic comparator
    // so that view_prev_room and view_next_room can do the right thing

    recentsComparator: function(roomA, roomB) {
        return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA);
    },

    manualComparator: function(roomA, roomB) {
        if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0;
        var a = roomA.tags[this.props.tagName].order;
        var b = roomB.tags[this.props.tagName].order;
        return a == b ? this.recentsComparator(roomA, roomB) : ( a > b  ? 1 : -1);
    },

    sortList: function(list, order) {
        if (list === undefined) list = this.state.sortedList;
        if (order === undefined) order = this.props.order;
        var comparator;
        list = list || [];
        if (order === "manual") comparator = this.manualComparator;
        if (order === "recent") comparator = this.recentsComparator;

        //if (debug) console.log("sorting list for sublist " + this.props.label + " with length " + list.length + ", this.props.list = " + this.props.list);
        this.setState({ sortedList: list.sort(comparator) });
    },

    moveRoomTile: function(room, atIndex) {
        if (debug) console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex);
        //console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms));
        var found = this.findRoomTile(room);
        var rooms = this.state.sortedList;
        if (found.room) {
            if (debug) console.log("removing at index " + found.index + " and adding at index " + atIndex);
            rooms.splice(found.index, 1);
            rooms.splice(atIndex, 0, found.room);
        }
        else {
            if (debug) console.log("Adding at index " + atIndex);
            rooms.splice(atIndex, 0, room);
        }
        this.setState({ sortedList: rooms });
        // console.log("moveRoomTile after: " + JSON.stringify(this.state.rooms));
    },

    // XXX: this isn't invoked via a property method but indirectly via
    // the roomList property method.  Unsure how evil this is.
    removeRoomTile: function(room) {
        if (debug) console.log("remove room " + room.roomId);
        var found = this.findRoomTile(room);
        var rooms = this.state.sortedList;
        if (found.room) {
            rooms.splice(found.index, 1);
        }
        else {
            console.warn("Can't remove room " + room.roomId + " - can't find it");
        }
        this.setState({ sortedList: rooms });
    },

    findRoomTile: function(room) {
        var index = this.state.sortedList.indexOf(room);
        if (index >= 0) {
            // console.log("found: room: " + room.roomId + " with index " + index);
        }
        else {
            if (debug) console.log("didn't find room");
            room = null;
        }
        return ({
            room: room,
            index: index,
        });
    },

    calcManualOrderTagData: function(room) {
        var index = this.state.sortedList.indexOf(room);

        // we sort rooms by the lexicographic ordering of the 'order' metadata on their tags.
        // for convenience, we calculate this for now a floating point number between 0.0 and 1.0.

        var orderA = 0.0; // by default we're next to the beginning of the list
        if (index > 0) {
            var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
            if (!prevTag) {
                console.error("Previous room in sublist is not tagged to be in this list. This should never happen.")
            }
            else if (prevTag.order === undefined) {
                console.error("Previous room in sublist has no ordering metadata. This should never happen.");
            }
            else {
                orderA = prevTag.order;
            }
        }

        var orderB = 1.0; // by default we're next to the end of the list too
        if (index < this.state.sortedList.length - 1) {
            var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
            if (!nextTag) {
                console.error("Next room in sublist is not tagged to be in this list. This should never happen.")
            }
            else if (nextTag.order === undefined) {
                console.error("Next room in sublist has no ordering metadata. This should never happen.");
            }
            else {
                orderB = nextTag.order;
            }
        }

        var order = (orderA + orderB) / 2.0;
        if (order === orderA || order === orderB) {
            console.error("Cannot describe new list position.  This should be incredibly unlikely.");
            // TODO: renumber the list
        }

        return order;
    },

    makeRoomTiles: function() {
        var self = this;
        var RoomTile = sdk.getComponent("rooms.RoomTile");
        return this.state.sortedList.map(function(room) {
            var selected = room.roomId == self.props.selectedRoom;
            // XXX: is it evil to pass in self as a prop to RoomTile?
            return (
                <RoomTile
                    room={ room }
                    roomSubList={ self }
                    key={ room.roomId }
                    collapsed={ self.props.collapsed || false}
                    selected={ selected }
                    unread={ self.props.activityMap[room.roomId] === 1 }
                    highlight={ self.props.activityMap[room.roomId] === 2 }
                    isInvite={ self.props.label === 'Invites' }
                    incomingCall={ self.props.incomingCall && (self.props.incomingCall.roomId === room.roomId) ? self.props.incomingCall : null } />
            );
        });
    },

    onSvgLoad: function(event) {
        dis.dispatch({ action: "svg_onload", svg: event.target });
    },

    _getHeaderJsx: function() {
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
        return (

                { this.props.collapsed ? '' : this.props.label }
                <TintableSvg className="mx_RoomSubList_chevron"
                    src={ this.state.hidden ? "img/list-close.svg" : "img/list-open.svg" }
                    width="10" height="10" />

        );
    },

    render: function() {
        var connectDropTarget = this.props.connectDropTarget;
        var RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');

        var label = this.props.collapsed ? null : this.props.label;

        //console.log("render: " + JSON.stringify(this.state.sortedList));

        var target;
        if (this.state.sortedList.length == 0 && this.props.editable) {
            target = <RoomDropTarget label={ 'Drop here to ' + this.props.verb }/>;
        }

        if (this.state.sortedList.length > 0 || this.props.editable) {
            var subList;
            var classes = "mx_RoomSubList";

            if (!this.state.hidden) {
                subList =

                                { target }
                                { this.makeRoomTiles() }

;
            }
            else {
                subList =

;
            }

            return connectDropTarget(

                    { this._getHeaderJsx() }
                    { subList }

            );
        }
        else {
            var Loader = sdk.getComponent("elements.Spinner");
            return (

                    { this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
                    { (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }

            );
        }
    }
});

// Export the wrapped version, inlining the 'collect' functions
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomListTarget, function(connect) {
    return {
        connectDropTarget: connect.dropTarget(),
    }
})(RoomSubList);
<commit_msg>remove stale onSvgLoad<commit_after>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

var React = require('react');
var DropTarget = require('react-dnd').DropTarget;
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var UnreadStatus = require('matrix-react-sdk/lib/UnreadStatus');

// turn this on for drop & drag console debugging galore
var debug = false;

var roomListTarget = {
    canDrop: function() {
        return true;
    },

    drop: function(props, monitor, component) {
        if (debug) console.log("dropped on sublist")
    },

    hover: function(props, monitor, component) {
        var item = monitor.getItem();

        if (component.state.sortedList.length == 0 && props.editable) {
            if (debug) console.log("hovering on sublist " + props.label + ", isOver=" + monitor.isOver());

            if (item.targetList !== component) {
                 item.targetList.removeRoomTile(item.room);
                 item.targetList = component;
            }

            component.moveRoomTile(item.room, 0);
        }
    },
};

var RoomSubList = React.createClass({
    displayName: 'RoomSubList',

    debug: debug,

    propTypes: {
        list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
        label: React.PropTypes.string.isRequired,
        tagName: React.PropTypes.string,
        editable: React.PropTypes.bool,
        order: React.PropTypes.string.isRequired,
        selectedRoom: React.PropTypes.string.isRequired,
        activityMap: React.PropTypes.object.isRequired,
        startAsHidden: React.PropTypes.bool,
        showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded

        // TODO: Fix the name of this. This is too easily confused with the
        // "hidden" state which is the expanded (or not) view of the list of rooms.
        // What this prop *really* does is control whether the room name is displayed
        // so it should be named as such.
        collapsed: React.PropTypes.bool.isRequired,
        onHeaderClick: React.PropTypes.func,
        alwaysShowHeader: React.PropTypes.bool,
        incomingCall: React.PropTypes.object
    },

    getInitialState: function() {
        return {
            hidden: this.props.startAsHidden || false,
            sortedList: [],
        };
    },

    getDefaultProps: function() {
        return {
            onHeaderClick: function() {} // NOP
        };
    },

    componentWillMount: function() {
        this.sortList(this.props.list, this.props.order);
    },

    componentWillReceiveProps: function(newProps) {
        // order the room list appropriately before we re-render
        //if (debug) console.log("received new props, list = " + newProps.list);
        this.sortList(newProps.list, newProps.order);
    },

    onClick: function(ev) {
        var isHidden = !this.state.hidden;
        this.setState({ hidden : isHidden });
        this.props.onHeaderClick(isHidden);
    },

    tsOfNewestEvent: function(room) {
        for (var i = room.timeline.length - 1; i >= 0; --i) {
            var ev = room.timeline[i];
            // logic copied from RoomList.js for when we do/don't highlight
            if (UnreadStatus.eventTriggersUnreadCount(ev)) {
                return ev.getTs();
            }
        }

        // we might only have events that don't trigger the unread indicator,
        // in which case use the oldest event even if normally it wouldn't count.
        // This is better than just assuming the last event was forever ago.
        if (room.timeline.length) {
            return room.timeline[0].getTs();
        } else {
            return Number.MAX_SAFE_INTEGER;
        }
    },

    // TODO: factor the comparators back out into a generic comparator
    // so that view_prev_room and view_next_room can do the right thing

    recentsComparator: function(roomA, roomB) {
        return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA);
    },

    manualComparator: function(roomA, roomB) {
        if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0;
        var a = roomA.tags[this.props.tagName].order;
        var b = roomB.tags[this.props.tagName].order;
        return a == b ? this.recentsComparator(roomA, roomB) : ( a > b  ? 1 : -1);
    },

    sortList: function(list, order) {
        if (list === undefined) list = this.state.sortedList;
        if (order === undefined) order = this.props.order;
        var comparator;
        list = list || [];
        if (order === "manual") comparator = this.manualComparator;
        if (order === "recent") comparator = this.recentsComparator;

        //if (debug) console.log("sorting list for sublist " + this.props.label + " with length " + list.length + ", this.props.list = " + this.props.list);
        this.setState({ sortedList: list.sort(comparator) });
    },

    moveRoomTile: function(room, atIndex) {
        if (debug) console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex);
        //console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms));
        var found = this.findRoomTile(room);
        var rooms = this.state.sortedList;
        if (found.room) {
            if (debug) console.log("removing at index " + found.index + " and adding at index " + atIndex);
            rooms.splice(found.index, 1);
            rooms.splice(atIndex, 0, found.room);
        }
        else {
            if (debug) console.log("Adding at index " + atIndex);
            rooms.splice(atIndex, 0, room);
        }
        this.setState({ sortedList: rooms });
        // console.log("moveRoomTile after: " + JSON.stringify(this.state.rooms));
    },

    // XXX: this isn't invoked via a property method but indirectly via
    // the roomList property method.  Unsure how evil this is.
    removeRoomTile: function(room) {
        if (debug) console.log("remove room " + room.roomId);
        var found = this.findRoomTile(room);
        var rooms = this.state.sortedList;
        if (found.room) {
            rooms.splice(found.index, 1);
        }
        else {
            console.warn("Can't remove room " + room.roomId + " - can't find it");
        }
        this.setState({ sortedList: rooms });
    },

    findRoomTile: function(room) {
        var index = this.state.sortedList.indexOf(room);
        if (index >= 0) {
            // console.log("found: room: " + room.roomId + " with index " + index);
        }
        else {
            if (debug) console.log("didn't find room");
            room = null;
        }
        return ({
            room: room,
            index: index,
        });
    },

    calcManualOrderTagData: function(room) {
        var index = this.state.sortedList.indexOf(room);

        // we sort rooms by the lexicographic ordering of the 'order' metadata on their tags.
        // for convenience, we calculate this for now a floating point number between 0.0 and 1.0.

        var orderA = 0.0; // by default we're next to the beginning of the list
        if (index > 0) {
            var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName];
            if (!prevTag) {
                console.error("Previous room in sublist is not tagged to be in this list. This should never happen.")
            }
            else if (prevTag.order === undefined) {
                console.error("Previous room in sublist has no ordering metadata. This should never happen.");
            }
            else {
                orderA = prevTag.order;
            }
        }

        var orderB = 1.0; // by default we're next to the end of the list too
        if (index < this.state.sortedList.length - 1) {
            var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName];
            if (!nextTag) {
                console.error("Next room in sublist is not tagged to be in this list. This should never happen.")
            }
            else if (nextTag.order === undefined) {
                console.error("Next room in sublist has no ordering metadata. This should never happen.");
            }
            else {
                orderB = nextTag.order;
            }
        }

        var order = (orderA + orderB) / 2.0;
        if (order === orderA || order === orderB) {
            console.error("Cannot describe new list position.  This should be incredibly unlikely.");
            // TODO: renumber the list
        }

        return order;
    },

    makeRoomTiles: function() {
        var self = this;
        var RoomTile = sdk.getComponent("rooms.RoomTile");
        return this.state.sortedList.map(function(room) {
            var selected = room.roomId == self.props.selectedRoom;
            // XXX: is it evil to pass in self as a prop to RoomTile?
            return (
                <RoomTile
                    room={ room }
                    roomSubList={ self }
                    key={ room.roomId }
                    collapsed={ self.props.collapsed || false}
                    selected={ selected }
                    unread={ self.props.activityMap[room.roomId] === 1 }
                    highlight={ self.props.activityMap[room.roomId] === 2 }
                    isInvite={ self.props.label === 'Invites' }
                    incomingCall={ self.props.incomingCall && (self.props.incomingCall.roomId === room.roomId) ? self.props.incomingCall : null } />
            );
        });
    },

    _getHeaderJsx: function() {
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
        return (

                { this.props.collapsed ? '' : this.props.label }
                <TintableSvg className="mx_RoomSubList_chevron"
                    src={ this.state.hidden ? "img/list-close.svg" : "img/list-open.svg" }
                    width="10" height="10" />

        );
    },

    render: function() {
        var connectDropTarget = this.props.connectDropTarget;
        var RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');

        var label = this.props.collapsed ? null : this.props.label;

        //console.log("render: " + JSON.stringify(this.state.sortedList));

        var target;
        if (this.state.sortedList.length == 0 && this.props.editable) {
            target = <RoomDropTarget label={ 'Drop here to ' + this.props.verb }/>;
        }

        if (this.state.sortedList.length > 0 || this.props.editable) {
            var subList;
            var classes = "mx_RoomSubList";

            if (!this.state.hidden) {
                subList =

                                { target }
                                { this.makeRoomTiles() }

;
            }
            else {
                subList =

;
            }

            return connectDropTarget(

                    { this._getHeaderJsx() }
                    { subList }

            );
        }
        else {
            var Loader = sdk.getComponent("elements.Spinner");
            return (

                    { this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
                    { (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }

            );
        }
    }
});

// Export the wrapped version, inlining the 'collect' functions
// to more closely resemble the ES7
module.exports =
DropTarget('RoomTile', roomListTarget, function(connect) {
    return {
        connectDropTarget: connect.dropTarget(),
    }
})(RoomSubList);
<|endoftext|>