From 8e526b61edc24fd577ff99fea26de22727bb308d Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 2 Oct 2017 11:21:39 -0400 Subject: [PATCH] RN-379 Get posts since last websocket disconnect when viewing channel (#971) * RN-379 Added websocket state to device state * Fixed view store blacklist * RN-379 Get posts since last websocket disconnect when viewing channel * Used Date.now instead of new Date().getTime() --- app/actions/views/channel.js | 56 +++++++++++++++++++++++++------- app/constants/view.js | 4 ++- app/reducers/device/index.js | 4 ++- app/reducers/device/websocket.js | 34 +++++++++++++++++++ app/reducers/views/channel.js | 16 ++++++++- app/store/index.js | 2 +- 6 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 app/reducers/device/websocket.js diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 291838a9d..8bba01a13 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -13,7 +13,7 @@ import { leaveChannel as serviceLeaveChannel, unfavoriteChannel } from 'mattermost-redux/actions/channels'; -import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts'; +import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts'; import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; @@ -31,6 +31,8 @@ import { import {getLastCreateAt} from 'mattermost-redux/utils/post_utils'; import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils'; +const MAX_POST_TRIES = 3; + export function loadChannelsIfNecessary(teamId) { return async (dispatch, getState) => { await fetchMyChannelsAndMembers(teamId)(dispatch, getState); @@ -139,25 +141,57 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { } export function loadPostsIfNecessaryWithRetry(channelId) { - return (dispatch, getState) => { + return async (dispatch, getState) => { const state = getState(); const {posts, postsInChannel} = state.entities.posts; - const postsIds = postsInChannel[channelId]; - // Get the first page of posts if it appears we haven't gotten it yet, like the webapp + const time = Date.now(); + + let received; if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) { - getPostsWithRetry(channelId)(dispatch, getState); - return; + // Get the first page of posts if it appears we haven't gotten it yet, like the webapp + received = await retryGetPostsAction(getPosts(channelId), dispatch, getState); + } else { + const {lastConnectAt} = state.device.websocket; + const lastGetPosts = state.views.channel.lastGetPosts[channelId]; + + let since; + if (lastGetPosts && lastGetPosts < lastConnectAt) { + // Since the websocket disconnected, we may have missed some posts since then + since = lastGetPosts; + } else { + // Trust that we've received all posts since the last time the websocket disconnected + // so just get any that have changed since the latest one we've received + const postsForChannel = postsIds.map((id) => posts[id]); + since = getLastCreateAt(postsForChannel); + } + + received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState); } - const postsForChannel = postsIds.map((id) => posts[id]); - const latestPostTime = getLastCreateAt(postsForChannel); - - getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState); + if (received) { + dispatch({ + type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME, + channelId, + time + }); + } }; } +async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) { + for (let i = 0; i < maxTries; i++) { + const posts = await action(dispatch, getState); + + if (posts) { + return posts; + } + } + + return null; +} + export function loadFilesForPostIfNecessary(postId) { return async (dispatch, getState) => { const {files} = getState().entities; @@ -307,7 +341,7 @@ export function closeGMChannel(channel) { export function refreshChannelWithRetry(channelId) { return (dispatch, getState) => { - getPostsWithRetry(channelId)(dispatch, getState); + return retryGetPostsAction(getPosts(channelId), dispatch, getState); }; } diff --git a/app/constants/view.js b/app/constants/view.js index 799e13467..49a5242a3 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -46,7 +46,9 @@ const ViewTypes = keyMirror({ INCREASE_POST_VISIBILITY: null, RECEIVED_FOCUSED_POST: null, - LOADING_POSTS: null + LOADING_POSTS: null, + + RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null }); export default { diff --git a/app/reducers/device/index.js b/app/reducers/device/index.js index afbf13377..9d256784e 100644 --- a/app/reducers/device/index.js +++ b/app/reducers/device/index.js @@ -8,11 +8,13 @@ import dimension from './dimension'; import isTablet from './is_tablet'; import orientation from './orientation'; import statusBarHeight from './status_bar'; +import websocket from './websocket'; export default combineReducers({ connection, dimension, isTablet, orientation, - statusBarHeight + statusBarHeight, + websocket }); diff --git a/app/reducers/device/websocket.js b/app/reducers/device/websocket.js new file mode 100644 index 000000000..a370afd60 --- /dev/null +++ b/app/reducers/device/websocket.js @@ -0,0 +1,34 @@ +// Copyright (c) 2017-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/app/reducers/views/channel.js b/app/reducers/views/channel.js index 7b8b514f3..801106c72 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -246,6 +246,19 @@ function loadingPosts(state = {}, action) { } } +function lastGetPosts(state = {}, action) { + switch (action.type) { + case ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: + return { + ...state, + [action.channelId]: action.time + }; + + default: + return state; + } +} + export default combineReducers({ displayName, drafts, @@ -253,5 +266,6 @@ export default combineReducers({ refreshing, tooltipVisible, postVisibility, - loadingPosts + loadingPosts, + lastGetPosts }); diff --git a/app/store/index.js b/app/store/index.js index 876ca1e84..f260e9425 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -54,7 +54,7 @@ export default function configureAppStore(initialState) { const channel = {}; for (const channelKey of Object.keys(inboundState.channel)) { - if (channelViewBlackList[channelKey]) { + if (!channelViewBlackList[channelKey]) { channel[channelKey] = inboundState.channel[channelKey]; } }