Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

[MM-13663][MM-11504] Adds bi-directional scrolling and use unread API on channel load #2274

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions actions/post_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Constants,
RHSStates,
StoragePrefixes,
PostRequestTypes,
} from 'utils/constants';
import {EMOJI_PATTERN} from 'utils/emoticons.jsx';
import * as UserAgent from 'utils/user_agent';
Expand Down Expand Up @@ -132,13 +133,9 @@ export function addReaction(postId, emojiName) {
const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;

// Returns true if there are more posts to load
export function increasePostVisibility(channelId, focusedPostId) {
export function loadPosts({channelId, postId, type}) {
return async (dispatch, getState) => {
const state = getState();
if (state.views.channel.loadingPosts[channelId]) {
return true;
}

const currentPostVisibility = state.views.channel.postVisibility[channelId];

if (currentPostVisibility >= Constants.MAX_POST_VISIBILITY) {
Expand All @@ -151,11 +148,13 @@ export function increasePostVisibility(channelId, focusedPostId) {
channelId,
});

const page = Math.floor(currentPostVisibility / POST_INCREASE_AMOUNT);
const page = 0;

let result;
if (focusedPostId) {
result = await dispatch(PostActions.getPostsBefore(channelId, focusedPostId, page, POST_INCREASE_AMOUNT));
if (type === PostRequestTypes.BEFORE_ID) {
result = await dispatch(PostActions.getPostsBefore(channelId, postId, page, POST_INCREASE_AMOUNT));
} else if (type === PostRequestTypes.AFTER_ID) {
result = await dispatch(PostActions.getPostsAfter(channelId, postId, page, POST_INCREASE_AMOUNT));
} else {
result = await dispatch(PostActions.getPosts(channelId, page, POST_INCREASE_AMOUNT));
}
Expand Down Expand Up @@ -183,6 +182,18 @@ export function increasePostVisibility(channelId, focusedPostId) {
};
}

export function loadUnreads(channelId) {
return (dispatch) => {
dispatch({
type: ActionTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: Constants.POST_CHUNK_SIZE,
});

return dispatch(PostActions.getPostsUnread(channelId));
};
}

export function searchForTerm(term) {
return (dispatch) => {
dispatch(RhsActions.updateSearchTerms(term));
Expand Down
41 changes: 31 additions & 10 deletions actions/post_actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Posts} from 'mattermost-redux/constants';
import {SearchTypes} from 'mattermost-redux/action_types';

import * as Actions from 'actions/post_actions';
import {Constants, ActionTypes, RHSStates} from 'utils/constants';
import {Constants, ActionTypes, RHSStates, PostRequestTypes} from 'utils/constants';

const mockStore = configureStore([thunk]);

Expand All @@ -18,10 +18,12 @@ jest.mock('mattermost-redux/actions/posts', () => ({
createPostImmediately: (...args) => ({type: 'MOCK_CREATE_POST_IMMEDIATELY', args}),
getPosts: (...args) => ({type: 'MOCK_GET_POSTS', args}),
getPostsBefore: (...args) => ({type: 'MOCK_GET_POSTS_BEFORE', args}),
getPostsAfter: (...args) => ({type: 'MOCK_GET_POSTS_AFTER', args}),
flagPost: (...args) => ({type: 'MOCK_FLAG_POST', args}),
unflagPost: (...args) => ({type: 'MOCK_UNFLAG_POST', args}),
pinPost: (...args) => ({type: 'MOCK_PIN_POST', args}),
unpinPost: (...args) => ({type: 'MOCK_UNPIN_POST', args}),
getPostsUnread: (...args) => ({type: 'MOCK_GET_UNREADS_POSTS', args}),
}));

jest.mock('actions/emoji_actions', () => ({
Expand Down Expand Up @@ -226,13 +228,13 @@ describe('Actions.Posts', () => {
expect(testStore.getActions()).toEqual([{type: ActionTypes.HIDE_EDIT_POST_MODAL}]);
});

test('increasePostVisibility', async () => {
test('loadPosts for getPosts', async () => {
const testStore = await mockStore(initialState);

await testStore.dispatch(Actions.increasePostVisibility('current_channel_id'));
await testStore.dispatch(Actions.loadPosts({channelId: 'current_channel_id'}));
expect(testStore.getActions()).toEqual([
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{args: ['current_channel_id', 0, 30], type: 'MOCK_GET_POSTS'},
{
meta: {batch: true},
payload: [
Expand All @@ -241,23 +243,32 @@ describe('Actions.Posts', () => {
type: 'BATCHING_REDUCER.BATCH',
},
]);
});

test('loadPosts for getPostsBefore', async () => {
const testStore = await mockStore(initialState);

await testStore.dispatch(Actions.increasePostVisibility('current_channel_id', 'latest_post_id'));
await testStore.dispatch(Actions.loadPosts({channelId: 'current_channel_id', type: PostRequestTypes.BEFORE_ID, postId: 'test'}));
expect(testStore.getActions()).toEqual([
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{args: ['current_channel_id', 2, 30], type: 'MOCK_GET_POSTS'},
{args: ['current_channel_id', 'test', 0, 30], type: 'MOCK_GET_POSTS_BEFORE'},
{
meta: {batch: true},
payload: [
{channelId: 'current_channel_id', data: false, type: 'LOADING_POSTS'},
],
type: 'BATCHING_REDUCER.BATCH',
},
]);
});

test('loadPosts for getPostsAfter', async () => {
const testStore = await mockStore(initialState);

await testStore.dispatch(Actions.loadPosts({channelId: 'current_channel_id', type: PostRequestTypes.AFTER_ID, postId: 'test'}));
expect(testStore.getActions()).toEqual([
{channelId: 'current_channel_id', data: true, type: 'LOADING_POSTS'},
{
args: ['current_channel_id', 'latest_post_id', 2, 30],
type: 'MOCK_GET_POSTS_BEFORE',
},
{args: ['current_channel_id', 'test', 0, 30], type: 'MOCK_GET_POSTS_AFTER'},
{
meta: {batch: true},
payload: [
Expand All @@ -268,6 +279,16 @@ describe('Actions.Posts', () => {
]);
});

test('loadUnreads', async () => {
const testStore = await mockStore(initialState);

await testStore.dispatch(Actions.loadUnreads('current_channel_id'));
expect(testStore.getActions()).toEqual([
{amount: 60, data: 'current_channel_id', type: 'INCREASE_POST_VISIBILITY'},
{args: ['current_channel_id'], type: 'MOCK_GET_UNREADS_POSTS'},
]);
});

test('searchForTerm', async () => {
const testStore = await mockStore(initialState);

Expand Down
40 changes: 40 additions & 0 deletions actions/views/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// See LICENSE.txt for license information.

import {leaveChannel as leaveChannelRedux, joinChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
import {getPostsSince, getPostsBefore} from 'mattermost-redux/actions/posts';
import {getChannel, getChannelByName, getCurrentChannel, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId, getUserByUsername} from 'mattermost-redux/selectors/entities/users';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {isFavoriteChannel} from 'mattermost-redux/utils/channel_utils';
import {getNewestPostIdFromPosts, getOldestPostIdFromPosts} from 'mattermost-redux/utils/post_utils';
import {autocompleteUsers} from 'mattermost-redux/actions/users';

import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
Expand All @@ -16,6 +18,8 @@ import {browserHistory} from 'utils/browser_history';
import {Constants, ActionTypes} from 'utils/constants.jsx';
import {isMobile} from 'utils/utils.jsx';

const POST_INCREASE_AMOUNT = Constants.POST_CHUNK_SIZE / 2;

export function checkAndSetMobileView() {
return (dispatch) => {
dispatch({
Expand Down Expand Up @@ -117,3 +121,39 @@ export function autocompleteUsersInChannel(prefix, channelId) {
};
}

export function changeChannelPostsStatus(params) {
// takes params in the format of {channelId, atEnd: true/false} or {channelId, atStart: true/false}

return (dispatch) => {
dispatch({
type: ActionTypes.CHANNEL_POSTS_STATUS,
data: params,
});
};
}

export function channelSyncCompleted(channelId) {
return async (dispatch) => {
dispatch({
type: ActionTypes.CHANNEL_SYNC_STATUS,
data: channelId,
});
};
}

export function syncChannelPosts({channelId, channelPostsStatus, lastDisconnectAt, posts}) {
return async (dispatch) => {
if (channelPostsStatus.atEnd) {
await dispatch(getPostsSince(channelId, lastDisconnectAt));
} else {
let data;
const oldestPostId = getOldestPostIdFromPosts(posts);
let newestMessageId = getNewestPostIdFromPosts(posts);
do {
({data} = await dispatch(getPostsBefore(channelId, newestMessageId, 0, POST_INCREASE_AMOUNT))); // eslint-disable-line no-await-in-loop
newestMessageId = data.order[data.order.length - 1];
} while (data && !data.posts[oldestPostId]);
}
dispatch(channelSyncCompleted(channelId));
};
}
19 changes: 19 additions & 0 deletions actions/views/channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {leaveChannel} from 'mattermost-redux/actions/channels';
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';

const mockStore = configureStore([thunk]);

Expand Down Expand Up @@ -104,4 +105,22 @@ describe('channel view actions', () => {
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}/channels/${General.DEFAULT_CHANNEL}`);
});
});

test('changeChannelPostsStatus', async () => {
const params = {};
await store.dispatch(Actions.changeChannelPostsStatus(params));
expect(store.getActions()).toEqual([{
type: ActionTypes.CHANNEL_POSTS_STATUS,
data: params,
}]);
});

test('channelSyncCompleted', async () => {
const channelId = 'channelId';
await store.dispatch(Actions.channelSyncCompleted(channelId));
expect(store.getActions()).toEqual([{
type: ActionTypes.CHANNEL_SYNC_STATUS,
data: channelId,
}]);
});
});
23 changes: 20 additions & 3 deletions actions/websocket_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import {
import {setServerVersion} from 'mattermost-redux/actions/general';
import {clearErrors, logError} from 'mattermost-redux/actions/errors';

import {getPosts, getProfilesAndStatusesForPosts, getCustomEmojiForReaction} from 'mattermost-redux/actions/posts';
import {getProfilesAndStatusesForPosts, getCustomEmojiForReaction} from 'mattermost-redux/actions/posts';
import * as TeamActions from 'mattermost-redux/actions/teams';
import {getMe, getStatusesByIds, getProfilesByIds} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUser, getCurrentUserId, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getMyTeams, getCurrentRelativeTeamUrl, getCurrentTeamId, getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getChannel, getCurrentChannel, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common';

import {openModal} from 'actions/views/modals';
import {incrementWsErrorCount, resetWsErrorCount} from 'actions/views/system';
Expand Down Expand Up @@ -129,6 +130,10 @@ export function reconnect(includeWebSocket = true) {
reconnectWebSocket();
}

dispatch({
type: GeneralTypes.WEBSOCKET_SUCCESS,
});

loadPluginsIfNecessary();

Object.values(pluginReconnectHandlers).forEach((handler) => {
Expand All @@ -140,7 +145,6 @@ export function reconnect(includeWebSocket = true) {
const currentTeamId = getState().entities.teams.currentTeamId;
if (currentTeamId) {
dispatch(loadChannelsForCurrentUser());
dispatch(getPosts(getCurrentChannelId(getState())));
StatusActions.loadStatusesForChannelAndSidebar();
dispatch(TeamActions.getMyTeamUnreads());
}
Expand Down Expand Up @@ -190,13 +194,26 @@ export function unregisterAllPluginWebSocketEvents(pluginId) {
}

function handleFirstConnect() {
dispatch(clearErrors);
dispatch(batchActions([
{type: GeneralTypes.WEBSOCKET_SUCCESS},
]));
}

function handleClose(failCount) {
if (failCount > MAX_WEBSOCKET_FAILS) {
dispatch(logError({type: 'critical', message: AnnouncementBarMessages.WEBSOCKET_PORT_ERROR}, true));
}

dispatch(batchActions([{
type: GeneralTypes.WEBSOCKET_FAILURE,
}, {
type: ActionTypes.ALL_CHANNEL_SYNC_STATUS,
data: {
channelIds: Object.keys(getMyChannelMemberships(getState())),
status: false,
},
}]));

dispatch(incrementWsErrorCount());
}

Expand Down
21 changes: 18 additions & 3 deletions components/channel_view/channel_view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import PostView from 'components/post_view';
import TutorialView from 'components/tutorial';
import {clearMarks, mark, measure, trackEvent} from 'actions/diagnostics_actions.jsx';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import LoadingScreen from 'components/loading_screen.jsx';

export default class ChannelView extends React.PureComponent {
static propTypes = {
Expand All @@ -26,6 +27,12 @@ export default class ChannelView extends React.PureComponent {
showTutorial: PropTypes.bool.isRequired,
channelIsArchived: PropTypes.bool.isRequired,
viewArchivedChannels: PropTypes.bool.isRequired,

/**
* For indicating channelLoading
* To prevent child views from loading with wrong channel.
*/
channelLoading: PropTypes.bool,
actions: PropTypes.shape({
goToLastViewedChannel: PropTypes.func.isRequired,
}),
Expand Down Expand Up @@ -162,9 +169,17 @@ export default class ChannelView extends React.PureComponent {
<ChannelHeader
channelId={this.props.channelId}
/>
<DeferredPostView
channelId={this.props.channelId}
/>
{ this.props.channelLoading ? (
<LoadingScreen
position='relative'
style={{height: 'calc(100% - 139px)'}}
key='loading'
/>
) : (
<DeferredPostView
channelId={this.props.channelId}
/>
)}
{createPost}
</div>
);
Expand Down
Loading