[Glitch] Refactor conversations components in web UI (#2589)
Port 3205a654ca to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
			
			
This commit is contained in:
		
							parent
							
								
									a4e7cc2d94
								
							
						
					
					
						commit
						80308d384a
					
				| 
						 | 
				
			
			@ -1,17 +1,24 @@
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { withRouter } from 'react-router-dom';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { createSelector } from '@reduxjs/toolkit';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { HotKeys } from 'react-hotkeys';
 | 
			
		||||
 | 
			
		||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
 | 
			
		||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
 | 
			
		||||
import { replyCompose } from 'flavours/glitch/actions/compose';
 | 
			
		||||
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
 | 
			
		||||
import { openModal } from 'flavours/glitch/actions/modal';
 | 
			
		||||
import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'flavours/glitch/actions/statuses';
 | 
			
		||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
			
		||||
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
 | 
			
		||||
import { IconButton } from 'flavours/glitch/components/icon_button';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +27,7 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
 | 
			
		|||
import StatusContent from 'flavours/glitch/components/status_content';
 | 
			
		||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 | 
			
		||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
 | 
			
		||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 | 
			
		||||
import { makeGetStatus } from 'flavours/glitch/selectors';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  more: { id: 'status.more', defaultMessage: 'More' },
 | 
			
		||||
| 
						 | 
				
			
			@ -30,45 +37,48 @@ const messages = defineMessages({
 | 
			
		|||
  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
 | 
			
		||||
  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
 | 
			
		||||
  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
			
		||||
  replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
 | 
			
		||||
  replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Conversation extends ImmutablePureComponent {
 | 
			
		||||
const getAccounts = createSelector(
 | 
			
		||||
  (state) => state.get('accounts'),
 | 
			
		||||
  (_, accountIds) => accountIds,
 | 
			
		||||
  (accounts, accountIds) =>
 | 
			
		||||
    accountIds.map(id => accounts.get(id))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    conversationId: PropTypes.string.isRequired,
 | 
			
		||||
    accounts: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
    lastStatus: ImmutablePropTypes.map,
 | 
			
		||||
    unread:PropTypes.bool.isRequired,
 | 
			
		||||
    scrollKey: PropTypes.string,
 | 
			
		||||
    onMoveUp: PropTypes.func,
 | 
			
		||||
    onMoveDown: PropTypes.func,
 | 
			
		||||
    markRead: PropTypes.func.isRequired,
 | 
			
		||||
    delete: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    ...WithRouterPropTypes,
 | 
			
		||||
  };
 | 
			
		||||
const getStatus = makeGetStatus();
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    isExpanded: undefined,
 | 
			
		||||
  };
 | 
			
		||||
export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => {
 | 
			
		||||
  const id = conversation.get('id');
 | 
			
		||||
  const unread = conversation.get('unread');
 | 
			
		||||
  const lastStatusId = conversation.get('last_status');
 | 
			
		||||
  const accountIds = conversation.get('accounts');
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId }));
 | 
			
		||||
  const accounts = useSelector(state => getAccounts(state, accountIds));
 | 
			
		||||
 | 
			
		||||
  parseClick = (e, destination) => {
 | 
			
		||||
    const { history, lastStatus, unread, markRead } = this.props;
 | 
			
		||||
    if (!history) return;
 | 
			
		||||
  // glitch-soc additions
 | 
			
		||||
  const sharedCWState = useSelector(state => state.getIn(['state', 'content_warnings', 'shared_state']));
 | 
			
		||||
  const [expanded, setExpanded] = useState(undefined);
 | 
			
		||||
 | 
			
		||||
  const parseClick = useCallback((e, destination) => {
 | 
			
		||||
    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
 | 
			
		||||
      if (destination === undefined) {
 | 
			
		||||
        if (unread) {
 | 
			
		||||
          markRead();
 | 
			
		||||
          dispatch(markConversationRead(id));
 | 
			
		||||
        }
 | 
			
		||||
        destination = `/statuses/${lastStatus.get('id')}`;
 | 
			
		||||
      }
 | 
			
		||||
      history.push(destination);
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  }, [dispatch, history, unread, id, lastStatus]);
 | 
			
		||||
 | 
			
		||||
  handleMouseEnter = ({ currentTarget }) => {
 | 
			
		||||
  const handleMouseEnter = useCallback(({ currentTarget }) => {
 | 
			
		||||
    if (autoPlayGif) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -79,9 +89,9 @@ class Conversation extends ImmutablePureComponent {
 | 
			
		|||
      let emoji = emojis[i];
 | 
			
		||||
      emoji.src = emoji.getAttribute('data-original');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  handleMouseLeave = ({ currentTarget }) => {
 | 
			
		||||
  const handleMouseLeave = useCallback(({ currentTarget }) => {
 | 
			
		||||
    if (autoPlayGif) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -92,145 +102,160 @@ class Conversation extends ImmutablePureComponent {
 | 
			
		|||
      let emoji = emojis[i];
 | 
			
		||||
      emoji.src = emoji.getAttribute('data-static');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    if (!this.props.history) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { lastStatus, unread, markRead } = this.props;
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleClick = useCallback(() => {
 | 
			
		||||
    if (unread) {
 | 
			
		||||
      markRead();
 | 
			
		||||
      dispatch(markConversationRead(id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
 | 
			
		||||
  };
 | 
			
		||||
    history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
 | 
			
		||||
  }, [dispatch, history, unread, id, lastStatus]);
 | 
			
		||||
 | 
			
		||||
  handleMarkAsRead = () => {
 | 
			
		||||
    this.props.markRead();
 | 
			
		||||
  };
 | 
			
		||||
  const handleMarkAsRead = useCallback(() => {
 | 
			
		||||
    dispatch(markConversationRead(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  handleReply = () => {
 | 
			
		||||
    this.props.reply(this.props.lastStatus, this.props.history);
 | 
			
		||||
  };
 | 
			
		||||
  const handleReply = useCallback(() => {
 | 
			
		||||
    dispatch((_, getState) => {
 | 
			
		||||
      let state = getState();
 | 
			
		||||
 | 
			
		||||
  handleDelete = () => {
 | 
			
		||||
    this.props.delete();
 | 
			
		||||
  };
 | 
			
		||||
      if (state.getIn(['compose', 'text']).trim().length !== 0) {
 | 
			
		||||
        dispatch(openModal({
 | 
			
		||||
          modalType: 'CONFIRM',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            message: intl.formatMessage(messages.replyMessage),
 | 
			
		||||
            confirm: intl.formatMessage(messages.replyConfirm),
 | 
			
		||||
            onConfirm: () => dispatch(replyCompose(lastStatus, history)),
 | 
			
		||||
          },
 | 
			
		||||
        }));
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(replyCompose(lastStatus, history));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }, [dispatch, lastStatus, history, intl]);
 | 
			
		||||
 | 
			
		||||
  handleHotkeyMoveUp = () => {
 | 
			
		||||
    this.props.onMoveUp(this.props.conversationId);
 | 
			
		||||
  };
 | 
			
		||||
  const handleDelete = useCallback(() => {
 | 
			
		||||
    dispatch(deleteConversation(id));
 | 
			
		||||
  }, [dispatch, id]);
 | 
			
		||||
 | 
			
		||||
  handleHotkeyMoveDown = () => {
 | 
			
		||||
    this.props.onMoveDown(this.props.conversationId);
 | 
			
		||||
  };
 | 
			
		||||
  const handleHotkeyMoveUp = useCallback(() => {
 | 
			
		||||
    onMoveUp(id);
 | 
			
		||||
  }, [id, onMoveUp]);
 | 
			
		||||
 | 
			
		||||
  handleConversationMute = () => {
 | 
			
		||||
    this.props.onMute(this.props.lastStatus);
 | 
			
		||||
  };
 | 
			
		||||
  const handleHotkeyMoveDown = useCallback(() => {
 | 
			
		||||
    onMoveDown(id);
 | 
			
		||||
  }, [id, onMoveDown]);
 | 
			
		||||
 | 
			
		||||
  handleShowMore = () => {
 | 
			
		||||
    this.props.onToggleHidden(this.props.lastStatus);
 | 
			
		||||
 | 
			
		||||
    if (this.props.lastStatus.get('spoiler_text')) {
 | 
			
		||||
      this.setExpansion(!this.state.isExpanded);
 | 
			
		||||
  const handleConversationMute = useCallback(() => {
 | 
			
		||||
    if (lastStatus.get('muted')) {
 | 
			
		||||
      dispatch(unmuteStatus(lastStatus.get('id')));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(muteStatus(lastStatus.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  }, [dispatch, lastStatus]);
 | 
			
		||||
 | 
			
		||||
  setExpansion = value => {
 | 
			
		||||
    this.setState({ isExpanded: value });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (lastStatus === null) {
 | 
			
		||||
      return null;
 | 
			
		||||
  const handleShowMore = useCallback(() => {
 | 
			
		||||
    if (lastStatus.get('hidden')) {
 | 
			
		||||
      dispatch(revealStatus(lastStatus.get('id')));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(hideStatus(lastStatus.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded;
 | 
			
		||||
 | 
			
		||||
    const menu = [
 | 
			
		||||
      { text: intl.formatMessage(messages.open), action: this.handleClick },
 | 
			
		||||
      null,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
 | 
			
		||||
 | 
			
		||||
    if (unread) {
 | 
			
		||||
      menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
 | 
			
		||||
      menu.push(null);
 | 
			
		||||
    if (lastStatus.get('spoiler_text')) {
 | 
			
		||||
      setExpanded(!expanded);
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch, lastStatus, expanded]);
 | 
			
		||||
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
 | 
			
		||||
  const menu = [
 | 
			
		||||
    { text: intl.formatMessage(messages.open), action: handleClick },
 | 
			
		||||
    null,
 | 
			
		||||
    { text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
    const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
 | 
			
		||||
  if (unread) {
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead });
 | 
			
		||||
    menu.push(null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    const handlers = {
 | 
			
		||||
      reply: this.handleReply,
 | 
			
		||||
      open: this.handleClick,
 | 
			
		||||
      moveUp: this.handleHotkeyMoveUp,
 | 
			
		||||
      moveDown: this.handleHotkeyMoveDown,
 | 
			
		||||
      toggleHidden: this.handleShowMore,
 | 
			
		||||
    };
 | 
			
		||||
  menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 | 
			
		||||
 | 
			
		||||
    let media = null;
 | 
			
		||||
    if (lastStatus.get('media_attachments').size > 0) {
 | 
			
		||||
      media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
 | 
			
		||||
    }
 | 
			
		||||
  const names = accounts.map(a => (
 | 
			
		||||
    <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}>
 | 
			
		||||
      <bdi>
 | 
			
		||||
        <strong
 | 
			
		||||
          className='display-name__html'
 | 
			
		||||
          dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
 | 
			
		||||
        />
 | 
			
		||||
      </bdi>
 | 
			
		||||
    </Permalink>
 | 
			
		||||
  )).reduce((prev, cur) => [prev, ', ', cur]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <HotKeys handlers={handlers}>
 | 
			
		||||
        <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
 | 
			
		||||
          <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
 | 
			
		||||
            <AvatarComposite accounts={accounts} size={48} />
 | 
			
		||||
          </div>
 | 
			
		||||
  const handlers = {
 | 
			
		||||
    reply: handleReply,
 | 
			
		||||
    open: handleClick,
 | 
			
		||||
    moveUp: handleHotkeyMoveUp,
 | 
			
		||||
    moveDown: handleHotkeyMoveDown,
 | 
			
		||||
    toggleHidden: handleShowMore,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
          <div className='conversation__content'>
 | 
			
		||||
            <div className='conversation__content__info'>
 | 
			
		||||
              <div className='conversation__content__relative-time'>
 | 
			
		||||
                {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
 | 
			
		||||
              </div>
 | 
			
		||||
  let media = null;
 | 
			
		||||
  if (lastStatus.get('media_attachments').size > 0) {
 | 
			
		||||
    media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
              <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
 | 
			
		||||
                <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
 | 
			
		||||
              </div>
 | 
			
		||||
  return (
 | 
			
		||||
    <HotKeys handlers={handlers}>
 | 
			
		||||
      <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
 | 
			
		||||
        <div className='conversation__avatar' onClick={handleClick} role='presentation'>
 | 
			
		||||
          <AvatarComposite accounts={accounts} size={48} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='conversation__content'>
 | 
			
		||||
          <div className='conversation__content__info'>
 | 
			
		||||
            <div className='conversation__content__relative-time'>
 | 
			
		||||
              {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <StatusContent
 | 
			
		||||
              status={lastStatus}
 | 
			
		||||
              parseClick={this.parseClick}
 | 
			
		||||
              expanded={isExpanded}
 | 
			
		||||
              onExpandedToggle={this.handleShowMore}
 | 
			
		||||
              collapsible
 | 
			
		||||
              media={media}
 | 
			
		||||
            />
 | 
			
		||||
            <div className='conversation__content__names' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
 | 
			
		||||
              <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
            <div className='status__action-bar'>
 | 
			
		||||
              <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={this.handleReply} />
 | 
			
		||||
          <StatusContent
 | 
			
		||||
            status={lastStatus}
 | 
			
		||||
            parseClick={parseClick}
 | 
			
		||||
            expanded={sharedCWState ? lastStatus.get('hidden') : expanded}
 | 
			
		||||
            onExpandedToggle={handleShowMore}
 | 
			
		||||
            collapsible
 | 
			
		||||
            media={media}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
              <div className='status__action-bar-dropdown'>
 | 
			
		||||
                <DropdownMenuContainer
 | 
			
		||||
                  scrollKey={scrollKey}
 | 
			
		||||
                  status={lastStatus}
 | 
			
		||||
                  items={menu}
 | 
			
		||||
                  icon='ellipsis-h'
 | 
			
		||||
                  iconComponent={MoreHorizIcon}
 | 
			
		||||
                  size={18}
 | 
			
		||||
                  direction='right'
 | 
			
		||||
                  title={intl.formatMessage(messages.more)}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
          <div className='status__action-bar'>
 | 
			
		||||
            <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
 | 
			
		||||
 | 
			
		||||
            <div className='status__action-bar-dropdown'>
 | 
			
		||||
              <DropdownMenuContainer
 | 
			
		||||
                scrollKey={scrollKey}
 | 
			
		||||
                status={lastStatus}
 | 
			
		||||
                items={menu}
 | 
			
		||||
                icon='ellipsis-h'
 | 
			
		||||
                iconComponent={MoreHorizIcon}
 | 
			
		||||
                size={18}
 | 
			
		||||
                direction='right'
 | 
			
		||||
                title={intl.formatMessage(messages.more)}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </HotKeys>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
      </div>
 | 
			
		||||
    </HotKeys>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withRouter(injectIntl(Conversation));
 | 
			
		||||
Conversation.propTypes = {
 | 
			
		||||
  conversation: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
  scrollKey: PropTypes.string,
 | 
			
		||||
  onMoveUp: PropTypes.func,
 | 
			
		||||
  onMoveDown: PropTypes.func,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,77 +1,72 @@
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useRef, useMemo, useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { useSelector, useDispatch } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { debounce } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import ScrollableList from '../../../components/scrollable_list';
 | 
			
		||||
import ConversationContainer from '../containers/conversation_container';
 | 
			
		||||
import { expandConversations } from 'flavours/glitch/actions/conversations';
 | 
			
		||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
 | 
			
		||||
 | 
			
		||||
export default class ConversationsList extends ImmutablePureComponent {
 | 
			
		||||
import { Conversation } from './conversation';
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    conversations: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
    scrollKey: PropTypes.string.isRequired,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    isLoading: PropTypes.bool,
 | 
			
		||||
    onLoadMore: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
const focusChild = (node, index, alignTop) => {
 | 
			
		||||
  const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
 | 
			
		||||
 | 
			
		||||
  getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id);
 | 
			
		||||
 | 
			
		||||
  handleMoveUp = id => {
 | 
			
		||||
    const elementIndex = this.getCurrentIndex(id) - 1;
 | 
			
		||||
    this._selectChild(elementIndex, true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMoveDown = id => {
 | 
			
		||||
    const elementIndex = this.getCurrentIndex(id) + 1;
 | 
			
		||||
    this._selectChild(elementIndex, false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _selectChild (index, align_top) {
 | 
			
		||||
    const container = this.node.node;
 | 
			
		||||
    const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
 | 
			
		||||
 | 
			
		||||
    if (element) {
 | 
			
		||||
      if (align_top && container.scrollTop > element.offsetTop) {
 | 
			
		||||
        element.scrollIntoView(true);
 | 
			
		||||
      } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
 | 
			
		||||
        element.scrollIntoView(false);
 | 
			
		||||
      }
 | 
			
		||||
      element.focus();
 | 
			
		||||
  if (element) {
 | 
			
		||||
    if (alignTop && node.scrollTop > element.offsetTop) {
 | 
			
		||||
      element.scrollIntoView(true);
 | 
			
		||||
    } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) {
 | 
			
		||||
      element.scrollIntoView(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    element.focus();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
  };
 | 
			
		||||
export const ConversationsList = ({ scrollKey, ...other }) => {
 | 
			
		||||
  const listRef = useRef();
 | 
			
		||||
  const conversations = useSelector(state => state.getIn(['conversations', 'items']));
 | 
			
		||||
  const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true));
 | 
			
		||||
  const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false));
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const lastStatusId = conversations.last()?.get('last_status');
 | 
			
		||||
 | 
			
		||||
  handleLoadOlder = debounce(() => {
 | 
			
		||||
    const last = this.props.conversations.last();
 | 
			
		||||
  const handleMoveUp = useCallback(id => {
 | 
			
		||||
    const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1;
 | 
			
		||||
    focusChild(listRef.current.node, elementIndex, true);
 | 
			
		||||
  }, [listRef, conversations]);
 | 
			
		||||
 | 
			
		||||
    if (last && last.get('last_status')) {
 | 
			
		||||
      this.props.onLoadMore(last.get('last_status'));
 | 
			
		||||
  const handleMoveDown = useCallback(id => {
 | 
			
		||||
    const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1;
 | 
			
		||||
    focusChild(listRef.current.node, elementIndex, false);
 | 
			
		||||
  }, [listRef, conversations]);
 | 
			
		||||
 | 
			
		||||
  const debouncedLoadMore = useMemo(() => debounce(id => {
 | 
			
		||||
    dispatch(expandConversations({ maxId: id }));
 | 
			
		||||
  }, 300, { leading: true }), [dispatch]);
 | 
			
		||||
 | 
			
		||||
  const handleLoadMore = useCallback(() => {
 | 
			
		||||
    if (lastStatusId) {
 | 
			
		||||
      debouncedLoadMore(lastStatusId);
 | 
			
		||||
    }
 | 
			
		||||
  }, 300, { leading: true });
 | 
			
		||||
  }, [debouncedLoadMore, lastStatusId]);
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { conversations, isLoading, onLoadMore, ...other } = this.props;
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollableList {...other} scrollKey={scrollKey} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} hasMore={hasMore} onLoadMore={handleLoadMore} ref={listRef}>
 | 
			
		||||
      {conversations.map(item => (
 | 
			
		||||
        <Conversation
 | 
			
		||||
          key={item.get('id')}
 | 
			
		||||
          conversation={item}
 | 
			
		||||
          onMoveUp={handleMoveUp}
 | 
			
		||||
          onMoveDown={handleMoveDown}
 | 
			
		||||
          scrollKey={scrollKey}
 | 
			
		||||
        />
 | 
			
		||||
      ))}
 | 
			
		||||
    </ScrollableList>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
 | 
			
		||||
        {conversations.map(item => (
 | 
			
		||||
          <ConversationContainer
 | 
			
		||||
            key={item.get('id')}
 | 
			
		||||
            conversationId={item.get('id')}
 | 
			
		||||
            onMoveUp={this.handleMoveUp}
 | 
			
		||||
            onMoveDown={this.handleMoveDown}
 | 
			
		||||
            scrollKey={this.props.scrollKey}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ScrollableList>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
ConversationsList.propTypes = {
 | 
			
		||||
  scrollKey: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,81 +0,0 @@
 | 
			
		|||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { replyCompose } from 'flavours/glitch/actions/compose';
 | 
			
		||||
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
 | 
			
		||||
import { openModal } from 'flavours/glitch/actions/modal';
 | 
			
		||||
import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses';
 | 
			
		||||
import { makeGetStatus } from 'flavours/glitch/selectors';
 | 
			
		||||
 | 
			
		||||
import Conversation from '../components/conversation';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
 | 
			
		||||
  replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = () => {
 | 
			
		||||
  const getStatus = makeGetStatus();
 | 
			
		||||
 | 
			
		||||
  return (state, { conversationId }) => {
 | 
			
		||||
    const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
 | 
			
		||||
    const lastStatusId = conversation.get('last_status', null);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
 | 
			
		||||
      unread: conversation.get('unread'),
 | 
			
		||||
      lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
 | 
			
		||||
      settings: state.get('local_settings'),
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
 | 
			
		||||
 | 
			
		||||
  markRead () {
 | 
			
		||||
    dispatch(markConversationRead(conversationId));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  reply (status, router) {
 | 
			
		||||
    dispatch((_, getState) => {
 | 
			
		||||
      let state = getState();
 | 
			
		||||
 | 
			
		||||
      if (state.getIn(['compose', 'text']).trim().length !== 0) {
 | 
			
		||||
        dispatch(openModal({
 | 
			
		||||
          modalType: 'CONFIRM',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            message: intl.formatMessage(messages.replyMessage),
 | 
			
		||||
            confirm: intl.formatMessage(messages.replyConfirm),
 | 
			
		||||
            onConfirm: () => dispatch(replyCompose(status, router)),
 | 
			
		||||
          },
 | 
			
		||||
        }));
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(replyCompose(status, router));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  delete () {
 | 
			
		||||
    dispatch(deleteConversation(conversationId));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onMute (status) {
 | 
			
		||||
    if (status.get('muted')) {
 | 
			
		||||
      dispatch(unmuteStatus(status.get('id')));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(muteStatus(status.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onToggleHidden (status) {
 | 
			
		||||
    if (status.get('hidden')) {
 | 
			
		||||
      dispatch(revealStatus(status.get('id')));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(hideStatus(status.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { expandConversations } from '../../../actions/conversations';
 | 
			
		||||
import ConversationsList from '../components/conversations_list';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  conversations: state.getIn(['conversations', 'items']),
 | 
			
		||||
  isLoading: state.getIn(['conversations', 'isLoading'], true),
 | 
			
		||||
  hasMore: state.getIn(['conversations', 'hasMore'], false),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
  onLoadMore: maxId => dispatch(expandConversations({ maxId })),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,11 @@
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
import { useRef, useCallback, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { Helmet } from 'react-helmet';
 | 
			
		||||
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
 | 
			
		||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 | 
			
		||||
| 
						 | 
				
			
			@ -17,51 +16,44 @@ import Column from 'flavours/glitch/components/column';
 | 
			
		|||
import ColumnHeader from 'flavours/glitch/components/column_header';
 | 
			
		||||
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 | 
			
		||||
 | 
			
		||||
import { ConversationsList } from './components/conversations_list';
 | 
			
		||||
import ColumnSettingsContainer from './containers/column_settings_container';
 | 
			
		||||
import ConversationsListContainer from './containers/conversations_list_container';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  title: { id: 'column.direct', defaultMessage: 'Private mentions' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
 | 
			
		||||
  conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
 | 
			
		||||
});
 | 
			
		||||
const DirectTimeline = ({ columnId, multiColumn }) => {
 | 
			
		||||
  const columnRef = useRef();
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const pinned = !!columnId;
 | 
			
		||||
 | 
			
		||||
class DirectTimeline extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
    columnId: PropTypes.string,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    hasUnread: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
    conversationsMode: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handlePin = () => {
 | 
			
		||||
    const { columnId, dispatch } = this.props;
 | 
			
		||||
  // glitch-soc additions
 | 
			
		||||
  const hasUnread = useSelector(state => state.getIn(['timelines', 'direct', 'unread']) > 0);
 | 
			
		||||
  const conversationsMode = useSelector(state => state.getIn(['settings', 'direct', 'conversations']));
 | 
			
		||||
 | 
			
		||||
  const handlePin = useCallback(() => {
 | 
			
		||||
    if (columnId) {
 | 
			
		||||
      dispatch(removeColumn(columnId));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(addColumn('DIRECT', {}));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  }, [dispatch, columnId]);
 | 
			
		||||
 | 
			
		||||
  handleMove = (dir) => {
 | 
			
		||||
    const { columnId, dispatch } = this.props;
 | 
			
		||||
  const handleMove = useCallback((dir) => {
 | 
			
		||||
    dispatch(moveColumn(columnId, dir));
 | 
			
		||||
  };
 | 
			
		||||
  }, [dispatch, columnId]);
 | 
			
		||||
 | 
			
		||||
  handleHeaderClick = () => {
 | 
			
		||||
    this.column.scrollTop();
 | 
			
		||||
  };
 | 
			
		||||
  const handleHeaderClick = useCallback(() => {
 | 
			
		||||
    columnRef.current.scrollTop();
 | 
			
		||||
  }, [columnRef]);
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { dispatch, conversationsMode } = this.props;
 | 
			
		||||
  const handleLoadMoreTimeline = useCallback(maxId => {
 | 
			
		||||
    dispatch(expandDirectTimeline({ maxId }));
 | 
			
		||||
  }, [dispatch]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    dispatch(mountConversations());
 | 
			
		||||
 | 
			
		||||
    if (conversationsMode) {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,99 +62,67 @@ class DirectTimeline extends PureComponent {
 | 
			
		|||
      dispatch(expandDirectTimeline());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.disconnect = dispatch(connectDirectStream());
 | 
			
		||||
  }
 | 
			
		||||
    const disconnect = dispatch(connectDirectStream());
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    const { dispatch, conversationsMode } = this.props;
 | 
			
		||||
    return () => {
 | 
			
		||||
      dispatch(unmountConversations());
 | 
			
		||||
      disconnect();
 | 
			
		||||
    };
 | 
			
		||||
  }, [dispatch, conversationsMode]);
 | 
			
		||||
 | 
			
		||||
    if (prevProps.conversationsMode && !conversationsMode) {
 | 
			
		||||
      dispatch(expandDirectTimeline());
 | 
			
		||||
    } else if (!prevProps.conversationsMode && conversationsMode) {
 | 
			
		||||
      dispatch(expandConversations());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
 | 
			
		||||
      <ColumnHeader
 | 
			
		||||
        icon='envelope'
 | 
			
		||||
        iconComponent={MailIcon}
 | 
			
		||||
        active={hasUnread}
 | 
			
		||||
        title={intl.formatMessage(messages.title)}
 | 
			
		||||
        onPin={handlePin}
 | 
			
		||||
        onMove={handleMove}
 | 
			
		||||
        onClick={handleHeaderClick}
 | 
			
		||||
        pinned={pinned}
 | 
			
		||||
        multiColumn={multiColumn}
 | 
			
		||||
      >
 | 
			
		||||
        <ColumnSettingsContainer />
 | 
			
		||||
      </ColumnHeader>
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    this.props.dispatch(unmountConversations());
 | 
			
		||||
 | 
			
		||||
    if (this.disconnect) {
 | 
			
		||||
      this.disconnect();
 | 
			
		||||
      this.disconnect = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    this.column = c;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLoadMoreTimeline = maxId => {
 | 
			
		||||
    this.props.dispatch(expandDirectTimeline({ maxId }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLoadMoreConversations = maxId => {
 | 
			
		||||
    this.props.dispatch(expandConversations({ maxId }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
 | 
			
		||||
    const pinned = !!columnId;
 | 
			
		||||
 | 
			
		||||
    let contents;
 | 
			
		||||
    if (conversationsMode) {
 | 
			
		||||
      contents = (
 | 
			
		||||
        <ConversationsListContainer
 | 
			
		||||
      {conversationsMode ? (
 | 
			
		||||
        <ConversationsList
 | 
			
		||||
          trackScroll={!pinned}
 | 
			
		||||
          scrollKey={`direct_timeline-${columnId}`}
 | 
			
		||||
          timelineId='direct'
 | 
			
		||||
          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
 | 
			
		||||
          bindToDocument={!multiColumn}
 | 
			
		||||
          onLoadMore={this.handleLoadMore}
 | 
			
		||||
          prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
 | 
			
		||||
          alwaysPrepend
 | 
			
		||||
          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      contents = (
 | 
			
		||||
      ) : (
 | 
			
		||||
        <StatusListContainer
 | 
			
		||||
          trackScroll={!pinned}
 | 
			
		||||
          scrollKey={`direct_timeline-${columnId}`}
 | 
			
		||||
          timelineId='direct'
 | 
			
		||||
          bindToDocument={!multiColumn}
 | 
			
		||||
          onLoadMore={this.handleLoadMoreTimeline}
 | 
			
		||||
          prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
 | 
			
		||||
          onLoadMore={handleLoadMoreTimeline}
 | 
			
		||||
          prepend={
 | 
			
		||||
            <div className='follow_requests-unlocked_explanation'>
 | 
			
		||||
              <span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          alwaysPrepend
 | 
			
		||||
          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
			
		||||
        <ColumnHeader
 | 
			
		||||
          icon='envelope'
 | 
			
		||||
          iconComponent={MailIcon}
 | 
			
		||||
          active={hasUnread}
 | 
			
		||||
          title={intl.formatMessage(messages.title)}
 | 
			
		||||
          onPin={this.handlePin}
 | 
			
		||||
          onMove={this.handleMove}
 | 
			
		||||
          onClick={this.handleHeaderClick}
 | 
			
		||||
          pinned={pinned}
 | 
			
		||||
          multiColumn={multiColumn}
 | 
			
		||||
        >
 | 
			
		||||
          <ColumnSettingsContainer />
 | 
			
		||||
        </ColumnHeader>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>{intl.formatMessage(messages.title)}</title>
 | 
			
		||||
        <meta name='robots' content='noindex' />
 | 
			
		||||
      </Helmet>
 | 
			
		||||
    </Column>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
        {contents}
 | 
			
		||||
DirectTimeline.propTypes = {
 | 
			
		||||
  columnId: PropTypes.string,
 | 
			
		||||
  multiColumn: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>{intl.formatMessage(messages.title)}</title>
 | 
			
		||||
          <meta name='robots' content='noindex' />
 | 
			
		||||
        </Helmet>
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(injectIntl(DirectTimeline));
 | 
			
		||||
export default DirectTimeline;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue