mirror of https://github.com/mastodon/mastodon
Refactor conversations components in web UI (#28833)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>pull/22740/merge
parent
274a48a9f4
commit
3205a654ca
@ -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 'mastodon/actions/conversations';
|
||||||
import ConversationContainer from '../containers/conversation_container';
|
import ScrollableList from 'mastodon/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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 () {
|
element.focus();
|
||||||
const { conversations, isLoading, onLoadMore, ...other } = this.props;
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ConversationsList.propTypes = {
|
||||||
|
scrollKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { replyCompose } from 'mastodon/actions/compose';
|
|
||||||
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses';
|
|
||||||
import { makeGetStatus } from 'mastodon/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 }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
Loading…
Reference in New Issue