Chats: refactor ChatBox into its own component

loading-indicator-on-tls^2
Alex Gleason 2020-08-28 13:17:19 -05:00
rodzic d67d76bf3a
commit e7c6862fd0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
7 zmienionych plików z 170 dodań i 147 usunięć

Wyświetl plik

@ -111,6 +111,17 @@ export function toggleMainWindow() {
}; };
} }
export function fetchChat(chatId) {
return (dispatch, getState) => {
dispatch({ type: CHAT_FETCH_REQUEST, chatId });
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => {
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
}).catch(error => {
dispatch({ type: CHAT_FETCH_FAIL, chatId, error });
});
};
}
export function startChat(accountId) { export function startChat(accountId) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: CHAT_FETCH_REQUEST, accountId }); dispatch({ type: CHAT_FETCH_REQUEST, accountId });

Wyświetl plik

@ -4,104 +4,51 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { import { fetchChat } from 'soapbox/actions/chats';
fetchChatMessages, import ChatBox from './components/chat_box';
sendChatMessage,
markChatRead,
} from 'soapbox/actions/chats';
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import ChatMessageList from './components/chat_message_list';
import Column from 'soapbox/features/ui/components/column'; import Column from 'soapbox/features/ui/components/column';
const mapStateToProps = (state, { params }) => ({ const mapStateToProps = (state, { params }) => ({
me: state.get('me'), me: state.get('me'),
chat: state.getIn(['chats', params.chatId]), chat: state.getIn(['chats', params.chatId]),
chatMessageIds: state.getIn(['chat_message_lists', params.chatId], ImmutableOrderedSet()),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
class ChatWindow extends ImmutablePureComponent { class ChatRoom extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
chatMessageIds: ImmutablePropTypes.orderedSet,
chat: ImmutablePropTypes.map, chat: ImmutablePropTypes.map,
me: PropTypes.node, me: PropTypes.node,
} }
static defaultProps = { handleInputRef = (el) => {
chatMessages: ImmutableList(), this.inputElem = el;
} this.focusInput();
};
state = {
content: '',
}
handleKeyDown = (chatId) => {
return (e) => {
if (e.key === 'Enter') {
this.props.dispatch(sendChatMessage(chatId, this.state));
this.setState({ content: '' });
e.preventDefault();
}
};
}
handleContentChange = (e) => {
this.setState({ content: e.target.value });
}
handleReadChat = (e) => {
const { dispatch, chat } = this.props;
dispatch(markChatRead(chat.get('id')));
}
focusInput = () => { focusInput = () => {
if (!this.inputElem) return; if (!this.inputElem) return;
this.inputElem.focus(); this.inputElem.focus();
} }
setInputRef = (el) => {
this.inputElem = el;
this.focusInput();
};
componentDidMount() { componentDidMount() {
const { dispatch, chatMessages, params } = this.props; const { dispatch, params } = this.props;
if (chatMessages && chatMessages.count() < 1) dispatch(fetchChat(params.chatId));
dispatch(fetchChatMessages(params.chatId));
}
componentDidUpdate(prevProps) {
const markReadConditions = [
() => this.props.chat !== undefined,
() => document.activeElement === this.inputElem,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
this.handleReadChat();
} }
render() { render() {
const { chatMessageIds, chat } = this.props; const { chat } = this.props;
if (!chat) return null; if (!chat) return null;
return ( return (
<Column> <Column>
<ChatMessageList chatMessageIds={chatMessageIds} /> <ChatBox
<div className='pane__actions simple_form'> chatId={chat.get('id')}
<textarea onSetInputRef={this.handleInputRef}
rows={1} />
placeholder='Send a message...'
onKeyDown={this.handleKeyDown(chat.get('id'))}
onChange={this.handleContentChange}
value={this.state.content}
ref={this.setInputRef}
/>
</div>
</Column> </Column>
); );
} }

Wyświetl plik

@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import {
fetchChatMessages,
sendChatMessage,
markChatRead,
} from 'soapbox/actions/chats';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import ChatMessageList from './chat_message_list';
const messages = defineMessages({
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
});
const mapStateToProps = (state, { chatId }) => ({
me: state.get('me'),
chat: state.getIn(['chats', chatId]),
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
});
export default @connect(mapStateToProps)
@injectIntl
class ChatBox extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
chatId: PropTypes.string.isRequired,
chatMessageIds: ImmutablePropTypes.orderedSet,
chat: ImmutablePropTypes.map,
onSetInputRef: PropTypes.func,
me: PropTypes.node,
}
state = {
content: '',
}
handleKeyDown = (e) => {
const { chatId } = this.props;
if (e.key === 'Enter') {
this.props.dispatch(sendChatMessage(chatId, this.state));
this.setState({ content: '' });
e.preventDefault();
}
}
handleContentChange = (e) => {
this.setState({ content: e.target.value });
}
markRead = () => {
const { dispatch, chatId } = this.props;
dispatch(markChatRead(chatId));
}
handleHover = () => {
this.markRead();
}
setInputRef = (el) => {
const { onSetInputRef } = this.props;
this.inputElem = el;
onSetInputRef(el);
};
componentDidMount() {
const { dispatch, chatId } = this.props;
dispatch(fetchChatMessages(chatId));
}
componentDidUpdate(prevProps) {
const markReadConditions = [
() => this.props.chat !== undefined,
() => document.activeElement === this.inputElem,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
this.markRead();
}
render() {
const { chatMessageIds, intl } = this.props;
if (!chatMessageIds) return null;
return (
<div className='chat-box' onMouseOver={this.handleHover}>
<ChatMessageList chatMessageIds={chatMessageIds} />
<div className='chat-box__actions simple_form'>
<textarea
rows={1}
placeholder={intl.formatMessage(messages.placeholder)}
onKeyDown={this.handleKeyDown}
onChange={this.handleContentChange}
value={this.state.content}
ref={this.setInputRef}
/>
</div>
</div>
);
}
}

Wyświetl plik

@ -10,18 +10,13 @@ import IconButton from 'soapbox/components/icon_button';
import { import {
closeChat, closeChat,
toggleChat, toggleChat,
fetchChatMessages,
sendChatMessage,
markChatRead,
} from 'soapbox/actions/chats'; } from 'soapbox/actions/chats';
import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import ChatBox from './chat_box';
import ChatMessageList from './chat_message_list';
import { shortNumberFormat } from 'soapbox/utils/numbers'; import { shortNumberFormat } from 'soapbox/utils/numbers';
const mapStateToProps = (state, { pane }) => ({ const mapStateToProps = (state, { pane }) => ({
me: state.get('me'), me: state.get('me'),
chat: state.getIn(['chats', pane.get('chat_id')]), chat: state.getIn(['chats', pane.get('chat_id')]),
chatMessageIds: state.getIn(['chat_message_lists', pane.get('chat_id')], ImmutableOrderedSet()),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -33,15 +28,10 @@ class ChatWindow extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
pane: ImmutablePropTypes.map.isRequired, pane: ImmutablePropTypes.map.isRequired,
idx: PropTypes.number, idx: PropTypes.number,
chatMessageIds: ImmutablePropTypes.orderedSet,
chat: ImmutablePropTypes.map, chat: ImmutablePropTypes.map,
me: PropTypes.node, me: PropTypes.node,
} }
static defaultProps = {
chatMessages: ImmutableList(),
}
state = { state = {
content: '', content: '',
} }
@ -58,65 +48,30 @@ class ChatWindow extends ImmutablePureComponent {
}; };
} }
handleKeyDown = (chatId) => {
return (e) => {
if (e.key === 'Enter') {
this.props.dispatch(sendChatMessage(chatId, this.state));
this.setState({ content: '' });
e.preventDefault();
}
};
}
handleContentChange = (e) => { handleContentChange = (e) => {
this.setState({ content: e.target.value }); this.setState({ content: e.target.value });
} }
handleHover = () => { handleInputRef = (el) => {
if (this.props.pane.get('state') === 'open') this.markRead(); this.inputElem = el;
} this.focusInput();
};
markRead = () => {
const { dispatch, chat } = this.props;
dispatch(markChatRead(chat.get('id')));
}
focusInput = () => { focusInput = () => {
if (!this.inputElem) return; if (!this.inputElem) return;
this.inputElem.focus(); this.inputElem.focus();
} }
setInputRef = (el) => {
const { pane } = this.props;
this.inputElem = el;
if (pane.get('state') === 'open') this.focusInput();
};
componentDidMount() {
const { dispatch, pane, chatMessages } = this.props;
if (chatMessages && chatMessages.count() < 1)
dispatch(fetchChatMessages(pane.get('chat_id')));
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const oldState = prevProps.pane.get('state'); const oldState = prevProps.pane.get('state');
const newState = this.props.pane.get('state'); const newState = this.props.pane.get('state');
if (oldState !== newState && newState === 'open') if (oldState !== newState && newState === 'open')
this.focusInput(); this.focusInput();
const markReadConditions = [
() => this.props.chat !== undefined,
() => document.activeElement === this.inputElem,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
this.markRead();
} }
render() { render() {
const { pane, idx, chatMessageIds, chat } = this.props; const { pane, idx, chat } = this.props;
const account = pane.getIn(['chat', 'account']); const account = pane.getIn(['chat', 'account']);
if (!chat || !account) return null; if (!chat || !account) return null;
@ -124,7 +79,7 @@ class ChatWindow extends ImmutablePureComponent {
const unreadCount = chat.get('unread'); const unreadCount = chat.get('unread');
return ( return (
<div className={`pane pane--${pane.get('state')}`} style={{ right: `${right}px` }} onMouseOver={this.handleHover}> <div className={`pane pane--${pane.get('state')}`} style={{ right: `${right}px` }}>
<div className='pane__header'> <div className='pane__header'>
{unreadCount > 0 {unreadCount > 0
? <i className='icon-with-badge__badge'>{shortNumberFormat(unreadCount)}</i> ? <i className='icon-with-badge__badge'>{shortNumberFormat(unreadCount)}</i>
@ -138,17 +93,10 @@ class ChatWindow extends ImmutablePureComponent {
</div> </div>
</div> </div>
<div className='pane__content'> <div className='pane__content'>
<ChatMessageList chatMessageIds={chatMessageIds} /> <ChatBox
<div className='pane__actions simple_form'> chatId={chat.get('id')}
<textarea onSetInputRef={this.handleInputRef}
rows={1} />
placeholder='Send a message...'
onKeyDown={this.handleKeyDown(chat.get('id'))}
onChange={this.handleContentChange}
value={this.state.content}
ref={this.setInputRef}
/>
</div>
</div> </div>
</div> </div>
); );

Wyświetl plik

@ -13,7 +13,6 @@ export default @injectIntl
class ChatIndex extends React.PureComponent { class ChatIndex extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };

Wyświetl plik

@ -32,6 +32,7 @@ export default function chatMessages(state = initialState, action) {
chat_id: action.chatId, chat_id: action.chatId,
account_id: action.me, account_id: action.me,
content: action.params.content, content: action.params.content,
created_at: (new Date()).toISOString(),
pending: true, pending: true,
})); }));
case CHATS_FETCH_SUCCESS: case CHATS_FETCH_SUCCESS:

Wyświetl plik

@ -86,23 +86,12 @@
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
}
&__actions { .chat-box {
background: var(--foreground-color); display: flex;
margin-top: auto; flex: 1;
padding: 6px; flex-direction: column;
overflow: hidden;
textarea {
width: 100%;
margin: 0;
box-sizing: border-box;
padding: 6px;
background: var(--background-color);
border: 0;
border-radius: 6px;
color: var(--primary-text-color);
font-size: 15px;
} }
} }
} }
@ -166,3 +155,23 @@
bottom: auto; bottom: auto;
} }
} }
.chat-box {
&__actions {
background: var(--foreground-color);
margin-top: auto;
padding: 6px;
textarea {
width: 100%;
margin: 0;
box-sizing: border-box;
padding: 6px;
background: var(--background-color);
border: 0;
border-radius: 6px;
color: var(--primary-text-color);
font-size: 15px;
}
}
}