Fetch messages fix (maximum page size is 100 on API)

This commit is contained in:
Vesa Luusua 2017-12-04 19:41:37 +02:00
parent ce75cee279
commit 01eff1ef3e
12 changed files with 127 additions and 90 deletions

View file

@ -136,7 +136,8 @@ export class OrderDetailsPanelComponent extends Component {
className,
currentUser,
transaction,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
initialMessageFailed,
fetchMessagesInProgress,
@ -222,7 +223,7 @@ export class OrderDetailsPanelComponent extends Component {
const txTransitions = currentTransaction.attributes.transitions
? currentTransaction.attributes.transitions
: [];
const hasOlderMessages = totalMessages > messages.length;
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
const handleShowOlderMessages = () => {
onShowMoreMessages(currentTransaction.id);
};
@ -413,7 +414,8 @@ OrderDetailsPanelComponent.propTypes = {
currentUser: propTypes.currentUser,
transaction: propTypes.transaction.isRequired,
totalMessages: number.isRequired,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: arrayOf(propTypes.message).isRequired,
initialMessageFailed: bool.isRequired,
fetchMessagesInProgress: bool.isRequired,

View file

@ -71,6 +71,8 @@ describe('OrderDetailsPanel', () => {
intl: fakeIntl,
currentUser: createCurrentUser('user2'),
totalMessages: 2,
totalMessagePages: 1,
oldestMessagePageFetched: 1,
messages: [
createMessage('msg1', {}, { sender: createUser('user1') }),
createMessage('msg2', {}, { sender: createUser('user2') }),

View file

@ -135,7 +135,8 @@ export class SaleDetailsPanelComponent extends Component {
declineSaleError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
@ -206,7 +207,7 @@ export class SaleDetailsPanelComponent extends Component {
const txTransitions = currentTransaction.attributes.transitions
? currentTransaction.attributes.transitions
: [];
const hasOlderMessages = totalMessages > messages.length;
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
const handleShowOlderMessages = () => {
onShowMoreMessages(currentTransaction.id);
};
@ -436,7 +437,8 @@ SaleDetailsPanelComponent.propTypes = {
declineSaleError: propTypes.error,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
totalMessages: number.isRequired,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: arrayOf(propTypes.message).isRequired,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,

View file

@ -74,6 +74,8 @@ describe('SaleDetailsPanel', () => {
declineInProgress: false,
currentUser: createCurrentUser('user1'),
totalMessages: 2,
totalMessagePages: 1,
oldestMessagePageFetched: 1,
messages: [
createMessage('msg1', {}, { sender: createUser('user1') }),
createMessage('msg2', {}, { sender: createUser('user2') }),

View file

@ -36,6 +36,8 @@ const initialState = {
fetchMessagesInProgress: false,
fetchMessagesError: null,
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
messageSendingFailedToTransaction: null,
sendMessageInProgress: false,
@ -44,6 +46,15 @@ const initialState = {
sendReviewError: null,
};
// Merge entity arrays using ids, so that conflicting items in newer array (b) overwrite old values (a).
// const a = [{ id: { uuid: 1 } }, { id: { uuid: 3 } }];
// const b = [{ id: : { uuid: 2 } }, { id: : { uuid: 1 } }];
// mergeEntityArrays(a, b)
// => [{ id: { uuid: 3 } }, { id: : { uuid: 2 } }, { id: : { uuid: 1 } }]
const mergeEntityArrays = (a, b) => {
return a.filter(aEntity => !b.find(bEntity => aEntity.id.uuid === bEntity.id.uuid)).concat(b);
};
export default function checkoutPageReducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
@ -62,13 +73,20 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
case FETCH_MESSAGES_REQUEST:
return { ...state, fetchMessagesInProgress: true, fetchMessagesError: null };
case FETCH_MESSAGES_SUCCESS:
case FETCH_MESSAGES_SUCCESS: {
const oldestMessagePageFetched =
state.oldestMessagePageFetched > payload.page
? state.oldestMessagePageFetched
: payload.page;
return {
...state,
fetchMessagesInProgress: false,
messages: payload.messages,
messages: mergeEntityArrays(state.messages, payload.messages),
totalMessages: payload.totalItems,
totalMessagePages: payload.totalPages,
oldestMessagePageFetched,
};
}
case FETCH_MESSAGES_ERROR:
return { ...state, fetchMessagesInProgress: false, fetchMessagesError: payload };
@ -91,12 +109,6 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
}
}
// ================ Selectors ================ //
export const fetchedMessagesCount = state => {
return state.OrderPage.messages.length;
};
// ================ Action creators ================ //
export const setInitialValues = initialValues => ({
@ -109,9 +121,9 @@ 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, totalItems) => ({
const fetchMessagesSuccess = (messages, pagination) => ({
type: FETCH_MESSAGES_SUCCESS,
payload: { messages, totalItems },
payload: { messages, ...pagination },
});
const fetchMessagesError = e => ({ type: FETCH_MESSAGES_ERROR, error: true, payload: e });
@ -181,7 +193,8 @@ export const fetchOrder = id => (dispatch, getState, sdk) => {
});
};
const fetchMessages = (txId, paging) => (dispatch, getState, sdk) => {
const fetchMessages = (txId, page) => (dispatch, getState, sdk) => {
const paging = { page, per_page: MESSAGES_PAGE_SIZE };
dispatch(fetchMessagesRequest());
return sdk.messages
@ -189,9 +202,11 @@ const fetchMessages = (txId, paging) => (dispatch, getState, sdk) => {
.then(response => {
const entities = updatedEntities({}, response.data);
const messageIds = response.data.data.map(d => d.id);
const denormalized = denormalisedEntities(entities, 'message', messageIds);
const denormalizedMessages = denormalisedEntities(entities, 'message', messageIds);
const { totalItems, totalPages, page: fetchedPage } = response.data.meta;
const pagination = { totalItems, totalPages, page: fetchedPage };
dispatch(fetchMessagesSuccess(denormalized, response.data.meta.totalItems));
dispatch(fetchMessagesSuccess(denormalizedMessages, pagination));
})
.catch(e => {
dispatch(fetchMessagesError(storableError(e)));
@ -199,23 +214,15 @@ const fetchMessages = (txId, paging) => (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));
const state = getState();
const { oldestMessagePageFetched, totalMessagePages } = state.OrderPage;
const hasMoreOldMessages = totalMessagePages > oldestMessagePageFetched;
// In case there're no more old pages left we default to fetching the current cursor position
const nextPage = hasMoreOldMessages ? oldestMessagePageFetched + 1 : oldestMessagePageFetched;
return dispatch(fetchMessages(txId, nextPage));
};
export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => {
@ -226,13 +233,11 @@ export const sendMessage = (orderId, message) => (dispatch, getState, sdk) => {
.then(response => {
const messageId = response.data.data.id;
// 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))
// We fetch the first page again to add sent message to the page data
// and update possible incoming messages too.
// TODO if there're more than 100 incoming messages,
// this should loop through most recent pages instead of fetching just the first one.
return dispatch(fetchMessages(orderId, 1))
.then(() => {
dispatch(sendMessageSuccess());
return messageId;
@ -322,8 +327,5 @@ export const loadData = params => dispatch => {
dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null }));
// Order (i.e. transaction entity in API, but from buyers perspective) contains order details
return Promise.all([
dispatch(fetchOrder(orderId)),
dispatch(fetchNLatestMessages(orderId, MESSAGES_PAGE_SIZE)),
]);
return Promise.all([dispatch(fetchOrder(orderId)), dispatch(fetchMessages(orderId, 1))]);
};

View file

@ -38,7 +38,8 @@ export const OrderPageComponent = props => {
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
@ -100,7 +101,8 @@ export const OrderPageComponent = props => {
currentUser={currentUser}
transaction={currentTransaction}
fetchMessagesInProgress={fetchMessagesInProgress}
totalMessages={totalMessages}
totalMessagePages={totalMessagePages}
oldestMessagePageFetched={oldestMessagePageFetched}
messages={messages}
initialMessageFailed={initialMessageFailed}
fetchMessagesError={fetchMessagesError}
@ -155,7 +157,8 @@ OrderPageComponent.propTypes = {
fetchOrderError: propTypes.error,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
totalMessages: number.isRequired,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: array.isRequired,
messageSendingFailedToTransaction: propTypes.uuid,
sendMessageInProgress: bool.isRequired,
@ -177,7 +180,8 @@ const mapStateToProps = state => {
transactionRef,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
@ -193,7 +197,8 @@ const mapStateToProps = state => {
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,

View file

@ -34,6 +34,8 @@ describe('OrderPage', () => {
currentUser: createCurrentUser('customer1'),
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
fetchMessagesInProgress: false,
sendMessageInProgress: false,

View file

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

View file

@ -50,6 +50,8 @@ const initialState = {
fetchMessagesInProgress: false,
fetchMessagesError: null,
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
sendMessageInProgress: false,
sendMessageError: null,
@ -57,6 +59,15 @@ const initialState = {
sendReviewError: null,
};
// Merge entity arrays using ids, so that conflicting items in newer array (b) overwrite old values (a).
// const a = [{ id: { uuid: 1 } }, { id: { uuid: 3 } }];
// const b = [{ id: : { uuid: 2 } }, { id: : { uuid: 1 } }];
// mergeEntityArrays(a, b)
// => [{ id: { uuid: 3 } }, { id: : { uuid: 2 } }, { id: : { uuid: 1 } }]
const mergeEntityArrays = (a, b) => {
return a.filter(aEntity => !b.find(bEntity => aEntity.id.uuid === bEntity.id.uuid)).concat(b);
};
export default function checkoutPageReducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
@ -89,13 +100,20 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
case FETCH_MESSAGES_REQUEST:
return { ...state, fetchMessagesInProgress: true, fetchMessagesError: null };
case FETCH_MESSAGES_SUCCESS:
case FETCH_MESSAGES_SUCCESS: {
const oldestMessagePageFetched =
state.oldestMessagePageFetched > payload.page
? state.oldestMessagePageFetched
: payload.page;
return {
...state,
fetchMessagesInProgress: false,
messages: payload.messages,
messages: mergeEntityArrays(state.messages, payload.messages),
totalMessages: payload.totalItems,
totalMessagePages: payload.totalPages,
oldestMessagePageFetched,
};
}
case FETCH_MESSAGES_ERROR:
return { ...state, fetchMessagesInProgress: false, fetchMessagesError: payload };
@ -124,10 +142,6 @@ export const acceptOrDeclineInProgress = state => {
return state.SalePage.acceptInProgress || state.SalePage.declineInProgress;
};
export const fetchedMessagesCount = state => {
return state.SalePage.messages.length;
};
// ================ Action creators ================ //
export const setInitialValues = initialValues => ({
type: SET_INITAL_VALUES,
@ -147,9 +161,9 @@ const declineSaleSuccess = () => ({ type: DECLINE_SALE_SUCCESS });
const declineSaleError = e => ({ type: DECLINE_SALE_ERROR, error: true, payload: e });
const fetchMessagesRequest = () => ({ type: FETCH_MESSAGES_REQUEST });
const fetchMessagesSuccess = (messages, totalItems) => ({
const fetchMessagesSuccess = (messages, pagination) => ({
type: FETCH_MESSAGES_SUCCESS,
payload: { messages, totalItems },
payload: { messages, ...pagination },
});
const fetchMessagesError = e => ({ type: FETCH_MESSAGES_ERROR, error: true, payload: e });
@ -257,7 +271,8 @@ export const declineSale = id => (dispatch, getState, sdk) => {
});
};
const fetchMessages = (txId, paging) => (dispatch, getState, sdk) => {
const fetchMessages = (txId, page) => (dispatch, getState, sdk) => {
const paging = { page, per_page: MESSAGES_PAGE_SIZE };
dispatch(fetchMessagesRequest());
return sdk.messages
@ -265,9 +280,11 @@ const fetchMessages = (txId, paging) => (dispatch, getState, sdk) => {
.then(response => {
const entities = updatedEntities({}, response.data);
const messageIds = response.data.data.map(d => d.id);
const denormalized = denormalisedEntities(entities, 'message', messageIds);
const denormalizedMessages = denormalisedEntities(entities, 'message', messageIds);
const { totalItems, totalPages, page: fetchedPage } = response.data.meta;
const pagination = { totalItems, totalPages, page: fetchedPage };
dispatch(fetchMessagesSuccess(denormalized, response.data.meta.totalItems));
dispatch(fetchMessagesSuccess(denormalizedMessages, pagination));
})
.catch(e => {
dispatch(fetchMessagesError(storableError(e)));
@ -275,23 +292,15 @@ const fetchMessages = (txId, paging) => (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));
const state = getState();
const { oldestMessagePageFetched, totalMessagePages } = state.SalePage;
const hasMoreOldMessages = totalMessagePages > oldestMessagePageFetched;
// In case there're no more old pages left we default to fetching the current cursor position
const nextPage = hasMoreOldMessages ? oldestMessagePageFetched + 1 : oldestMessagePageFetched;
return dispatch(fetchMessages(txId, nextPage));
};
export const sendMessage = (saleId, message) => (dispatch, getState, sdk) => {
@ -301,7 +310,12 @@ export const sendMessage = (saleId, message) => (dispatch, getState, sdk) => {
.send({ transactionId: saleId, content: message })
.then(response => {
const messageId = response.data.data.id;
return dispatch(fetchMessages(saleId))
// We fetch the first page again to add sent message to the page data
// and update possible incoming messages too.
// TODO if there're more than 100 incoming messages,
// this should loop through most recent pages instead of fetching just the first one.
return dispatch(fetchMessages(saleId, 1))
.then(() => {
dispatch(sendMessageSuccess());
return messageId;
@ -391,8 +405,5 @@ export const loadData = params => dispatch => {
dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null }));
// Sale (i.e. transaction entity in API, but from buyers perspective) contains sale details
return Promise.all([
dispatch(fetchSale(saleId)),
dispatch(fetchNLatestMessages(saleId, MESSAGES_PAGE_SIZE)),
]);
return Promise.all([dispatch(fetchSale(saleId)), dispatch(fetchMessages(saleId, 1))]);
};

View file

@ -49,7 +49,8 @@ export const SalePageComponent = props => {
transaction,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
@ -105,7 +106,8 @@ export const SalePageComponent = props => {
declineInProgress={declineInProgress}
acceptSaleError={acceptSaleError}
declineSaleError={declineSaleError}
totalMessages={totalMessages}
totalMessagePages={totalMessagePages}
oldestMessagePageFetched={oldestMessagePageFetched}
messages={messages}
fetchMessagesInProgress={fetchMessagesInProgress}
fetchMessagesError={fetchMessagesError}
@ -170,7 +172,8 @@ SalePageComponent.propTypes = {
scrollingDisabled: bool.isRequired,
transaction: propTypes.transaction,
fetchMessagesError: propTypes.error,
totalMessages: number.isRequired,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: arrayOf(propTypes.message).isRequired,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
@ -192,7 +195,8 @@ const mapStateToProps = state => {
transactionRef,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
@ -215,7 +219,8 @@ const mapStateToProps = state => {
transaction,
fetchMessagesInProgress,
fetchMessagesError,
totalMessages,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,

View file

@ -39,6 +39,8 @@ describe('SalePage', () => {
scrollingDisabled: false,
transaction,
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
sendMessageInProgress: false,
onShowMoreMessages: noop,

View file

@ -48,6 +48,7 @@ exports[`SalePage matches snapshot 1`] = `
declineSaleError={null}
fetchMessagesError={null}
messages={Array []}
oldestMessagePageFetched={0}
onAcceptSale={[Function]}
onDeclineSale={[Function]}
onResetForm={[Function]}
@ -55,7 +56,7 @@ exports[`SalePage matches snapshot 1`] = `
onShowMoreMessages={[Function]}
sendMessageError={null}
sendMessageInProgress={false}
totalMessages={0}
totalMessagePages={0}
transaction={
Object {
"attributes": Object {