diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 64e92c04ce..fe087cba1b 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -12,7 +12,7 @@ import { selectChannel, leaveChannel as serviceLeaveChannel } from 'mattermost-redux/actions/channels'; -import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts'; +import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts'; import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences'; import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; @@ -136,8 +136,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { }; } -export function loadPostsIfNecessary(channelId) { - return async (dispatch, getState) => { +export function loadPostsIfNecessaryWithRetry(channelId) { + return (dispatch, getState) => { const state = getState(); const {posts, postsInChannel} = state.entities.posts; @@ -145,13 +145,14 @@ export function loadPostsIfNecessary(channelId) { // Get the first page of posts if it appears we haven't gotten it yet, like the webapp if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) { - return getPosts(channelId)(dispatch, getState); + getPostsWithRetry(channelId)(dispatch, getState); + return; } const postsForChannel = postsIds.map((id) => posts[id]); const latestPostTime = getLastCreateAt(postsForChannel); - return getPostsSince(channelId, latestPostTime)(dispatch, getState); + getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState); }; } @@ -325,11 +326,9 @@ export function unmarkFavorite(channelId) { }; } -export function refreshChannel(channelId) { - return async (dispatch, getState) => { - dispatch(setChannelRefreshing()); - await getPosts(channelId)(dispatch, getState); - dispatch(setChannelRefreshing(false)); +export function refreshChannelWithRetry(channelId) { + return (dispatch, getState) => { + getPostsWithRetry(channelId)(dispatch, getState); }; } @@ -350,13 +349,6 @@ export function setChannelLoading(loading = true) { }; } -export function setChannelRefreshing(refreshing = true) { - return { - type: ViewTypes.SET_CHANNEL_REFRESHING, - refreshing - }; -} - export function setPostTooltipVisible(visible = true) { return { type: ViewTypes.POST_TOOLTIP_VISIBLE, diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index df1c1b515e..52601c54c0 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -4,18 +4,14 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {refreshChannel} from 'app/actions/views/channel'; +import {refreshChannelWithRetry} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; import PostList from './post_list'; function mapStateToProps(state, ownProps) { - const {loading, refreshing} = state.views.channel; - return { ...ownProps, - channelIsLoading: loading, - refreshing, theme: getTheme(state) }; } @@ -23,7 +19,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - refreshChannel + refreshChannelWithRetry }, dispatch) }; } diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 62270892b0..3972e49fe1 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -22,7 +22,7 @@ const LOAD_MORE_POSTS = 'load-more-posts'; export default class PostList extends PureComponent { static propTypes = { actions: PropTypes.shape({ - refreshChannel: PropTypes.func.isRequired + refreshChannelWithRetry: PropTypes.func.isRequired }).isRequired, channel: PropTypes.object, channelIsLoading: PropTypes.bool.isRequired, @@ -79,7 +79,7 @@ export default class PostList extends PureComponent { const {actions, channel} = this.props; if (Object.keys(channel).length) { - actions.refreshChannel(channel.id); + actions.refreshChannelWithRetry(channel.id); } }; diff --git a/app/constants/view.js b/app/constants/view.js index 11e0722d06..2028dc3206 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -36,7 +36,6 @@ const ViewTypes = keyMirror({ ADD_FILE_TO_FETCH_CACHE: null, SET_CHANNEL_LOADER: null, - SET_CHANNEL_REFRESHING: null, SET_CHANNEL_DISPLAY_NAME: null, POST_TOOLTIP_VISIBLE: null, diff --git a/app/initial_state.js b/app/initial_state.js index 5416e0a94b..c3d869a613 100644 --- a/app/initial_state.js +++ b/app/initial_state.js @@ -251,7 +251,6 @@ const state = { channel: { drafts: {}, loading: false, - refreshing: false, tooltipVisible: false }, connection: true, diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index a6e586bb15..be6bb1b0c9 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -181,15 +181,6 @@ function loading(state = false, action) { } } -function refreshing(state = false, action) { - switch (action.type) { - case ViewTypes.SET_CHANNEL_REFRESHING: - return action.refreshing; - default: - return state; - } -} - function tooltipVisible(state = false, action) { switch (action.type) { case ViewTypes.POST_TOOLTIP_VISIBLE: @@ -245,7 +236,6 @@ export default combineReducers({ displayName, drafts, loading, - refreshing, tooltipVisible, postVisibility, loadingPosts diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 2da5f724e9..108dd067f6 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -8,12 +8,14 @@ import { Animated, Dimensions, Platform, + StyleSheet, View } from 'react-native'; import {General} from 'mattermost-redux/constants'; import ChannelLoader from 'app/components/channel_loader'; +import FormattedText from 'app/components/formatted_text'; import PostList from 'app/components/post_list'; import PostListRetry from 'app/components/post_list_retry'; @@ -23,14 +25,16 @@ const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window'); class ChannelPostList extends PureComponent { static propTypes = { actions: PropTypes.shape({ - loadPostsIfNecessary: PropTypes.func.isRequired, + loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired, loadThreadIfNecessary: PropTypes.func.isRequired, increasePostVisibility: PropTypes.func.isRequired, - selectPost: PropTypes.func.isRequired + selectPost: PropTypes.func.isRequired, + refreshChannelWithRetry: PropTypes.func.isRequired }).isRequired, channel: PropTypes.object.isRequired, channelIsLoading: PropTypes.bool, channelIsRefreshing: PropTypes.bool, + channelRefreshingFailed: PropTypes.bool, currentChannelId: PropTypes.string, intl: intlShape.isRequired, loadingPosts: PropTypes.bool, @@ -51,26 +55,44 @@ class ChannelPostList extends PureComponent { super(props); this.state = { - loaderOpacity: new Animated.Value(1) + loaderOpacity: new Animated.Value(1), + retryMessageHeight: new Animated.Value(0) }; } componentDidMount() { + const {channel, posts, channelRefreshingFailed} = this.props; this.mounted = true; this.loadPosts(this.props.channel.id); + if (posts.length || channel.total_msg_count === 0 || channelRefreshingFailed) { + this.channelLoaded(); + } } componentWillReceiveProps(nextProps) { + const {currentChannelId, channel: currentChannel} = this.props; + const {currentChannelId: nextChannelId, channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps; + // Show the loader if the channel id change - if (this.props.currentChannelId !== nextProps.currentChannelId) { + if (currentChannelId !== nextChannelId) { this.setState({ loaderOpacity: new Animated.Value(1) + }, () => { + if (nextPosts.length || nextChannel.total_msg_count === 0 || nextChannelRefreshingFailed) { + this.channelLoaded(); + } }); } - if (this.props.channel.id !== nextProps.channel.id) { + if (currentChannel.id !== nextChannel.id) { // Load the posts when the channel actually changes - this.loadPosts(nextProps.channel.id); + this.loadPosts(nextChannel.id); + } + + if (nextChannelRefreshingFailed && this.state.channelLoaded && nextPosts.length) { + this.toggleRetryMessage(); + } else if (!nextChannelRefreshingFailed || !nextPosts.length) { + this.toggleRetryMessage(false); } const showLoadMore = nextProps.posts.length >= nextProps.postVisibility; @@ -94,6 +116,14 @@ class ChannelPostList extends PureComponent { }); }; + toggleRetryMessage = (show = true) => { + const value = show ? 38 : 0; + Animated.timing(this.state.retryMessageHeight, { + toValue: value, + duration: 350 + }).start(); + }; + goToThread = (post) => { const {actions, channel, intl, navigator, theme} = this.props; const channelId = post.channel_id; @@ -141,10 +171,9 @@ class ChannelPostList extends PureComponent { } }; - loadPosts = async (channelId) => { + loadPosts = (channelId) => { this.setState({channelLoaded: false}); - await this.props.actions.loadPostsIfNecessary(channelId); - this.channelLoaded(); + this.props.actions.loadPostsIfNecessaryWithRetry(channelId); }; render() { @@ -152,27 +181,26 @@ class ChannelPostList extends PureComponent { channel, channelIsLoading, channelIsRefreshing, + channelRefreshingFailed, loadingPosts, myMember, navigator, - networkOnline, posts, postVisibility, theme } = this.props; - const {channelLoaded, loaderOpacity} = this.state; + const {loaderOpacity, retryMessageHeight} = this.state; let component; - if (!posts.length && channel.total_msg_count > 0 && (!channelIsLoading || !networkOnline)) { - // If no posts has been loaded and we are offline + if (!posts.length && channelRefreshingFailed) { component = ( this.loadPosts(channel.id)} theme={theme} /> ); - } else if ((channelIsLoading || !channelLoaded) && !channelIsRefreshing && !loadingPosts) { + } else if (channelIsLoading) { component = ; } else { component = ( @@ -188,6 +216,8 @@ class ChannelPostList extends PureComponent { lastViewedAt={myMember.last_viewed_at} channel={channel} navigator={navigator} + refreshing={channelIsRefreshing} + channelIsLoading={channelIsLoading} /> ); } @@ -208,9 +238,28 @@ class ChannelPostList extends PureComponent { > + + + ); } } +const style = StyleSheet.create({ + refreshIndicator: { + alignItems: 'center', + backgroundColor: '#fb8000', + flexDirection: 'row', + paddingHorizontal: 10, + position: 'absolute', + top: 0, + width: deviceWidth + } +}); + export default injectIntl(ChannelPostList); diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index 3c0c98c66f..4601a56915 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -8,8 +8,7 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {RequestStatus} from 'mattermost-redux/constants'; import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentChannelId, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; - -import {loadPostsIfNecessary, loadThreadIfNecessary, increasePostVisibility} from 'app/actions/views/channel'; +import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; import ChannelPostList from './channel_post_list'; @@ -19,19 +18,36 @@ function makeMapStateToProps() { return function mapStateToProps(state, ownProps) { const channelId = ownProps.channel.id; - const {refreshing} = state.views.channel; - const {getPosts} = state.requests.posts; + const {getPosts, getPostsRetryAttempts, getPostsSince, getPostsSinceRetryAttempts} = state.requests.posts; const posts = getPostsInChannel(state, channelId) || []; + const {websocket: websocketRequest} = state.requests.general; + const {connection} = state.views; + const networkOnline = connection && websocketRequest.status === RequestStatus.SUCCESS; + + let getPostsStatus; + if (getPostsRetryAttempts > 0) { + getPostsStatus = getPosts.status; + } else if (getPostsSinceRetryAttempts > 1) { + getPostsStatus = getPostsSince.status; + } + + let channelIsRefreshing = getPostsStatus === RequestStatus.STARTED; + let channelRefreshingFailed = getPostsStatus === RequestStatus.FAILURE; + if (!networkOnline) { + channelIsRefreshing = false; + channelRefreshingFailed = true; + } return { - channelIsLoading: getPosts.status === RequestStatus.STARTED || state.views.channel.loading, - channelIsRefreshing: refreshing, + channelIsLoading: state.views.channel.loading, + channelIsRefreshing, + channelRefreshingFailed, currentChannelId: getCurrentChannelId(state), posts, postVisibility: state.views.channel.postVisibility[channelId], loadingPosts: state.views.channel.loadingPosts[channelId], myMember: getMyCurrentChannelMembership(state), - networkOnline: state.offline.online, + networkOnline, theme: getTheme(state), ...ownProps }; @@ -41,10 +57,11 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - loadPostsIfNecessary, + loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, - selectPost + selectPost, + refreshChannelWithRetry }, dispatch) }; } diff --git a/yarn.lock b/yarn.lock index d8453e8bc6..333edaf70c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4791,7 +4791,7 @@ redux-devtools-instrument@^1.3.3: "redux-offline@git+https://github.com/enahum/redux-offline.git": version "1.1.4" - resolved "git+https://github.com/enahum/redux-offline.git#454d32a14ae50b9727bccbcdd295cad3cd037335" + resolved "git+https://github.com/enahum/redux-offline.git#3907341aa13fe913ec02a939af76e381d369a91d" dependencies: redux-persist "^4.5.0"