-
- {unread && }
-
+
+
+
+ {unread && }
+
-
- {names} }} />
-
+
+ {names} }} />
+
-
-
-
-
- );
- }
-
-}
-
-export default withRouter(injectIntl(Conversation));
+
+
+ );
+};
+
+Conversation.propTypes = {
+ conversation: ImmutablePropTypes.map.isRequired,
+ scrollKey: PropTypes.string,
+ onMoveUp: PropTypes.func,
+ onMoveDown: PropTypes.func,
+};
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx
index 8c12ea9e5f68a2..b1a8fd09b6471b 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.jsx
@@ -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();
- }
- }
-
- 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'));
+ 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);
}
- }, 300, { leading: true });
- render () {
- const { conversations, isLoading, onLoadMore, ...other } = this.props;
-
- return (
-
- {conversations.map(item => (
-
- ))}
-
- );
+ element.focus();
}
-
-}
+};
+
+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');
+
+ const handleMoveUp = useCallback(id => {
+ const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1;
+ focusChild(listRef.current.node, elementIndex, true);
+ }, [listRef, conversations]);
+
+ 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);
+ }
+ }, [debouncedLoadMore, lastStatusId]);
+
+ return (
+
+ {conversations.map(item => (
+
+ ))}
+
+ );
+};
+
+ConversationsList.propTypes = {
+ scrollKey: PropTypes.string.isRequired,
+};
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
deleted file mode 100644
index 207d3ebb65a8ef..00000000000000
--- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js
+++ /dev/null
@@ -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));
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
deleted file mode 100644
index 1dcd3ec1bd4ad3..00000000000000
--- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversations_list_container.js
+++ /dev/null
@@ -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);
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx
index 9de5751ffbebd2..25f0dd9997f590 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.jsx
@@ -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']),
-});
-
-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,
- };
+const DirectTimeline = ({ columnId, multiColumn }) => {
+ const columnRef = useRef();
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const pinned = !!columnId;
- 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());
- }
-
- 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;
- }
- }
-
- 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 = (
-
{
+ dispatch(unmountConversations());
+ disconnect();
+ };
+ }, [dispatch, conversationsMode]);
+
+ return (
+
+
+
+
+
+ {conversationsMode ? (
+ }
bindToDocument={!multiColumn}
- onLoadMore={this.handleLoadMore}
prepend={}
alwaysPrepend
- emptyMessage={}
/>
- );
- } else {
- contents = (
+ ) : (
}
+ onLoadMore={handleLoadMoreTimeline}
+ prepend={
+
- );
- }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(DirectTimeline));
+ )}
+
+
+
+ );
+};
+
+DirectTimeline.propTypes = {
+ columnId: PropTypes.string,
+ multiColumn: PropTypes.bool,
+};
+
+export default DirectTimeline;