[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 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 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 ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import { useDispatch, useSelector } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { HotKeys } from 'react-hotkeys';
 | 
					import { HotKeys } from 'react-hotkeys';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
 | 
					import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
 | 
				
			||||||
import ReplyIcon from '@/material-icons/400-24px/reply.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 AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
				
			||||||
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
 | 
					import AvatarComposite from 'flavours/glitch/components/avatar_composite';
 | 
				
			||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
 | 
					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 StatusContent from 'flavours/glitch/components/status_content';
 | 
				
			||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 | 
					import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 | 
				
			||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
 | 
					import { autoPlayGif } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 | 
					import { makeGetStatus } from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  more: { id: 'status.more', defaultMessage: 'More' },
 | 
					  more: { id: 'status.more', defaultMessage: 'More' },
 | 
				
			||||||
| 
						 | 
					@ -30,45 +37,48 @@ const messages = defineMessages({
 | 
				
			||||||
  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
 | 
					  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
 | 
				
			||||||
  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
 | 
					  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
 | 
				
			||||||
  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute 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 = {
 | 
					const getStatus = makeGetStatus();
 | 
				
			||||||
    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,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => {
 | 
				
			||||||
    isExpanded: undefined,
 | 
					  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) => {
 | 
					  // glitch-soc additions
 | 
				
			||||||
    const { history, lastStatus, unread, markRead } = this.props;
 | 
					  const sharedCWState = useSelector(state => state.getIn(['state', 'content_warnings', 'shared_state']));
 | 
				
			||||||
    if (!history) return;
 | 
					  const [expanded, setExpanded] = useState(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const parseClick = useCallback((e, destination) => {
 | 
				
			||||||
    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
 | 
					    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
 | 
				
			||||||
      if (destination === undefined) {
 | 
					      if (destination === undefined) {
 | 
				
			||||||
        if (unread) {
 | 
					        if (unread) {
 | 
				
			||||||
          markRead();
 | 
					          dispatch(markConversationRead(id));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        destination = `/statuses/${lastStatus.get('id')}`;
 | 
					        destination = `/statuses/${lastStatus.get('id')}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      history.push(destination);
 | 
					      history.push(destination);
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }, [dispatch, history, unread, id, lastStatus]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMouseEnter = ({ currentTarget }) => {
 | 
					  const handleMouseEnter = useCallback(({ currentTarget }) => {
 | 
				
			||||||
    if (autoPlayGif) {
 | 
					    if (autoPlayGif) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -79,9 +89,9 @@ class Conversation extends ImmutablePureComponent {
 | 
				
			||||||
      let emoji = emojis[i];
 | 
					      let emoji = emojis[i];
 | 
				
			||||||
      emoji.src = emoji.getAttribute('data-original');
 | 
					      emoji.src = emoji.getAttribute('data-original');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMouseLeave = ({ currentTarget }) => {
 | 
					  const handleMouseLeave = useCallback(({ currentTarget }) => {
 | 
				
			||||||
    if (autoPlayGif) {
 | 
					    if (autoPlayGif) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -92,145 +102,160 @@ class Conversation extends ImmutablePureComponent {
 | 
				
			||||||
      let emoji = emojis[i];
 | 
					      let emoji = emojis[i];
 | 
				
			||||||
      emoji.src = emoji.getAttribute('data-static');
 | 
					      emoji.src = emoji.getAttribute('data-static');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }, []);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleClick = () => {
 | 
					 | 
				
			||||||
    if (!this.props.history) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { lastStatus, unread, markRead } = this.props;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClick = useCallback(() => {
 | 
				
			||||||
    if (unread) {
 | 
					    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 = () => {
 | 
					  const handleMarkAsRead = useCallback(() => {
 | 
				
			||||||
    this.props.markRead();
 | 
					    dispatch(markConversationRead(id));
 | 
				
			||||||
  };
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleReply = () => {
 | 
					  const handleReply = useCallback(() => {
 | 
				
			||||||
    this.props.reply(this.props.lastStatus, this.props.history);
 | 
					    dispatch((_, getState) => {
 | 
				
			||||||
  };
 | 
					      let state = getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleDelete = () => {
 | 
					      if (state.getIn(['compose', 'text']).trim().length !== 0) {
 | 
				
			||||||
    this.props.delete();
 | 
					        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 = () => {
 | 
					  const handleDelete = useCallback(() => {
 | 
				
			||||||
    this.props.onMoveUp(this.props.conversationId);
 | 
					    dispatch(deleteConversation(id));
 | 
				
			||||||
  };
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleHotkeyMoveDown = () => {
 | 
					  const handleHotkeyMoveUp = useCallback(() => {
 | 
				
			||||||
    this.props.onMoveDown(this.props.conversationId);
 | 
					    onMoveUp(id);
 | 
				
			||||||
  };
 | 
					  }, [id, onMoveUp]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleConversationMute = () => {
 | 
					  const handleHotkeyMoveDown = useCallback(() => {
 | 
				
			||||||
    this.props.onMute(this.props.lastStatus);
 | 
					    onMoveDown(id);
 | 
				
			||||||
  };
 | 
					  }, [id, onMoveDown]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleShowMore = () => {
 | 
					  const handleConversationMute = useCallback(() => {
 | 
				
			||||||
    this.props.onToggleHidden(this.props.lastStatus);
 | 
					    if (lastStatus.get('muted')) {
 | 
				
			||||||
 | 
					      dispatch(unmuteStatus(lastStatus.get('id')));
 | 
				
			||||||
    if (this.props.lastStatus.get('spoiler_text')) {
 | 
					    } else {
 | 
				
			||||||
      this.setExpansion(!this.state.isExpanded);
 | 
					      dispatch(muteStatus(lastStatus.get('id')));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }, [dispatch, lastStatus]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setExpansion = value => {
 | 
					  const handleShowMore = useCallback(() => {
 | 
				
			||||||
    this.setState({ isExpanded: value });
 | 
					    if (lastStatus.get('hidden')) {
 | 
				
			||||||
  };
 | 
					      dispatch(revealStatus(lastStatus.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
  render () {
 | 
					      dispatch(hideStatus(lastStatus.get('id')));
 | 
				
			||||||
    const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (lastStatus === null) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded;
 | 
					    if (lastStatus.get('spoiler_text')) {
 | 
				
			||||||
 | 
					      setExpanded(!expanded);
 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }, [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 = {
 | 
					  menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 | 
				
			||||||
      reply: this.handleReply,
 | 
					 | 
				
			||||||
      open: this.handleClick,
 | 
					 | 
				
			||||||
      moveUp: this.handleHotkeyMoveUp,
 | 
					 | 
				
			||||||
      moveDown: this.handleHotkeyMoveDown,
 | 
					 | 
				
			||||||
      toggleHidden: this.handleShowMore,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let media = null;
 | 
					  const names = accounts.map(a => (
 | 
				
			||||||
    if (lastStatus.get('media_attachments').size > 0) {
 | 
					    <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}>
 | 
				
			||||||
      media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
 | 
					      <bdi>
 | 
				
			||||||
    }
 | 
					        <strong
 | 
				
			||||||
 | 
					          className='display-name__html'
 | 
				
			||||||
 | 
					          dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </bdi>
 | 
				
			||||||
 | 
					    </Permalink>
 | 
				
			||||||
 | 
					  )).reduce((prev, cur) => [prev, ', ', cur]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					  const handlers = {
 | 
				
			||||||
      <HotKeys handlers={handlers}>
 | 
					    reply: handleReply,
 | 
				
			||||||
        <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
 | 
					    open: handleClick,
 | 
				
			||||||
          <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
 | 
					    moveUp: handleHotkeyMoveUp,
 | 
				
			||||||
            <AvatarComposite accounts={accounts} size={48} />
 | 
					    moveDown: handleHotkeyMoveDown,
 | 
				
			||||||
          </div>
 | 
					    toggleHidden: handleShowMore,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div className='conversation__content'>
 | 
					  let media = null;
 | 
				
			||||||
            <div className='conversation__content__info'>
 | 
					  if (lastStatus.get('media_attachments').size > 0) {
 | 
				
			||||||
              <div className='conversation__content__relative-time'>
 | 
					    media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
 | 
				
			||||||
                {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
 | 
					  }
 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
 | 
					  return (
 | 
				
			||||||
                <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
 | 
					    <HotKeys handlers={handlers}>
 | 
				
			||||||
              </div>
 | 
					      <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>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <StatusContent
 | 
					            <div className='conversation__content__names' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
 | 
				
			||||||
              status={lastStatus}
 | 
					              <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
 | 
				
			||||||
              parseClick={this.parseClick}
 | 
					            </div>
 | 
				
			||||||
              expanded={isExpanded}
 | 
					          </div>
 | 
				
			||||||
              onExpandedToggle={this.handleShowMore}
 | 
					 | 
				
			||||||
              collapsible
 | 
					 | 
				
			||||||
              media={media}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div className='status__action-bar'>
 | 
					          <StatusContent
 | 
				
			||||||
              <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={this.handleReply} />
 | 
					            status={lastStatus}
 | 
				
			||||||
 | 
					            parseClick={parseClick}
 | 
				
			||||||
 | 
					            expanded={sharedCWState ? lastStatus.get('hidden') : expanded}
 | 
				
			||||||
 | 
					            onExpandedToggle={handleShowMore}
 | 
				
			||||||
 | 
					            collapsible
 | 
				
			||||||
 | 
					            media={media}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div className='status__action-bar-dropdown'>
 | 
					          <div className='status__action-bar'>
 | 
				
			||||||
                <DropdownMenuContainer
 | 
					            <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
 | 
				
			||||||
                  scrollKey={scrollKey}
 | 
					
 | 
				
			||||||
                  status={lastStatus}
 | 
					            <div className='status__action-bar-dropdown'>
 | 
				
			||||||
                  items={menu}
 | 
					              <DropdownMenuContainer
 | 
				
			||||||
                  icon='ellipsis-h'
 | 
					                scrollKey={scrollKey}
 | 
				
			||||||
                  iconComponent={MoreHorizIcon}
 | 
					                status={lastStatus}
 | 
				
			||||||
                  size={18}
 | 
					                items={menu}
 | 
				
			||||||
                  direction='right'
 | 
					                icon='ellipsis-h'
 | 
				
			||||||
                  title={intl.formatMessage(messages.more)}
 | 
					                iconComponent={MoreHorizIcon}
 | 
				
			||||||
                />
 | 
					                size={18}
 | 
				
			||||||
              </div>
 | 
					                direction='right'
 | 
				
			||||||
 | 
					                title={intl.formatMessage(messages.more)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </HotKeys>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    </HotKeys>
 | 
				
			||||||
  }
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					Conversation.propTypes = {
 | 
				
			||||||
 | 
					  conversation: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
export default withRouter(injectIntl(Conversation));
 | 
					  scrollKey: PropTypes.string,
 | 
				
			||||||
 | 
					  onMoveUp: PropTypes.func,
 | 
				
			||||||
 | 
					  onMoveDown: PropTypes.func,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,77 +1,72 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { useRef, useMemo, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import { useSelector, useDispatch } from 'react-redux';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					import { debounce } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ScrollableList from '../../../components/scrollable_list';
 | 
					import { expandConversations } from 'flavours/glitch/actions/conversations';
 | 
				
			||||||
import ConversationContainer from '../containers/conversation_container';
 | 
					import ScrollableList from 'flavours/glitch/components/scrollable_list';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ConversationsList extends ImmutablePureComponent {
 | 
					import { Conversation } from './conversation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					const focusChild = (node, index, alignTop) => {
 | 
				
			||||||
    conversations: ImmutablePropTypes.list.isRequired,
 | 
					  const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
 | 
				
			||||||
    scrollKey: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    hasMore: PropTypes.bool,
 | 
					 | 
				
			||||||
    isLoading: PropTypes.bool,
 | 
					 | 
				
			||||||
    onLoadMore: PropTypes.func,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id);
 | 
					  if (element) {
 | 
				
			||||||
 | 
					    if (alignTop && node.scrollTop > element.offsetTop) {
 | 
				
			||||||
  handleMoveUp = id => {
 | 
					      element.scrollIntoView(true);
 | 
				
			||||||
    const elementIndex = this.getCurrentIndex(id) - 1;
 | 
					    } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) {
 | 
				
			||||||
    this._selectChild(elementIndex, true);
 | 
					      element.scrollIntoView(false);
 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    element.focus();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setRef = c => {
 | 
					export const ConversationsList = ({ scrollKey, ...other }) => {
 | 
				
			||||||
    this.node = c;
 | 
					  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 handleMoveUp = useCallback(id => {
 | 
				
			||||||
    const last = this.props.conversations.last();
 | 
					    const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1;
 | 
				
			||||||
 | 
					    focusChild(listRef.current.node, elementIndex, true);
 | 
				
			||||||
 | 
					  }, [listRef, conversations]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (last && last.get('last_status')) {
 | 
					  const handleMoveDown = useCallback(id => {
 | 
				
			||||||
      this.props.onLoadMore(last.get('last_status'));
 | 
					    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 () {
 | 
					  return (
 | 
				
			||||||
    const { conversations, isLoading, onLoadMore, ...other } = this.props;
 | 
					    <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 (
 | 
					ConversationsList.propTypes = {
 | 
				
			||||||
      <ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
 | 
					  scrollKey: PropTypes.string.isRequired,
 | 
				
			||||||
        {conversations.map(item => (
 | 
					};
 | 
				
			||||||
          <ConversationContainer
 | 
					 | 
				
			||||||
            key={item.get('id')}
 | 
					 | 
				
			||||||
            conversationId={item.get('id')}
 | 
					 | 
				
			||||||
            onMoveUp={this.handleMoveUp}
 | 
					 | 
				
			||||||
            onMoveDown={this.handleMoveDown}
 | 
					 | 
				
			||||||
            scrollKey={this.props.scrollKey}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        ))}
 | 
					 | 
				
			||||||
      </ScrollableList>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 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 { 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 MailIcon from '@/material-icons/400-24px/mail.svg?react';
 | 
				
			||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 | 
					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 ColumnHeader from 'flavours/glitch/components/column_header';
 | 
				
			||||||
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 | 
					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 ColumnSettingsContainer from './containers/column_settings_container';
 | 
				
			||||||
import ConversationsListContainer from './containers/conversations_list_container';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  title: { id: 'column.direct', defaultMessage: 'Private mentions' },
 | 
					  title: { id: 'column.direct', defaultMessage: 'Private mentions' },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const DirectTimeline = ({ columnId, multiColumn }) => {
 | 
				
			||||||
  hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
 | 
					  const columnRef = useRef();
 | 
				
			||||||
  conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
 | 
					  const intl = useIntl();
 | 
				
			||||||
});
 | 
					  const dispatch = useDispatch();
 | 
				
			||||||
 | 
					  const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DirectTimeline extends PureComponent {
 | 
					  // glitch-soc additions
 | 
				
			||||||
 | 
					  const hasUnread = useSelector(state => state.getIn(['timelines', 'direct', 'unread']) > 0);
 | 
				
			||||||
  static propTypes = {
 | 
					  const conversationsMode = useSelector(state => state.getIn(['settings', 'direct', 'conversations']));
 | 
				
			||||||
    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;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlePin = useCallback(() => {
 | 
				
			||||||
    if (columnId) {
 | 
					    if (columnId) {
 | 
				
			||||||
      dispatch(removeColumn(columnId));
 | 
					      dispatch(removeColumn(columnId));
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      dispatch(addColumn('DIRECT', {}));
 | 
					      dispatch(addColumn('DIRECT', {}));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }, [dispatch, columnId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMove = (dir) => {
 | 
					  const handleMove = useCallback((dir) => {
 | 
				
			||||||
    const { columnId, dispatch } = this.props;
 | 
					 | 
				
			||||||
    dispatch(moveColumn(columnId, dir));
 | 
					    dispatch(moveColumn(columnId, dir));
 | 
				
			||||||
  };
 | 
					  }, [dispatch, columnId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleHeaderClick = () => {
 | 
					  const handleHeaderClick = useCallback(() => {
 | 
				
			||||||
    this.column.scrollTop();
 | 
					    columnRef.current.scrollTop();
 | 
				
			||||||
  };
 | 
					  }, [columnRef]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidMount () {
 | 
					  const handleLoadMoreTimeline = useCallback(maxId => {
 | 
				
			||||||
    const { dispatch, conversationsMode } = this.props;
 | 
					    dispatch(expandDirectTimeline({ maxId }));
 | 
				
			||||||
 | 
					  }, [dispatch]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
    dispatch(mountConversations());
 | 
					    dispatch(mountConversations());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (conversationsMode) {
 | 
					    if (conversationsMode) {
 | 
				
			||||||
| 
						 | 
					@ -70,99 +62,67 @@ class DirectTimeline extends PureComponent {
 | 
				
			||||||
      dispatch(expandDirectTimeline());
 | 
					      dispatch(expandDirectTimeline());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.disconnect = dispatch(connectDirectStream());
 | 
					    const disconnect = dispatch(connectDirectStream());
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidUpdate(prevProps) {
 | 
					    return () => {
 | 
				
			||||||
    const { dispatch, conversationsMode } = this.props;
 | 
					      dispatch(unmountConversations());
 | 
				
			||||||
 | 
					      disconnect();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [dispatch, conversationsMode]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (prevProps.conversationsMode && !conversationsMode) {
 | 
					  return (
 | 
				
			||||||
      dispatch(expandDirectTimeline());
 | 
					    <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
    } else if (!prevProps.conversationsMode && conversationsMode) {
 | 
					      <ColumnHeader
 | 
				
			||||||
      dispatch(expandConversations());
 | 
					        icon='envelope'
 | 
				
			||||||
    }
 | 
					        iconComponent={MailIcon}
 | 
				
			||||||
  }
 | 
					        active={hasUnread}
 | 
				
			||||||
 | 
					        title={intl.formatMessage(messages.title)}
 | 
				
			||||||
 | 
					        onPin={handlePin}
 | 
				
			||||||
 | 
					        onMove={handleMove}
 | 
				
			||||||
 | 
					        onClick={handleHeaderClick}
 | 
				
			||||||
 | 
					        pinned={pinned}
 | 
				
			||||||
 | 
					        multiColumn={multiColumn}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ColumnSettingsContainer />
 | 
				
			||||||
 | 
					      </ColumnHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					      {conversationsMode ? (
 | 
				
			||||||
    this.props.dispatch(unmountConversations());
 | 
					        <ConversationsList
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
          trackScroll={!pinned}
 | 
					          trackScroll={!pinned}
 | 
				
			||||||
          scrollKey={`direct_timeline-${columnId}`}
 | 
					          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}
 | 
					          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>}
 | 
					          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
 | 
					          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
 | 
					        <StatusListContainer
 | 
				
			||||||
          trackScroll={!pinned}
 | 
					          trackScroll={!pinned}
 | 
				
			||||||
          scrollKey={`direct_timeline-${columnId}`}
 | 
					          scrollKey={`direct_timeline-${columnId}`}
 | 
				
			||||||
          timelineId='direct'
 | 
					          timelineId='direct'
 | 
				
			||||||
          bindToDocument={!multiColumn}
 | 
					          bindToDocument={!multiColumn}
 | 
				
			||||||
          onLoadMore={this.handleLoadMoreTimeline}
 | 
					          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>}
 | 
					          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
 | 
					          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." />}
 | 
					          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 (
 | 
					      <Helmet>
 | 
				
			||||||
      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
					        <title>{intl.formatMessage(messages.title)}</title>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <meta name='robots' content='noindex' />
 | 
				
			||||||
          icon='envelope'
 | 
					      </Helmet>
 | 
				
			||||||
          iconComponent={MailIcon}
 | 
					    </Column>
 | 
				
			||||||
          active={hasUnread}
 | 
					  );
 | 
				
			||||||
          title={intl.formatMessage(messages.title)}
 | 
					};
 | 
				
			||||||
          onPin={this.handlePin}
 | 
					 | 
				
			||||||
          onMove={this.handleMove}
 | 
					 | 
				
			||||||
          onClick={this.handleHeaderClick}
 | 
					 | 
				
			||||||
          pinned={pinned}
 | 
					 | 
				
			||||||
          multiColumn={multiColumn}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ColumnSettingsContainer />
 | 
					 | 
				
			||||||
        </ColumnHeader>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {contents}
 | 
					DirectTimeline.propTypes = {
 | 
				
			||||||
 | 
					  columnId: PropTypes.string,
 | 
				
			||||||
 | 
					  multiColumn: PropTypes.bool,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Helmet>
 | 
					export default DirectTimeline;
 | 
				
			||||||
          <title>{intl.formatMessage(messages.title)}</title>
 | 
					 | 
				
			||||||
          <meta name='robots' content='noindex' />
 | 
					 | 
				
			||||||
        </Helmet>
 | 
					 | 
				
			||||||
      </Column>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default connect(mapStateToProps)(injectIntl(DirectTimeline));
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue