about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/direct_timeline
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-06-09 12:07:23 +0200
committerThibG <thib@sitedethib.com>2019-06-10 16:23:42 +0200
commitd61a6271c68ecca1745f2683d25ec58573dd2819 (patch)
treeadbff73f64f99a3460650e33c049c834274d2032 /app/javascript/flavours/glitch/features/direct_timeline
parente16c8fbc7a2b5a866960a87bc8c950ad0d38f61b (diff)
Add DM conversations mode similar to upstream
Diffstat (limited to 'app/javascript/flavours/glitch/features/direct_timeline')
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js64
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js73
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js19
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js15
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/index.js96
5 files changed, 256 insertions, 11 deletions
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
new file mode 100644
index 000000000..9ddeabe75
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import StatusContainer from 'flavours/glitch/containers/status_container';
+
+export default class Conversation extends ImmutablePureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static propTypes = {
+    conversationId: PropTypes.string.isRequired,
+    accounts: ImmutablePropTypes.list.isRequired,
+    lastStatusId: PropTypes.string,
+    unread:PropTypes.bool.isRequired,
+    onMoveUp: PropTypes.func,
+    onMoveDown: PropTypes.func,
+    markRead: PropTypes.func.isRequired,
+  };
+
+  handleClick = () => {
+    if (!this.context.router) {
+      return;
+    }
+
+    const { lastStatusId, unread, markRead } = this.props;
+
+    if (unread) {
+      markRead();
+    }
+
+    this.context.router.history.push(`/statuses/${lastStatusId}`);
+  }
+
+  handleHotkeyMoveUp = () => {
+    this.props.onMoveUp(this.props.conversationId);
+  }
+
+  handleHotkeyMoveDown = () => {
+    this.props.onMoveDown(this.props.conversationId);
+  }
+
+  render () {
+    const { accounts, lastStatusId, unread } = this.props;
+
+    if (lastStatusId === null) {
+      return null;
+    }
+
+    return (
+      <StatusContainer
+        id={lastStatusId}
+        unread={unread}
+        otherAccounts={accounts}
+        onMoveUp={this.handleHotkeyMoveUp}
+        onMoveDown={this.handleHotkeyMoveDown}
+        onClick={this.handleClick}
+      />
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js
new file mode 100644
index 000000000..4fa76fd6d
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ConversationContainer from '../containers/conversation_container';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
+import { debounce } from 'lodash';
+
+export default class ConversationsList extends ImmutablePureComponent {
+
+  static propTypes = {
+    conversations: ImmutablePropTypes.list.isRequired,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
+    onLoadMore: PropTypes.func,
+  };
+
+  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();
+    }
+  }
+
+  setRef = c => {
+    this.node = c;
+  }
+
+  handleLoadOlder = debounce(() => {
+    const last = this.props.conversations.last();
+
+    if (last && last.get('last_status')) {
+      this.props.onLoadMore(last.get('last_status'));
+    }
+  }, 300, { leading: true })
+
+  render () {
+    const { conversations, onLoadMore, ...other } = this.props;
+
+    return (
+      <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
+        {conversations.map(item => (
+          <ConversationContainer
+            key={item.get('id')}
+            conversationId={item.get('id')}
+            onMoveUp={this.handleMoveUp}
+            onMoveDown={this.handleMoveDown}
+          />
+        ))}
+      </ScrollableList>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
new file mode 100644
index 000000000..bd6f6bfb0
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+import Conversation from '../components/conversation';
+import { markConversationRead } from '../../../actions/conversations';
+
+const mapStateToProps = (state, { conversationId }) => {
+  const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+
+  return {
+    accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
+    unread: conversation.get('unread'),
+    lastStatusId: conversation.get('last_status', null),
+  };
+};
+
+const mapDispatchToProps = (dispatch, { conversationId }) => ({
+  markRead: () => dispatch(markConversationRead(conversationId)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js
new file mode 100644
index 000000000..e10558f3a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import ConversationsList from '../components/conversations_list';
+import { expandConversations } from 'flavours/glitch/actions/conversations';
+
+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);
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index dc7e0534d..6fe8a1ce8 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js
@@ -5,10 +5,13 @@ import StatusListContainer from 'flavours/glitch/features/ui/containers/status_l
 import Column from 'flavours/glitch/components/column';
 import ColumnHeader from 'flavours/glitch/components/column_header';
 import { expandDirectTimeline } from 'flavours/glitch/actions/timelines';
+import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations';
 import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { connectDirectStream } from 'flavours/glitch/actions/streaming';
+import { changeSetting } from 'flavours/glitch/actions/settings';
+import ConversationsListContainer from './containers/conversations_list_container';
 
 const messages = defineMessages({
   title: { id: 'column.direct', defaultMessage: 'Direct messages' },
@@ -16,6 +19,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
+  conversationsMode: state.getIn(['settings', 'direct', 'conversations']),
 });
 
 @connect(mapStateToProps)
@@ -28,6 +32,7 @@ export default class DirectTimeline extends React.PureComponent {
     intl: PropTypes.object.isRequired,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
+    conversationsMode: PropTypes.bool,
   };
 
   handlePin = () => {
@@ -50,13 +55,32 @@ export default class DirectTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch } = this.props;
+    const { dispatch, conversationsMode } = this.props;
+
+    dispatch(mountConversations());
+
+    if (conversationsMode) {
+      dispatch(expandConversations());
+    } else {
+      dispatch(expandDirectTimeline());
+    }
 
-    dispatch(expandDirectTimeline());
     this.disconnect = dispatch(connectDirectStream());
   }
 
+  componentDidUpdate(prevProps) {
+    const { dispatch, conversationsMode } = this.props;
+
+    if (prevProps.conversationsMode && !conversationsMode) {
+      dispatch(expandDirectTimeline());
+    } else if (!prevProps.conversationsMode && conversationsMode) {
+      dispatch(expandConversations());
+    }
+  }
+
   componentWillUnmount () {
+    this.props.dispatch(unmountConversations());
+
     if (this.disconnect) {
       this.disconnect();
       this.disconnect = null;
@@ -67,14 +91,49 @@ export default class DirectTimeline extends React.PureComponent {
     this.column = c;
   }
 
-  handleLoadMore = maxId => {
+  handleLoadMoreTimeline = maxId => {
     this.props.dispatch(expandDirectTimeline({ maxId }));
   }
 
+  handleLoadMoreConversations = maxId => {
+    this.props.dispatch(expandConversations({ maxId }));
+  }
+
+  handleTimelineClick = () => {
+    this.props.dispatch(changeSetting(['direct', 'conversations'], false));
+  }
+
+  handleConversationsClick = () => {
+    this.props.dispatch(changeSetting(['direct', 'conversations'], true));
+  }
+
   render () {
-    const { intl, hasUnread, columnId, multiColumn } = this.props;
+    const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
     const pinned = !!columnId;
 
+    let contents;
+    if (conversationsMode) {
+      contents = (
+        <ConversationsListContainer
+          trackScroll={!pinned}
+          scrollKey={`direct_timeline-${columnId}`}
+          timelineId='direct'
+          onLoadMore={this.handleLoadMore}
+          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
+        />
+      );
+    } else {
+      contents = (
+        <StatusListContainer
+          trackScroll={!pinned}
+          scrollKey={`direct_timeline-${columnId}`}
+          timelineId='direct'
+          onLoadMore={this.handleLoadMoreTimeline}
+          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
+        />
+      );
+    }
+
     return (
       <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
         <ColumnHeader
@@ -90,13 +149,28 @@ export default class DirectTimeline extends React.PureComponent {
           <ColumnSettingsContainer />
         </ColumnHeader>
 
-        <StatusListContainer
-          trackScroll={!pinned}
-          scrollKey={`direct_timeline-${columnId}`}
-          timelineId='direct'
-          onLoadMore={this.handleLoadMore}
-          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
-        />
+        <div className='notification__filter-bar'>
+          <button
+            className={conversationsMode ? 'active' : ''}
+            onClick={this.handleConversationsClick}
+          >
+            <FormattedMessage
+              id='direct.conversations_mode'
+              defaultMessage='Conversations'
+            />
+          </button>
+          <button
+            className={conversationsMode ? '' : 'active'}
+            onClick={this.handleTimelineClick}
+          >
+            <FormattedMessage
+              id='direct.timeline_mode'
+              defaultMessage='Timeline'
+            />
+          </button>
+        </div>
+
+        {contents}
       </Column>
     );
   }