Merge branch 'develop' into 'chat_notifications'

Develop

See merge request soapbox-pub/soapbox-fe!213
better-alerts
Curtis 2020-09-05 02:17:36 +00:00
commit 34a575482c
10 zmienionych plików z 97 dodań i 38 usunięć

Wyświetl plik

@ -34,20 +34,20 @@ export function fetchChats() {
}; };
} }
export function fetchChatMessages(chatId) { export function fetchChatMessages(chatId, maxId = null) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId }); dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => { return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, chatMessages: data }); dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
}).catch(error => { }).catch(error => {
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error }); dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
}); });
}; };
} }
export function sendChatMessage(chatId, params) { export function sendChatMessage(chatId, params) {
return (dispatch, getState) => { return (dispatch, getState) => {
const uuid = uuidv4(); const uuid = `末_${Date.now()}_${uuidv4()}`;
const me = getState().get('me'); const me = getState().get('me');
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me }); dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => { return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {

Wyświetl plik

@ -58,11 +58,11 @@ class ChatRoom extends ImmutablePureComponent {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const markReadConditions = [ const markReadConditions = [
() => this.props.chat !== undefined, () => this.props.chat,
() => this.props.chat.get('unread') > 0, () => this.props.chat.get('unread') > 0,
]; ];
if (markReadConditions.every(c => c() === true)) if (markReadConditions.every(c => c()))
this.markRead(); this.markRead();
} }

Wyświetl plik

@ -5,7 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { import {
fetchChatMessages,
sendChatMessage, sendChatMessage,
markChatRead, markChatRead,
} from 'soapbox/actions/chats'; } from 'soapbox/actions/chats';
@ -81,11 +80,6 @@ class ChatBox extends ImmutablePureComponent {
onSetInputRef(el); onSetInputRef(el);
}; };
componentDidMount() {
const { dispatch, chatId } = this.props;
dispatch(fetchChatMessages(chatId));
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const markReadConditions = [ const markReadConditions = [
() => this.props.chat !== undefined, () => this.props.chat !== undefined,
@ -98,12 +92,12 @@ class ChatBox extends ImmutablePureComponent {
} }
render() { render() {
const { chatMessageIds, intl } = this.props; const { chatMessageIds, chatId, intl } = this.props;
if (!chatMessageIds) return null; if (!chatMessageIds) return null;
return ( return (
<div className='chat-box' onMouseOver={this.handleHover}> <div className='chat-box' onMouseOver={this.handleHover}>
<ChatMessageList chatMessageIds={chatMessageIds} /> <ChatMessageList chatMessageIds={chatMessageIds} chatId={chatId} />
<div className='chat-box__actions simple_form'> <div className='chat-box__actions simple_form'>
<textarea <textarea
rows={1} rows={1}

Wyświetl plik

@ -5,9 +5,12 @@ 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 { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { fetchChatMessages } from 'soapbox/actions/chats';
import emojify from 'soapbox/features/emoji/emoji'; import emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames'; import classNames from 'classnames';
import { escape } from 'lodash'; import { escape, throttle } from 'lodash';
const scrollBottom = (elem) => elem.scrollHeight - elem.offsetHeight - elem.scrollTop;
const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => { const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => {
return map.set(`:${emoji.get('shortcode')}:`, emoji); return map.set(`:${emoji.get('shortcode')}:`, emoji);
@ -18,7 +21,7 @@ const mapStateToProps = (state, { chatMessageIds }) => ({
chatMessages: chatMessageIds.reduce((acc, curr) => { chatMessages: chatMessageIds.reduce((acc, curr) => {
const chatMessage = state.getIn(['chat_messages', curr]); const chatMessage = state.getIn(['chat_messages', curr]);
return chatMessage ? acc.push(chatMessage) : acc; return chatMessage ? acc.push(chatMessage) : acc;
}, ImmutableList()).sort(), }, ImmutableList()),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -28,6 +31,7 @@ class ChatMessageList extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
chatId: PropTypes.string,
chatMessages: ImmutablePropTypes.list, chatMessages: ImmutablePropTypes.list,
chatMessageIds: ImmutablePropTypes.orderedSet, chatMessageIds: ImmutablePropTypes.orderedSet,
me: PropTypes.node, me: PropTypes.node,
@ -37,6 +41,10 @@ class ChatMessageList extends ImmutablePureComponent {
chatMessages: ImmutableList(), chatMessages: ImmutableList(),
} }
state = {
isLoading: false,
}
scrollToBottom = () => { scrollToBottom = () => {
if (!this.messagesEnd) return; if (!this.messagesEnd) return;
this.messagesEnd.scrollIntoView(); this.messagesEnd.scrollIntoView();
@ -44,7 +52,6 @@ class ChatMessageList extends ImmutablePureComponent {
setMessageEndRef = (el) => { setMessageEndRef = (el) => {
this.messagesEnd = el; this.messagesEnd = el;
this.scrollToBottom();
}; };
getFormattedTimestamp = (chatMessage) => { getFormattedTimestamp = (chatMessage) => {
@ -61,7 +68,7 @@ class ChatMessageList extends ImmutablePureComponent {
); );
}; };
setRef = (c) => { setBubbleRef = (c) => {
if (!c) return; if (!c) return;
const links = c.querySelectorAll('a[rel="ugc"]'); const links = c.querySelectorAll('a[rel="ugc"]');
@ -72,11 +79,42 @@ class ChatMessageList extends ImmutablePureComponent {
}); });
} }
componentDidUpdate(prevProps) { componentDidMount() {
if (prevProps.chatMessages !== this.props.chatMessages) const { dispatch, chatId } = this.props;
this.scrollToBottom(); dispatch(fetchChatMessages(chatId));
this.node.addEventListener('scroll', this.handleScroll);
} }
componentDidUpdate(prevProps) {
const oldCount = prevProps.chatMessages.count();
const newCount = this.props.chatMessages.count();
const isNearBottom = scrollBottom(this.node) < 150;
const historyAdded = prevProps.chatMessages.getIn([-1, 'id']) !== this.props.chatMessages.getIn([-1, 'id']);
if (oldCount !== newCount) {
if (isNearBottom) this.scrollToBottom();
if (historyAdded) this.setState({ isLoading: false });
}
}
componentWillUnmount() {
this.node.removeEventListener('scroll', this.handleScroll);
}
handleLoadMore = () => {
const { dispatch, chatId, chatMessages } = this.props;
const maxId = chatMessages.getIn([-1, 'id']);
dispatch(fetchChatMessages(chatId, maxId));
this.setState({ isLoading: true });
}
handleScroll = throttle(() => {
if (this.node.scrollTop < 150 && !this.state.isLoading) this.handleLoadMore();
}, 150, {
trailing: true,
});
parsePendingContent = content => { parsePendingContent = content => {
return escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>'); return escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
} }
@ -89,11 +127,16 @@ class ChatMessageList extends ImmutablePureComponent {
return emojify(formatted, emojiMap.toJS()); return emojify(formatted, emojiMap.toJS());
} }
setRef = (c) => {
this.node = c;
}
render() { render() {
const { chatMessages, me } = this.props; const { chatMessages, me } = this.props;
return ( return (
<div className='chat-messages'> <div className='chat-messages' ref={this.setRef}>
<div style={{ float: 'left', clear: 'both' }} ref={this.setMessageEndRef} />
{chatMessages.map(chatMessage => ( {chatMessages.map(chatMessage => (
<div <div
className={classNames('chat-message', { className={classNames('chat-message', {
@ -106,11 +149,10 @@ class ChatMessageList extends ImmutablePureComponent {
title={this.getFormattedTimestamp(chatMessage)} title={this.getFormattedTimestamp(chatMessage)}
className='chat-message__bubble' className='chat-message__bubble'
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }} dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
ref={this.setRef} ref={this.setBubbleRef}
/> />
</div> </div>
))} ))}
<div style={{ float: 'left', clear: 'both' }} ref={this.setMessageEndRef} />
</div> </div>
); );
} }

Wyświetl plik

@ -173,7 +173,7 @@ class PrivacyDropdown extends React.PureComponent {
const { intl: { formatMessage } } = props; const { intl: { formatMessage } } = props;
this.options = [ this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'globe-w', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },

Wyświetl plik

@ -52,7 +52,7 @@ class UploadButton extends ImmutablePureComponent {
} }
render() { render() {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props; const { intl, resetFileKey, unavailable, disabled } = this.props;
if (unavailable) { if (unavailable) {
return null; return null;
@ -60,7 +60,7 @@ class UploadButton extends ImmutablePureComponent {
return ( return (
<div className='compose-form__upload-button'> <div className='compose-form__upload-button'>
<IconButton icon='upload' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <IconButton icon='paperclip' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
<input <input
@ -68,7 +68,8 @@ class UploadButton extends ImmutablePureComponent {
ref={this.setRef} ref={this.setRef}
type='file' type='file'
multiple multiple
accept={acceptContentTypes.toArray().join(',')} // Accept all types for now.
// accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange} onChange={this.handleChange}
disabled={disabled} disabled={disabled}
style={{ display: 'none' }} style={{ display: 'none' }}

Wyświetl plik

@ -88,7 +88,7 @@ class Compose extends React.PureComponent {
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
)} )}
{!columns.some(column => column.get('id') === 'PUBLIC') && ( {!columns.some(column => column.get('id') === 'PUBLIC') && (
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe-w' fixedWidth /></Link>
)} )}
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a> <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a> <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>

Wyświetl plik

@ -9,9 +9,15 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
const initialState = ImmutableMap(); const initialState = ImmutableMap();
const idComparator = (a, b) => {
if (a < b) return 1;
if (a > b) return -1;
return 0;
};
const updateList = (state, chatId, messageIds) => { const updateList = (state, chatId, messageIds) => {
const ids = state.get(chatId, ImmutableOrderedSet()); const ids = state.get(chatId, ImmutableOrderedSet());
const newIds = ids.union(messageIds); const newIds = ids.union(messageIds).sort(idComparator);
return state.set(chatId, newIds); return state.set(chatId, newIds);
}; };
@ -31,22 +37,28 @@ const importLastMessages = (state, chats) =>
if (chat.last_message) importMessage(mutable, chat.last_message); if (chat.last_message) importMessage(mutable, chat.last_message);
})); }));
const replaceMessage = (state, chatId, oldId, newId) => {
const ids = state.get(chatId, ImmutableOrderedSet());
const newIds = ids.delete(oldId).add(newId).sort(idComparator);
return state.set(chatId, newIds);
};
export default function chatMessageLists(state = initialState, action) { export default function chatMessageLists(state = initialState, action) {
switch(action.type) { switch(action.type) {
case CHAT_MESSAGE_SEND_REQUEST: case CHAT_MESSAGE_SEND_REQUEST:
return updateList(state, action.chatId, [action.uuid]).sort(); return updateList(state, action.chatId, [action.uuid]);
case CHATS_FETCH_SUCCESS: case CHATS_FETCH_SUCCESS:
return importLastMessages(state, action.chats).sort(); return importLastMessages(state, action.chats);
case STREAMING_CHAT_UPDATE: case STREAMING_CHAT_UPDATE:
if (action.chat.last_message && if (action.chat.last_message &&
action.chat.last_message.account_id !== action.me) action.chat.last_message.account_id !== action.me)
return importMessages(state, [action.chat.last_message]).sort(); return importMessages(state, [action.chat.last_message]);
else else
return state; return state;
case CHAT_MESSAGES_FETCH_SUCCESS: case CHAT_MESSAGES_FETCH_SUCCESS:
return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id).reverse()).sort(); return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
case CHAT_MESSAGE_SEND_SUCCESS: case CHAT_MESSAGE_SEND_SUCCESS:
return updateList(state, action.chatId, [action.chatMessage.id]).sort(); return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
default: default:
return state; return state;
} }

Wyświetl plik

@ -99,12 +99,18 @@
.chat-messages { .chat-messages {
overflow-y: scroll; overflow-y: scroll;
flex: 1; flex: 1;
display: flex;
flex-direction: column-reverse;
} }
.chat-message { .chat-message {
margin: 14px 10px; padding: 7px 10px;
display: flex; display: flex;
&:last-child {
padding-top: 14px;
}
&__bubble { &__bubble {
font-size: 15px; font-size: 15px;
padding: 4px 10px; padding: 4px 10px;

Wyświetl plik

@ -143,6 +143,10 @@
.setting-toggle { .setting-toggle {
margin-left: 10px; margin-left: 10px;
.react-toggle-track {
background-color: var(--foreground-color);
}
.react-toggle--checked { .react-toggle--checked {
.react-toggle-track { .react-toggle-track {
background-color: var(--accent-color); background-color: var(--accent-color);