Merge branch 'chats-pagination' into 'develop'
use `/api/v2/pleroma/chats` See merge request soapbox-pub/soapbox-fe!911
This commit is contained in:
commit
ff26336f3a
22 changed files with 201 additions and 64 deletions
|
@ -1,5 +1,6 @@
|
||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
@ -7,6 +8,10 @@ export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
||||||
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
||||||
export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
|
export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
|
||||||
|
export const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
|
||||||
|
export const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
|
export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
|
||||||
export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
|
export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
|
||||||
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
|
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
|
||||||
|
@ -27,14 +32,61 @@ export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
|
||||||
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
|
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
|
||||||
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
||||||
|
|
||||||
export function fetchChats() {
|
export function fetchChatsV1() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) =>
|
||||||
dispatch({ type: CHATS_FETCH_REQUEST });
|
api(getState).get('/api/v1/pleroma/chats').then((response) => {
|
||||||
return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => {
|
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
|
||||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: data });
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchChatsV2() {
|
||||||
|
return (dispatch, getState) =>
|
||||||
|
api(getState).get('/api/v2/pleroma/chats').then((response) => {
|
||||||
|
let next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
if (!next && response.data.length) {
|
||||||
|
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchChats() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: CHATS_FETCH_REQUEST });
|
||||||
|
if (features.chatsV2) {
|
||||||
|
dispatch(fetchChatsV2());
|
||||||
|
} else {
|
||||||
|
dispatch(fetchChatsV1());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandChats() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['chats', 'next']);
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: CHATS_EXPAND_REQUEST });
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch({ type: CHATS_EXPAND_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: CHATS_EXPAND_FAIL, error });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +192,7 @@ export function startChat(accountId) {
|
||||||
|
|
||||||
export function markChatRead(chatId, lastReadId) {
|
export function markChatRead(chatId, lastReadId) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const chat = getState().getIn(['chats', chatId]);
|
const chat = getState().getIn(['chats', 'items', chatId]);
|
||||||
if (!lastReadId) lastReadId = chat.get('last_message');
|
if (!lastReadId) lastReadId = chat.get('last_message');
|
||||||
|
|
||||||
if (chat.get('unread') < 1) return;
|
if (chat.get('unread') < 1) return;
|
||||||
|
|
|
@ -11,7 +11,7 @@ FaviconService.initFaviconService();
|
||||||
|
|
||||||
const getNotifTotals = state => {
|
const getNotifTotals = state => {
|
||||||
const notifications = state.getIn(['notifications', 'unread'], 0);
|
const notifications = state.getIn(['notifications', 'unread'], 0);
|
||||||
const chats = state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
const chats = state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
||||||
const reports = state.getIn(['admin', 'openReports']).count();
|
const reports = state.getIn(['admin', 'openReports']).count();
|
||||||
const approvals = state.getIn(['admin', 'awaitingApproval']).count();
|
const approvals = state.getIn(['admin', 'awaitingApproval']).count();
|
||||||
return notifications + chats + reports + approvals;
|
return notifications + chats + reports + approvals;
|
||||||
|
|
|
@ -25,7 +25,7 @@ const mapStateToProps = state => {
|
||||||
account,
|
account,
|
||||||
logo: getSoapboxConfig(state).get('logo'),
|
logo: getSoapboxConfig(state).get('logo'),
|
||||||
notificationCount: state.getIn(['notifications', 'unread']),
|
notificationCount: state.getIn(['notifications', 'unread']),
|
||||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||||
dashboardCount: reportsCount + approvalCount,
|
dashboardCount: reportsCount + approvalCount,
|
||||||
baseURL: getBaseURL(account),
|
baseURL: getBaseURL(account),
|
||||||
settings: getSettings(state),
|
settings: getSettings(state),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
import MoreFollows from './more_follows';
|
import MoreFollows from './more_follows';
|
||||||
|
@ -45,6 +46,7 @@ class ScrollableList extends PureComponent {
|
||||||
placeholderCount: PropTypes.number,
|
placeholderCount: PropTypes.number,
|
||||||
autoload: PropTypes.bool,
|
autoload: PropTypes.bool,
|
||||||
onRefresh: PropTypes.func,
|
onRefresh: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -240,16 +242,22 @@ class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoading = () => {
|
renderLoading = () => {
|
||||||
const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
||||||
|
|
||||||
if (Placeholder && placeholderCount > 0) {
|
if (Placeholder && placeholderCount > 0) {
|
||||||
return Array(placeholderCount).fill().map((_, i) => (
|
return (
|
||||||
|
<div className={classNames('slist slist--flex', className)}>
|
||||||
|
<div role='feed' className='item-list'>
|
||||||
|
{Array(placeholderCount).fill().map((_, i) => (
|
||||||
<Placeholder key={i} />
|
<Placeholder key={i} />
|
||||||
));
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='slist slist--flex'>
|
<div className={classNames('slist slist--flex', className)}>
|
||||||
<div role='feed' className='item-list'>
|
<div role='feed' className='item-list'>
|
||||||
{prepend}
|
{prepend}
|
||||||
</div>
|
</div>
|
||||||
|
@ -262,10 +270,10 @@ class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmptyMessage = () => {
|
renderEmptyMessage = () => {
|
||||||
const { prepend, alwaysPrepend, emptyMessage } = this.props;
|
const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='slist slist--flex' ref={this.setRef}>
|
<div className={classNames('slist slist--flex', className)} ref={this.setRef}>
|
||||||
{alwaysPrepend && prepend}
|
{alwaysPrepend && prepend}
|
||||||
|
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
|
@ -276,13 +284,13 @@ class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFeed = () => {
|
renderFeed = () => {
|
||||||
const { children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
|
const { className, children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
const trackScroll = true; //placeholder
|
const trackScroll = true; //placeholder
|
||||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||||
|
|
||||||
const feed = (
|
const feed = (
|
||||||
<div className='slist' ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('slist', className)} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
<div role='feed' className='item-list'>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const mapStateToProps = state => {
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
logo: getSoapboxConfig(state).get('logo'),
|
logo: getSoapboxConfig(state).get('logo'),
|
||||||
notificationCount: state.getIn(['notifications', 'unread']),
|
notificationCount: state.getIn(['notifications', 'unread']),
|
||||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||||
dashboardCount: reportsCount + approvalCount,
|
dashboardCount: reportsCount + approvalCount,
|
||||||
features: getFeatures(instance),
|
features: getFeatures(instance),
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { displayFqn } from 'soapbox/utils/state';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params }) => {
|
const mapStateToProps = (state, { params }) => {
|
||||||
const getChat = makeGetChat();
|
const getChat = makeGetChat();
|
||||||
const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
|
const chat = state.getIn(['chats', 'items', params.chatId], ImmutableMap()).toJS();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
|
|
|
@ -15,7 +15,7 @@ const makeMapStateToProps = () => {
|
||||||
const getChat = makeGetChat();
|
const getChat = makeGetChat();
|
||||||
|
|
||||||
const mapStateToProps = (state, { chatId }) => {
|
const mapStateToProps = (state, { chatId }) => {
|
||||||
const chat = state.getIn(['chats', chatId]);
|
const chat = state.getIn(['chats', 'items', chatId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat: chat ? getChat(state, chat.toJS()) : undefined,
|
chat: chat ? getChat(state, chat.toJS()) : undefined,
|
||||||
|
|
|
@ -23,7 +23,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = (state, { chatId }) => ({
|
const mapStateToProps = (state, { chatId }) => ({
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
chat: state.getIn(['chats', chatId]),
|
chat: state.getIn(['chats', 'items', chatId]),
|
||||||
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
|
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,19 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { expandChats } from 'soapbox/actions/chats';
|
||||||
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
|
import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
|
||||||
import Chat from './chat';
|
import Chat from './chat';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
emptyMessage: { id: 'chat_panels.main_window.empty', defaultMessage: 'No chats found. To start a chat, visit a user\'s profile' },
|
||||||
|
});
|
||||||
|
|
||||||
const getSortedChatIds = chats => (
|
const getSortedChatIds = chats => (
|
||||||
chats
|
chats
|
||||||
.toList()
|
.toList()
|
||||||
|
@ -32,7 +40,9 @@ const makeMapStateToProps = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
chatIds: sortedChatIdsSelector(state.get('chats')),
|
chatIds: sortedChatIdsSelector(state.getIn(['chats', 'items'])),
|
||||||
|
hasMore: !!state.getIn(['chats', 'next']),
|
||||||
|
isLoading: state.getIn(['chats', 'loading']),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -47,18 +57,31 @@ class ChatList extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
chatIds: ImmutablePropTypes.list,
|
chatIds: ImmutablePropTypes.list,
|
||||||
onClickChat: PropTypes.func,
|
onClickChat: PropTypes.func,
|
||||||
emptyMessage: PropTypes.node,
|
onRefresh: PropTypes.func,
|
||||||
|
hasMore: PropTypes.func,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandChats());
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { chatIds, emptyMessage } = this.props;
|
const { intl, chatIds, hasMore, isLoading } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='chat-list'>
|
<ScrollableList
|
||||||
<div className='chat-list__content'>
|
className='chat-list'
|
||||||
{chatIds.count() === 0 &&
|
scrollKey='awaiting-approval'
|
||||||
<div className='empty-column-indicator'>{emptyMessage}</div>
|
emptyMessage={intl.formatMessage(messages.emptyMessage)}
|
||||||
}
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading && chatIds.size === 0}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
onRefresh={this.props.onRefresh}
|
||||||
|
placeholderComponent={PlaceholderChat}
|
||||||
|
placeholderCount={20}
|
||||||
|
>
|
||||||
{chatIds.map(chatId => (
|
{chatIds.map(chatId => (
|
||||||
<div key={chatId} className='chat-list-item'>
|
<div key={chatId} className='chat-list-item'>
|
||||||
<Chat
|
<Chat
|
||||||
|
@ -67,8 +90,7 @@ class ChatList extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ScrollableList>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChatsUnreadCount = state => {
|
const getChatsUnreadCount = state => {
|
||||||
const chats = state.get('chats');
|
const chats = state.getIn(['chats', 'items']);
|
||||||
return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ const normalizePanes = (chats, panes = ImmutableList()) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const makeNormalizeChatPanes = () => createSelector([
|
const makeNormalizeChatPanes = () => createSelector([
|
||||||
state => state.get('chats'),
|
state => state.getIn(['chats', 'items']),
|
||||||
state => getSettings(state).getIn(['chats', 'panes']),
|
state => getSettings(state).getIn(['chats', 'panes']),
|
||||||
], normalizePanes);
|
], normalizePanes);
|
||||||
|
|
||||||
|
@ -93,7 +93,6 @@ class ChatPanes extends ImmutablePureComponent {
|
||||||
<>
|
<>
|
||||||
<ChatList
|
<ChatList
|
||||||
onClickChat={this.handleClickChat}
|
onClickChat={this.handleClickChat}
|
||||||
emptyMessage={<FormattedMessage id='chat_panels.main_window.empty' defaultMessage="No chats found. To start a chat, visit a user's profile." />}
|
|
||||||
/>
|
/>
|
||||||
<AccountSearch
|
<AccountSearch
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const makeMapStateToProps = () => {
|
||||||
const getChat = makeGetChat();
|
const getChat = makeGetChat();
|
||||||
|
|
||||||
const mapStateToProps = (state, { chatId }) => {
|
const mapStateToProps = (state, { chatId }) => {
|
||||||
const chat = state.getIn(['chats', chatId]);
|
const chat = state.getIn(['chats', 'items', chatId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
|
|
|
@ -4,11 +4,10 @@ import { connect } from 'react-redux';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import { fetchChats, launchChat } from 'soapbox/actions/chats';
|
import { fetchChats, launchChat } from 'soapbox/actions/chats';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ChatList from './components/chat_list';
|
import ChatList from './components/chat_list';
|
||||||
import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
|
import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
|
||||||
import AccountSearch from 'soapbox/components/account_search';
|
import AccountSearch from 'soapbox/components/account_search';
|
||||||
import PullToRefresh from 'soapbox/components/pull_to_refresh';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.chats', defaultMessage: 'Chats' },
|
title: { id: 'column.chats', defaultMessage: 'Chats' },
|
||||||
|
@ -60,12 +59,10 @@ class ChatIndex extends React.PureComponent {
|
||||||
onSelected={this.handleSuggestion}
|
onSelected={this.handleSuggestion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PullToRefresh onRefresh={this.handleRefresh}>
|
|
||||||
<ChatList
|
<ChatList
|
||||||
onClickChat={this.handleClickChat}
|
onClickChat={this.handleClickChat}
|
||||||
emptyMessage={<FormattedMessage id='chat_panels.main_window.empty' defaultMessage="No chats found. To start a chat, visit a user's profile." />}
|
onRefresh={this.handleRefresh}
|
||||||
/>
|
/>
|
||||||
</PullToRefresh>
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PlaceholderAvatar from './placeholder_avatar';
|
||||||
|
import PlaceholderDisplayName from './placeholder_display_name';
|
||||||
|
import { randomIntFromInterval, generateText } from '../utils';
|
||||||
|
|
||||||
|
export default class PlaceholderAccount extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const messageLength = randomIntFromInterval(5, 75);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='chat-list-item chat-list-item--placeholder'>
|
||||||
|
<div className='account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<div className='account__display-name'>
|
||||||
|
<div className='account__avatar-wrapper'>
|
||||||
|
<PlaceholderAvatar size={36} />
|
||||||
|
</div>
|
||||||
|
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
||||||
|
<span className='chat__last-message'>
|
||||||
|
{generateText(messageLength)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -171,7 +171,7 @@ const mapStateToProps = state => {
|
||||||
logo: getSoapboxConfig(state).get('logo'),
|
logo: getSoapboxConfig(state).get('logo'),
|
||||||
features: getFeatures(instance),
|
features: getFeatures(instance),
|
||||||
notificationCount: state.getIn(['notifications', 'unread']),
|
notificationCount: state.getIn(['notifications', 'unread']),
|
||||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||||
dashboardCount: reportsCount + approvalCount,
|
dashboardCount: reportsCount + approvalCount,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
ACCOUNTS_IMPORT,
|
ACCOUNTS_IMPORT,
|
||||||
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
||||||
} from '../actions/importer';
|
} from '../actions/importer';
|
||||||
import { CHATS_FETCH_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats';
|
import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats';
|
||||||
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||||
import { normalizeAccount as normalizeAccount2 } from 'soapbox/actions/importer/normalizer';
|
import { normalizeAccount as normalizeAccount2 } from 'soapbox/actions/importer/normalizer';
|
||||||
import {
|
import {
|
||||||
|
@ -208,6 +208,7 @@ export default function accounts(state = initialState, action) {
|
||||||
username: action.username,
|
username: action.username,
|
||||||
}));
|
}));
|
||||||
case CHATS_FETCH_SUCCESS:
|
case CHATS_FETCH_SUCCESS:
|
||||||
|
case CHATS_EXPAND_SUCCESS:
|
||||||
return importAccountsFromChats(state, action.chats);
|
return importAccountsFromChats(state, action.chats);
|
||||||
case CHAT_FETCH_SUCCESS:
|
case CHAT_FETCH_SUCCESS:
|
||||||
case STREAMING_CHAT_UPDATE:
|
case STREAMING_CHAT_UPDATE:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CHATS_FETCH_SUCCESS,
|
CHATS_FETCH_SUCCESS,
|
||||||
|
CHATS_EXPAND_SUCCESS,
|
||||||
CHAT_MESSAGES_FETCH_SUCCESS,
|
CHAT_MESSAGES_FETCH_SUCCESS,
|
||||||
CHAT_MESSAGE_SEND_REQUEST,
|
CHAT_MESSAGE_SEND_REQUEST,
|
||||||
CHAT_MESSAGE_SEND_SUCCESS,
|
CHAT_MESSAGE_SEND_SUCCESS,
|
||||||
|
@ -47,6 +48,7 @@ export default function chatMessageLists(state = initialState, action) {
|
||||||
case CHAT_MESSAGE_SEND_REQUEST:
|
case CHAT_MESSAGE_SEND_REQUEST:
|
||||||
return updateList(state, action.chatId, [action.uuid]);
|
return updateList(state, action.chatId, [action.uuid]);
|
||||||
case CHATS_FETCH_SUCCESS:
|
case CHATS_FETCH_SUCCESS:
|
||||||
|
case CHATS_EXPAND_SUCCESS:
|
||||||
return importLastMessages(state, action.chats);
|
return importLastMessages(state, action.chats);
|
||||||
case STREAMING_CHAT_UPDATE:
|
case STREAMING_CHAT_UPDATE:
|
||||||
if (action.chat.last_message &&
|
if (action.chat.last_message &&
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CHATS_FETCH_SUCCESS,
|
CHATS_FETCH_SUCCESS,
|
||||||
|
CHATS_EXPAND_SUCCESS,
|
||||||
CHAT_MESSAGES_FETCH_SUCCESS,
|
CHAT_MESSAGES_FETCH_SUCCESS,
|
||||||
CHAT_MESSAGE_SEND_REQUEST,
|
CHAT_MESSAGE_SEND_REQUEST,
|
||||||
CHAT_MESSAGE_SEND_SUCCESS,
|
CHAT_MESSAGE_SEND_SUCCESS,
|
||||||
|
@ -38,6 +39,7 @@ export default function chatMessages(state = initialState, action) {
|
||||||
pending: true,
|
pending: true,
|
||||||
}));
|
}));
|
||||||
case CHATS_FETCH_SUCCESS:
|
case CHATS_FETCH_SUCCESS:
|
||||||
|
case CHATS_EXPAND_SUCCESS:
|
||||||
return importLastMessages(state, fromJS(action.chats));
|
return importLastMessages(state, fromJS(action.chats));
|
||||||
case CHAT_MESSAGES_FETCH_SUCCESS:
|
case CHAT_MESSAGES_FETCH_SUCCESS:
|
||||||
return importMessages(state, fromJS(action.chatMessages));
|
return importMessages(state, fromJS(action.chatMessages));
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import {
|
import {
|
||||||
CHATS_FETCH_SUCCESS,
|
CHATS_FETCH_SUCCESS,
|
||||||
|
CHATS_FETCH_REQUEST,
|
||||||
|
CHATS_EXPAND_SUCCESS,
|
||||||
|
CHATS_EXPAND_REQUEST,
|
||||||
CHAT_FETCH_SUCCESS,
|
CHAT_FETCH_SUCCESS,
|
||||||
CHAT_READ_SUCCESS,
|
CHAT_READ_SUCCESS,
|
||||||
CHAT_READ_REQUEST,
|
CHAT_READ_REQUEST,
|
||||||
|
@ -8,17 +11,29 @@ import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||||
import { normalizeChat } from 'soapbox/actions/importer/normalizer';
|
import { normalizeChat } from 'soapbox/actions/importer/normalizer';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const importChat = (state, chat) => state.set(chat.id, fromJS(normalizeChat(chat)));
|
const importChat = (state, chat) => state.setIn(['items', chat.id], fromJS(normalizeChat(chat)));
|
||||||
|
|
||||||
const importChats = (state, chats) =>
|
const importChats = (state, chats, next) =>
|
||||||
state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat)));
|
state.withMutations(mutable => {
|
||||||
|
if (next !== undefined) mutable.set('next', next);
|
||||||
|
chats.forEach(chat => importChat(mutable, chat));
|
||||||
|
mutable.set('loading', false);
|
||||||
|
});
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
items: ImmutableMap({}),
|
||||||
|
});
|
||||||
|
|
||||||
export default function chats(state = initialState, action) {
|
export default function chats(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case CHATS_FETCH_REQUEST:
|
||||||
|
case CHATS_EXPAND_REQUEST:
|
||||||
|
return state.set('loading', true);
|
||||||
case CHATS_FETCH_SUCCESS:
|
case CHATS_FETCH_SUCCESS:
|
||||||
return importChats(state, action.chats);
|
case CHATS_EXPAND_SUCCESS:
|
||||||
|
return importChats(state, action.chats, action.next);
|
||||||
case STREAMING_CHAT_UPDATE:
|
case STREAMING_CHAT_UPDATE:
|
||||||
return importChats(state, [action.chat]);
|
return importChats(state, [action.chat]);
|
||||||
case CHAT_FETCH_SUCCESS:
|
case CHAT_FETCH_SUCCESS:
|
||||||
|
|
|
@ -209,8 +209,8 @@ export const getAccountGallery = createSelector([
|
||||||
export const makeGetChat = () => {
|
export const makeGetChat = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[
|
[
|
||||||
(state, { id }) => state.getIn(['chats', id]),
|
(state, { id }) => state.getIn(['chats', 'items', id]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['chats', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['chats', 'items', id, 'account'])]),
|
||||||
(state, { last_message }) => state.getIn(['chat_messages', last_message]),
|
(state, { last_message }) => state.getIn(['chat_messages', last_message]),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ export const getFeatures = createSelector([
|
||||||
importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'),
|
||||||
emailList: features.includes('email_list'),
|
emailList: features.includes('email_list'),
|
||||||
chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
|
chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
|
||||||
|
chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
|
||||||
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
|
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
|
||||||
federating: federation.get('enabled', true), // Assume true unless explicitly false
|
federating: federation.get('enabled', true), // Assume true unless explicitly false
|
||||||
richText: v.software === PLEROMA,
|
richText: v.software === PLEROMA,
|
||||||
|
|
|
@ -116,6 +116,10 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-toggle .react-toggle-thumb {
|
.audio-toggle .react-toggle-thumb {
|
||||||
|
@ -219,7 +223,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list {
|
.chat-list {
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
@ -233,6 +236,10 @@
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content--placeholder,
|
.status__content--placeholder,
|
||||||
.display-name--placeholder {
|
.display-name--placeholder,
|
||||||
|
.chat-list-item--placeholder .chat__last-message {
|
||||||
letter-spacing: -1px;
|
letter-spacing: -1px;
|
||||||
color: var(--brand-color) !important;
|
color: var(--brand-color) !important;
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
|
|
Loading…
Reference in a new issue