From 3652f10f7f51bfb25b78e0b8d1431122f09226fd Mon Sep 17 00:00:00 2001 From: Sudheer Date: Wed, 10 Jul 2019 04:26:28 +0530 Subject: [PATCH] MM-14977 Add bi-directional with loading of last unread post (#3049) * MM-14977 Add bi-directional with loading of last unread post * Fix tests * update depedency hashes * Preevnt loading auto load of posts when postList does not exist prevent loading older posts when a callback is in place Fix range Sync posts only if postListIds exist Sync posts only if lastPostTimestamp exists * Change variable and state names for unread chunk and timestamp reference Use the new util func for selecting unread chunk Remove the check for loading posts when calling loadPosts again * Fix review comments * Redux hash update --- actions/views/channel.js | 139 +++++--- actions/views/channel.test.js | 300 ++++++++--------- .../permalink_view.test.jsx.snap | 4 +- .../__snapshots__/post_view.test.js.snap | 25 ++ .../post_view/channel_intro_message/index.js | 3 +- components/post_view/index.js | 45 +-- .../__snapshots__/post_list.test.jsx.snap | 65 ++++ components/post_view/post_list/index.js | 93 +++++ components/post_view/post_list/post_list.jsx | 317 ++++++++++++++++++ .../post_view/post_list/post_list.test.jsx | 243 ++++++++++++++ .../post_list_virtualized.jsx | 261 ++++++-------- .../post_list_virtualized.test.jsx | 146 +++----- .../post_view/post_list_ie/post_list_ie.jsx | 12 +- .../__snapshots__/post_list_row.test.jsx.snap | 10 +- .../post_view/post_list_row/post_list_row.jsx | 14 +- .../post_list_row/post_list_row.test.jsx | 10 +- .../post_view/post_list_virtualized/index.js | 47 --- components/post_view/post_view.jsx | 77 +++++ components/post_view/post_view.test.js | 53 +++ .../at_mention_provider.test.jsx | 2 - package-lock.json | 61 ++-- package.json | 6 +- utils/constants.jsx | 15 +- utils/post_utils.jsx | 4 +- utils/post_utils.test.jsx | 16 +- 25 files changed, 1326 insertions(+), 642 deletions(-) create mode 100644 components/post_view/__snapshots__/post_view.test.js.snap create mode 100644 components/post_view/post_list/__snapshots__/post_list.test.jsx.snap create mode 100644 components/post_view/post_list/index.js create mode 100644 components/post_view/post_list/post_list.jsx create mode 100644 components/post_view/post_list/post_list.test.jsx rename components/post_view/{post_list_virtualized => post_list}/post_list_virtualized.jsx (69%) rename components/post_view/{post_list_virtualized => post_list}/post_list_virtualized.test.jsx (78%) delete mode 100644 components/post_view/post_list_virtualized/index.js create mode 100644 components/post_view/post_view.jsx create mode 100644 components/post_view/post_view.test.js diff --git a/actions/views/channel.js b/actions/views/channel.js index a71699578f39..4e04a99a1a44 100644 --- a/actions/views/channel.js +++ b/actions/views/channel.js @@ -20,7 +20,7 @@ import {getLastPostsApiTimeForChannel} from 'selectors/views/channel'; import {getSocketStatus} from 'selectors/views/websocket'; import {browserHistory} from 'utils/browser_history'; -import {Constants, ActionTypes, EventTypes} from 'utils/constants.jsx'; +import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants.jsx'; import {isMobile} from 'utils/utils.jsx'; import LocalStorageStore from 'stores/local_storage_store.jsx'; @@ -130,63 +130,84 @@ export function autocompleteUsersInChannel(prefix, channelId) { }; } -export function loadInitialPosts(channelId, focusedPostId) { +export function loadUnreads(channelId) { return async (dispatch) => { - let posts; - let hasMoreBefore = false; - let hasMoreAfter = false; - const time = Date.now(); - if (focusedPostId) { - const result = await dispatch(PostActions.getPostsAround(channelId, focusedPostId, Posts.POST_CHUNK_SIZE / 2)); - - posts = result.data; - - if (posts) { - // If the post is at index i, there are i posts after it and len - i - 1 before it - const numPostsAfter = posts.order.indexOf(focusedPostId); - const numPostsBefore = posts.order.length - numPostsAfter - 1; - - hasMoreBefore = numPostsBefore >= Posts.POST_CHUNK_SIZE / 2; - hasMoreAfter = numPostsAfter >= Posts.POST_CHUNK_SIZE / 2; - } - } else { - const result = await dispatch(PostActions.getPosts(channelId, 0, Posts.POST_CHUNK_SIZE / 2)); + const {data, error} = await dispatch(PostActions.getPostsUnread(channelId)); + if (error) { + return { + error, + atLatestMessage: false, + atOldestmessage: false, + }; + } - posts = result.data; + dispatch({ + type: ActionTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount: data.order.length, + }); - if (posts) { - hasMoreBefore = posts && posts.order.length >= Posts.POST_CHUNK_SIZE / 2; - } - } + return { + atLatestMessage: data.next_post_id === '', + atOldestmessage: data.prev_post_id === '', + }; + }; +} - if (posts) { - dispatch({ - type: ActionTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME, - channelId, - time, - }); +export function loadPostsAround(channelId, focusedPostId) { + return async (dispatch) => { + const {data, error} = await dispatch(PostActions.getPostsAround(channelId, focusedPostId, Posts.POST_CHUNK_SIZE / 2)); + if (error) { + return { + error, + atLatestMessage: false, + atOldestmessage: false, + }; } + dispatch({ + type: ActionTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount: data.order.length, + }); return { - posts, - hasMoreBefore, - hasMoreAfter, + atLatestMessage: data.next_post_id === '', + atOldestmessage: data.prev_post_id === '', }; }; } -export function increasePostVisibility(channelId, beforePostId) { - return async (dispatch, getState) => { - const state = getState(); - if (state.views.channel.loadingPosts[channelId]) { - return true; +export function loadLatestPosts(channelId) { + return async (dispatch) => { + const time = Date.now(); + const {data, error} = await dispatch(PostActions.getPosts(channelId, 0, Posts.POST_CHUNK_SIZE / 2)); + + if (error) { + return { + error, + atLatestMessage: false, + atOldestmessage: false, + }; } - const currentPostVisibility = state.views.channel.postVisibility[channelId]; + dispatch({ + type: ActionTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME, + channelId, + time, + }); - if (currentPostVisibility >= Constants.MAX_POST_VISIBILITY) { - return true; - } + return { + data, + atLatestMessage: data.next_post_id === '', + atOldestmessage: data.prev_post_id === '', + }; + }; +} + +export function loadPosts({channelId, postId, type}) { + //type here can be BEFORE_ID or AFTER_ID + return async (dispatch) => { + const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2; dispatch({ type: ActionTypes.LOADING_POSTS, @@ -194,8 +215,15 @@ export function increasePostVisibility(channelId, beforePostId) { channelId, }); - const result = await dispatch(PostActions.getPostsBefore(channelId, beforePostId, 0, Posts.POST_CHUNK_SIZE / 2)); - const posts = result.data; + const page = 0; + let result; + if (type === PostRequestTypes.BEFORE_ID) { + result = await dispatch(PostActions.getPostsBefore(channelId, postId, page, POST_INCREASE_AMOUNT)); + } else { + result = await dispatch(PostActions.getPostsAfter(channelId, postId, page, POST_INCREASE_AMOUNT)); + } + + const {data} = result; const actions = [{ type: ActionTypes.LOADING_POSTS, @@ -203,19 +231,22 @@ export function increasePostVisibility(channelId, beforePostId) { channelId, }]; - if (posts) { - actions.push({ - type: ActionTypes.INCREASE_POST_VISIBILITY, - data: channelId, - amount: posts.order.length, - }); + if (result.error) { + return { + error: result.error, + moreToLoad: true, + }; } + actions.push({ + type: ActionTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount: data.order.length, + }); dispatch(batchActions(actions)); return { - moreToLoad: posts ? posts.order.length >= Posts.POST_CHUNK_SIZE / 2 : false, - error: result.error, + moreToLoad: type === PostRequestTypes.BEFORE_ID ? data.prev_post_id !== '' : data.next_post_id !== '', }; }; } diff --git a/actions/views/channel.test.js b/actions/views/channel.test.js index 2e54e7657ff5..42fae770a25b 100644 --- a/actions/views/channel.test.js +++ b/actions/views/channel.test.js @@ -11,7 +11,7 @@ import * as PostActions from 'mattermost-redux/actions/posts'; import {browserHistory} from 'utils/browser_history'; import * as Actions from 'actions/views/channel'; import {openDirectChannelToUserId} from 'actions/channel_actions.jsx'; -import {ActionTypes} from 'utils/constants.jsx'; +import {ActionTypes, PostRequestTypes} from 'utils/constants.jsx'; const mockStore = configureStore([thunk]); @@ -129,156 +129,194 @@ describe('channel view actions', () => { }); }); - describe('loadInitialPosts', () => { - describe('without a focused post', () => { - test('should call getPosts and return the results', async () => { - const posts = {posts: {}, order: []}; + describe('loadLatestPosts', () => { + test('should call getPosts and return the results', async () => { + const posts = {posts: {}, order: []}; - PostActions.getPosts.mockReturnValue(() => ({data: posts})); + PostActions.getPosts.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.loadInitialPosts('channel')); + const result = await store.dispatch(Actions.loadLatestPosts('channel')); - expect(result.posts).toBe(posts); + expect(result.data).toBe(posts); - expect(PostActions.getPosts).toHaveBeenCalledWith('channel', 0, Posts.POST_CHUNK_SIZE / 2); - }); + expect(PostActions.getPosts).toHaveBeenCalledWith('channel', 0, Posts.POST_CHUNK_SIZE / 2); + }); - test('when enough posts are received', async () => { - const posts = {posts: {}, order: new Array(Posts.POST_CHUNK_SIZE)}; + test('when oldest posts are recived', async () => { + const posts = {posts: {}, order: new Array(Posts.POST_CHUNK_SIZE), next_post_id: 'test', prev_post_id: ''}; - PostActions.getPosts.mockReturnValue(() => ({data: posts})); + PostActions.getPosts.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.loadInitialPosts('channel')); + const result = await store.dispatch(Actions.loadLatestPosts('channel')); - expect(result.hasMoreBefore).toBe(true); - expect(result.hasMoreAfter).toBe(false); - }); + expect(result.atLatestMessage).toBe(false); + expect(result.atOldestmessage).toBe(true); + }); - test('when not enough posts are received', async () => { - const posts = {posts: {}, order: new Array((Posts.POST_CHUNK_SIZE / 2) - 1)}; + test('when latest posts are received', async () => { + Date.now = jest.fn().mockReturnValue(12344); - PostActions.getPosts.mockReturnValue(() => ({data: posts})); + const posts = {posts: {}, order: new Array((Posts.POST_CHUNK_SIZE / 2) - 1), next_post_id: '', prev_post_id: 'test'}; - const result = await store.dispatch(Actions.loadInitialPosts('channel')); + PostActions.getPosts.mockReturnValue(() => ({data: posts})); - expect(result.hasMoreBefore).toBe(false); - expect(result.hasMoreAfter).toBe(false); - }); + const result = await store.dispatch(Actions.loadLatestPosts('channel')); + + expect(result.atLatestMessage).toBe(true); + expect(result.atOldestmessage).toBe(false); + + expect(store.getActions()).toEqual([ + { + channelId: 'channel', + time: 12344, + type: ActionTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME, + }, + ]); }); + }); - describe('with a focused post', () => { - test('should call getPostsAround and return the results', async () => { - Date.now = jest.fn().mockReturnValue(12344); + describe('loadUnreads', () => { + test('when there are no posts after and before the response', async () => { + const posts = {posts: {}, order: [], next_post_id: '', prev_post_id: ''}; - const posts = {posts: {}, order: []}; + PostActions.getPostsUnread.mockReturnValue(() => ({data: posts})); - PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + const result = await store.dispatch(Actions.loadUnreads('channel', 'post')); - const result = await store.dispatch(Actions.loadInitialPosts('channel', 'post')); + expect(result).toEqual({atLatestMessage: true, atOldestmessage: true}); + expect(PostActions.getPostsUnread).toHaveBeenCalledWith('channel'); + }); - expect(result.posts).toBe(posts); + test('when there are posts before and after the response', async () => { + const posts = { + posts: {}, + order: [ + ...new Array(Posts.POST_CHUNK_SIZE / 2), // after + 'post', + ...new Array(Posts.POST_CHUNK_SIZE / 2), // before + ], + next_post_id: 'test', + prev_post_id: 'test', + }; - expect(PostActions.getPostsAround).toHaveBeenCalledWith('channel', 'post', Posts.POST_CHUNK_SIZE / 2); + PostActions.getPostsUnread.mockReturnValue(() => ({data: posts})); - expect(store.getActions()).toEqual([ - { - channelId: 'channel', - time: 12344, - type: ActionTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME, - }, - ]); - }); + const result = await store.dispatch(Actions.loadUnreads('channel', 'post')); + expect(result).toEqual({atLatestMessage: false, atOldestmessage: false}); + expect(PostActions.getPostsUnread).toHaveBeenCalledWith('channel'); + }); + }); - test('when enough posts are received before and after the focused post', async () => { - const posts = { - posts: {}, - order: [ - ...new Array(Posts.POST_CHUNK_SIZE / 2), // after - 'post', - ...new Array(Posts.POST_CHUNK_SIZE / 2), // before - ], - }; + describe('loadPostsAround', () => { + test('should call getPostsAround and return the results', async () => { + const posts = {posts: {}, order: [], next_post_id: '', prev_post_id: ''}; - PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.loadInitialPosts('channel', 'post')); + const result = await store.dispatch(Actions.loadPostsAround('channel', 'post')); - expect(result.hasMoreAfter).toBe(true); - expect(result.hasMoreBefore).toBe(true); - }); + expect(result).toEqual({atLatestMessage: true, atOldestmessage: true}); - test('when not enough posts are received before the focused post', async () => { - const posts = { - posts: {}, - order: [ - ...new Array(Posts.POST_CHUNK_SIZE / 2), // after - 'post', - ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // before - ], - }; + expect(PostActions.getPostsAround).toHaveBeenCalledWith('channel', 'post', Posts.POST_CHUNK_SIZE / 2); + }); - PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + test('when there are posts before and after reponse posts chunk', async () => { + const posts = { + posts: {}, + order: [ + ...new Array(Posts.POST_CHUNK_SIZE / 2), // after + 'post', + ...new Array(Posts.POST_CHUNK_SIZE / 2), // before + ], + next_post_id: 'test', + prev_post_id: 'test', + }; - const result = await store.dispatch(Actions.loadInitialPosts('channel', 'post')); + PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); - expect(result.hasMoreAfter).toBe(true); - expect(result.hasMoreBefore).toBe(false); - }); + const result = await store.dispatch(Actions.loadPostsAround('channel', 'post')); - test('when not enough posts are received after the focused post', async () => { - const posts = { - posts: {}, - order: [ - ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // after - 'post', - ...new Array(Posts.POST_CHUNK_SIZE / 2), // before - ], - }; + expect(result).toEqual({atLatestMessage: false, atOldestmessage: false}); + }); - PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + test('when there are posts before the reponse posts chunk', async () => { + const posts = { + posts: {}, + order: [ + ...new Array(Posts.POST_CHUNK_SIZE / 2), // after + 'post', + ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // before + ], + next_post_id: '', + prev_post_id: 'test', + }; - const result = await store.dispatch(Actions.loadInitialPosts('channel', 'post')); + PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); - expect(result.hasMoreAfter).toBe(false); - expect(result.hasMoreBefore).toBe(true); - }); + const result = await store.dispatch(Actions.loadPostsAround('channel', 'post')); - test('when not enough posts are received before and after the focused post', async () => { - const posts = { - posts: {}, - order: [ - ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // after - 'post', - ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // before - ], - }; + expect(result).toEqual({atLatestMessage: true, atOldestmessage: false}); + }); + + test('when there are posts before the reponse posts chunk', async () => { + const posts = { + posts: {}, + order: [ + ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // after + 'post', + ...new Array(Posts.POST_CHUNK_SIZE / 2), // before + ], + next_post_id: 'test', + prev_post_id: '', + }; - PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.loadInitialPosts('channel', 'post')); + const result = await store.dispatch(Actions.loadPostsAround('channel', 'post')); - expect(result.hasMoreAfter).toBe(false); - expect(result.hasMoreBefore).toBe(false); - }); + expect(result).toEqual({atLatestMessage: false, atOldestmessage: true}); + }); + + test('when there are no posts before and after the posts chunk', async () => { + const posts = { + posts: {}, + order: [ + ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // after + 'post', + ...new Array((Posts.POST_CHUNK_SIZE / 2) - 1), // before + ], + next_post_id: '', + prev_post_id: '', + }; + + PostActions.getPostsAround.mockReturnValue(() => ({data: posts})); + + const result = await store.dispatch(Actions.loadPostsAround('channel', 'post')); + + expect(result).toEqual({atLatestMessage: true, atOldestmessage: true}); }); }); describe('increasePostVisibility', () => { test('should dispatch the correct actions', async () => { - PostActions.getPostsBefore.mockImplementation((...args) => ({type: 'MOCK_GET_POSTS_BEFORE', args})); + const posts = { + posts: {}, + order: new Array(7), + prev_post_id: '', + next_post_id: '', + }; - await store.dispatch(Actions.increasePostVisibility('current_channel_id', 'oldest_post_id')); + PostActions.getPostsBefore.mockReturnValue(() => ({data: posts})); + + await store.dispatch(Actions.loadPosts({channelId: 'current_channel_id', postId: 'oldest_post_id', type: PostRequestTypes.BEFORE_ID})); expect(store.getActions()).toEqual([ {channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'}, - { - args: ['current_channel_id', 'oldest_post_id', 0, 30], - type: 'MOCK_GET_POSTS_BEFORE', - }, { meta: {batch: true}, payload: [ {channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'}, + {amount: 7, data: 'current_channel_id', type: 'INCREASE_POST_VISIBILITY'}, ], type: 'BATCHING_REDUCER.BATCH', }, @@ -296,7 +334,7 @@ describe('channel view actions', () => { PostActions.getPostsBefore.mockReturnValue(() => ({data: posts})); - await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); + await store.dispatch(Actions.loadPosts({channelId, postId: 'oldest_post_id', type: PostRequestTypes.BEFORE_ID})); expect(store.getActions()).toContainEqual({ meta: {batch: true}, @@ -321,11 +359,12 @@ describe('channel view actions', () => { const posts = { posts: {}, order: new Array(Posts.POST_CHUNK_SIZE / 2), + prev_post_id: 'saasdsd', }; PostActions.getPostsBefore.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); + const result = await store.dispatch(Actions.loadPosts({channelId, postId: 'oldest_post_id', type: PostRequestTypes.BEFORE_ID})); expect(result).toEqual({ moreToLoad: true, @@ -337,11 +376,12 @@ describe('channel view actions', () => { const posts = { posts: {}, order: new Array((Posts.POST_CHUNK_SIZE / 2) - 1), + prev_post_id: '', }; PostActions.getPostsBefore.mockReturnValue(() => ({data: posts})); - const result = await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); + const result = await store.dispatch(Actions.loadPosts({channelId, postId: 'oldest_post_id', type: PostRequestTypes.BEFORE_ID})); expect(result).toEqual({ moreToLoad: false, @@ -354,58 +394,12 @@ describe('channel view actions', () => { PostActions.getPostsBefore.mockReturnValue(() => ({error})); - const result = await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); + const result = await store.dispatch(Actions.loadPosts({channelId, postId: 'oldest_post_id', type: PostRequestTypes.BEFORE_ID})); expect(result).toEqual({ error, - moreToLoad: false, - }); - }); - - test('should do nothing when already loading posts', async () => { - const channelId = 'channel1'; - - store = mockStore({ - ...initialState, - views: { - ...initialState.views, - channel: { - ...initialState.views.channel, - loadingPosts: { - [channelId]: true, - }, - }, - }, - }); - - const result = await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); - - expect(result).toBe(true); - - expect(PostActions.getPostsBefore).not.toHaveBeenCalled(); - }); - - test('should do nothing with too many posts loaded', async () => { - const channelId = 'channel1'; - - store = mockStore({ - ...initialState, - views: { - ...initialState.views, - channel: { - ...initialState.views.channel, - postVisibility: { - [channelId]: 100000000, - }, - }, - }, + moreToLoad: true, }); - - const result = await store.dispatch(Actions.increasePostVisibility(channelId, 'oldest_post_id')); - - expect(result).toBe(true); - - expect(PostActions.getPostsBefore).not.toHaveBeenCalled(); }); }); diff --git a/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap b/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap index 9a354ad92b32..9c9dbd844565 100644 --- a/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap +++ b/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap @@ -8,7 +8,7 @@ exports[`components/PermalinkView should match snapshot 1`] = ` - @@ -40,7 +40,7 @@ exports[`components/PermalinkView should match snapshot with archived channel 1` - diff --git a/components/post_view/__snapshots__/post_view.test.js.snap b/components/post_view/__snapshots__/post_view.test.js.snap new file mode 100644 index 000000000000..a652e50019e1 --- /dev/null +++ b/components/post_view/__snapshots__/post_view.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/post_view/post_view should match snapshot for channel loading 1`] = ` +
+ +
+`; + +exports[`components/post_view/post_view should match snapshot for loaderForChangeOfPostsChunk 1`] = ` +
+ +
+`; diff --git a/components/post_view/channel_intro_message/index.js b/components/post_view/channel_intro_message/index.js index 7bae9d4eaef2..ad1ce116814d 100644 --- a/components/post_view/channel_intro_message/index.js +++ b/components/post_view/channel_intro_message/index.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels'; +import {isCurrentChannelReadOnly, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; import {getProfilesInCurrentChannel, getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {get} from 'mattermost-redux/selectors/entities/preferences'; @@ -22,6 +22,7 @@ function mapStateToProps(state) { return { currentUserId: getCurrentUserId(state), + channel: getCurrentChannel(state), locale: getCurrentLocale(state), channelProfiles: getProfilesInCurrentChannel(state), enableUserCreation, diff --git a/components/post_view/index.js b/components/post_view/index.js index d10c39460e2f..c90559b8b25b 100644 --- a/components/post_view/index.js +++ b/components/post_view/index.js @@ -2,33 +2,15 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; import {withRouter} from 'react-router-dom'; - import {getChannel} from 'mattermost-redux/selectors/entities/channels'; import {getUser} from 'mattermost-redux/selectors/entities/users'; import {getTeamByName} from 'mattermost-redux/selectors/entities/teams'; -import { - checkAndSetMobileView, - increasePostVisibility, - loadInitialPosts, - syncPostsInChannel, -} from 'actions/views/channel'; import {Constants} from 'utils/constants.jsx'; -import {disableVirtList} from 'utils/utils.jsx'; - -import IePostList from './post_list_ie'; -import VirtPostList from './post_list_virtualized'; -let PostList = VirtPostList; -if (disableVirtList()) { - PostList = IePostList; -} +import PostView from './post_view.jsx'; -// This function is added as a fail safe for the channel sync issue we have. -// When the user switches to a team for the first time we show the channel of previous team and then settle for the right channel after that -// This causes the scroll correction etc an issue because post_list is not mounted for new channel instead it is updated const isChannelLoading = (params, channel, team, teammate) => { if (params.postid) { return false; @@ -53,34 +35,21 @@ const isChannelLoading = (params, channel, team, teammate) => { function makeMapStateToProps() { return function mapStateToProps(state, ownProps) { - const postVisibility = state.views.channel.postVisibility[ownProps.channelId]; - - const channel = getChannel(state, ownProps.channelId); const team = getTeamByName(state, ownProps.match.params.team); let teammate; + const channel = getChannel(state, ownProps.channelId); + if (channel && channel.type === Constants.DM_CHANNEL && channel.teammate_id) { teammate = getUser(state, channel.teammate_id); } - const channelLoading = isChannelLoading(ownProps.match.params, channel, team, teammate); - + const lastViewedAt = state.views.channel.lastChannelViewTime[ownProps.channelId]; return { - channel, - postVisibility, + lastViewedAt, channelLoading, + channel, }; }; } -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - loadInitialPosts, - increasePostVisibility, - checkAndSetMobileView, - syncPostsInChannel, - }, dispatch), - }; -} - -export default withRouter(connect(makeMapStateToProps, mapDispatchToProps)(PostList)); +export default withRouter(connect(makeMapStateToProps)(PostView)); diff --git a/components/post_view/post_list/__snapshots__/post_list.test.jsx.snap b/components/post_view/post_list/__snapshots__/post_list.test.jsx.snap new file mode 100644 index 000000000000..c6aaa2d84f4a --- /dev/null +++ b/components/post_view/post_list/__snapshots__/post_list.test.jsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/post_view/post_list snapshot for loading when there are no posts 1`] = ` +
+ +
+`; + +exports[`components/post_view/post_list snapshot with couple of posts 1`] = ` +
+
+
+
+ +
+
+
+
+`; diff --git a/components/post_view/post_list/index.js b/components/post_view/post_list/index.js new file mode 100644 index 000000000000..5843dd5bc646 --- /dev/null +++ b/components/post_view/post_list/index.js @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {withRouter} from 'react-router-dom'; +import {getRecentPostsChunkInChannel, makeGetPostsChunkAroundPost, getUnreadPostsChunk, getPost} from 'mattermost-redux/selectors/entities/posts'; +import {memoizeResult} from 'mattermost-redux/utils/helpers'; +import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list'; + +import {getLatestPostId} from 'utils/post_utils.jsx'; +import { + checkAndSetMobileView, + loadPosts, + loadUnreads, + loadPostsAround, + syncPostsInChannel, + loadLatestPosts, +} from 'actions/views/channel'; + +import {disableVirtList} from 'utils/utils.jsx'; + +import IePostList from '../post_list_ie'; + +import PostListWrapper from './post_list.jsx'; + +let PostList = PostListWrapper; +if (disableVirtList()) { + PostList = IePostList; +} + +const isFirstLoad = (state, channelId) => !state.entities.posts.postsInChannel[channelId]; +const memoizedGetLatestPostId = memoizeResult((postIds) => getLatestPostId(postIds)); + +// This function is added as a fail safe for the channel sync issue we have. +// When the user switches to a team for the first time we show the channel of previous team and then settle for the right channel after that +// This causes the scroll correction etc an issue because post_list is not mounted for new channel instead it is updated + +function makeMapStateToProps() { + const getPostsChunkAroundPost = makeGetPostsChunkAroundPost(); + const preparePostIdsForPostList = makePreparePostIdsForPostList(); + return function mapStateToProps(state, ownProps) { + let latestPostTimeStamp = 0; + let postIds; + let chunk; + let atLatestPost = false; + const lastViewedAt = state.views.channel.lastChannelViewTime[ownProps.channelId]; + + if (ownProps.match.params.postid) { + chunk = getPostsChunkAroundPost(state, ownProps.match.params.postid, ownProps.channelId); + } else if (ownProps.unreadChunkTimeStamp) { + chunk = getUnreadPostsChunk(state, ownProps.channelId, ownProps.unreadChunkTimeStamp); + } else { + chunk = getRecentPostsChunkInChannel(state, ownProps.channelId); + } + + if (chunk) { + postIds = chunk.order; + atLatestPost = chunk.recent; + } + + if (postIds && postIds.length) { + postIds = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); + const latestPostId = memoizedGetLatestPostId(postIds); + const latestPost = getPost(state, latestPostId); + latestPostTimeStamp = latestPost.create_at; + } + + return { + lastViewedAt, + isFirstLoad: isFirstLoad(state, ownProps.channelId), + postListIds: postIds, + atLatestPost, + focusedPostId: ownProps.match.params.postid, + latestPostTimeStamp, + }; + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + loadUnreads, + loadPosts, + loadLatestPosts, + loadPostsAround, + checkAndSetMobileView, + syncPostsInChannel, + }, dispatch), + }; +} + +export default withRouter(connect(makeMapStateToProps, mapDispatchToProps)(PostList)); diff --git a/components/post_view/post_list/post_list.jsx b/components/post_view/post_list/post_list.jsx new file mode 100644 index 000000000000..b8836c78f0f4 --- /dev/null +++ b/components/post_view/post_list/post_list.jsx @@ -0,0 +1,317 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; + +import LoadingScreen from 'components/loading_screen.jsx'; +import {PostRequestTypes} from 'utils/constants.jsx'; +import {getOldestPostId, getLatestPostId} from 'utils/post_utils.jsx'; + +import VirtPostList from './post_list_virtualized.jsx'; + +const MAX_NUMBER_OF_AUTO_RETRIES = 3; +export const MAX_EXTRA_PAGES_LOADED = 10; + +export default class PostList extends React.PureComponent { + static propTypes = { + + /** + * Array of post ids in the channel, ordered from newest to oldest + */ + postListIds: PropTypes.array, + + /** + * The channel the posts are in + */ + channelId: PropTypes.string, + + /* + * To get posts for perma view + */ + focusedPostId: PropTypes.string, + + /* + * Used for determining if we are not at the recent most chunk in channel + */ + atLatestPost: PropTypes.bool, + + /* + * Used for loading posts using unread API + */ + isFirstLoad: PropTypes.bool, + + /* + * Used for syncing posts and is also passed down to virt list for newMessages indicator + */ + latestPostTimeStamp: PropTypes.number, + + /* + * Used for padding down to virt list so it can change the chunk of posts selected + */ + changeUnreadChunkTimeStamp: PropTypes.func.isRequired, + actions: PropTypes.shape({ + + /* + * Used for getting permalink view posts + */ + loadPostsAround: PropTypes.func.isRequired, + + /* + * Used for geting unreads posts + */ + loadUnreads: PropTypes.func.isRequired, + + /* + * Used for getting posts using BEFORE_ID and AFTER_ID + */ + loadPosts: PropTypes.func.isRequired, + + /* + * Used to set mobile view on resize + */ + checkAndSetMobileView: PropTypes.func.isRequired, + + /* + * Used to loading posts since a timestamp to sync the posts + */ + syncPostsInChannel: PropTypes.func.isRequired, + + /* + * Used to loading posts if it not first visit, permalink or there exists any postListIds + * This happens when previous channel visit has a chunk which is not the latest set of posts + */ + loadLatestPosts: PropTypes.func.isRequired, + }).isRequired, + } + + constructor(props) { + super(props); + this.state = { + newerPosts: { + loading: false, + allLoaded: props.atLatestPost, + }, + olderPosts: { + loading: false, + allLoaded: false, + }, + loadingFirstSetOfPosts: !props.postListIds, + autoRetryEnable: true, + }; + + this.autoRetriesCount = 0; + this.loadingMorePosts = null; + this.actionsForPostList = { + loadOlderPosts: this.getPostsBefore, + loadNewerPosts: this.getPostsAfter, + checkAndSetMobileView: props.actions.checkAndSetMobileView, + canLoadMorePosts: this.canLoadMorePosts, + changeUnreadChunkTimeStamp: props.changeUnreadChunkTimeStamp, + }; + } + + componentDidMount() { + this.mounted = true; + if (this.props.channelId) { + this.postsOnLoad(this.props.channelId); + } + } + + componentDidUpdate(prevProps) { + if (this.props.channelId !== prevProps.channelId) { + this.postsOnLoad(this.props.channelId); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + postsOnLoad = async (channelId) => { + let error; + let atOldestmessage; + let atLatestMessage; + if (this.props.focusedPostId) { + ({atLatestMessage, atOldestmessage, error} = await this.props.actions.loadPostsAround(channelId, this.props.focusedPostId)); + } else if (this.props.isFirstLoad) { + ({atLatestMessage, atOldestmessage, error} = await this.props.actions.loadUnreads(channelId)); + } else if (this.props.latestPostTimeStamp) { + ({error} = await this.props.actions.syncPostsInChannel(channelId, this.props.latestPostTimeStamp)); + } else { + ({atLatestMessage, atOldestmessage, error} = await this.props.actions.loadLatestPosts(channelId)); + } + + if (error) { + // leave the loader if it exists as it is + return; + } + + // atLatestMessage does not exist for syncPostsInChannel call + // We dont need to setState on syncPostsInChannel call as the loader does not exist and atLatestMessage state will be taken care by the prop + if (typeof atLatestMessage !== 'undefined') { + this.setState({ + olderPosts: { + loading: false, + allLoaded: atOldestmessage, + }, + newerPosts: { + loading: false, + allLoaded: atLatestMessage, + }, + loadingFirstSetOfPosts: false, + }); + } + } + + setLoadingPosts = (type) => { + this.setState({ + [type]: { + ...this.state[type], + loading: true, + }, + }); + } + + callLoadPosts = async (channelId, postId, type) => { + let newState = {}; + const {moreToLoad, error} = await this.props.actions.loadPosts({ + channelId, + postId, + type, + }); + + if (type === PostRequestTypes.BEFORE_ID) { + newState = { + olderPosts: { + loading: false, + allLoaded: !moreToLoad, + }, + }; + } else { + newState = { + newerPosts: { + loading: false, + allLoaded: !moreToLoad, + }, + }; + } + + this.setState(newState); + + if (error) { + if (this.autoRetriesCount < MAX_NUMBER_OF_AUTO_RETRIES) { + this.autoRetriesCount++; + await this.callLoadPosts(channelId, postId, type); + } else if (this.mounted) { + this.setState({autoRetryEnable: false}); + } + } else { + if (this.mounted) { + this.setState({autoRetryEnable: true}); + } + + if (!this.state.autoRetryEnable) { + this.autoRetriesCount = 0; + } + } + + return {moreToLoad, error}; + } + + getOldestVisiblePostId = () => { + return getOldestPostId(this.props.postListIds); + } + + getLatestVisiblePostId = () => { + return getLatestPostId(this.props.postListIds); + } + + canLoadMorePosts = async () => { + if (!this.props.postListIds || !this.props.postListIds.length) { + return; + } + + if (this.state.olderPosts.loading || this.state.newerPosts.loading) { + return; + } + + if (this.extraPagesLoaded > MAX_EXTRA_PAGES_LOADED) { + // Prevent this from loading a lot of pages in a channel with only hidden messages + // Enable load more messages manual link + this.setState({autoRetryEnable: false}); + return; + } + + if (!this.state.olderPosts.allLoaded) { + const oldestPostId = this.getOldestVisiblePostId(); + await this.getPostsBefore(oldestPostId); + } else if (!this.state.newerPosts.allLoaded) { + // if all olderPosts are loaded load new ones + const latestPostId = this.getLatestVisiblePostId(); + await this.getPostsAfter(latestPostId); + } + + this.extraPagesLoaded += 1; + } + + getPostsBefore = async () => { + if (this.state.olderPosts.loading) { + return; + } + const oldestPostId = this.getOldestVisiblePostId(); + this.setLoadingPosts('olderPosts'); + await this.callLoadPosts(this.props.channelId, oldestPostId, PostRequestTypes.BEFORE_ID); + } + + getPostsAfter = async () => { + if (this.state.newerPosts.loading) { + return; + } + const latestPostId = this.getLatestVisiblePostId(); + this.setLoadingPosts('newerPosts'); + await this.callLoadPosts(this.props.channelId, latestPostId, PostRequestTypes.AFTER_ID); + } + + render() { + if (this.state.loadingFirstSetOfPosts) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+ +
+
+
+
+ ); + } +} diff --git a/components/post_view/post_list/post_list.test.jsx b/components/post_view/post_list/post_list.test.jsx new file mode 100644 index 000000000000..410e4f4ec24c --- /dev/null +++ b/components/post_view/post_list/post_list.test.jsx @@ -0,0 +1,243 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {PostRequestTypes} from 'utils/constants.jsx'; + +import PostList, {MAX_EXTRA_PAGES_LOADED} from './post_list.jsx'; +import VirtPostList from './post_list_virtualized.jsx'; + +const actionsProp = { + loadPostsAround: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})), + loadUnreads: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})), + loadPosts: jest.fn().mockImplementation(() => Promise.resolve({moreToLoad: false})), + syncPostsInChannel: jest.fn(), + loadLatestPosts: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})), + checkAndSetMobileView: jest.fn(), +}; + +const lastViewedAt = 1532345226632; +const channelId = 'fake-id'; + +const createFakePosIds = (num) => { + const postIds = []; + for (let i = 1; i <= num; i++) { + postIds.push(`1234${i}`); + } + + return postIds; +}; + +const baseProps = { + actions: actionsProp, + lastViewedAt, + channelId, + postListIds: [], + changeUnreadChunkTimeStamp: jest.fn(), + isFirstLoad: true, + atLatestPost: false, +}; + +describe('components/post_view/post_list', () => { + it('snapshot for loading when there are no posts', () => { + const noPostList = undefined; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('snapshot with couple of posts', () => { + const postIds = createFakePosIds(2); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('Should call postsOnLoad', async () => { + const emptyPostList = []; + + const wrapper = shallow( + + ); + + expect(actionsProp.loadUnreads).toHaveBeenCalledWith(baseProps.channelId); + await wrapper.instance().postsOnLoad(); + expect(wrapper.state('newerPosts').allLoaded).toBe(true); + expect(wrapper.state('olderPosts').allLoaded).toBe(true); + }); + + it('Should have loadingFirstSetOfPosts set to true if postsOnLoad fails', async () => { + const loadUnreads = jest.fn().mockImplementation(() => Promise.resolve({error: {}})); + + const props = { + ...baseProps, + postListIds: undefined, + actions: { + ...actionsProp, + loadUnreads, + }, + }; + + const wrapper = shallow( + + ); + + expect(loadUnreads).toHaveBeenCalledWith(baseProps.channelId); + await loadUnreads(); + expect(wrapper.state('loadingFirstSetOfPosts')).toBe(true); + }); + + it('Should call for before and afterPosts', async () => { + const postIds = createFakePosIds(2); + const wrapper = shallow( + + ); + + wrapper.find(VirtPostList).prop('actions').loadOlderPosts(); + expect(wrapper.state('olderPosts').loading).toEqual(true); + expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID}); + await wrapper.instance().callLoadPosts(); + expect(wrapper.state('olderPosts')).toEqual({allLoaded: true, loading: false}); + + wrapper.find(VirtPostList).prop('actions').loadNewerPosts(); + expect(wrapper.state('newerPosts').loading).toEqual(true); + expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[0], type: PostRequestTypes.AFTER_ID}); + await wrapper.instance().callLoadPosts(); + expect(wrapper.state('newerPosts')).toEqual({allLoaded: true, loading: false}); + }); + + it('Should call for permalink posts', async () => { + const focusedPostId = 'new'; + const wrapper = shallow( + + ); + + expect(actionsProp.loadPostsAround).toHaveBeenCalledWith(baseProps.channelId, focusedPostId); + await actionsProp.loadPostsAround(); + expect(wrapper.state('olderPosts')).toEqual({allLoaded: true, loading: false}); + expect(wrapper.state('newerPosts')).toEqual({allLoaded: true, loading: false}); + }); + + it('Should call for loadLatestPosts', async () => { + const wrapper = shallow( + + ); + + expect(actionsProp.loadLatestPosts).toHaveBeenCalledWith(baseProps.channelId); + await actionsProp.loadLatestPosts(); + expect(wrapper.state('olderPosts')).toEqual({allLoaded: true, loading: false}); + expect(wrapper.state('newerPosts')).toEqual({allLoaded: true, loading: false}); + }); + + describe('getPostsSince', () => { + test('should call getPostsSince on channel switch', () => { + const postIds = createFakePosIds(2); + shallow(); + expect(actionsProp.syncPostsInChannel).toHaveBeenCalledWith(baseProps.channelId, 1234); + }); + + test('getPostsSince should not change olderPosts or newerPosts state on error', async () => { + const postIds = createFakePosIds(2); + const syncPostsInChannel = jest.fn().mockImplementation(() => Promise.resolve({error: {}})); + const props = { + ...baseProps, + postListIds: postIds, + latestPostTimeStamp: 1234, + isFirstLoad: false, + atLatestPost: true, + actions: { + ...actionsProp, + syncPostsInChannel, + }, + }; + + const wrapper = shallow(); + expect(syncPostsInChannel).toHaveBeenCalledWith(baseProps.channelId, 1234); + await syncPostsInChannel(); + expect(wrapper.state('newerPosts')).toEqual({allLoaded: true, loading: false}); + wrapper.setState({ + olderPosts: {allLoaded: true, loading: false}, + newerPosts: {allLoaded: true, loading: false}, + }); + expect(wrapper.state('newerPosts')).toEqual({allLoaded: true, loading: false}); + expect(wrapper.state('olderPosts')).toEqual({allLoaded: true, loading: false}); + }); + }); + + describe('canLoadMorePosts', () => { + test('Should not call loadPosts if postListIds is empty', async () => { + const wrapper = shallow(); + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + + expect(actionsProp.loadPosts).not.toHaveBeenCalled(); + }); + + test('Should not call loadPosts if olderPosts or newerPosts are loading', async () => { + const postIds = createFakePosIds(2); + const wrapper = shallow(); + wrapper.setState({olderPosts: {allLoaded: false, loading: true}}); + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + expect(actionsProp.loadPosts).not.toHaveBeenCalled(); + wrapper.setState({olderPosts: {allLoaded: false, loading: false}}); + wrapper.setState({newerPosts: {allLoaded: false, loading: true}}); + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + expect(actionsProp.loadPosts).not.toHaveBeenCalled(); + }); + + test('Should not call loadPosts if there were more than MAX_EXTRA_PAGES_LOADED', async () => { + const postIds = createFakePosIds(2); + const wrapper = shallow(); + wrapper.instance().extraPagesLoaded = MAX_EXTRA_PAGES_LOADED + 1; + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + expect(actionsProp.loadPosts).not.toHaveBeenCalled(); + }); + + test('Should call getPostsBefore if not all older posts are loaded', async () => { + const postIds = createFakePosIds(2); + const wrapper = shallow(); + wrapper.setState({olderPosts: {allLoaded: false, loading: false}}); + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID}); + }); + + test('Should call getPostsAfter if all older posts are loaded and not newerPosts', async () => { + const postIds = createFakePosIds(2); + const wrapper = shallow(); + wrapper.setState({olderPosts: {allLoaded: true, loading: false}}); + wrapper.setState({newerPosts: {allLoaded: false, loading: false}}); + wrapper.find(VirtPostList).prop('actions').canLoadMorePosts(); + expect(actionsProp.loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[0], type: PostRequestTypes.AFTER_ID}); + }); + }); + + describe('Auto retry of load more posts', () => { + test('Should retry loadPosts on failure of loadPosts', async () => { + const postIds = createFakePosIds(2); + const loadPosts = jest.fn().mockImplementation(() => Promise.resolve({moreToLoad: true, error: {}})); + const props = { + ...baseProps, + postListIds: postIds, + actions: { + ...actionsProp, + loadPosts, + }, + }; + + const wrapper = shallow( + + ); + + wrapper.find(VirtPostList).prop('actions').loadOlderPosts(); + expect(wrapper.state('olderPosts').loading).toEqual(true); + expect(loadPosts).toHaveBeenCalledTimes(1); + expect(loadPosts).toHaveBeenCalledWith({channelId: baseProps.channelId, postId: postIds[postIds.length - 1], type: PostRequestTypes.BEFORE_ID}); + await loadPosts(); + expect(wrapper.state('olderPosts')).toEqual({allLoaded: false, loading: false}); + expect(loadPosts).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/components/post_view/post_list_virtualized/post_list_virtualized.jsx b/components/post_view/post_list/post_list_virtualized.jsx similarity index 69% rename from components/post_view/post_list_virtualized/post_list_virtualized.jsx rename to components/post_view/post_list/post_list_virtualized.jsx index e2c03ba18ac8..a39e21b4d0f5 100644 --- a/components/post_view/post_list_virtualized/post_list_virtualized.jsx +++ b/components/post_view/post_list/post_list_virtualized.jsx @@ -7,14 +7,11 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import {DynamicSizeList} from 'react-window'; import {isDateLine, isStartOfNewMessages} from 'mattermost-redux/utils/post_list'; -import {debounce} from 'mattermost-redux/actions/helpers'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import LoadingScreen from 'components/loading_screen.jsx'; - import Constants, {PostListRowListIds, EventTypes} from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; -import {getOldestPostId, getPreviousPostId, getLatestPostId} from 'utils/post_utils.jsx'; +import {getPreviousPostId, getLatestPostId} from 'utils/post_utils.jsx'; import * as Utils from 'utils/utils.jsx'; import FloatingTimestamp from 'components/post_view/floating_timestamp'; @@ -22,9 +19,6 @@ import NewMessagesBelow from 'components/post_view/new_messages_below'; import PostListRow from 'components/post_view/post_list_row'; import ScrollToBottomArrows from 'components/post_view/scroll_to_bottom_arrows'; -const MAX_NUMBER_OF_AUTO_RETRIES = 3; - -const MAX_EXTRA_PAGES_LOADED = 10; const OVERSCAN_COUNT_BACKWARD = window.OVERSCAN_COUNT_BACKWARD || 80; // Exposing the value for PM to test will be removed soon const OVERSCAN_COUNT_FORWARD = window.OVERSCAN_COUNT_FORWARD || 80; // Exposing the value for PM to test will be removed soon const HEIGHT_TRIGGER_FOR_MORE_POSTS = window.HEIGHT_TRIGGER_FOR_MORE_POSTS || 1000; // Exposing the value for PM to test will be removed soon @@ -55,36 +49,54 @@ export default class PostList extends React.PureComponent { postListIds: PropTypes.array, /** - * The number of posts that should be rendered + * Set to focus this post */ - postVisibility: PropTypes.number, + focusedPostId: PropTypes.string, /** - * The channel the posts are in + * The current channel id */ - channel: PropTypes.object.isRequired, + channelId: PropTypes.string.isRequired, /** - * Set to focus this post + * Used for disabling auto retry of posts and enabling manual link for loading posts */ - focusedPostId: PropTypes.string, + autoRetryEnable: PropTypes.bool, /** - * When switching teams we might end up in a state where we select channel from previous team - * This flag is explicitly added for adding a loader in these cases to not mounting post list + * Data used for determining more messages state at the bottom */ - channelLoading: PropTypes.bool, + newerPosts: PropTypes.shape({ + loading: PropTypes.bool, + allLoaded: PropTypes.bool, + }), + + /** + * Data used for determining more messages state at the top + */ + olderPosts: PropTypes.shape({ + loading: PropTypes.bool, + allLoaded: PropTypes.bool, + }), latestPostTimeStamp: PropTypes.number, actions: PropTypes.shape({ - loadInitialPosts: PropTypes.func.isRequired, + /** + * Function to get older posts in the channel + */ + loadOlderPosts: PropTypes.func.isRequired, + + /** + * Function to get newer posts in the channel + */ + loadNewerPosts: PropTypes.func.isRequired, /** - * Function to increase the number of posts being rendered + * Function used for autoLoad of posts incase screen is not filled with posts */ - increasePostVisibility: PropTypes.func.isRequired, + canLoadMorePosts: PropTypes.func.isRequired, /** * Function to check and set if app is in mobile view @@ -92,27 +104,21 @@ export default class PostList extends React.PureComponent { checkAndSetMobileView: PropTypes.func.isRequired, /** - * Function to be called on recurring channel visits to get any possible missing latest posts + * Function to change the post selected for postList */ - syncPostsInChannel: PropTypes.func.isRequired, + changeUnreadChunkTimeStamp: PropTypes.func.isRequired, }).isRequired, } constructor(props) { super(props); - this.loadingMorePosts = false; - this.extraPagesLoaded = 0; - const channelIntroMessage = PostListRowListIds.CHANNEL_INTRO_MESSAGE; const isMobile = Utils.isMobile(); this.state = { - atEnd: false, - loadingFirstSetOfPosts: Boolean(!props.postListIds || props.channelLoading), isScrolling: false, - autoRetryEnable: true, isMobile, - atBottom: true, + atBottom: false, lastViewedBottom: Date.now(), postListIds: [channelIntroMessage], topPostId: '', @@ -130,20 +136,24 @@ export default class PostList extends React.PureComponent { this.initRangeToRender = this.props.focusedPostId ? [0, MAXIMUM_POSTS_FOR_SLICING.permalink] : [0, MAXIMUM_POSTS_FOR_SLICING.channel]; - if (!this.state.loadingFirstSetOfPosts) { - const newMessagesSeparatorIndex = this.getNewMessagesSeparatorIndex(props.postListIds); + if (props.postListIds) { + let postIndex = 0; + if (props.focusedPostId) { + postIndex = this.props.postListIds.findIndex((postId) => postId === this.props.focusedPostId); + } else { + postIndex = this.getNewMessagesSeparatorIndex(props.postListIds); + } + + const maxPostsForSlicing = props.focusedPostId ? MAXIMUM_POSTS_FOR_SLICING.permalink : MAXIMUM_POSTS_FOR_SLICING.channel; this.initRangeToRender = [ - Math.max(newMessagesSeparatorIndex - 30, 0), - Math.max(newMessagesSeparatorIndex + 30, Math.min(props.postListIds.length - 1, MAXIMUM_POSTS_FOR_SLICING.channel)), + Math.max(postIndex - 30, 0), + Math.max(postIndex + 30, Math.min(props.postListIds.length - 1, maxPostsForSlicing)), ]; } } componentDidMount() { this.mounted = true; - if (!this.props.channelLoading) { - this.loadPosts(this.props.channel.id, this.props.focusedPostId); - } this.props.actions.checkAndSetMobileView(); window.addEventListener('resize', this.handleWindowResize); @@ -151,10 +161,10 @@ export default class PostList extends React.PureComponent { EventEmitter.addListener(EventTypes.POST_LIST_SCROLL_CHANGE, this.scrollChange); } - getSnapshotBeforeUpdate(prevProps, prevState) { + getSnapshotBeforeUpdate(prevProps) { if (this.postListRef && this.postListRef.current) { - const postsAddedAtTop = this.state.postListIds.length !== prevState.postListIds.length && this.state.postListIds[0] === prevState.postListIds[0]; - const channelHeaderAdded = this.state.atEnd !== prevState.atEnd; + const postsAddedAtTop = this.props.postListIds.length !== prevProps.postListIds.length && this.props.postListIds[0] === prevProps.postListIds[0]; + const channelHeaderAdded = this.props.olderPosts.allLoaded !== prevProps.olderPosts.allLoaded; if ((postsAddedAtTop || channelHeaderAdded) && !this.state.atBottom) { const previousScrollTop = this.postListRef.current.scrollTop; const previousScrollHeight = this.postListRef.current.scrollHeight; @@ -169,17 +179,13 @@ export default class PostList extends React.PureComponent { } componentDidUpdate(prevProps, prevState, snapshot) { - if (prevProps.channelLoading && !this.props.channelLoading) { - this.loadPosts(this.props.channel.id, this.props.focusedPostId); - } - if (!this.postListRef.current || !snapshot) { return; } const postlistScrollHeight = this.postListRef.current.scrollHeight; - const postsAddedAtTop = this.state.postListIds.length !== prevState.postListIds.length && this.state.postListIds[0] === prevState.postListIds[0]; - const channelHeaderAdded = this.state.atEnd !== prevState.atEnd; + const postsAddedAtTop = this.props.postListIds.length !== prevProps.postListIds.length && this.props.postListIds[0] === prevProps.postListIds[0]; + const channelHeaderAdded = this.props.olderPosts.allLoaded !== prevProps.olderPosts.allLoaded; if ((postsAddedAtTop || channelHeaderAdded) && !this.state.atBottom) { const scrollValue = snapshot.previousScrollTop + (postlistScrollHeight - snapshot.previousScrollHeight); if (scrollValue !== 0 && (scrollValue - snapshot.previousScrollTop) !== 0) { @@ -196,18 +202,24 @@ export default class PostList extends React.PureComponent { EventEmitter.removeListener(EventTypes.POST_LIST_SCROLL_CHANGE, this.scrollChange); } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props) { const postListIds = props.postListIds || []; let newPostListIds; - if (state.atEnd) { + if (props.olderPosts.allLoaded) { newPostListIds = [...postListIds, PostListRowListIds.CHANNEL_INTRO_MESSAGE]; - } else if (props.postVisibility >= Constants.MAX_POST_VISIBILITY) { - newPostListIds = [...postListIds, PostListRowListIds.MAX_MESSAGES_LOADED]; - } else if (state.autoRetryEnable) { - newPostListIds = [...postListIds, PostListRowListIds.MORE_MESSAGES_LOADER]; + } else if (props.autoRetryEnable) { + newPostListIds = [...postListIds, PostListRowListIds.OLDER_MESSAGES_LOADER]; } else { - newPostListIds = [...postListIds, PostListRowListIds.MANUAL_TRIGGER_LOAD_MESSAGES]; + newPostListIds = [...postListIds, PostListRowListIds.LOAD_OLDER_MESSAGES_TRIGGER]; + } + + if (!props.newerPosts.allLoaded) { + if (props.autoRetryEnable) { + newPostListIds = [PostListRowListIds.NEWER_MESSAGES_LOADER, ...newPostListIds]; + } else { + newPostListIds = [...postListIds, PostListRowListIds.LOAD_NEWER_MESSAGES_TRIGGER]; + } } return { @@ -223,7 +235,7 @@ export default class PostList extends React.PureComponent { scrollChange = (toBottom) => { if (toBottom) { - this.scrollToBottom(); + this.scrollToLatestMessages(); } } @@ -248,63 +260,6 @@ export default class PostList extends React.PureComponent { } } - loadPosts = async (channelId, focusedPostId) => { - if (!channelId) { - return; - } - - if (this.state.loadingFirstSetOfPosts) { - const {hasMoreBefore} = await this.props.actions.loadInitialPosts(channelId, focusedPostId); - - if (this.mounted) { - this.setState({ - loadingFirstSetOfPosts: false, - atEnd: !hasMoreBefore, - }); - } - } else { - await this.props.actions.syncPostsInChannel(channelId, this.props.latestPostTimeStamp); - } - } - - loadMorePosts = async () => { - const oldestPostId = this.getOldestVisiblePostId(); - - if (!oldestPostId) { - // loadMorePosts shouldn't be called if we don't already have posts - return; - } - - this.loadingMorePosts = true; - - const {moreToLoad, error} = await this.props.actions.increasePostVisibility(this.props.channel.id, oldestPostId); - if (error) { - if (this.autoRetriesCount < MAX_NUMBER_OF_AUTO_RETRIES) { - this.autoRetriesCount++; - debounce(this.loadMorePosts()); - } else if (this.mounted) { - this.setState({autoRetryEnable: false}); - } - } else { - this.loadingMorePosts = false; - - if (this.mounted && this.props.postListIds) { - this.setState({ - atEnd: !moreToLoad, - autoRetryEnable: true, - }); - } - - if (!this.state.autoRetryEnable) { - this.autoRetriesCount = 0; - } - } - }; - - getOldestVisiblePostId = () => { - return getOldestPostId(this.state.postListIds); - } - togglePostMenu = (opened) => { const dynamicListStyle = this.state.dynamicListStyle; if (this.state.isMobile) { @@ -344,9 +299,9 @@ export default class PostList extends React.PureComponent { @@ -359,11 +314,14 @@ export default class PostList extends React.PureComponent { } onScroll = ({scrollDirection, scrollOffset, scrollUpdateWasRequested, clientHeight, scrollHeight}) => { - const isNotLoadingPosts = !this.state.loadingFirstSetOfPosts && !this.loadingMorePosts; const didUserScrollBackwards = scrollDirection === 'backward' && !scrollUpdateWasRequested; + const didUserScrollForwards = scrollDirection === 'forward' && !scrollUpdateWasRequested; const isOffsetWithInRange = scrollOffset < HEIGHT_TRIGGER_FOR_MORE_POSTS; - if (isNotLoadingPosts && didUserScrollBackwards && isOffsetWithInRange && !this.state.atEnd) { - this.loadMorePosts(); + const offsetFromBottom = (scrollHeight - clientHeight) - scrollOffset < HEIGHT_TRIGGER_FOR_MORE_POSTS; + if (didUserScrollBackwards && isOffsetWithInRange && !this.props.olderPosts.allLoaded && !this.props.olderPosts.loading) { + this.props.actions.loadOlderPosts(); + } else if (didUserScrollForwards && offsetFromBottom && !this.props.newerPosts.allLoaded && !this.props.newerPosts.loading) { + this.props.actions.loadNewerPosts(); } if (this.state.isMobile) { @@ -389,7 +347,7 @@ export default class PostList extends React.PureComponent { // Calculate how far the post list is from being scrolled to the bottom const offsetFromBottom = scrollHeight - clientHeight - scrollOffset; - return offsetFromBottom <= BUFFER_TO_BE_CONSIDERED_BOTTOM; + return offsetFromBottom <= BUFFER_TO_BE_CONSIDERED_BOTTOM && scrollHeight > 0; } updateAtBottom = (atBottom) => { @@ -443,73 +401,41 @@ export default class PostList extends React.PureComponent { position: 'center', }; } - const newMessagesSeparatorIndex = this.getNewMessagesSeparatorIndex(this.state.postListIds); + + const newMessagesSeparatorIndex = this.state.postListIds.findIndex( + (item) => item.indexOf(PostListRowListIds.START_OF_NEW_MESSAGES) === 0 + ); if (newMessagesSeparatorIndex > 0) { - const topMostPostIndex = this.state.postListIds.indexOf(getOldestPostId(this.state.postListIds)); - if (newMessagesSeparatorIndex === topMostPostIndex + 1) { - this.loadMorePosts(); - return { - index: this.state.postListIds.length - 1, - position: 'start', - }; - } return { index: newMessagesSeparatorIndex, position: 'start', }; } + + this.setState({atBottom: true}); return { index: 0, position: 'end', }; } - scrollToBottom = () => { - this.listRef.current.scrollToItem(0, 'end'); - } - - canLoadMorePosts = async () => { - if (this.props.focusedPostId) { - return; - } - - if (this.state.loadingFirstSetOfPosts || this.loadingMorePosts) { - // Should already be loading posts - return; - } - - if (this.state.atEnd) { - // Screen is full - return; - } - - if (this.extraPagesLoaded > MAX_EXTRA_PAGES_LOADED) { - // Prevent this from loading a lot of pages in a channel with only hidden messages - // Enable load more messages manual link - this.setState({autoRetryEnable: false}); - return; + scrollToLatestMessages = () => { + if (this.props.olderPosts.allLoaded) { + this.scrollToBottom(); + } else { + this.props.actions.changeUnreadChunkTimeStamp(''); } + } - await this.loadMorePosts(); - this.extraPagesLoaded += 1; + scrollToBottom = () => { + this.listRef.current.scrollToItem(0, 'end'); } render() { - const channel = this.props.channel; + const channelId = this.props.channelId; const {dynamicListStyle} = this.state; - if (this.state.loadingFirstSetOfPosts) { - return ( -
- -
- ); - } - let newMessagesBelow = null; if (!this.props.focusedPostId) { newMessagesBelow = ( @@ -517,8 +443,8 @@ export default class PostList extends React.PureComponent { atBottom={this.state.atBottom} lastViewedBottom={this.state.lastViewedBottom} postIds={this.state.postListIds} - onClick={this.scrollToBottom} - channelId={this.props.channel.id} + onClick={this.scrollToLatestMessages} + channelId={channelId} /> ); } @@ -546,7 +472,7 @@ export default class PostList extends React.PureComponent {
{this.renderRow} diff --git a/components/post_view/post_list_virtualized/post_list_virtualized.test.jsx b/components/post_view/post_list/post_list_virtualized.test.jsx similarity index 78% rename from components/post_view/post_list_virtualized/post_list_virtualized.test.jsx rename to components/post_view/post_list/post_list_virtualized.test.jsx index d94291549755..50825f51db54 100644 --- a/components/post_view/post_list_virtualized/post_list_virtualized.test.jsx +++ b/components/post_view/post_list/post_list_virtualized.test.jsx @@ -6,7 +6,6 @@ import {shallow} from 'enzyme'; import {DATE_LINE} from 'mattermost-redux/utils/post_list'; -import LoadingScreen from 'components/loading_screen'; import {PostListRowListIds} from 'utils/constants'; import NewMessagesBelow from 'components/post_view/new_messages_below'; @@ -15,8 +14,26 @@ import PostListRow from 'components/post_view/post_list_row'; import PostList from './post_list_virtualized'; describe('PostList', () => { + const baseActions = { + checkAndSetMobileView: jest.fn(), + loadOlderPosts: jest.fn(), + loadNewerPosts: jest.fn(), + canLoadMorePosts: jest.fn(), + changeUnreadChunkTimeStamp: jest.fn(), + }; + + const newerPosts = { + loading: false, + allLoaded: false, + }; + + const olderPosts = { + loading: false, + allLoaded: false, + }; + const baseProps = { - channel: {id: 'channel'}, + channelId: 'channel', focusedPostId: '', postListIds: [ 'post1', @@ -25,13 +42,9 @@ describe('PostList', () => { DATE_LINE + 1551711600000, ], latestPostTimeStamp: 12345, - postVisibility: 10, - actions: { - checkAndSetMobileView: jest.fn(), - increasePostVisibility: jest.fn(), - loadInitialPosts: jest.fn(() => ({posts: {posts: {}, order: []}, hasMoreBefore: false})), - syncPostsInChannel: jest.fn(), - }, + newerPosts, + olderPosts, + actions: baseActions, }; const postListIdsForClassNames = [ @@ -44,22 +57,6 @@ describe('PostList', () => { 'post5', ]; - test('should render loading screen while loading posts', () => { - const props = { - ...baseProps, - postListIds: null, - }; - - const wrapper = shallow(); - - expect(wrapper.state('loadingFirstSetOfPosts')).toBe(true); - expect(wrapper.find(LoadingScreen).exists()).toBe(true); - - wrapper.setState({loadingFirstSetOfPosts: false}); - - expect(wrapper.find(LoadingScreen).exists()).toBe(false); - }); - describe('renderRow', () => { const postListIds = ['a', 'b', 'c', 'd']; @@ -108,7 +105,6 @@ describe('PostList', () => { describe('new messages below', () => { test('should mount outside of permalink view', () => { const wrapper = shallow(); - wrapper.setState({loadingFirstSetOfPosts: false}); expect(wrapper.find(NewMessagesBelow).exists()).toBe(true); }); @@ -120,52 +116,10 @@ describe('PostList', () => { }; const wrapper = shallow(); - wrapper.setState({loadingFirstSetOfPosts: false}); - expect(wrapper.find(NewMessagesBelow).exists()).toBe(false); }); }); - describe('initScrollToIndex', () => { - test('should return index of start of new messages and call increasePostVisibility when all posts are unread', () => { - baseProps.actions.increasePostVisibility.mockResolvedValue({moreToLoad: false}); - const postListIds = []; - for (let i = 0; i < 30; i++) { - postListIds.push(`post${i}`); - } - postListIds.push(PostListRowListIds.START_OF_NEW_MESSAGES); - - const props = { - ...baseProps, - postListIds, - }; - - const wrapper = shallow(); - const initScrollToIndex = wrapper.instance().initScrollToIndex(); - - expect(initScrollToIndex).toEqual({index: 31, position: 'start'}); - - expect(baseProps.actions.increasePostVisibility).toHaveBeenCalledTimes(1); - }); - }); - - describe('loadMorePosts', () => { - test('should set state.atEnd to true after loading all posts in the channel', async () => { - baseProps.actions.increasePostVisibility.mockResolvedValue({moreToLoad: false}); - - const wrapper = shallow(); - - await wrapper.instance().loadMorePosts(); - - expect(baseProps.actions.increasePostVisibility).toHaveBeenCalledTimes(1); - expect(baseProps.actions.increasePostVisibility).toHaveBeenCalledWith(baseProps.channel.id, 'post3'); - - wrapper.update(); - - expect(wrapper.state('atEnd')).toEqual(true); - }); - }); - describe('onScroll', () => { test('should call checkBottom', () => { const wrapper = shallow(); @@ -225,9 +179,9 @@ describe('PostList', () => { const wrapper = shallow(); wrapper.setState({lastViewedBottom: 1234}); - wrapper.instance().updateAtBottom(false); + wrapper.instance().updateAtBottom(true); - expect(wrapper.state('atBottom')).toBe(false); + expect(wrapper.state('atBottom')).toBe(true); expect(wrapper.state('lastViewedBottom')).not.toBe(1234); }); @@ -235,7 +189,7 @@ describe('PostList', () => { const wrapper = shallow(); wrapper.setState({lastViewedBottom: 1234}); - wrapper.instance().updateAtBottom(true); + wrapper.instance().updateAtBottom(false); expect(wrapper.state('lastViewedBottom')).toBe(1234); }); @@ -246,7 +200,7 @@ describe('PostList', () => { const wrapper = shallow(); wrapper.setState({lastViewedBottom: 1234}); - wrapper.instance().updateAtBottom(false); + wrapper.instance().updateAtBottom(true); expect(wrapper.state('lastViewedBottom')).toBe(12345); }); @@ -257,7 +211,7 @@ describe('PostList', () => { const wrapper = shallow(); wrapper.setState({lastViewedBottom: 1234}); - wrapper.instance().updateAtBottom(false); + wrapper.instance().updateAtBottom(true); expect(wrapper.state('lastViewedBottom')).toBe(12346); }); @@ -270,27 +224,22 @@ describe('PostList', () => { instance.componentDidUpdate = jest.fn(); instance.postListRef = {current: {scrollTop: 10, scrollHeight: 100}}; - wrapper.setState({atEnd: true, atBottom: false}); + + wrapper.setProps({olderPosts: {allLoaded: true, loading: false}}); expect(instance.componentDidUpdate).toHaveBeenCalledTimes(1); expect(instance.componentDidUpdate.mock.calls[0][2]).toEqual({previousScrollTop: 10, previousScrollHeight: 100}); instance.postListRef = {current: {scrollTop: 30, scrollHeight: 200}}; - wrapper.setState({atEnd: false}); - expect(instance.componentDidUpdate).toHaveBeenCalledTimes(2); - expect(instance.componentDidUpdate.mock.calls[1][2]).toEqual({previousScrollTop: 30, previousScrollHeight: 200}); - - /*instance.postListRef = {current: {scrollTop: 40, scrollHeight: 400}}; wrapper.setProps({postListIds: [ 'post1', 'post2', 'post3', - 'post4', - 'post5', DATE_LINE + 1551711600000, + 'post4', ]}); - expect(instance.componentDidUpdate).toHaveBeenCalledTimes(3); - expect(instance.componentDidUpdate.mock.calls[2][2]).toEqual({previousScrollTop: 40, previousScrollHeight: 400});*/ + expect(instance.componentDidUpdate).toHaveBeenCalledTimes(2); + expect(instance.componentDidUpdate.mock.calls[1][2]).toEqual({previousScrollTop: 30, previousScrollHeight: 200}); }); test('should not return previous scroll position from getSnapshotBeforeUpdate as list is at bottom', () => { @@ -299,10 +248,10 @@ describe('PostList', () => { instance.componentDidUpdate = jest.fn(); instance.postListRef = {current: {scrollTop: 10, scrollHeight: 100}}; - wrapper.setState({atEnd: true, atBottom: true}); - expect(instance.componentDidUpdate.mock.calls[0][2]).toEqual(null); + wrapper.setProps({olderPosts: {allLoaded: true, loading: false}}); + expect(instance.componentDidUpdate.mock.calls[0][2]).toEqual({previousScrollHeight: 100, previousScrollTop: 10}); - /*wrapper.setState({atEnd: false}); + wrapper.setState({atBottom: true}); instance.postListRef = {current: {scrollTop: 40, scrollHeight: 400}}; wrapper.setProps({postListIds: [ 'post1', @@ -313,14 +262,14 @@ describe('PostList', () => { DATE_LINE + 1551711600000, ]}); - expect(instance.componentDidUpdate.mock.calls[2][2]).toEqual(null);*/ + expect(instance.componentDidUpdate.mock.calls[2][2]).toEqual(null); }); }); describe('initRangeToRender', () => { - test('should return 0 to 50 for channel with more than 50 messages', () => { + test('should return 0 to 50 for channel with more than 100 messages', () => { const postListIds = []; - for (let i = 0; i < 70; i++) { + for (let i = 0; i < 110; i++) { postListIds.push(`post${i}`); } @@ -445,10 +394,21 @@ describe('PostList', () => { }); }); - describe('getPostsSince', () => { - test('should call getPostsSince on channel switch', () => { - shallow(); - expect(baseProps.actions.syncPostsInChannel).toHaveBeenCalledWith(baseProps.channel.id, baseProps.latestPostTimeStamp); + describe('scrollToLatestMessages', () => { + test('should call scrollToBottom', () => { + const wrapper = shallow(); + wrapper.setProps({olderPosts: {allLoaded: true, loading: false}}); + const instance = wrapper.instance(); + instance.scrollToBottom = jest.fn(); + instance.scrollToLatestMessages(); + expect(instance.scrollToBottom).toHaveBeenCalled(); + }); + + test('should call changeUnreadChunkTimeStamp', () => { + const wrapper = shallow(); + const instance = wrapper.instance(); + instance.scrollToLatestMessages(); + expect(baseActions.changeUnreadChunkTimeStamp).toHaveBeenCalledWith(''); }); }); }); diff --git a/components/post_view/post_list_ie/post_list_ie.jsx b/components/post_view/post_list_ie/post_list_ie.jsx index 6535140813f8..db02ca54c0a4 100644 --- a/components/post_view/post_list_ie/post_list_ie.jsx +++ b/components/post_view/post_list_ie/post_list_ie.jsx @@ -10,7 +10,7 @@ import {isCombinedUserActivityPost} from 'mattermost-redux/utils/post_list'; import {debounce} from 'mattermost-redux/actions/helpers'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import Constants, {PostTypes, EventTypes} from 'utils/constants.jsx'; +import Constants, {PostTypes, EventTypes, PostRequestTypes} from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; @@ -66,12 +66,12 @@ export default class PostList extends React.PureComponent { actions: PropTypes.shape({ - loadInitialPosts: PropTypes.func.isRequired, + loadLatestPosts: PropTypes.func.isRequired, /** * Function to increase the number of posts being rendered */ - increasePostVisibility: PropTypes.func.isRequired, + loadPosts: PropTypes.func.isRequired, /** * Function to check and set if app is in mobile view @@ -386,12 +386,12 @@ export default class PostList extends React.PureComponent { } let posts; - const {hasMoreBefore} = await this.props.actions.loadInitialPosts(channelId, focusedPostId); + const {atOldestmessage} = await this.props.actions.loadLatestPosts(channelId, focusedPostId); if (this.mounted) { this.setState({ isDoingInitialLoad: false, - atEnd: !hasMoreBefore, + atEnd: atOldestmessage, }); } @@ -419,7 +419,7 @@ export default class PostList extends React.PureComponent { return; } - const {moreToLoad} = await this.props.actions.increasePostVisibility(channel.id, posts[postsLength - 1].id); + const {moreToLoad} = await this.props.actions.loadPosts({channelId: channel.id, postId: posts[postsLength - 1].id, type: PostRequestTypes.BEFORE_ID}); this.setState({atEnd: !moreToLoad}); } diff --git a/components/post_view/post_list_row/__snapshots__/post_list_row.test.jsx.snap b/components/post_view/post_list_row/__snapshots__/post_list_row.test.jsx.snap index b5f6104bd195..e5a1bbf8effd 100644 --- a/components/post_view/post_list_row/__snapshots__/post_list_row.test.jsx.snap +++ b/components/post_view/post_list_row/__snapshots__/post_list_row.test.jsx.snap @@ -1,14 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/post_view/post_list_row should render channel intro message 1`] = ` - -`; +exports[`components/post_view/post_list_row should render channel intro message 1`] = ``; exports[`components/post_view/post_list_row should render combined post 1`] = ` + ); } - if (listId === PostListRowListIds.MANUAL_TRIGGER_LOAD_MESSAGES) { + if (listId === PostListRowListIds.LOAD_OLDER_MESSAGES_TRIGGER || listId === PostListRowListIds.LOAD_NEWER_MESSAGES_TRIGGER) { return (