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) => {
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId });
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => {
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, chatMessages: data });
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
}).catch(error => {
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error });
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
});
};
}
export function sendChatMessage(chatId, params) {
return (dispatch, getState) => {
const uuid = uuidv4();
const uuid = `末_${Date.now()}_${uuidv4()}`;
const me = getState().get('me');
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
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) {
const markReadConditions = [
() => this.props.chat !== undefined,
() => this.props.chat,
() => this.props.chat.get('unread') > 0,
];
if (markReadConditions.every(c => c() === true))
if (markReadConditions.every(c => c()))
this.markRead();
}

Wyświetl plik

@ -5,7 +5,6 @@ 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';
@ -81,11 +80,6 @@ class ChatBox extends ImmutablePureComponent {
onSetInputRef(el);
};
componentDidMount() {
const { dispatch, chatId } = this.props;
dispatch(fetchChatMessages(chatId));
}
componentDidUpdate(prevProps) {
const markReadConditions = [
() => this.props.chat !== undefined,
@ -98,12 +92,12 @@ class ChatBox extends ImmutablePureComponent {
}
render() {
const { chatMessageIds, intl } = this.props;
const { chatMessageIds, chatId, intl } = this.props;
if (!chatMessageIds) return null;
return (
<div className='chat-box' onMouseOver={this.handleHover}>
<ChatMessageList chatMessageIds={chatMessageIds} />
<ChatMessageList chatMessageIds={chatMessageIds} chatId={chatId} />
<div className='chat-box__actions simple_form'>
<textarea
rows={1}

Wyświetl plik

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

Wyświetl plik

@ -173,7 +173,7 @@ class PrivacyDropdown extends React.PureComponent {
const { intl: { formatMessage } } = props;
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: '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) },

Wyświetl plik

@ -52,7 +52,7 @@ class UploadButton extends ImmutablePureComponent {
}
render() {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
const { intl, resetFileKey, unavailable, disabled } = this.props;
if (unavailable) {
return null;
@ -60,7 +60,7 @@ class UploadButton extends ImmutablePureComponent {
return (
<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>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
<input
@ -68,7 +68,8 @@ class UploadButton extends ImmutablePureComponent {
ref={this.setRef}
type='file'
multiple
accept={acceptContentTypes.toArray().join(',')}
// Accept all types for now.
// accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
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>
)}
{!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='/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 idComparator = (a, b) => {
if (a < b) return 1;
if (a > b) return -1;
return 0;
};
const updateList = (state, chatId, messageIds) => {
const ids = state.get(chatId, ImmutableOrderedSet());
const newIds = ids.union(messageIds);
const newIds = ids.union(messageIds).sort(idComparator);
return state.set(chatId, newIds);
};
@ -31,22 +37,28 @@ const importLastMessages = (state, chats) =>
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) {
switch(action.type) {
case CHAT_MESSAGE_SEND_REQUEST:
return updateList(state, action.chatId, [action.uuid]).sort();
return updateList(state, action.chatId, [action.uuid]);
case CHATS_FETCH_SUCCESS:
return importLastMessages(state, action.chats).sort();
return importLastMessages(state, action.chats);
case STREAMING_CHAT_UPDATE:
if (action.chat.last_message &&
action.chat.last_message.account_id !== action.me)
return importMessages(state, [action.chat.last_message]).sort();
return importMessages(state, [action.chat.last_message]);
else
return state;
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:
return updateList(state, action.chatId, [action.chatMessage.id]).sort();
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
default:
return state;
}

Wyświetl plik

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

Wyświetl plik

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