diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 8393cc2ef..b04a1d68f 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -13,12 +13,6 @@ import {
markChannelAsViewed,
leaveChannel as serviceLeaveChannel,
} from 'mattermost-redux/actions/channels';
-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 {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
@@ -56,6 +50,7 @@ import telemetry from 'app/telemetry';
import {isDirectChannelVisible, isGroupChannelVisible, isDirectMessageVisible, isGroupMessageVisible, isDirectChannelAutoClosed} from 'app/utils/channels';
import {buildPreference} from 'app/utils/preferences';
+import {getPosts, getPostsBefore, getPostsSince, getPostThread} from './post';
import {forceLogoutIfNecessary} from './user';
const MAX_RETRIES = 3;
@@ -359,7 +354,8 @@ export function handleSelectChannel(channelId) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
if (channel && currentChannelId !== channelId) {
- dispatch({
+ const actions = markAsViewedAndReadBatch(state, channelId, currentChannelId);
+ actions.push({
type: ChannelTypes.SELECT_CHANNEL,
data: channelId,
extra: {
@@ -368,11 +364,10 @@ export function handleSelectChannel(channelId) {
teamId: channel.team_id || currentTeamId,
},
});
-
- dispatch(markChannelViewedAndRead(channelId, currentChannelId));
+ dispatch(batchActions(actions));
}
- console.log('channel switch to', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line
+ console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
};
}
@@ -435,6 +430,81 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
};
}
+function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', markOnServer = true) {
+ const actions = [];
+ const {channels, myMembers} = state.entities.channels;
+ const channel = channels[channelId];
+ const member = myMembers[channelId];
+ const prevMember = myMembers[prevChannelId];
+ const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
+ const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional
+
+ if (markOnServer) {
+ Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
+ }
+
+ if (member) {
+ actions.push({
+ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
+ data: {...member, last_viewed_at: Date.now()},
+ });
+
+ if (isManuallyUnread(state, channelId)) {
+ actions.push({
+ type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
+ data: {channelId},
+ });
+ }
+
+ if (channel) {
+ actions.push({
+ type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
+ data: {
+ teamId: channel.team_id,
+ channelId,
+ amount: channel.total_msg_count - member.msg_count,
+ },
+ }, {
+ type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
+ data: {
+ teamId: channel.team_id,
+ channelId,
+ amount: member.mention_count,
+ },
+ });
+ }
+ }
+
+ if (prevMember) {
+ if (!prevChanManuallyUnread) {
+ actions.push({
+ type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
+ data: {...prevMember, last_viewed_at: Date.now()},
+ });
+ }
+
+ if (prevChannel) {
+ actions.push({
+ type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
+ data: {
+ teamId: prevChannel.team_id,
+ channelId: prevChannelId,
+ amount: prevChannel.total_msg_count - prevMember.msg_count,
+ },
+ }, {
+ type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
+ data: {
+ teamId: prevChannel.team_id,
+ channelId: prevChannelId,
+ amount: prevMember.mention_count,
+ },
+ });
+ }
+ }
+
+ return actions;
+}
+
export function markChannelViewedAndReadOnReconnect(channelId) {
return (dispatch, getState) => {
if (isManuallyUnread(getState(), channelId)) {
diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js
index fefeb4ab0..03615be55 100644
--- a/app/actions/views/channel.test.js
+++ b/app/actions/views/channel.test.js
@@ -66,7 +66,7 @@ describe('Actions.Views.Channel', () => {
type: MOCK_SELECT_CHANNEL_TYPE,
data: 'selected-channel-id',
});
- const postActions = require('mattermost-redux/actions/posts');
+ const postActions = require('./post');
postActions.getPostsSince = jest.fn(() => {
return {
type: MOCK_RECEIVED_POSTS_SINCE,
@@ -116,6 +116,7 @@ describe('Actions.Views.Channel', () => {
},
channels: {
currentChannelId,
+ manuallyUnread: {},
channels: {
'channel-id': {id: 'channel-id', display_name: 'Test Channel'},
'channel-id-2': {id: 'channel-id-2', display_name: 'Test Channel'},
@@ -296,7 +297,8 @@ describe('Actions.Views.Channel', () => {
await store.dispatch(handleSelectChannel(channelId));
const storeActions = store.getActions();
- const selectChannelWithMember = storeActions.find(({type}) => type === ChannelTypes.SELECT_CHANNEL);
+ const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH');
+ const selectChannelWithMember = storeBatchActions?.payload.find(({type}) => type === ChannelTypes.SELECT_CHANNEL);
const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED);
const readAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_READ);
diff --git a/app/actions/views/emoji.js b/app/actions/views/emoji.js
index eb62d2904..87978f1f0 100644
--- a/app/actions/views/emoji.js
+++ b/app/actions/views/emoji.js
@@ -1,7 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
+import {batchActions} from 'redux-batched-actions';
+
+import {EmojiTypes} from 'mattermost-redux/action_types';
+import {addReaction as serviceAddReaction, getNeededCustomEmojis} from 'mattermost-redux/actions/posts';
+import {Client4} from 'mattermost-redux/client';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';
@@ -42,3 +46,55 @@ export function incrementEmojiPickerPage() {
return {data: true};
};
}
+
+export function getEmojisInPosts(posts) {
+ return async (dispatch, getState) => {
+ const state = getState();
+
+ // Do not wait for this as they need to be loaded one by one
+ const emojisToLoad = getNeededCustomEmojis(state, posts);
+
+ if (emojisToLoad?.size > 0) {
+ const promises = emojisToLoad.map((name) => getCustomEmojiByName(name));
+ const result = await Promise.all(promises);
+ const actions = [];
+ const data = [];
+
+ result.forEach((emoji, index) => {
+ const name = emojisToLoad[index];
+
+ if (emoji) {
+ switch (emoji) {
+ case 404:
+ actions.push({type: EmojiTypes.CUSTOM_EMOJI_DOES_NOT_EXIST, data: name});
+ break;
+ default:
+ data.push(emoji);
+ }
+ }
+ });
+
+ if (data.length) {
+ actions.push({type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, data});
+ }
+
+ if (actions.length) {
+ dispatch(batchActions(actions));
+ }
+ }
+ };
+}
+
+async function getCustomEmojiByName(name) {
+ try {
+ const data = await Client4.getCustomEmojiByName(name);
+
+ return data;
+ } catch (error) {
+ if (error.status_code === 404) {
+ return 404;
+ }
+ }
+
+ return null;
+}
\ No newline at end of file
diff --git a/app/actions/views/post.js b/app/actions/views/post.js
index 2bfae5354..d6a14f57a 100644
--- a/app/actions/views/post.js
+++ b/app/actions/views/post.js
@@ -1,13 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import {batchActions} from 'redux-batched-actions';
+
+import {UserTypes} from 'mattermost-redux/action_types';
+import {
+ doPostAction,
+ getNeededAtMentionedUsernames,
+ receivedNewPost,
+ receivedPosts,
+ receivedPostsBefore,
+ receivedPostsInChannel,
+ receivedPostsSince,
+ receivedPostsInThread,
+} from 'mattermost-redux/actions/posts';
+import {Client4} from 'mattermost-redux/client';
import {Posts} from 'mattermost-redux/constants';
-import {doPostAction, receivedNewPost} from 'mattermost-redux/actions/posts';
+import {removeUserFromList} from 'mattermost-redux/utils/user_utils';
import {ViewTypes} from 'app/constants';
-
import {generateId} from 'app/utils/file';
+import {getEmojisInPosts} from './emoji';
+
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
return async (dispatch) => {
const timestamp = Date.now();
@@ -58,3 +73,261 @@ export function selectAttachmentMenuAction(postId, actionId, text, value) {
dispatch(doPostAction(postId, actionId, value));
};
}
+
+export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
+ return async (dispatch) => {
+ try {
+ const data = await Client4.getPosts(channelId, page, perPage);
+ const posts = Object.values(data.posts);
+
+ if (posts?.length) {
+ const actions = [
+ receivedPosts(data),
+ receivedPostsInChannel(data, channelId, page === 0, data.prev_post_id === ''),
+ ];
+
+ const additional = await dispatch(getPostsAdditionalDataBatch(posts));
+ if (additional.length) {
+ actions.push(...additional);
+ }
+
+ dispatch(batchActions(actions));
+ }
+
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ };
+}
+
+export function getPostsSince(channelId, since) {
+ return async (dispatch) => {
+ try {
+ const data = await Client4.getPostsSince(channelId, since);
+ const posts = Object.values(data.posts);
+
+ if (posts?.length) {
+ const actions = [
+ receivedPosts(data),
+ receivedPostsSince(data, channelId),
+ ];
+
+ const additional = await dispatch(getPostsAdditionalDataBatch(posts));
+ if (additional.length) {
+ actions.push(...additional);
+ }
+
+ dispatch(batchActions(actions));
+ }
+
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ };
+}
+
+export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
+ return async (dispatch) => {
+ try {
+ const data = await Client4.getPostsBefore(channelId, postId, page, perPage);
+ const posts = Object.values(data.posts);
+
+ if (posts?.length) {
+ const actions = [
+ receivedPosts(data),
+ receivedPostsBefore(data, channelId, postId, data.prev_post_id === ''),
+ ];
+
+ const additional = await dispatch(getPostsAdditionalDataBatch(posts));
+ if (additional.length) {
+ actions.push(...additional);
+ }
+
+ dispatch(batchActions(actions));
+ }
+
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ };
+}
+
+export function getPostThread(rootId) {
+ return async (dispatch) => {
+ try {
+ const data = await Client4.getPostThread(rootId);
+ const posts = Object.values(data.posts);
+
+ if (posts.length) {
+ const actions = [
+ receivedPosts(data),
+ receivedPostsInThread(data, rootId),
+ ];
+
+ const additional = await dispatch(getPostsAdditionalDataBatch(posts));
+ if (additional.length) {
+ actions.push(...additional);
+ }
+
+ dispatch(batchActions(actions));
+ }
+
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ };
+}
+
+export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZE / 2) {
+ return async (dispatch) => {
+ try {
+ const [before, thread, after] = await Promise.all([
+ Client4.getPostsBefore(channelId, postId, 0, perPage),
+ Client4.getPostThread(postId),
+ Client4.getPostsAfter(channelId, postId, 0, perPage),
+ ]);
+
+ const data = {
+ posts: {
+ ...after.posts,
+ ...thread.posts,
+ ...before.posts,
+ },
+ order: [ // Remember that the order is newest posts first
+ ...after.order,
+ postId,
+ ...before.order,
+ ],
+ next_post_id: after.next_post_id,
+ prev_post_id: before.prev_post_id,
+ };
+
+ const posts = Object.values(data.posts);
+
+ if (posts?.length) {
+ const actions = [
+ receivedPosts(data),
+ receivedPostsInChannel(data, channelId, after.next_post_id === '', before.prev_post_id === ''),
+ ];
+
+ const additional = await dispatch(getPostsAdditionalDataBatch(posts));
+ if (additional.length) {
+ actions.push(...additional);
+ }
+
+ dispatch(batchActions(actions));
+ }
+
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ };
+}
+
+function getPostsAdditionalDataBatch(posts = []) {
+ return async (dispatch, getState) => {
+ const actions = [];
+
+ if (!posts.length) {
+ return actions;
+ }
+
+ // Custom Emojis used in the posts
+ // Do not wait for this as they need to be loaded one by one
+ dispatch(getEmojisInPosts(posts));
+
+ try {
+ const state = getState();
+ const promises = [];
+ const promiseTrace = [];
+ const extra = dispatch(profilesStatusesAndToLoadFromPosts(posts));
+
+ if (extra?.userIds.length) {
+ promises.push(Client4.getProfilesByIds(extra.userIds));
+ promiseTrace.push('ids');
+ }
+
+ if (extra?.usernames.length) {
+ promises.push(Client4.getProfilesByUsernames(extra.usernames));
+ promiseTrace.push('usernames');
+ }
+
+ if (extra?.statuses.length) {
+ promises.push(Client4.getStatusesByIds(extra.statuses));
+ promiseTrace.push('statuses');
+ }
+
+ if (promises.length) {
+ const result = await Promise.all(promises);
+ result.forEach((p, index) => {
+ if (p.length) {
+ const type = promiseTrace[index];
+ switch (type) {
+ case 'statuses':
+ actions.push({
+ type: UserTypes.RECEIVED_STATUSES,
+ data: p,
+ });
+ break;
+ default: {
+ const {currentUserId} = state.entities.users;
+
+ removeUserFromList(currentUserId, p);
+ actions.push({
+ type: UserTypes.RECEIVED_PROFILES_LIST,
+ data: p,
+ });
+ break;
+ }
+ }
+ }
+ });
+ }
+ } catch (error) {
+ // do nothing
+ }
+
+ return actions;
+ };
+}
+
+function profilesStatusesAndToLoadFromPosts(posts = []) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const {currentUserId, profiles, statuses} = state.entities.users;
+
+ // Profiles of users mentioned in the posts
+ const usernamesToLoad = getNeededAtMentionedUsernames(state, posts);
+
+ // Statuses and profiles of the users who made the posts
+ const userIdsToLoad = new Set();
+ const statusesToLoad = new Set();
+
+ posts.forEach((post) => {
+ const userId = post.user_id;
+
+ if (!statuses[userId]) {
+ statusesToLoad.add(userId);
+ }
+
+ if (userId === currentUserId) {
+ return;
+ }
+
+ if (!profiles[userId]) {
+ userIdsToLoad.add(userId);
+ }
+ });
+
+ return {
+ usernames: Array.from(usernamesToLoad),
+ userIds: Array.from(userIdsToLoad),
+ statuses: Array.from(statusesToLoad),
+ };
+ };
+}
\ No newline at end of file
diff --git a/app/components/post_list/__snapshots__/post_list.test.js.snap b/app/components/post_list/__snapshots__/post_list.test.js.snap
index d3223c26a..6ff13c964 100644
--- a/app/components/post_list/__snapshots__/post_list.test.js.snap
+++ b/app/components/post_list/__snapshots__/post_list.test.js.snap
@@ -14,7 +14,7 @@ exports[`PostList setting channel deep link 1`] = `
"post-id-2",
]
}
- disableVirtualization={false}
+ disableVirtualization={true}
extraData={
Array [
"channel-id",
@@ -24,18 +24,19 @@ exports[`PostList setting channel deep link 1`] = `
]
}
horizontal={false}
- initialNumToRender={7}
+ initialNumToRender={10}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
+ listKey="recyclerFor-channel-id"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
- maxToRenderPerBatch={10}
+ maxToRenderPerBatch={5}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
@@ -55,7 +56,7 @@ exports[`PostList setting channel deep link 1`] = `
tintColor="#3d3c40"
/>
}
- removeClippedSubviews={true}
+ removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={60}
style={
@@ -64,7 +65,7 @@ exports[`PostList setting channel deep link 1`] = `
}
}
updateCellsBatchingPeriod={50}
- windowSize={50}
+ windowSize={11}
/>
`;
@@ -82,7 +83,7 @@ exports[`PostList setting permalink deep link 1`] = `
"post-id-2",
]
}
- disableVirtualization={false}
+ disableVirtualization={true}
extraData={
Array [
"channel-id",
@@ -92,18 +93,19 @@ exports[`PostList setting permalink deep link 1`] = `
]
}
horizontal={false}
- initialNumToRender={7}
+ initialNumToRender={10}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
+ listKey="recyclerFor-channel-id"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
- maxToRenderPerBatch={10}
+ maxToRenderPerBatch={5}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
@@ -123,7 +125,7 @@ exports[`PostList setting permalink deep link 1`] = `
tintColor="#3d3c40"
/>
}
- removeClippedSubviews={true}
+ removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={60}
style={
@@ -132,7 +134,7 @@ exports[`PostList setting permalink deep link 1`] = `
}
}
updateCellsBatchingPeriod={50}
- windowSize={50}
+ windowSize={11}
/>
`;
@@ -150,7 +152,7 @@ exports[`PostList should match snapshot 1`] = `
"post-id-2",
]
}
- disableVirtualization={false}
+ disableVirtualization={true}
extraData={
Array [
"channel-id",
@@ -160,18 +162,19 @@ exports[`PostList should match snapshot 1`] = `
]
}
horizontal={false}
- initialNumToRender={7}
+ initialNumToRender={10}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
+ listKey="recyclerFor-channel-id"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
"minIndexForVisible": 0,
}
}
- maxToRenderPerBatch={10}
+ maxToRenderPerBatch={5}
numColumns={1}
onContentSizeChange={[Function]}
onEndReachedThreshold={2}
@@ -191,7 +194,7 @@ exports[`PostList should match snapshot 1`] = `
tintColor="#3d3c40"
/>
}
- removeClippedSubviews={true}
+ removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={60}
style={
@@ -200,6 +203,6 @@ exports[`PostList should match snapshot 1`] = `
}
}
updateCellsBatchingPeriod={50}
- windowSize={50}
+ windowSize={11}
/>
`;
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index 1dc5573c5..b6ddbfc6f 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {Alert, FlatList, RefreshControl, StyleSheet} from 'react-native';
+import {Alert, FlatList, InteractionManager, RefreshControl, StyleSheet} from 'react-native';
import {intlShape} from 'react-intl';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@@ -24,8 +24,7 @@ import {t} from 'app/utils/i18n';
import DateHeader from './date_header';
import NewMessagesDivider from './new_messages_divider';
-const INITIAL_BATCH_TO_RENDER = 7;
-const LOADING_POSTS_HEIGHT = 53;
+const INITIAL_BATCH_TO_RENDER = 10;
const SCROLL_UP_MULTIPLIER = 3.5;
const SCROLL_POSITION_CONFIG = {
@@ -90,20 +89,18 @@ export default class PostList extends PureComponent {
constructor(props) {
super(props);
- this.hasDoneInitialScroll = false;
+ this.cancelScrollToIndex = false;
this.contentOffsetY = 0;
+ this.contentHeight = 0;
+ this.hasDoneInitialScroll = false;
this.shouldScrollToBottom = false;
this.cancelScrollToIndex = false;
this.makeExtraData = makeExtraData();
this.flatListRef = React.createRef();
-
- this.state = {
- postListHeight: 0,
- };
}
componentDidMount() {
- const {actions, deepLinkURL} = this.props;
+ const {actions, deepLinkURL, highlightPostId, initialIndex} = this.props;
EventEmitter.on('scroll-to-bottom', this.handleSetScrollToBottom);
@@ -112,6 +109,11 @@ export default class PostList extends PureComponent {
this.handleDeepLink(deepLinkURL);
actions.setDeepLinkURL('');
}
+
+ // Scroll to highlighted post for permalinks
+ if (!this.hasDoneInitialScroll && initialIndex > 0 && !this.cancelScrollToIndex && highlightPostId) {
+ this.scrollToInitialIndexIfNeeded(initialIndex);
+ }
}
componentDidUpdate(prevProps) {
@@ -132,16 +134,16 @@ export default class PostList extends PureComponent {
this.shouldScrollToBottom = false;
}
- if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && this.state.contentHeight > LOADING_POSTS_HEIGHT) {
+ if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && !this.cancelScrollToIndex) {
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
}
if (
this.props.channelId === prevProps.channelId &&
this.props.postIds.length &&
- this.state.contentHeight &&
- this.state.contentHeight < this.state.postListHeight &&
- !this.props.extraData
+ this.contentHeight &&
+ this.contentHeight < this.postListHeight &&
+ this.props.extraData
) {
this.loadToFillContent();
}
@@ -176,14 +178,12 @@ export default class PostList extends PureComponent {
this.showingPermalink = false;
};
- handleContentSizeChange = (contentWidth, contentHeight) => {
- if (this.state.contentHeight !== contentHeight) {
- this.setState({contentHeight}, () => {
- if (this.state.postListHeight && contentHeight < this.state.postListHeight && !this.props.extraData && contentHeight > LOADING_POSTS_HEIGHT) {
- // We still have less than 1 screen of posts loaded with more to get, so load more
- this.props.onLoadMoreUp();
- }
- });
+ handleContentSizeChange = (contentWidth, contentHeight, forceLoad) => {
+ this.contentHeight = contentHeight;
+ const loadMore = forceLoad || !this.props.extraData;
+ if (this.postListHeight && contentHeight < this.postListHeight && loadMore) {
+ // We still have less than 1 screen of posts loaded with more to get, so load more
+ this.props.onLoadMoreUp();
}
};
@@ -194,7 +194,7 @@ export default class PostList extends PureComponent {
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
- this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
+ this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.permalinkBadChannel);
} else if (match.type === DeepLinkTypes.PERMALINK) {
this.handlePermalinkPress(match.postId, match.teamName);
}
@@ -215,31 +215,11 @@ export default class PostList extends PureComponent {
handleLayout = (event) => {
const {height} = event.nativeEvent.layout;
- if (this.state.postListHeight !== height) {
- this.setState({postListHeight: height});
+ if (this.postListHeight !== height) {
+ this.postListHeight = height;
}
};
- errorBadTeam = () => {
- const {intl} = this.context;
- const message = {
- id: t('mobile.server_link.unreachable_team.error'),
- defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
- };
-
- alertErrorWithFallback(intl, {}, message);
- };
-
- errorBadChannel = () => {
- const {intl} = this.context;
- const message = {
- id: t('mobile.server_link.unreachable_channel.error'),
- defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
- };
-
- alertErrorWithFallback(intl, {}, message);
- };
-
handlePermalinkPress = (postId, teamName) => {
telemetry.start(['post_list:permalink']);
const {actions, onPermalinkPress} = this.props;
@@ -247,7 +227,7 @@ export default class PostList extends PureComponent {
if (onPermalinkPress) {
onPermalinkPress(postId, true);
} else {
- actions.loadChannelsByTeamName(teamName, this.errorBadTeam);
+ actions.loadChannelsByTeamName(teamName, this.permalinkBadTeam);
this.showPermalinkView(postId);
}
};
@@ -272,14 +252,12 @@ export default class PostList extends PureComponent {
const pageOffsetY = event.nativeEvent.contentOffset.y;
if (pageOffsetY > 0) {
const contentHeight = event.nativeEvent.contentSize.height;
- const direction = (this.contentOffsetY < pageOffsetY) ?
- ListTypes.VISIBILITY_SCROLL_UP :
- ListTypes.VISIBILITY_SCROLL_DOWN;
+ const direction = (this.contentOffsetY < pageOffsetY) ? ListTypes.VISIBILITY_SCROLL_UP : ListTypes.VISIBILITY_SCROLL_DOWN;
this.contentOffsetY = pageOffsetY;
if (
direction === ListTypes.VISIBILITY_SCROLL_UP &&
- (contentHeight - pageOffsetY) < (this.state.postListHeight * SCROLL_UP_MULTIPLIER)
+ (contentHeight - pageOffsetY) < (this.postListHeight * SCROLL_UP_MULTIPLIER)
) {
this.props.onLoadMoreUp();
}
@@ -290,15 +268,25 @@ export default class PostList extends PureComponent {
this.cancelScrollToIndex = true;
}
- handleScrollToIndexFailed = () => {
+ handleScrollToIndexFailed = (info) => {
this.animationFrameIndexFailed = requestAnimationFrame(() => {
- if (this.props.initialIndex > 0 && this.state.contentHeight > 0) {
+ if (this.props.initialIndex > 0 && this.contentHeight > 0) {
this.hasDoneInitialScroll = false;
- this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
+ if (info.highestMeasuredFrameIndex) {
+ this.scrollToInitialIndexIfNeeded(info.highestMeasuredFrameIndex);
+ } else {
+ this.scrollAfterInteraction = InteractionManager.runAfterInteractions(() => {
+ this.scrollToInitialIndexIfNeeded(info.index);
+ });
+ }
}
});
};
+ handleScrollBeginDrag = () => {
+ this.cancelScrollToIndex = true;
+ };
+
handleSetScrollToBottom = () => {
this.shouldScrollToBottom = true;
}
@@ -310,10 +298,30 @@ export default class PostList extends PureComponent {
loadToFillContent = () => {
this.fillContentTimer = setTimeout(() => {
- this.handleContentSizeChange(0, this.state.contentHeight);
+ this.handleContentSizeChange(0, this.contentHeight, true);
});
};
+ permalinkBadTeam = () => {
+ const {intl} = this.context;
+ const message = {
+ id: t('mobile.server_link.unreachable_team.error'),
+ defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
+ };
+
+ alertErrorWithFallback(intl, {}, message);
+ };
+
+ permalinkBadChannel = () => {
+ const {intl} = this.context;
+ const message = {
+ id: t('mobile.server_link.unreachable_channel.error'),
+ defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
+ };
+
+ alertErrorWithFallback(intl, {}, message);
+ };
+
renderItem = ({item, index}) => {
const {
highlightPinnedOrFlagged,
@@ -397,19 +405,15 @@ export default class PostList extends PureComponent {
);
};
- scrollToBottom = () => {
- this.scrollToBottomTimer = setTimeout(() => {
- if (this.flatListRef.current) {
- this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
- }
- }, 250);
- };
-
resetPostList = () => {
this.contentOffsetY = 0;
this.hasDoneInitialScroll = false;
this.cancelScrollToIndex = false;
+ if (this.scrollAfterInteraction) {
+ this.scrollAfterInteraction.cancel();
+ }
+
if (this.animationFrameIndexFailed) {
cancelAnimationFrame(this.animationFrameIndexFailed);
}
@@ -430,11 +434,19 @@ export default class PostList extends PureComponent {
clearTimeout(this.scrollToInitialTimer);
}
- if (this.state.contentHeight !== 0) {
- this.setState({contentHeight: 0});
+ if (this.contentHeight !== 0) {
+ this.contentHeight = 0;
}
}
+ scrollToBottom = () => {
+ this.scrollToBottomTimer = setTimeout(() => {
+ if (this.flatListRef.current) {
+ this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
+ }
+ }, 250);
+ };
+
scrollToIndex = (index) => {
this.animationFrameInitialIndex = requestAnimationFrame(() => {
if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) {
@@ -443,15 +455,18 @@ export default class PostList extends PureComponent {
});
};
- scrollToInitialIndexIfNeeded = (index, count = 0) => {
+ scrollToInitialIndexIfNeeded = (index) => {
if (!this.hasDoneInitialScroll) {
if (index > 0 && index <= this.getItemCount()) {
this.hasDoneInitialScroll = true;
this.scrollToIndex(index);
- } else if (count < 3) {
- this.scrollToInitialTimer = setTimeout(() => {
- this.scrollToInitialIndexIfNeeded(index, count + 1);
- }, 300);
+
+ if (index !== this.props.initialIndex) {
+ this.hasDoneInitialScroll = false;
+ this.scrollToInitialTimer = setTimeout(() => {
+ this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
+ });
+ }
}
}
};
@@ -499,40 +514,44 @@ export default class PostList extends PureComponent {
tintColor={theme.centerChannelColor}
/>);
- const hasPostsKey = postIds.length ? 'true' : 'false';
-
return (
);
}
}
const styles = StyleSheet.create({
+ flex: {
+ flex: 1,
+ },
postListContent: {
paddingTop: 5,
},
diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js
index eb33aee35..fb6d43e18 100644
--- a/app/components/post_list/post_list.test.js
+++ b/app/components/post_list/post_list.test.js
@@ -113,12 +113,8 @@ describe('PostList', () => {
});
expect(instance.loadToFillContent).toHaveBeenCalledTimes(0);
- wrapper.setState({
- postListHeight: 500,
- contentHeight: 200,
- });
- expect(instance.loadToFillContent).toHaveBeenCalledTimes(1);
-
+ instance.postListHeight = 500;
+ instance.contentHeight = 200;
wrapper.setProps({
extraData: true,
});
diff --git a/app/constants/index.js b/app/constants/index.js
index 89fecaa62..c059bf3ce 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -5,6 +5,7 @@ import DeepLinkTypes from './deep_linking';
import DeviceTypes from './device';
import ListTypes from './list';
import NavigationTypes from './navigation';
+import Types from './types';
import ViewTypes, {UpgradeTypes} from './view';
export {
@@ -13,5 +14,6 @@ export {
ListTypes,
NavigationTypes,
UpgradeTypes,
+ Types,
ViewTypes,
};
diff --git a/app/constants/types.js b/app/constants/types.js
new file mode 100644
index 000000000..c1b8df4c1
--- /dev/null
+++ b/app/constants/types.js
@@ -0,0 +1,7 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export default {
+ EMPTY_ARRAY: [],
+ EMTPY_OBJECT: {},
+};
\ No newline at end of file
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 41428f84d..3f47d87dc 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.js
+++ b/app/screens/channel/channel_post_list/channel_post_list.js
@@ -177,7 +177,7 @@ export default class ChannelPostList extends PureComponent {