Merge pull request #562 from sharetribe/tx-message-paging

Order message paging
This commit is contained in:
Kimmo Puputti 2017-11-23 09:11:36 +02:00 committed by GitHub
commit f3fc4acd7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 14 deletions

View file

@ -90,11 +90,14 @@ export class OrderDetailsPanelComponent extends Component {
className,
currentUser,
transaction,
totalMessages,
messages,
initialMessageFailed,
fetchMessagesInProgress,
fetchMessagesError,
sendMessageInProgress,
sendMessageError,
onShowMoreMessages,
onSendMessage,
onResetForm,
intl,
@ -170,8 +173,10 @@ export class OrderDetailsPanelComponent extends Component {
const txTransitions = currentTransaction.attributes.transitions
? currentTransaction.attributes.transitions
: [];
const hasOlderMessages = false; // TODO
const showOlderMessages = () => null; // TODO
const hasOlderMessages = totalMessages > messages.length;
const handleShowOlderMessages = () => {
onShowMoreMessages(currentTransaction.id);
};
const showFeed =
messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
const feedContainer = showFeed ? (
@ -194,8 +199,9 @@ export class OrderDetailsPanelComponent extends Component {
messages={messages}
transaction={currentTransaction}
currentUser={currentUser}
hasOlderMessages={hasOlderMessages}
onShowOlderMessages={showOlderMessages}
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
onShowOlderMessages={handleShowOlderMessages}
fetchMessagesInProgress={fetchMessagesInProgress}
/>
</div>
) : null;
@ -337,7 +343,7 @@ OrderDetailsPanelComponent.defaultProps = {
sendMessageError: null,
};
const { string, arrayOf, bool, func } = PropTypes;
const { string, arrayOf, bool, func, number } = PropTypes;
OrderDetailsPanelComponent.propTypes = {
rootClassName: string,
@ -345,11 +351,14 @@ OrderDetailsPanelComponent.propTypes = {
currentUser: propTypes.currentUser,
transaction: propTypes.transaction.isRequired,
totalMessages: number.isRequired,
messages: arrayOf(propTypes.message).isRequired,
initialMessageFailed: bool.isRequired,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
onShowMoreMessages: func.isRequired,
onSendMessage: func.isRequired,
onResetForm: func.isRequired,

View file

@ -70,12 +70,15 @@ describe('OrderDetailsPanel', () => {
const panelBaseProps = {
intl: fakeIntl,
currentUser: createCurrentUser('user2'),
totalMessages: 2,
messages: [
createMessage('msg1', {}, { sender: createUser('user1') }),
createMessage('msg2', {}, { sender: createUser('user2') }),
],
initialMessageFailed: false,
fetchMessagesInProgress: false,
sendMessageInProgress: false,
onShowMoreMessages: noop,
onSendMessage: noop,
onResetForm: noop,
};

View file

@ -253,6 +253,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [
@ -884,6 +885,7 @@ exports[`OrderDetailsPanel autodeclined matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [
@ -1527,6 +1529,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [
@ -2158,6 +2161,7 @@ exports[`OrderDetailsPanel declined matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [
@ -2789,6 +2793,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [
@ -3443,6 +3448,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
"type": "currentUser",
}
}
fetchMessagesInProgress={false}
hasOlderMessages={false}
messages={
Array [

View file

@ -4,6 +4,8 @@ import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { updatedEntities, denormalisedEntities } from '../../util/data';
const MESSAGES_PAGE_SIZE = 100;
// ================ Action types ================ //
export const SET_INITAL_VALUES = 'app/OrderPage/SET_INITIAL_VALUES';
@ -28,6 +30,7 @@ const initialState = {
transactionRef: null,
fetchMessagesInProgress: false,
fetchMessagesError: null,
totalMessages: 0,
messages: [],
messageSendingFailedToTransaction: null,
sendMessageInProgress: false,
@ -53,7 +56,12 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
case FETCH_MESSAGES_REQUEST:
return { ...state, fetchMessagesInProgress: true, fetchMessagesError: null };
case FETCH_MESSAGES_SUCCESS:
return { ...state, fetchMessagesInProgress: false, messages: payload };
return {
...state,
fetchMessagesInProgress: false,
messages: payload.messages,
totalMessages: payload.totalItems,
};
case FETCH_MESSAGES_ERROR:
return { ...state, fetchMessagesInProgress: false, fetchMessagesError: payload };
@ -69,6 +77,12 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
}
}
// ================ Selectors ================ //
export const fetchedMessagesCount = state => {
return state.OrderPage.messages.length;
};
// ================ Action creators ================ //
export const setInitialValues = initialValues => ({
@ -81,7 +95,10 @@ const fetchOrderSuccess = response => ({ type: FETCH_ORDER_SUCCESS, payload: res
const fetchOrderError = e => ({ type: FETCH_ORDER_ERROR, error: true, payload: e });
const fetchMessagesRequest = () => ({ type: FETCH_MESSAGES_REQUEST });
const fetchMessagesSuccess = messages => ({ type: FETCH_MESSAGES_SUCCESS, payload: messages });
const fetchMessagesSuccess = (messages, totalItems) => ({
type: FETCH_MESSAGES_SUCCESS,
payload: { messages, totalItems },
});
const fetchMessagesError = e => ({ type: FETCH_MESSAGES_ERROR, error: true, payload: e });
const sendMessageRequest = () => ({ type: SEND_MESSAGE_REQUEST });
@ -132,17 +149,17 @@ export const fetchOrder = id => (dispatch, getState, sdk) => {
});
};
export const fetchMessages = txId => (dispatch, getState, sdk) => {
const fetchMessages = (txId, paging) => (dispatch, getState, sdk) => {
dispatch(fetchMessagesRequest());
return sdk.messages
.query({ transaction_id: txId, include: ['sender', 'sender.profileImage'] })
.query({ transaction_id: txId, include: ['sender', 'sender.profileImage'], ...paging })
.then(response => {
const entities = updatedEntities({}, response.data);
const messageIds = response.data.data.map(d => d.id);
const denormalized = denormalisedEntities(entities, 'message', messageIds);
dispatch(fetchMessagesSuccess(denormalized));
dispatch(fetchMessagesSuccess(denormalized, response.data.meta.totalItems));
})
.catch(e => {
dispatch(fetchMessagesError(storableError(e)));
@ -150,6 +167,25 @@ export const fetchMessages = txId => (dispatch, getState, sdk) => {
});
};
const fetchNLatestMessages = (txId, n) => (dispatch, getState, sdk) => {
const paging = {
page: 1,
per_page: n,
};
return dispatch(fetchMessages(txId, paging));
};
export const fetchMoreMessages = txId => (dispatch, getState, sdk) => {
// This is clearly not the most sophisticated solution, but the
// default page size should be large enough that seeing the "Show
// older" button is very rare.
//
// This compromises on the network request size in favor of correct
// page offset handling that is quite tricky.
const messagesToFetch = fetchedMessagesCount(getState()) + MESSAGES_PAGE_SIZE;
return dispatch(fetchNLatestMessages(txId, messagesToFetch));
};
// loadData is a collection of async calls that need to be made
// before page has all the info it needs to render itself
export const loadData = params => dispatch => {
@ -159,7 +195,10 @@ export const loadData = params => dispatch => {
dispatch(setInitialValues({ sendMessageError: null }));
// Order (i.e. transaction entity in API, but from buyers perspective) contains order details
return Promise.all([dispatch(fetchOrder(orderId)), dispatch(fetchMessages(orderId))]);
return Promise.all([
dispatch(fetchOrder(orderId)),
dispatch(fetchNLatestMessages(orderId, MESSAGES_PAGE_SIZE)),
]);
};
export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => {
@ -169,7 +208,14 @@ export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => {
.send({ transactionId: orderId, content: message })
.then(response => {
const messageId = response.data.data.id;
return dispatch(fetchMessages(orderId))
// Try to keep the fetched messages in the store by fetching the
// sent message and as much messages as there were before. Some
// of the older ones might be lost if there are also other new
// messages received in addition to this message.
const messagesToFetch = fetchedMessagesCount(getState()) + 1;
return dispatch(fetchNLatestMessages(orderId, messagesToFetch))
.then(() => {
dispatch(sendMessageSuccess());
return messageId;

View file

@ -21,7 +21,7 @@ import {
} from '../../components';
import { TopbarContainer } from '../../containers';
import { loadData, setInitialValues, sendMessage } from './OrderPage.duck';
import { loadData, setInitialValues, sendMessage, fetchMoreMessages } from './OrderPage.duck';
import css from './OrderPage.css';
// OrderPage handles data loading
@ -30,11 +30,14 @@ export const OrderPageComponent = props => {
const {
currentUser,
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
sendMessageError,
onShowMoreMessages,
onSendMessage,
onResetForm,
intl,
@ -86,11 +89,14 @@ export const OrderPageComponent = props => {
className={detailsClassName}
currentUser={currentUser}
transaction={currentTransaction}
fetchMessagesInProgress={fetchMessagesInProgress}
totalMessages={totalMessages}
messages={messages}
initialMessageFailed={initialMessageFailed}
fetchMessagesError={fetchMessagesError}
sendMessageInProgress={sendMessageInProgress}
sendMessageError={sendMessageError}
onShowMoreMessages={onShowMoreMessages}
onSendMessage={onSendMessage}
onResetForm={onResetForm}
/>
@ -125,7 +131,7 @@ OrderPageComponent.defaultProps = {
transaction: null,
};
const { bool, oneOf, shape, string, array, func } = PropTypes;
const { bool, oneOf, shape, string, array, func, number } = PropTypes;
OrderPageComponent.propTypes = {
params: shape({ id: string }).isRequired,
@ -133,13 +139,16 @@ OrderPageComponent.propTypes = {
currentUser: propTypes.currentUser,
fetchOrderError: propTypes.error,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
totalMessages: number.isRequired,
messages: array.isRequired,
messageSendingFailedToTransaction: propTypes.uuid,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
scrollingDisabled: bool.isRequired,
transaction: propTypes.transaction,
onShowMoreMessages: func.isRequired,
onSendMessage: func.isRequired,
onResetForm: func.isRequired,
@ -152,7 +161,9 @@ const mapStateToProps = state => {
const {
fetchOrderError,
transactionRef,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
@ -164,7 +175,9 @@ const mapStateToProps = state => {
return {
currentUser,
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
@ -175,6 +188,7 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
onShowMoreMessages: orderId => dispatch(fetchMoreMessages(orderId)),
onSendMessage: (orderId, message) => dispatch(sendMessage(orderId, message)),
onResetForm: formName => dispatch(resetForm(formName)),
});

View file

@ -33,10 +33,13 @@ describe('OrderPage', () => {
tab: 'details',
currentUser: createCurrentUser('customer1'),
totalMessages: 0,
messages: [],
fetchMessagesInProgress: false,
sendMessageInProgress: false,
scrollingDisabled: false,
transaction,
onShowMoreMessages: noop,
onSendMessage: noop,
onResetForm: noop,

View file

@ -42,12 +42,15 @@ exports[`OrderPage matches snapshot 1`] = `
}
}
fetchMessagesError={null}
fetchMessagesInProgress={false}
initialMessageFailed={false}
messages={Array []}
onResetForm={[Function]}
onSendMessage={[Function]}
onShowMoreMessages={[Function]}
sendMessageError={null}
sendMessageInProgress={false}
totalMessages={0}
transaction={
Object {
"attributes": Object {