diff --git a/actions/post_actions.jsx b/actions/post_actions.jsx
index b6ef1c2e7ee7..0a9c989aa466 100644
--- a/actions/post_actions.jsx
+++ b/actions/post_actions.jsx
@@ -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';
@@ -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) {
@@ -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));
}
@@ -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));
diff --git a/actions/post_actions.test.js b/actions/post_actions.test.js
index 4112032d3bdf..1ce7324604b0 100644
--- a/actions/post_actions.test.js
+++ b/actions/post_actions.test.js
@@ -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]);
@@ -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', () => ({
@@ -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: [
@@ -241,11 +243,15 @@ 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: [
@@ -253,11 +259,16 @@ describe('Actions.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: [
@@ -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);
diff --git a/actions/views/channel.js b/actions/views/channel.js
index 6a24bd18a25a..734b9e960bf2 100644
--- a/actions/views/channel.js
+++ b/actions/views/channel.js
@@ -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';
@@ -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({
@@ -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));
+ };
+}
diff --git a/actions/views/channel.test.js b/actions/views/channel.test.js
index bd0c531d44b5..9505606c7891 100644
--- a/actions/views/channel.test.js
+++ b/actions/views/channel.test.js
@@ -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]);
@@ -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,
+ }]);
+ });
});
diff --git a/actions/websocket_actions.jsx b/actions/websocket_actions.jsx
index 6f55373d2508..f0493cdacdd4 100644
--- a/actions/websocket_actions.jsx
+++ b/actions/websocket_actions.jsx
@@ -24,7 +24,7 @@ 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';
@@ -32,6 +32,7 @@ import {getCurrentUser, getCurrentUserId, getStatusForUserId, getUser} from 'mat
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';
@@ -129,6 +130,10 @@ export function reconnect(includeWebSocket = true) {
reconnectWebSocket();
}
+ dispatch({
+ type: GeneralTypes.WEBSOCKET_SUCCESS,
+ });
+
loadPluginsIfNecessary();
Object.values(pluginReconnectHandlers).forEach((handler) => {
@@ -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());
}
@@ -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());
}
diff --git a/components/channel_view/channel_view.jsx b/components/channel_view/channel_view.jsx
index d5ebcfb5345f..e43e059840ce 100644
--- a/components/channel_view/channel_view.jsx
+++ b/components/channel_view/channel_view.jsx
@@ -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 = {
@@ -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,
}),
@@ -162,9 +169,17 @@ export default class ChannelView extends React.PureComponent {
-
+ { this.props.channelLoading ? (
+
+ ) : (
+
+ )}
{createPost}
);
diff --git a/components/channel_view/index.js b/components/channel_view/index.js
index 4b5155c07089..53d71981119f 100644
--- a/components/channel_view/index.js
+++ b/components/channel_view/index.js
@@ -4,14 +4,15 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
+import {withRouter} from 'react-router-dom';
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
-import {withRouter} from 'react-router-dom';
+import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {getDirectTeammate} from 'utils/utils.jsx';
-import {TutorialSteps, Preferences} from 'utils/constants.jsx';
+import {TutorialSteps, Preferences, Constants} from 'utils/constants.jsx';
import {goToLastViewedChannel} from 'actions/views/channel';
@@ -27,13 +28,32 @@ const getDeactivatedChannel = createSelector(
}
);
-function mapStateToProps(state) {
+function mapStateToProps(state, ownProps) {
const channel = getCurrentChannel(state);
-
+ let channelLoading = false;
const config = getConfig(state);
const enableTutorial = config.EnableTutorial === 'true';
const tutorialStep = getInt(state, Preferences.TUTORIAL_STEP, getCurrentUserId(state), TutorialSteps.FINISHED);
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
+ const team = getTeamByName(state, ownProps.match.params.team);
+
+ if (channel) {
+ if (channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL) {
+ if (channel.name !== ownProps.match.params.identifier) {
+ channelLoading = true;
+ }
+
+ if (channel.team_id && channel.team_id !== team.id) {
+ channelLoading = true;
+ }
+ }
+ } else {
+ channelLoading = true;
+ }
+
+ if (channel && (channel.team_id && channel.team_id !== team.id)) {
+ channelLoading = true;
+ }
return {
channelId: channel ? channel.id : '',
@@ -41,6 +61,7 @@ function mapStateToProps(state) {
showTutorial: enableTutorial && tutorialStep <= TutorialSteps.INTRO_SCREENS,
channelIsArchived: channel ? channel.delete_at !== 0 : false,
viewArchivedChannels,
+ channelLoading,
};
}
diff --git a/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap b/components/permalink_view/__snapshots__/permalink_view.test.jsx.snap
index bdfd8379ed18..c52717f0e135 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`] = `
-
@@ -41,7 +41,7 @@ exports[`components/PermalinkView should match snapshot with archived channel 1`
-
diff --git a/components/post_view/__snapshots__/post_view.test.jsx.snap b/components/post_view/__snapshots__/post_view.test.jsx.snap
new file mode 100644
index 000000000000..937bac16b9c6
--- /dev/null
+++ b/components/post_view/__snapshots__/post_view.test.jsx.snap
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/post_view/post_list Should call postsOnLoad 1`] = `
+
+`;
+
+exports[`components/post_view/post_list init snapshot for max postVisibility 1`] = `
+
+`;
+
+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/channel_intro_message/index.js b/components/post_view/channel_intro_message/index.js
index 0758acd4be1d..f5b19657a538 100644
--- a/components/post_view/channel_intro_message/index.js
+++ b/components/post_view/channel_intro_message/index.js
@@ -4,9 +4,11 @@
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
-import {isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentChannel, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
+import {get} from 'mattermost-redux/selectors/entities/preferences';
+import {Preferences} from 'utils/constants.jsx';
import {getCurrentLocale} from 'selectors/i18n';
import ChannelIntroMessage from './channel_intro_message.jsx';
@@ -17,10 +19,12 @@ function mapStateToProps(state) {
const isReadOnly = isCurrentChannelReadOnly(state);
return {
+ channel: getCurrentChannel(state),
locale: getCurrentLocale(state),
channelProfiles: getProfilesInCurrentChannel(state),
enableUserCreation,
isReadOnly,
+ fullWidth: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
};
}
diff --git a/components/post_view/index.js b/components/post_view/index.js
index 6e273225ea5b..7f306ce35389 100644
--- a/components/post_view/index.js
+++ b/components/post_view/index.js
@@ -3,24 +3,28 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {getPosts, getPostsAfter, getPostsBefore, getPostThread} from 'mattermost-redux/actions/posts';
+import {getPostThread} from 'mattermost-redux/actions/posts';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
-import {makeGetPostsAroundPost, makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
-import {get} from 'mattermost-redux/selectors/entities/preferences';
+import {makeGetPostsAroundPost, makeGetPostsInChannel, getPostIdsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
+import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common';
-import {increasePostVisibility} from 'actions/post_actions.jsx';
-import {checkAndSetMobileView} from 'actions/views/channel';
-import {Preferences} from 'utils/constants.jsx';
+import {makeGetChannelPostStatus, makeGetChannelSyncStatus} from 'selectors/views/channel';
+import {loadPosts, loadUnreads} from 'actions/post_actions';
+import {changeChannelPostsStatus, channelSyncCompleted, syncChannelPosts, checkAndSetMobileView} from 'actions/views/channel';
+import {getSocketStatus} from 'selectors/views/websocket';
-import PostList from './post_list.jsx';
+import PostView from './post_view.jsx';
function makeMapStateToProps() {
const getPostsInChannel = makeGetPostsInChannel();
const getPostsAroundPost = makeGetPostsAroundPost();
+ const getChannelPostStatus = makeGetChannelPostStatus();
+ const getChannelSyncStatus = makeGetChannelSyncStatus();
return function mapStateToProps(state, ownProps) {
const postVisibility = state.views.channel.postVisibility[ownProps.channelId];
+ const member = getMyChannelMemberships(state)[ownProps.channelId];
let posts;
if (ownProps.focusedPostId) {
@@ -34,10 +38,12 @@ function makeMapStateToProps() {
lastViewedAt: state.views.channel.lastChannelViewTime[ownProps.channelId],
posts,
postVisibility,
- loadingPosts: state.views.channel.loadingPosts[ownProps.channelId],
- focusedPostId: ownProps.focusedPostId,
currentUserId: getCurrentUserId(state),
- fullWidth: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
+ member,
+ channelPostsStatus: getChannelPostStatus(state, ownProps.channelId),
+ postIdsInCurrentChannel: getPostIdsInCurrentChannel(state),
+ channelSyncStatus: getChannelSyncStatus(state, ownProps.channelId),
+ socketStatus: getSocketStatus(state),
};
};
}
@@ -45,14 +51,15 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
- getPosts,
- getPostsBefore,
- getPostsAfter,
getPostThread,
- increasePostVisibility,
+ loadUnreads,
+ loadPosts,
+ changeChannelPostsStatus,
+ channelSyncCompleted,
+ syncChannelPosts,
checkAndSetMobileView,
}, dispatch),
};
}
-export default connect(makeMapStateToProps, mapDispatchToProps)(PostList);
+export default connect(makeMapStateToProps, mapDispatchToProps)(PostView);
diff --git a/components/post_view/post_list.jsx b/components/post_view/post_list.jsx
index 92930891453d..e13c93acdbaa 100644
--- a/components/post_view/post_list.jsx
+++ b/components/post_view/post_list.jsx
@@ -3,30 +3,28 @@
import PropTypes from 'prop-types';
import React from 'react';
-import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
+import ReactDOM from 'react-dom';
import {isUserActivityPost} from 'mattermost-redux/utils/post_utils';
-import Constants, {PostTypes} from 'utils/constants.jsx';
+import Constants, {PostTypes, PostRequestTypes} from 'utils/constants.jsx';
+import {isFromWebhook} from 'utils/post_utils.jsx';
+import * as Utils from 'utils/utils.jsx';
+import * as UserAgent from 'utils/user_agent.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
-import EventTypes from 'utils/event_types.jsx';
import GlobalEventEmitter from 'utils/global_event_emitter.jsx';
-import * as UserAgent from 'utils/user_agent.jsx';
-import * as Utils from 'utils/utils.jsx';
-import {isFromWebhook} from 'utils/post_utils.jsx';
+import EventTypes from 'utils/event_types.jsx';
-import LoadingScreen from 'components/loading_screen.jsx';
import DateSeparator from 'components/post_view/date_separator.jsx';
+import Post from 'components/post_view/post';
import FloatingTimestamp from './floating_timestamp.jsx';
import NewMessageIndicator from './new_message_indicator.jsx';
-import Post from './post';
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
import CreateChannelIntroMessage from './channel_intro_message';
const CLOSE_TO_BOTTOM_SCROLL_MARGIN = 10;
-const POSTS_PER_PAGE = Constants.POST_CHUNK_SIZE / 2;
const MAX_EXTRA_PAGES_LOADED = 10;
const MAX_NUMBER_OF_AUTO_RETRIES = 3;
@@ -43,67 +41,50 @@ export default class PostList extends React.PureComponent {
posts: PropTypes.array,
/**
- * The number of posts that should be rendered
+ * Timestamp used for populating new messages indicator
*/
- postVisibility: PropTypes.number,
+ lastViewedAt: PropTypes.number,
/**
- * The channel the posts are in
+ * Used for excluding own messages for new messages indicator
*/
- channel: PropTypes.object.isRequired,
+ currentUserId: PropTypes.string,
/**
- * The last time the channel was viewed, sets the new message separator
+ * Data used for determining more messages state at the bottom
*/
- lastViewedAt: PropTypes.number,
+ newerPosts: PropTypes.shape({
+ loading: PropTypes.bool,
+ allLoaded: PropTypes.bool,
+ }),
/**
- * The user id of the logged in user
+ * Data used for determining more messages state at the top
*/
- currentUserId: PropTypes.string,
+ olderPosts: PropTypes.shape({
+ loading: PropTypes.bool,
+ allLoaded: PropTypes.bool,
+ }),
/**
- * Set to focus this post
+ * Function to get older posts in the channel
*/
- focusedPostId: PropTypes.string,
+ loadOlderPosts: PropTypes.func.isRequired,
/**
- * Whether to display the channel intro at full width
+ * Function to get newer posts in the channel
*/
- fullWidth: PropTypes.bool,
-
- actions: PropTypes.shape({
-
- /**
- * Function to get posts in the channel
- */
- getPosts: PropTypes.func.isRequired,
-
- /**
- * Function to get posts in the channel older than the focused post
- */
- getPostsBefore: PropTypes.func.isRequired,
-
- /**
- * Function to get posts in the channel newer than the focused post
- */
- getPostsAfter: PropTypes.func.isRequired,
-
- /**
- * Function to get the post thread for the focused post
- */
- getPostThread: PropTypes.func.isRequired,
-
- /**
- * Function to increase the number of posts being rendered
- */
- increasePostVisibility: PropTypes.func.isRequired,
-
- /**
- * Function to check and set if app is in mobile view
- */
- checkAndSetMobileView: PropTypes.func.isRequired,
- }).isRequired,
+ loadNewerPosts: PropTypes.func.isRequired,
+
+ /**
+ * Used for disabling loading more messages
+ */
+ postVisibility: PropTypes.number,
+
+ /**
+ * Used for focussing post on load
+ */
+ focusedPostId: PropTypes.string,
}
constructor(props) {
@@ -122,20 +103,14 @@ export default class PostList extends React.PureComponent {
this.atBottom = false;
this.state = {
- atEnd: false,
unViewedCount: 0,
- isDoingInitialLoad: true,
isScrolling: false,
- lastViewed: props.lastViewedAt,
autoRetryEnable: true,
};
}
componentDidMount() {
- this.loadPosts(this.props.channel.id, this.props.focusedPostId);
- this.props.actions.checkAndSetMobileView();
GlobalEventEmitter.addListener(EventTypes.POST_LIST_SCROLL_CHANGE, this.handleResize);
-
window.addEventListener('resize', this.handleWindowResize);
this.initialScroll();
@@ -150,7 +125,7 @@ export default class PostList extends React.PureComponent {
if (this.postListRef.current) {
const previousScrollTop = this.postListRef.current.scrollTop;
const previousScrollHeight = this.postListRef.current.scrollHeight;
- const wasAtBottom = this.checkBottom();
+ const wasAtBottom = this.checkBottom() && this.props.newerPosts.allLoaded;
return {
previousScrollTop,
@@ -163,26 +138,10 @@ export default class PostList extends React.PureComponent {
componentDidUpdate(prevProps, prevState, snapshot) {
this.previousScrollHeight = snapshot && snapshot.previousScrollHeight;
- if (this.props.focusedPostId && this.props.focusedPostId !== prevProps.focusedPostId) {
- this.hasScrolledToFocusedPost = false;
- this.hasScrolledToNewMessageSeparator = false;
- this.loadPosts(this.props.channel.id, this.props.focusedPostId);
- } else if (this.props.channel && (!prevProps.channel || this.props.channel.id !== prevProps.channel.id)) {
- this.hasScrolled = false;
- this.hasScrolledToFocusedPost = false;
- this.hasScrolledToNewMessageSeparator = false;
- this.atBottom = false;
- this.extraPagesLoaded = 0;
-
- this.setState({atEnd: false, isDoingInitialLoad: !this.props.posts, unViewedCount: 0}); // eslint-disable-line react/no-did-update-set-state
-
- this.loadPosts(this.props.channel.id);
- }
-
this.loadPostsToFillScreenIfNecessary();
// Do not update scrolling unless posts, visibility or intro message change
- if (this.props.posts === prevProps.posts && this.props.postVisibility === prevProps.postVisibility && this.state.atEnd === prevState.atEnd && this.state.loadingPosts === prevState.loadingPosts) {
+ if (this.props.posts === prevProps.posts && this.props.postVisibility === prevProps.postVisibility) {
return;
}
@@ -203,31 +162,6 @@ export default class PostList extends React.PureComponent {
}
const postlistScrollHeight = postList.scrollHeight;
- // Scroll to focused post on first load
- const focusedPost = this.refs[this.props.focusedPostId];
- if (focusedPost && this.props.posts) {
- if (!this.hasScrolledToFocusedPost) {
- const element = ReactDOM.findDOMNode(focusedPost);
- const rect = element.getBoundingClientRect();
- const listHeight = postList.clientHeight / 2;
- postList.scrollTop = (rect.top - listHeight) + postList.scrollTop;
- this.atBottom = this.checkBottom();
- } else if (snapshot.previousScrollHeight !== postlistScrollHeight && posts[0].id === prevPosts[0].id) {
- postList.scrollTop = snapshot.previousScrollTop + (postlistScrollHeight - snapshot.previousScrollHeight);
- }
- return;
- }
-
- const didInitialScroll = this.initialScroll();
-
- if (posts.length >= POSTS_PER_PAGE) {
- this.hasScrolledToNewMessageSeparator = true;
- }
-
- if (didInitialScroll) {
- return;
- }
-
if (postList && prevPosts && posts && posts[0] && prevPosts[0]) {
// A new message was posted, so scroll to bottom if user
// was already scrolled close to bottom
@@ -235,7 +169,7 @@ export default class PostList extends React.PureComponent {
const postId = posts[0].id;
const prevPostId = prevPosts[0].id;
const pendingPostId = posts[0].pending_post_id;
- if (postId !== prevPostId || pendingPostId === prevPostId) {
+ if ((postId !== prevPostId || pendingPostId === prevPostId) && this.props.newerPosts.allLoaded) {
// If already scrolled to bottom
if (snapshot.wasAtBottom) {
doScrollToBottom = true;
@@ -254,7 +188,7 @@ export default class PostList extends React.PureComponent {
}
// New posts added at the top, maintain scroll position
- if (snapshot.previousScrollHeight !== postlistScrollHeight && posts[0].id === prevPosts[0].id) {
+ if (snapshot.previousScrollHeight !== postlistScrollHeight && (posts[0].id === prevPosts[0].id)) {
window.requestAnimationFrame(() => {
postList.scrollTop = snapshot.previousScrollTop + (postlistScrollHeight - snapshot.previousScrollHeight);
});
@@ -267,7 +201,7 @@ export default class PostList extends React.PureComponent {
return;
}
- if (this.state.isDoingInitialLoad || this.state.loadingPosts) {
+ if (this.props.olderPosts.loading) {
// Should already be loading posts
return;
}
@@ -277,7 +211,7 @@ export default class PostList extends React.PureComponent {
return;
}
- if (this.state.atEnd || this.postListContentRef.current.scrollHeight >= this.postListRef.current.clientHeight) {
+ if (this.props.olderPosts.allLoaded || this.postListContentRef.current.scrollHeight >= this.postListRef.current.clientHeight) {
// Screen is full
return;
}
@@ -289,38 +223,34 @@ export default class PostList extends React.PureComponent {
return;
}
- await this.loadMorePosts();
+ await this.loadMoreOlderPosts();
this.extraPagesLoaded += 1;
};
// Scroll to new message indicator or bottom on first load. Returns true
// if we just scrolled for the initial load.
initialScroll = () => {
- if (this.hasScrolledToNewMessageSeparator) {
- // Already scrolled to new messages indicator
- return false;
- }
-
const postList = this.postListRef.current;
- const posts = this.props.posts;
- if (!postList || !posts) {
- // Not able to do initial scroll yet
- return false;
- }
+ const {posts, focusedPostId} = this.props;
const messageSeparator = this.refs.newMessageSeparator;
- // Scroll to new message indicator since we have unread posts and we can't show every new post in the screen
- if (messageSeparator && (postList.scrollHeight - messageSeparator.offsetTop) > postList.clientHeight) {
+ if (focusedPostId) {
+ const focusedPost = this.refs[this.props.focusedPostId];
+ const element = ReactDOM.findDOMNode(focusedPost);
+ const rect = element.getBoundingClientRect();
+ const listHeight = postList.clientHeight / 2;
+ postList.scrollTop = (rect.top - listHeight) + postList.scrollTop;
+ this.atBottom = this.checkBottom();
+ } else if (messageSeparator && (postList.scrollHeight - messageSeparator.offsetTop) > postList.clientHeight) {
+ // Scroll to new message indicator since we have unread posts and we can't show every new post in the screen
messageSeparator.scrollIntoView();
this.setUnreadsBelow(posts, this.props.currentUserId);
- return true;
+ } else {
+ // Scroll to bottom since we don't have unread posts or we can show every new post in the screen
+ this.atBottom = true;
+ postList.scrollTop = postList.scrollHeight;
}
-
- // Scroll to bottom since we don't have unread posts or we can show every new post in the screen
- this.atBottom = true;
- postList.scrollTop = postList.scrollHeight;
- return true;
}
setUnreadsBelow = (posts, currentUserId) => {
@@ -368,71 +298,59 @@ export default class PostList extends React.PureComponent {
this.resizeAnimationFrame = window.requestAnimationFrame(() => {
const postList = this.postListRef.current;
const messageSeparator = this.refs.newMessageSeparator;
- const doScrollToBottom = this.atBottom || forceScrollToBottom;
+ const doScrollToBottom = (this.atBottom && this.props.newerPosts.allLoaded) || forceScrollToBottom;
if (postList) {
if (doScrollToBottom) {
postList.scrollTop = postList.scrollHeight;
- } else if (!this.hasScrolled && messageSeparator) {
+ } else if (messageSeparator) {
const element = ReactDOM.findDOMNode(messageSeparator);
element.scrollIntoView();
this.atBottom = this.checkBottom();
}
this.previousScrollHeight = postList.scrollHeight;
}
- this.props.actions.checkAndSetMobileView();
+
+ // this.props.actions.checkAndSetMobileView();
});
}
- loadPosts = async (channelId, focusedPostId) => {
- let posts;
- if (focusedPostId) {
- const getPostThreadAsync = this.props.actions.getPostThread(focusedPostId, false);
- const getPostsBeforeAsync = this.props.actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE);
- const getPostsAfterAsync = this.props.actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE);
+ loadMoreOlderPosts = () => {
+ return this.loadMorePosts(PostRequestTypes.BEFORE_ID);
+ }
- const result = await getPostsBeforeAsync;
- posts = result.data;
- await getPostsAfterAsync;
- await getPostThreadAsync;
+ loadMoreNewerPosts = () => {
+ this.loadingNewPosts = true;
+ return this.loadMorePosts(PostRequestTypes.AFTER_ID);
+ }
- this.hasScrolledToFocusedPost = true;
+ loadMorePosts = async (type) => {
+ let error;
+ if (type === PostRequestTypes.BEFORE_ID) {
+ ({error} = await this.props.loadOlderPosts());
} else {
- const result = await this.props.actions.getPosts(channelId, 0, POSTS_PER_PAGE);
- posts = result.data;
-
- if (!this.checkBottom()) {
- const postsArray = posts.order.map((id) => posts.posts[id]);
- this.setUnreadsBelow(postsArray, this.props.currentUserId);
- }
-
- this.hasScrolledToNewMessageSeparator = true;
+ ({error} = await this.props.loadNewerPosts());
}
- this.setState({
- isDoingInitialLoad: false,
- atEnd: Boolean(posts && posts.order.length < POSTS_PER_PAGE),
- });
- }
-
- loadMorePosts = async () => {
- const {moreToLoad, error} = await this.props.actions.increasePostVisibility(this.props.channel.id, this.props.focusedPostId);
if (error) {
if (this.autoRetriesCount < MAX_NUMBER_OF_AUTO_RETRIES) {
this.autoRetriesCount++;
- this.loadMorePosts();
+ if (type === PostRequestTypes.BEFORE_ID) {
+ this.loadMoreOlderPosts();
+ } else {
+ this.loadMoreNewerPosts();
+ }
} else {
this.setState({autoRetryEnable: false});
}
} else {
this.setState({
- atEnd: !moreToLoad && this.props.posts.length < this.props.postVisibility,
autoRetryEnable: true,
- loadingPosts: false,
});
if (!this.state.autoRetryEnable) {
this.autoRetriesCount = 0;
}
+ this.loadingNewPosts = false;
}
}
@@ -443,11 +361,11 @@ export default class PostList extends React.PureComponent {
this.scrollAnimationFrame = window.requestAnimationFrame(() => {
// Only count as user scroll if we've already performed our first load scroll
- this.hasScrolled = this.hasScrolledToNewMessageSeparator || this.hasScrolledToFocusedPost;
const postList = this.postListRef.current;
const postListScrollTop = postList.scrollTop;
+ const postlistScrollHeight = postList.scrollHeight;
- if (postList.scrollHeight === this.previousScrollHeight) {
+ if (postlistScrollHeight === this.previousScrollHeight) {
this.atBottom = this.checkBottom();
}
@@ -467,19 +385,29 @@ export default class PostList extends React.PureComponent {
});
}
- let shouldLoadPosts = false;
+ let shouldLoadOldPosts = false;
+ let shouldLoadNewPosts = false;
+
+ const postlistClientHeight = postList.clientHeight;
const scrollHeightAoveFoldForLoad = LOADPOSTS_SCROLL_RATIO * (postList.scrollHeight - postList.clientHeight);
if (postListScrollTop < LOADPOSTS_MIN_HEIGHT) {
- shouldLoadPosts = true;
+ shouldLoadOldPosts = true;
} else if ((postListScrollTop < LOADPOSTS_MAX_HEIGHT) && (postListScrollTop < scrollHeightAoveFoldForLoad)) {
- shouldLoadPosts = true;
+ shouldLoadOldPosts = true;
+ } else if (postlistClientHeight + postListScrollTop >= postlistScrollHeight - LOADPOSTS_MIN_HEIGHT) {
+ shouldLoadNewPosts = true;
+ }
+
+ if (shouldLoadOldPosts && !this.props.olderPosts.loading && !this.props.olderPosts.allLoaded && this.state.autoRetryEnable) {
+ this.loadMoreOlderPosts();
}
- if (shouldLoadPosts && !this.state.loadingPosts && !this.state.atEnd && this.state.autoRetryEnable) {
- this.setState({loadingPosts: true});
- this.loadMorePosts();
+ if (shouldLoadNewPosts && !this.props.newerPosts.loading && !this.props.newerPosts.allLoaded && this.state.autoRetryEnable) {
+ this.loadingNewPosts = true;
+ this.loadMoreNewerPosts();
}
+
this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY);
});
}
@@ -609,76 +537,76 @@ export default class PostList extends React.PureComponent {
return this.postListRef.current;
}
+ makeLoadingPlaceHolder = () => {
+ return (
+
+ );
+ }
+
+ makeLoadMoreMessagesHandler = (type) => {
+ const clickHandler = type === 'top' ? this.loadMoreOlderPosts : this.loadMoreNewerPosts;
+
+ return (
+
+ );
+ }
+
+ makeMaxMessagesLoadedMessage = () => {
+ return (
+
+
+
+ );
+ }
+
render() {
const posts = this.props.posts || [];
- const channel = this.props.channel;
-
- if ((posts.length === 0 && this.state.isDoingInitialLoad) || channel == null) {
- return (
-
-
-
- );
- }
let topRow;
- if (this.state.atEnd) {
- topRow = (
-
- );
- } else if (this.props.postVisibility >= Constants.MAX_POST_VISIBILITY) {
- topRow = (
-
-
-
- );
- } else if (this.state.isDoingInitialLoad) {
- topRow = ;
+ let bottomRow;
+ if (this.props.postVisibility >= Constants.MAX_POST_VISIBILITY) {
+ topRow = this.makeMaxMessagesLoadedMessage();
+ bottomRow = topRow;
} else if (this.state.autoRetryEnable) {
- topRow = (
-
- );
+ topRow = this.makeLoadingPlaceHolder();
+ bottomRow = topRow;
} else {
+ topRow = this.makeLoadMoreMessagesHandler('top');
+ bottomRow = this.makeLoadMoreMessagesHandler('bottom');
+ }
+
+ if (this.props.olderPosts.allLoaded) {
topRow = (
-
+
);
}
- const topPostCreateAt = this.state.topPost ? this.state.topPost.create_at : 0;
-
- let postVisibility = this.props.postVisibility;
-
- // In focus mode there's an extra (Constants.POST_CHUNK_SIZE / 2) posts to show
- if (this.props.focusedPostId) {
- postVisibility += Constants.POST_CHUNK_SIZE / 2;
+ if (this.props.newerPosts.allLoaded) {
+ bottomRow = null;
}
+ const topPostCreateAt = this.state.topPost ? this.state.topPost.create_at : 0;
+
return (
@@ -708,7 +635,8 @@ export default class PostList extends React.PureComponent {
className='post-list__content'
>
{topRow}
- {this.createPosts(posts.slice(0, postVisibility))}
+ {this.createPosts(posts)}
+ {bottomRow}
diff --git a/components/post_view/post_view.jsx b/components/post_view/post_view.jsx
new file mode 100644
index 000000000000..07c40fb96550
--- /dev/null
+++ b/components/post_view/post_view.jsx
@@ -0,0 +1,358 @@
+// 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 {getNewestPostIdFromPosts, getOldestPostIdFromPosts} from 'mattermost-redux/utils/post_utils';
+
+import LoadingScreen from 'components/loading_screen.jsx';
+import Constants, {PostRequestTypes} from 'utils/constants.jsx';
+
+import PostList from './post_list.jsx';
+
+const POSTS_PER_PAGE = Constants.POST_CHUNK_SIZE / 2;
+
+export default class PostView extends React.PureComponent {
+ static propTypes = {
+
+ /**
+ * Array of posts in the channel, ordered from newest to oldest
+ */
+ posts: PropTypes.array,
+
+ /**
+ * The channel the posts are in
+ */
+ channelId: PropTypes.string,
+
+ /**
+ * Used for disabling loading more messages
+ */
+ postVisibility: PropTypes.number,
+
+ /**
+ * Used for showing more message indicators and channel intro message
+ */
+ channelPostsStatus: PropTypes.object,
+
+ /**
+ * Used for unread count
+ */
+ currentUserId: PropTypes.string,
+
+ /**
+ * Used for unread count
+ */
+ lastViewedAt: PropTypes.number,
+
+ /**
+ * Flag used to determine if channel has to call sync actions.
+ */
+ channelSyncStatus: PropTypes.bool,
+
+ /**
+ * Used for websocket connect change to trigger sync.
+ */
+ socketStatus: PropTypes.object,
+
+ /**
+ * Used for determining if we reached to the top of channel.
+ */
+ postIdsInCurrentChannel: PropTypes.array,
+
+ /*
+ * To get posts for perma view
+ */
+ focusedPostId: PropTypes.string,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Get post for permaview
+ */
+ getPostThread: PropTypes.func.isRequired,
+
+ /*
+ * Get unreads posts onload
+ */
+ loadUnreads: PropTypes.func.isRequired,
+
+ /*
+ * Get posts using BEFORE_ID and AFTER_ID
+ */
+ loadPosts: PropTypes.func.isRequired,
+
+ /*
+ * Used for changing posts status of channel
+ * For notifying redux store when we hit bottom or top of posts
+ */
+ changeChannelPostsStatus: PropTypes.func.isRequired,
+
+ /*
+ * Used for notifying redux store on channel sync complete
+ */
+ channelSyncCompleted: PropTypes.func.isRequired,
+
+ /*
+ * Used to sync when channel is out of sync because of socket disconnect
+ */
+ syncChannelPosts: PropTypes.func.isRequired,
+
+ /*
+ * Used to set mobile view on resize
+ */
+ checkAndSetMobileView: PropTypes.func.isRequired,
+
+ }).isRequired,
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ newerPosts: {
+ loading: false,
+ allLoaded: (props.channelPostsStatus && props.channelPostsStatus.atEnd) || false,
+ },
+ olderPosts: {
+ loading: false,
+ allLoaded: PostView.oldestMessageLoadedInView(props),
+ },
+ posts: this.props.posts,
+ };
+ }
+
+ componentDidMount() {
+ if (this.shouldLoadPosts(this.props)) {
+ this.postsOnLoad(this.props.channelId);
+ }
+ if (!this.props.channelSyncStatus && this.props.channelPostsStatus) {
+ this.syncChannelPosts(this.props);
+ }
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (prevState.posts && nextProps.posts && (prevState.posts.length !== nextProps.posts.length)) {
+ if (PostView.oldestMessageLoadedInView(nextProps) !== prevState.olderPosts.allLoaded) {
+ return {
+ olderPosts: {
+ loading: false,
+ allLoaded: PostView.oldestMessageLoadedInView(nextProps),
+ },
+ posts: nextProps.posts,
+ };
+ }
+ return {
+ posts: nextProps.posts,
+ };
+ }
+ return null;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.socketStatus && this.props.socketStatus.connected && !prevProps.socketStatus.connected) {
+ this.syncChannelPosts(this.props);
+ }
+ }
+
+ shouldLoadPosts(props) {
+ if (!props.channelPostsStatus || !props.channelPostsStatus.atEnd || props.focusedPostId) {
+ return true;
+ }
+
+ return false;
+ }
+
+ syncChannelPosts = ({channelId, channelPostsStatus, socketStatus, posts}) => {
+ this.props.actions.syncChannelPosts({
+ channelId,
+ channelPostsStatus,
+ posts,
+ lastDisconnectAt: socketStatus.lastDisconnectAt,
+ });
+ }
+
+ static oldestMessageLoadedInView({postIdsInCurrentChannel, posts, channelPostsStatus}) {
+ if (channelPostsStatus && channelPostsStatus.atStart) {
+ if (postIdsInCurrentChannel && postIdsInCurrentChannel[postIdsInCurrentChannel.length - 1] === posts[posts.length - 1].id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ postsOnLoad = async (channelId) => {
+ if (this.props.focusedPostId) {
+ await this.loadPermalinkPosts(channelId);
+ } else {
+ await this.loadUnreadPosts(channelId);
+ this.props.actions.channelSyncCompleted(channelId);
+ }
+ }
+
+ 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,
+ },
+ };
+ this.changeChannelPostsStatus({atStart: !moreToLoad});
+ } else {
+ newState = {
+ newerPosts: {
+ loading: false,
+ allLoaded: !moreToLoad,
+ },
+ };
+ this.changeChannelPostsStatus({atEnd: !moreToLoad});
+ }
+
+ this.setState(newState);
+ return {moreToLoad, error};
+ }
+
+ loadPermalinkPosts = (channelId) => {
+ const getPostThread = this.props.actions.getPostThread(this.props.focusedPostId, false);
+ const afterPosts = this.callLoadPosts(channelId, this.props.focusedPostId, PostRequestTypes.AFTER_ID);
+ const beforePosts = this.callLoadPosts(channelId, this.props.focusedPostId, PostRequestTypes.BEFORE_ID);
+ return Promise.all([
+ beforePosts,
+ afterPosts,
+ getPostThread,
+ ]);
+ }
+
+ loadUnreadPosts = async (channelId) => {
+ let atLatestMessage = false;
+ let atOldestmessage = false;
+
+ const {data} = await this.props.actions.loadUnreads(channelId);
+
+ // API returns 2*POSTS_PER_PAGE and if it less than 1*POSTS_PER_PAGE then we loaded all the posts.
+ if (data && Object.keys(data.posts).length < POSTS_PER_PAGE) {
+ atLatestMessage = true;
+ atOldestmessage = true;
+ } else {
+ const postsArray = data.order.map((id) => data.posts[id]);
+ const unreadCount = this.getUnreadPostsCount(postsArray, this.props.currentUserId);
+ if (unreadCount < POSTS_PER_PAGE) {
+ atLatestMessage = true;
+ }
+ }
+
+ this.setState({
+ olderPosts: {
+ loading: false,
+ allLoaded: atOldestmessage,
+ },
+ newerPosts: {
+ loading: false,
+ allLoaded: atLatestMessage,
+ },
+ });
+
+ this.changeChannelPostsStatus({atEnd: atLatestMessage});
+ this.changeChannelPostsStatus({atStart: atOldestmessage});
+ }
+
+ changeChannelPostsStatus = (status) => {
+ this.props.actions.changeChannelPostsStatus({
+ channelId: this.props.channelId,
+ ...status,
+ });
+ }
+
+ getPostsBefore = () => {
+ this.setLoadingPosts('olderPosts');
+ const oldestPostId = getOldestPostIdFromPosts(this.props.posts);
+ return this.callLoadPosts(this.props.channelId, oldestPostId, PostRequestTypes.BEFORE_ID);
+ }
+
+ getPostsAfter = () => {
+ this.setLoadingPosts('newerPosts');
+ const newestMessageId = getNewestPostIdFromPosts(this.props.posts);
+ return this.callLoadPosts(this.props.channelId, newestMessageId, PostRequestTypes.AFTER_ID);
+ }
+
+ getUnreadPostsCount = (posts, currentUserId) => {
+ //This can be different than the unreadCount on the sidebar as sytem messages
+ //are not considered for the count.
+ return posts.reduce((count, post) => {
+ if (post.create_at > this.props.lastViewedAt &&
+ post.user_id !== currentUserId &&
+ post.state !== Constants.POST_DELETED) {
+ return count + 1;
+ }
+ return count;
+ }, 0);
+ }
+
+ render() {
+ const posts = this.props.posts;
+ let postList;
+
+ if (!posts) {
+ return (
+
+
+
+ );
+ }
+
+ if (posts.length) {
+ postList = (
+
+ );
+ }
+ return (
+
+ );
+ }
+}
diff --git a/components/post_view/post_view.test.jsx b/components/post_view/post_view.test.jsx
new file mode 100644
index 000000000000..425bd5d5ac00
--- /dev/null
+++ b/components/post_view/post_view.test.jsx
@@ -0,0 +1,278 @@
+// 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 PostView from './post_view';
+
+function emptyFunction() {} //eslint-disable-line no-empty-function
+
+const mockApiAscSuccess = async () => {
+ return {
+ data: {posts: {}, order: []},
+ };
+};
+
+const loadPostsMock = async () => {
+ return {
+ moreToLoad: true,
+ };
+};
+
+const actionsProp = {
+ getPostThread: emptyFunction,
+ loadUnreads: mockApiAscSuccess,
+ loadPosts: loadPostsMock,
+ changeChannelPostsStatus: jest.fn(),
+ syncChannelPosts: jest.fn(),
+ channelSyncCompleted: emptyFunction,
+ checkAndSetMobileView: emptyFunction,
+};
+
+let channelPostsStatus;
+const lastViewedAt = 1532345226632;
+const channelId = 'fake-id';
+
+const createFakePosts = (num) => {
+ const posts = [];
+ for (let i = 1; i <= num; i++) {
+ posts.push({
+ id: `1234${i}`,
+ user_id: 'someone',
+ create_at: 1532345226631, // 1 less than lastViewedAt
+ });
+ }
+
+ return posts;
+};
+
+describe('components/post_view/post_list', () => {
+ it('snapshot for loading when there are no posts', () => {
+ const emptyPostList = undefined;
+ const wrapper = shallow(
+
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('snapshot with couple of posts', () => {
+ const postList = createFakePosts(2);
+ const wrapper = shallow(
+
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('init snapshot for max postVisibility', async () => {
+ const emptyPostList = [];
+
+ const channelPostsStatusObj = {
+ atEnd: false,
+ atStart: false,
+ };
+
+ const wrapper = shallow(
+
+ );
+
+ await actionsProp.loadUnreads();
+ await wrapper.instance().postsOnLoad();
+ wrapper.setState({olderPosts: {loading: false, allLoaded: false}, newerPosts: {loading: false, allLoaded: false}});
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('Should call postsOnLoad', async () => {
+ const emptyPostList = [];
+
+ const wrapper = shallow(
+
+ );
+
+ await actionsProp.loadUnreads();
+ await wrapper.instance().postsOnLoad();
+ wrapper.update();
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atEnd: true});
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atStart: true});
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('Should call for before and afterPosts', async () => {
+ const wrapper = shallow(
+
+ );
+
+ wrapper.instance().getPostsBefore('1234');
+ expect(wrapper.state().olderPosts.loading).toEqual(true);
+ await actionsProp.loadPosts();
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atStart: false});
+ expect(wrapper.state().olderPosts.loading).toEqual(false);
+
+ wrapper.instance().getPostsAfter('1234');
+ expect(wrapper.state().newerPosts.loading).toEqual(true);
+ await actionsProp.loadPosts();
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atEnd: false});
+ expect(wrapper.state().newerPosts.loading).toEqual(false);
+ });
+
+ it('Should handle changeChannelPostsStatus call for more than 30 posts onLoad with 0 unread', async () => {
+ const fakePosts = [
+ ...createFakePosts(35),
+ {
+ id: '123456',
+ user_id: 'someone',
+ create_at: 1532345226634,
+ },
+ ];
+
+ const wrapper = shallow(
+
+ );
+
+ await actionsProp.loadUnreads();
+ await wrapper.instance().postsOnLoad();
+ wrapper.update();
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atEnd: true});
+ expect(actionsProp.changeChannelPostsStatus).toHaveBeenCalledWith({channelId, atStart: true});
+ });
+
+ it('should call sync posts if syncStatus is false and posts exist ', async () => {
+ const channelPostsStatusObj = {
+ atEnd: true,
+ atStart: false,
+ };
+
+ const postsObj = createFakePosts(3);
+ const channelSyncStatus = false;
+ const syncChannelPosts = jest.fn();
+ const socketStatus = {
+ lastDisconnectAt: 1234,
+ };
+ const channelSyncCompleted = jest.fn();
+
+ shallow(
+
+ );
+ expect(syncChannelPosts).toHaveBeenCalledTimes(1);
+ expect(syncChannelPosts).toHaveBeenCalledWith({
+ channelId,
+ channelPostsStatus: channelPostsStatusObj,
+ posts: postsObj,
+ lastDisconnectAt: socketStatus.lastDisconnectAt,
+ });
+ });
+
+ it('Should call APIs for permalink posts', () => {
+ const emptyPostList = [];
+ const focusedPostId = 'new';
+
+ const channelPostsStatusObj = {atEnd: false, atStart: false};
+ const getPostThread = jest.fn();
+ const loadPosts = jest.fn();
+
+ shallow(
+
+ );
+ expect(getPostThread).toHaveBeenCalledWith('new', false);
+ expect(loadPosts).toHaveBeenCalledWith({channelId, postId: 'new', type: PostRequestTypes.AFTER_ID});
+ expect(loadPosts).toHaveBeenCalledWith({channelId, postId: 'new', type: PostRequestTypes.BEFORE_ID});
+ });
+
+ it('Should sync posts on socket status change', () => {
+ const postsObj = createFakePosts(3);
+ const socketStatus = {
+ lastDisconnectAt: 1234,
+ connected: false,
+ };
+ const channelPostsStatusObj = {atEnd: true, atStart: false};
+
+ const wrapper = shallow(
+
+ );
+
+ wrapper.setProps({
+ socketStatus: {
+ lastDisconnectAt: 1234,
+ connected: true,
+ },
+ });
+ expect(actionsProp.syncChannelPosts).toHaveBeenCalledWith({
+ channelId,
+ channelPostsStatus: channelPostsStatusObj,
+ posts: postsObj,
+ lastDisconnectAt: socketStatus.lastDisconnectAt,
+ });
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 3995c8b8359b..b6d73d7a6494 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14406,7 +14406,7 @@
},
"serialize-error": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
"integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go="
},
"serialize-javascript": {
diff --git a/reducers/views/channel.js b/reducers/views/channel.js
index ae8842875185..e7c10195d75f 100644
--- a/reducers/views/channel.js
+++ b/reducers/views/channel.js
@@ -52,18 +52,6 @@ function lastChannelViewTime(state = {}, action) {
}
}
-function loadingPosts(state = {}, action) {
- switch (action.type) {
- case ActionTypes.LOADING_POSTS: {
- const nextState = {...state};
- nextState[action.channelId] = action.data;
- return nextState;
- }
- default:
- return state;
- }
-}
-
function focusedPostId(state = '', action) {
switch (action.type) {
case ActionTypes.RECEIVED_FOCUSED_POST:
@@ -115,11 +103,57 @@ function keepChannelIdAsUnread(state = null, action) {
}
}
+function channelPostsStatus(state = {}, action) {
+ switch (action.type) {
+ case ActionTypes.CHANNEL_POSTS_STATUS: {
+ const channelId = action.data.channelId;
+ if ((action.data).hasOwnProperty('atEnd')) {
+ return {
+ ...state,
+ [channelId]: {
+ ...state[channelId],
+ atEnd: action.data.atEnd,
+ },
+ };
+ }
+ return {
+ ...state,
+ [channelId]: {
+ ...state[channelId],
+ atStart: action.data.atStart,
+ },
+ };
+ }
+ default:
+ return state;
+ }
+}
+
+function channelSyncStatus(state = {}, action) {
+ switch (action.type) {
+ case ActionTypes.ALL_CHANNEL_SYNC_STATUS: {
+ const nextState = action.data.channelIds.reduce((channelStatusObj, channelId) => ({
+ ...channelStatusObj,
+ [channelId]: action.data.status,
+ }), {});
+ return nextState;
+ }
+ case ActionTypes.CHANNEL_SYNC_STATUS:
+ return {
+ ...state,
+ [action.data]: true,
+ };
+ default:
+ return state;
+ }
+}
+
export default combineReducers({
postVisibility,
lastChannelViewTime,
- loadingPosts,
focusedPostId,
mobileView,
keepChannelIdAsUnread,
+ channelPostsStatus,
+ channelSyncStatus,
});
diff --git a/reducers/views/channel.test.js b/reducers/views/channel.test.js
new file mode 100644
index 000000000000..1c2118bc88c9
--- /dev/null
+++ b/reducers/views/channel.test.js
@@ -0,0 +1,96 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import channelReducer from 'reducers/views/channel';
+import {ActionTypes} from 'utils/constants.jsx';
+
+describe('Reducers.Channel', () => {
+ const initialState = {
+ channelSyncStatus: {},
+ channelPostsStatus: {},
+ focusedPostId: '',
+ keepChannelIdAsUnread: null,
+ lastChannelViewTime: {},
+ mobileView: false,
+ postVisibility: {},
+ };
+
+ test('Sync status true for a channel', () => {
+ const nextState = channelReducer(
+ initialState,
+ {type: ActionTypes.CHANNEL_SYNC_STATUS, data: 'channelId'}
+ );
+
+ expect(nextState).toEqual({
+ ...initialState,
+ channelSyncStatus: {
+ channelId: true,
+ },
+ });
+ });
+
+ test('Sync status false for all channels when socket disconnets', () => {
+ const nextState = channelReducer(
+ initialState,
+ {
+ type: ActionTypes.ALL_CHANNEL_SYNC_STATUS,
+ data: {
+ channelIds: ['channel1', 'channel2'],
+ status: false,
+ },
+ }
+ );
+
+ expect(nextState).toEqual({
+ ...initialState,
+ channelSyncStatus: {
+ channel1: false,
+ channel2: false,
+ },
+ });
+ });
+
+ test('channelPostsStatus change for atEnd', () => {
+ const nextState = channelReducer(
+ initialState,
+ {
+ type: ActionTypes.CHANNEL_POSTS_STATUS,
+ data: {
+ channelId: 'channelId',
+ atEnd: false,
+ },
+ }
+ );
+
+ expect(nextState).toEqual({
+ ...initialState,
+ channelPostsStatus: {
+ channelId: {
+ atEnd: false,
+ },
+ },
+ });
+ });
+
+ test('channelPostsStatus change for atStart', () => {
+ const nextState = channelReducer(
+ initialState,
+ {
+ type: ActionTypes.CHANNEL_POSTS_STATUS,
+ data: {
+ channelId: 'channelId',
+ atStart: false,
+ },
+ }
+ );
+
+ expect(nextState).toEqual({
+ ...initialState,
+ channelPostsStatus: {
+ channelId: {
+ atStart: false,
+ },
+ },
+ });
+ });
+});
diff --git a/reducers/views/index.js b/reducers/views/index.js
index f4c0432fea6b..22887ad06bcc 100644
--- a/reducers/views/index.js
+++ b/reducers/views/index.js
@@ -16,6 +16,7 @@ import search from './search';
import notice from './notice';
import system from './system';
import channelSelectorModal from './channel_selector_modal';
+import websocket from './websocket';
export default combineReducers({
admin,
@@ -31,4 +32,5 @@ export default combineReducers({
notice,
system,
channelSelectorModal,
+ websocket,
});
diff --git a/reducers/views/websocket.js b/reducers/views/websocket.js
new file mode 100644
index 000000000000..803e8fdde1f8
--- /dev/null
+++ b/reducers/views/websocket.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types';
+
+function getInitialState() {
+ return {
+ connected: false,
+ lastConnectAt: 0,
+ lastDisconnectAt: 0,
+ };
+}
+
+export default function(state = getInitialState(), action) {
+ if (!state.connected && action.type === GeneralTypes.WEBSOCKET_SUCCESS) {
+ return {
+ ...state,
+ connected: true,
+ lastConnectAt: new Date().getTime(),
+ };
+ } else if (state.connected && (action.type === GeneralTypes.WEBSOCKET_FAILURE || action.type === GeneralTypes.WEBSOCKET_CLOSED)) {
+ return {
+ ...state,
+ connected: false,
+ lastDisconnectAt: new Date().getTime(),
+ };
+ }
+
+ if (action.type === UserTypes.LOGOUT_SUCCESS) {
+ return getInitialState();
+ }
+
+ return state;
+}
diff --git a/reducers/views/websocket.test.js b/reducers/views/websocket.test.js
new file mode 100644
index 000000000000..99875f6bbd2e
--- /dev/null
+++ b/reducers/views/websocket.test.js
@@ -0,0 +1,54 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types';
+
+import websocketReducer from 'reducers/views/websocket';
+
+describe('Reducers.Channel', () => {
+ const constantDate = new Date('2018-01-01T12:00:00');
+
+ beforeAll(() => {
+ global.Date = class extends Date {
+ constructor() {
+ super();
+ return constantDate;
+ }
+ };
+ });
+
+ const initialState = {
+ connected: false,
+ lastConnectAt: 0,
+ lastDisconnectAt: 0,
+ };
+
+ test('init state', () => {
+ const nextState = websocketReducer(undefined, {});
+ expect(nextState).toEqual(initialState);
+ });
+
+ test('websocket success should have connected flag true and timestamp saved', () => {
+ const nextState = websocketReducer(initialState, {type: GeneralTypes.WEBSOCKET_SUCCESS});
+
+ expect(nextState).toEqual({
+ ...initialState,
+ connected: true,
+ lastConnectAt: 1514788200000,
+ });
+ });
+
+ test('websocket success should have connected flag false and timestamp saved', () => {
+ const nextState = websocketReducer({...initialState, connected: true}, {type: GeneralTypes.WEBSOCKET_FAILURE});
+
+ expect(nextState).toEqual({
+ ...initialState,
+ connected: false,
+ lastDisconnectAt: 1514788200000,
+ });
+ });
+
+ test('reset to initstate on logout', () => {
+ const nextState = websocketReducer({...initialState, connected: true}, {type: UserTypes.LOGOUT_SUCCESS});
+ expect(nextState).toEqual(initialState);
+ });
+});
diff --git a/sass/layout/_content.scss b/sass/layout/_content.scss
index e1e013dbeeaa..b4d75011d5f2 100644
--- a/sass/layout/_content.scss
+++ b/sass/layout/_content.scss
@@ -91,4 +91,3 @@
padding-left: 15px;
padding-right: 15px;
}
-
diff --git a/sass/routes/_print.scss b/sass/routes/_print.scss
index 43d9a0dba26a..16f5fd9e6d49 100644
--- a/sass/routes/_print.scss
+++ b/sass/routes/_print.scss
@@ -62,7 +62,6 @@
.post__body .img-div,
.post-image__thumbnail,
.post.post--system,
- #archive-link-home,
.channel-intro {
display: none;
}
diff --git a/selectors/views/channel.js b/selectors/views/channel.js
new file mode 100644
index 000000000000..c5e9acf017ac
--- /dev/null
+++ b/selectors/views/channel.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {createSelector} from 'reselect';
+
+export const makeGetChannelPostStatus = () => createSelector(
+ (state) => state.views.channel.channelPostsStatus,
+ (_, channelId) => (channelId),
+ (channelPostsStatus, channelId) => {
+ return channelPostsStatus[channelId];
+ },
+);
+
+export const makeGetChannelSyncStatus = () => createSelector(
+ (state) => state.views.channel.channelSyncStatus,
+ (_, channelId) => (channelId),
+ (channelSyncStatus, channelId) => {
+ return channelSyncStatus[channelId];
+ },
+);
diff --git a/selectors/views/channel.test.js b/selectors/views/channel.test.js
new file mode 100644
index 000000000000..9e69c60be922
--- /dev/null
+++ b/selectors/views/channel.test.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import {makeGetChannelPostStatus, makeGetChannelSyncStatus} from './channel';
+
+describe('modals selector', () => {
+ const state = {
+ views: {
+ channel: {
+ channelSyncStatus: {
+ channelId: true,
+ },
+ channelPostsStatus: {
+ channelId: {
+ atStart: true,
+ atEnd: true,
+ },
+ },
+ },
+ },
+ };
+
+ it('makeGetChannelPostStatus should return true for channelId', () => {
+ expect(makeGetChannelPostStatus()(state, 'channelId')).toEqual({
+ atStart: true,
+ atEnd: true,
+ });
+ });
+
+ it('makeGetChannelSyncStatus should return true for channelId', () => {
+ expect(makeGetChannelSyncStatus()(state, 'channelId')).toBeTruthy();
+ });
+});
diff --git a/selectors/views/websocket.js b/selectors/views/websocket.js
new file mode 100644
index 000000000000..4d35e8d6d2a1
--- /dev/null
+++ b/selectors/views/websocket.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export function getSocketStatus(state) {
+ return state.views.websocket;
+}
diff --git a/utils/constants.jsx b/utils/constants.jsx
index ce48e36d39ba..92376baad7a6 100644
--- a/utils/constants.jsx
+++ b/utils/constants.jsx
@@ -168,6 +168,10 @@ export const ActionTypes = keyMirror({
INCREMENT_WS_ERROR_COUNT: null,
RESET_WS_ERROR_COUNT: null,
+
+ CHANNEL_POSTS_STATUS: null,
+ CHANNEL_SYNC_STATUS: null,
+ ALL_CHANNEL_SYNC_STATUS: null,
});
export const ModalIdentifiers = {
@@ -279,6 +283,11 @@ export const PostTypes = {
REMOVE_LINK_PREVIEW: 'remove_link_preview',
};
+export const PostRequestTypes = keyMirror({
+ BEFORE_ID: null,
+ AFTER_ID: null,
+});
+
export const StatTypes = keyMirror({
TOTAL_USERS: null,
TOTAL_PUBLIC_CHANNELS: null,