wip chat pagination

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
merge-requests/911/head
marcin mikołajczak 2021-12-12 20:45:59 +01:00
rodzic f52b69ebbb
commit 12e4c6c083
21 zmienionych plików z 170 dodań i 61 usunięć

Wyświetl plik

@ -1,4 +1,4 @@
import api from '../api';
import api, { getLinks } from '../api';
import { getSettings, changeSetting } from 'soapbox/actions/settings';
import { v4 as uuidv4 } from 'uuid';
import { Map as ImmutableMap } from 'immutable';
@ -7,6 +7,10 @@ export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
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_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
@ -30,14 +34,35 @@ export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
export function fetchChats() {
return (dispatch, getState) => {
dispatch({ type: CHATS_FETCH_REQUEST });
return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => {
dispatch({ type: CHATS_FETCH_SUCCESS, chats: data });
api(getState).get('/api/v2/pleroma/chats').then((response) => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null });
}).catch(error => {
dispatch({ type: CHATS_FETCH_FAIL, error });
});
};
}
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 });
});
};
}
export function fetchChatMessages(chatId, maxId = null) {
return (dispatch, getState) => {
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
@ -140,7 +165,7 @@ export function startChat(accountId) {
export function markChatRead(chatId, lastReadId) {
return (dispatch, getState) => {
const chat = getState().getIn(['chats', chatId]);
const chat = getState().getIn(['chats', 'items', chatId]);
if (!lastReadId) lastReadId = chat.get('last_message');
if (chat.get('unread') < 1) return;

Wyświetl plik

@ -11,7 +11,7 @@ FaviconService.initFaviconService();
const getNotifTotals = state => {
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 approvals = state.getIn(['admin', 'awaitingApproval']).count();
return notifications + chats + reports + approvals;

Wyświetl plik

@ -25,7 +25,7 @@ const mapStateToProps = state => {
account,
logo: getSoapboxConfig(state).get('logo'),
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,
baseURL: getBaseURL(account),
settings: getSettings(state),

Wyświetl plik

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
import MoreFollows from './more_follows';
@ -45,6 +46,7 @@ class ScrollableList extends PureComponent {
placeholderCount: PropTypes.number,
autoload: PropTypes.bool,
onRefresh: PropTypes.func,
className: PropTypes.string,
};
state = {
@ -240,16 +242,22 @@ class ScrollableList extends PureComponent {
}
renderLoading = () => {
const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
if (Placeholder && placeholderCount > 0) {
return Array(placeholderCount).fill().map((_, i) => (
<Placeholder key={i} />
));
return (
<div className={classNames('slist slist--flex', className)}>
<div role='feed' className='item-list'>
{Array(placeholderCount).fill().map((_, i) => (
<Placeholder key={i} />
))}
</div>
</div>
);
}
return (
<div className='slist slist--flex'>
<div className={classNames('slist slist--flex', className)}>
<div role='feed' className='item-list'>
{prepend}
</div>
@ -262,10 +270,10 @@ class ScrollableList extends PureComponent {
}
renderEmptyMessage = () => {
const { prepend, alwaysPrepend, emptyMessage } = this.props;
const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
return (
<div className='slist slist--flex' ref={this.setRef}>
<div className={classNames('slist slist--flex', className)} ref={this.setRef}>
{alwaysPrepend && prepend}
<div className='empty-column-indicator'>
@ -276,13 +284,13 @@ class ScrollableList extends PureComponent {
}
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 trackScroll = true; //placeholder
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
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'>
{prepend}

Wyświetl plik

@ -21,7 +21,7 @@ const mapStateToProps = state => {
account: state.getIn(['accounts', me]),
logo: getSoapboxConfig(state).get('logo'),
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,
features: getFeatures(instance),
};

Wyświetl plik

@ -17,7 +17,7 @@ import { displayFqn } from 'soapbox/utils/state';
const mapStateToProps = (state, { params }) => {
const getChat = makeGetChat();
const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
const chat = state.getIn(['chats', 'items', params.chatId], ImmutableMap()).toJS();
return {
me: state.get('me'),

Wyświetl plik

@ -15,7 +15,7 @@ const makeMapStateToProps = () => {
const getChat = makeGetChat();
const mapStateToProps = (state, { chatId }) => {
const chat = state.getIn(['chats', chatId]);
const chat = state.getIn(['chats', 'items', chatId]);
return {
chat: chat ? getChat(state, chat.toJS()) : undefined,

Wyświetl plik

@ -23,7 +23,7 @@ const messages = defineMessages({
const mapStateToProps = (state, { chatId }) => ({
me: state.get('me'),
chat: state.getIn(['chats', chatId]),
chat: state.getIn(['chats', 'items', chatId]),
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
});

Wyświetl plik

@ -2,11 +2,19 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
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 { 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 { 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 => (
chats
.toList()
@ -32,7 +40,9 @@ const makeMapStateToProps = () => {
);
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;
@ -47,28 +57,40 @@ class ChatList extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
chatIds: ImmutablePropTypes.list,
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() {
const { chatIds, emptyMessage } = this.props;
const { intl, chatIds, hasMore, isLoading } = this.props;
return (
<div className='chat-list'>
<div className='chat-list__content'>
{chatIds.count() === 0 &&
<div className='empty-column-indicator'>{emptyMessage}</div>
}
{chatIds.map(chatId => (
<div key={chatId} className='chat-list-item'>
<Chat
chatId={chatId}
onClick={this.props.onClickChat}
/>
</div>
))}
</div>
</div>
<ScrollableList
className='chat-list'
scrollKey='awaiting-approval'
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 => (
<div key={chatId} className='chat-list-item'>
<Chat
chatId={chatId}
onClick={this.props.onClickChat}
/>
</div>
))}
</ScrollableList>
);
}

Wyświetl plik

@ -20,7 +20,7 @@ const messages = defineMessages({
});
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);
};
@ -30,7 +30,7 @@ const normalizePanes = (chats, panes = ImmutableList()) => (
);
const makeNormalizeChatPanes = () => createSelector([
state => state.get('chats'),
state => state.getIn(['chats', 'items']),
state => getSettings(state).getIn(['chats', 'panes']),
], normalizePanes);
@ -93,7 +93,6 @@ class ChatPanes extends ImmutablePureComponent {
<>
<ChatList
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
placeholder={intl.formatMessage(messages.searchPlaceholder)}

Wyświetl plik

@ -22,7 +22,7 @@ const makeMapStateToProps = () => {
const getChat = makeGetChat();
const mapStateToProps = (state, { chatId }) => {
const chat = state.getIn(['chats', chatId]);
const chat = state.getIn(['chats', 'items', chatId]);
return {
me: state.get('me'),

Wyświetl plik

@ -4,11 +4,10 @@ import { connect } from 'react-redux';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
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 AudioToggle from 'soapbox/features/chats/components/audio_toggle';
import AccountSearch from 'soapbox/components/account_search';
import PullToRefresh from 'soapbox/components/pull_to_refresh';
const messages = defineMessages({
title: { id: 'column.chats', defaultMessage: 'Chats' },
@ -60,12 +59,10 @@ class ChatIndex extends React.PureComponent {
onSelected={this.handleSuggestion}
/>
<PullToRefresh onRefresh={this.handleRefresh}>
<ChatList
onClickChat={this.handleClickChat}
emptyMessage={<FormattedMessage id='chat_panels.main_window.empty' defaultMessage="No chats found. To start a chat, visit a user's profile." />}
/>
</PullToRefresh>
<ChatList
onClickChat={this.handleClickChat}
onRefresh={this.handleRefresh}
/>
</Column>
);
}

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -171,7 +171,7 @@ const mapStateToProps = state => {
logo: getSoapboxConfig(state).get('logo'),
features: getFeatures(instance),
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,
};
};

Wyświetl plik

@ -3,7 +3,7 @@ import {
ACCOUNTS_IMPORT,
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
} 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 { normalizeAccount as normalizeAccount2 } from 'soapbox/actions/importer/normalizer';
import {
@ -208,6 +208,7 @@ export default function accounts(state = initialState, action) {
username: action.username,
}));
case CHATS_FETCH_SUCCESS:
case CHATS_EXPAND_SUCCESS:
return importAccountsFromChats(state, action.chats);
case CHAT_FETCH_SUCCESS:
case STREAMING_CHAT_UPDATE:

Wyświetl plik

@ -1,5 +1,6 @@
import {
CHATS_FETCH_SUCCESS,
CHATS_EXPAND_SUCCESS,
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
@ -47,6 +48,7 @@ export default function chatMessageLists(state = initialState, action) {
case CHAT_MESSAGE_SEND_REQUEST:
return updateList(state, action.chatId, [action.uuid]);
case CHATS_FETCH_SUCCESS:
case CHATS_EXPAND_SUCCESS:
return importLastMessages(state, action.chats);
case STREAMING_CHAT_UPDATE:
if (action.chat.last_message &&

Wyświetl plik

@ -1,5 +1,6 @@
import {
CHATS_FETCH_SUCCESS,
CHATS_EXPAND_SUCCESS,
CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_MESSAGE_SEND_REQUEST,
CHAT_MESSAGE_SEND_SUCCESS,
@ -38,6 +39,7 @@ export default function chatMessages(state = initialState, action) {
pending: true,
}));
case CHATS_FETCH_SUCCESS:
case CHATS_EXPAND_SUCCESS:
return importLastMessages(state, fromJS(action.chats));
case CHAT_MESSAGES_FETCH_SUCCESS:
return importMessages(state, fromJS(action.chatMessages));

Wyświetl plik

@ -1,5 +1,8 @@
import {
CHATS_FETCH_SUCCESS,
CHATS_FETCH_REQUEST,
CHATS_EXPAND_SUCCESS,
CHATS_EXPAND_REQUEST,
CHAT_FETCH_SUCCESS,
CHAT_READ_SUCCESS,
CHAT_READ_REQUEST,
@ -8,17 +11,29 @@ import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { normalizeChat } from 'soapbox/actions/importer/normalizer';
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) =>
state.withMutations(mutable => chats.forEach(chat => importChat(mutable, chat)));
const importChats = (state, chats, next) =>
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) {
switch(action.type) {
case CHATS_FETCH_REQUEST:
case CHATS_EXPAND_REQUEST:
return state.set('loading', true);
case CHATS_FETCH_SUCCESS:
return importChats(state, action.chats);
case CHATS_EXPAND_SUCCESS:
return importChats(state, action.chats, action.next);
case STREAMING_CHAT_UPDATE:
return importChats(state, [action.chat]);
case CHAT_FETCH_SUCCESS:

Wyświetl plik

@ -209,8 +209,8 @@ export const getAccountGallery = createSelector([
export const makeGetChat = () => {
return createSelector(
[
(state, { id }) => state.getIn(['chats', id]),
(state, { id }) => state.getIn(['accounts', state.getIn(['chats', id, 'account'])]),
(state, { id }) => state.getIn(['chats', 'items', id]),
(state, { id }) => state.getIn(['accounts', state.getIn(['chats', 'items', id, 'account'])]),
(state, { last_message }) => state.getIn(['chat_messages', last_message]),
],

Wyświetl plik

@ -116,6 +116,10 @@
flex-direction: column;
overflow: hidden;
}
.chat-list {
overflow-y: auto;
}
}
.audio-toggle .react-toggle-thumb {
@ -219,7 +223,6 @@
}
.chat-list {
overflow-y: auto;
flex: 1;
&__content {
@ -233,6 +236,10 @@
align-items: start;
}
.account {
border-bottom: none;
}
.account__display-name {
position: relative;

Wyświetl plik

@ -46,7 +46,8 @@
}
.status__content--placeholder,
.display-name--placeholder {
.display-name--placeholder,
.chat-list-item--placeholder .chat__last-message {
letter-spacing: -1px;
color: var(--brand-color) !important;
opacity: 0.1;