Re-factor post_list with VirtualizedList (#665)
* Re-factor postlist with VirtualizedList * Remove react-native-invertible-scroll-view dependency * Fix navigator not being pass to post * Feedback review * Feedback second review
This commit is contained in:
parent
a614a2835b
commit
2a7c487463
13 changed files with 481 additions and 231 deletions
|
|
@ -12,12 +12,12 @@ import {
|
|||
selectChannel,
|
||||
leaveChannel as serviceLeaveChannel
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getPosts, getPostsSince} from 'mattermost-redux/actions/posts';
|
||||
import {getPosts, getPostsBefore, getPostsSince} 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';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {General, Posts, Preferences} from 'mattermost-redux/constants';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {
|
||||
getChannelByName,
|
||||
getDirectChannelName,
|
||||
|
|
@ -136,22 +136,22 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function loadPostsIfNecessary(channel) {
|
||||
export function loadPostsIfNecessary(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
|
||||
const postsIds = postsInChannel[channel.id];
|
||||
const postsIds = postsInChannel[channelId];
|
||||
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
if (!postsIds || postsIds.length < Posts.POST_CHUNK_SIZE) {
|
||||
return getPosts(channel.id)(dispatch, getState);
|
||||
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
return getPosts(channelId)(dispatch, getState);
|
||||
}
|
||||
|
||||
const postsForChannel = postsIds.map((id) => posts[id]);
|
||||
const latestPostTime = getLastCreateAt(postsForChannel);
|
||||
|
||||
return getPostsSince(channel.id, latestPostTime)(dispatch, getState);
|
||||
return getPostsSince(channelId, latestPostTime)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -314,6 +314,14 @@ export function unmarkFavorite(channelId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function refreshChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(setChannelRefreshing());
|
||||
await getPosts(channelId)(dispatch, getState);
|
||||
dispatch(setChannelRefreshing(false));
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveChannel(channel, reset = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
|
|
@ -351,3 +359,51 @@ export function setChannelDisplayName(displayName) {
|
|||
displayName
|
||||
};
|
||||
}
|
||||
|
||||
// Returns true if there are more posts to load
|
||||
export function increasePostVisibility(channelId, focusedPostId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {loadingPosts, postVisibility} = state.views.channel;
|
||||
const currentPostVisibility = postVisibility[channelId] || 0;
|
||||
|
||||
if (loadingPosts[channelId]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: true,
|
||||
channelId
|
||||
}
|
||||
]));
|
||||
|
||||
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
|
||||
|
||||
let posts;
|
||||
if (focusedPostId) {
|
||||
posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
|
||||
} else {
|
||||
posts = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
|
||||
}
|
||||
|
||||
if (posts) {
|
||||
// make sure to increment the posts visibility
|
||||
// only if we got results
|
||||
dispatch({
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: false,
|
||||
channelId
|
||||
});
|
||||
|
||||
return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,13 +86,17 @@ export default class ChannelDrawer extends PureComponent {
|
|||
|
||||
handleDrawerClose = () => {
|
||||
this.resetDrawer();
|
||||
setTimeout(() => {
|
||||
|
||||
if (this.closeLeftHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
|
||||
});
|
||||
this.closeLeftHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerCloseStart = () => {
|
||||
this.closeLeftHandle = InteractionManager.createInteractionHandle();
|
||||
if (!this.closeLeftHandle) {
|
||||
this.closeLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
|
|
@ -100,13 +104,17 @@ export default class ChannelDrawer extends PureComponent {
|
|||
if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
setTimeout(() => {
|
||||
|
||||
if (this.openLeftHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.openLeftHandle);
|
||||
});
|
||||
this.openLeftHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerOpenStart = () => {
|
||||
this.openLeftHandle = InteractionManager.createInteractionHandle();
|
||||
if (!this.openLeftHandle) {
|
||||
this.openLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerTween = (ratio) => {
|
||||
|
|
|
|||
139
app/components/inverted_flat_list/index.js
Normal file
139
app/components/inverted_flat_list/index.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
|
||||
|
||||
import VirtualList from './virtual_list';
|
||||
|
||||
export default class InvertibleFlatList extends PureComponent {
|
||||
static propTypes = {
|
||||
horizontal: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
ListFooterComponent: PropTypes.func,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
horizontal: false,
|
||||
inverted: true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.inversionDirection = props.horizontal ? styles.horizontal : styles.vertical;
|
||||
}
|
||||
|
||||
getMetrics = () => {
|
||||
return this.flatListRef.getMetrics();
|
||||
};
|
||||
|
||||
recordInteraction = () => {
|
||||
this.flatListRef.recordInteraction();
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {ListFooterComponent: footer} = this.props;
|
||||
if (!footer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{footer()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = (info) => {
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{this.props.renderItem(info)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderScrollComponent = (props) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
if (props.onRefresh) {
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
tintColor={theme.centerChannelColor}
|
||||
colors={[theme.centerChannelColor]}
|
||||
style={this.inversionDirection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ScrollView {...props}/>;
|
||||
};
|
||||
|
||||
scrollToEnd = (params) => {
|
||||
this.flatListRef.scrollToEnd(params);
|
||||
};
|
||||
|
||||
scrollToIndex = (params) => {
|
||||
this.flatListRef.scrollToIndex(params);
|
||||
};
|
||||
|
||||
scrollToItem = (params) => {
|
||||
this.flatListRef.scrollToItem(params);
|
||||
};
|
||||
|
||||
scrollToOffset = (params) => {
|
||||
this.flatListRef.scrollToOffset(params);
|
||||
};
|
||||
|
||||
setFlatListRef = (flatListRef) => {
|
||||
this.flatListRef = flatListRef;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {inverted, ...forwardedProps} = this.props;
|
||||
|
||||
// If not inverted, render as an ordinary FlatList
|
||||
if (!inverted) {
|
||||
return (
|
||||
<FlatList
|
||||
{...forwardedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
<VirtualList
|
||||
ref={this.setFlatListRef}
|
||||
{...forwardedProps}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
renderItem={this.renderItem}
|
||||
renderScrollComponent={this.renderScrollComponent}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
vertical: {
|
||||
transform: [{scaleY: -1}]
|
||||
},
|
||||
horizontal: {
|
||||
transform: [{scaleX: -1}]
|
||||
}
|
||||
});
|
||||
|
||||
54
app/components/inverted_flat_list/virtual_list.js
Normal file
54
app/components/inverted_flat_list/virtual_list.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {VirtualizedList} from 'react-native';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export default class Virtualized extends VirtualizedList {
|
||||
_onScroll = (e) => {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e);
|
||||
}
|
||||
const timestamp = e.timeStamp;
|
||||
const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
|
||||
const contentLength = this._selectLength(e.nativeEvent.contentSize);
|
||||
const offset = this._selectOffset(e.nativeEvent.contentOffset);
|
||||
const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp);
|
||||
const dOffset = offset - this._scrollMetrics.offset;
|
||||
const velocity = dOffset / dt;
|
||||
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const itemCount = getItemCount(data);
|
||||
if (this.state.last === itemCount - 1 &&
|
||||
distanceFromEnd <= onEndReachedThreshold &&
|
||||
(this._hasDataChangedSinceEndReached ||
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
|
||||
// Only call onEndReached once for a given dataset + content length.
|
||||
this._hasDataChangedSinceEndReached = false;
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
const {first, last} = this.state;
|
||||
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
|
||||
const distanceToContentEdge = Math.min(
|
||||
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
|
||||
Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)),
|
||||
);
|
||||
const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4);
|
||||
if (hiPri) {
|
||||
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
|
||||
// as possible.
|
||||
this._updateCellsToRenderBatcher.dispose({abort: true});
|
||||
this._updateCellsToRender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
}
|
||||
|
|
@ -4,11 +4,9 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {setChannelRefreshing} from 'app/actions/views/channel';
|
||||
import {refreshChannel} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import {getPosts} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
|
|
@ -25,8 +23,7 @@ function mapStateToProps(state, ownProps) {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPosts,
|
||||
setChannelRefreshing
|
||||
refreshChannel
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ListView,
|
||||
RefreshControl,
|
||||
View
|
||||
} from 'react-native';
|
||||
import InvertibleScrollView from 'react-native-invertible-scroll-view';
|
||||
import FlatList from 'app/components/inverted_flat_list';
|
||||
|
||||
import {General, Posts} from 'mattermost-redux/constants';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {addDatesToPostList} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import ChannelIntro from 'app/components/channel_intro';
|
||||
|
|
@ -21,72 +19,56 @@ import NewMessagesDivider from './new_messages_divider';
|
|||
|
||||
const LOAD_MORE_POSTS = 'load-more-posts';
|
||||
|
||||
export default class PostList extends Component {
|
||||
export default class PostList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getPosts: PropTypes.func.isRequired,
|
||||
setChannelRefreshing: PropTypes.func.isRequired
|
||||
refreshChannel: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
posts: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
lastViewedAt: PropTypes.number,
|
||||
loadMore: PropTypes.func,
|
||||
navigator: PropTypes.object,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
showLoadMore: PropTypes.bool,
|
||||
onPostPress: PropTypes.func,
|
||||
posts: PropTypes.array.isRequired,
|
||||
refreshing: PropTypes.bool,
|
||||
renderReplies: PropTypes.bool,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
currentUserId: PropTypes.string,
|
||||
lastViewedAt: PropTypes.number
|
||||
showLoadMore: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
channel: {}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
posts: this.getPostsWithDates(props),
|
||||
dataSource: new ListView.DataSource({
|
||||
rowHasChanged: (a, b) => a !== b
|
||||
})
|
||||
};
|
||||
}
|
||||
getPostsWithDates = () => {
|
||||
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
|
||||
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.posts !== this.props.posts) {
|
||||
const posts = this.getPostsWithDates(nextProps);
|
||||
this.setState({posts}, () => {
|
||||
if (nextProps.refreshing) {
|
||||
this.props.actions.setChannelRefreshing(false);
|
||||
}
|
||||
});
|
||||
} else if (nextProps.refreshing) {
|
||||
this.props.actions.setChannelRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
getPostsWithDates(props) {
|
||||
const {posts, indicateNewMessages, currentUserId, lastViewedAt} = props;
|
||||
return addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
|
||||
}
|
||||
|
||||
getPostsWithLoadMore() {
|
||||
const {showLoadMore} = this.props;
|
||||
const {posts} = this.state;
|
||||
if (showLoadMore) {
|
||||
return [...posts, LOAD_MORE_POSTS];
|
||||
return [...list, LOAD_MORE_POSTS];
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
const {loadMore} = this.props;
|
||||
if (typeof loadMore === 'function') {
|
||||
return list;
|
||||
};
|
||||
|
||||
keyExtractor = (item) => {
|
||||
if (item instanceof Date) {
|
||||
return item.getTime();
|
||||
}
|
||||
if (item === General.START_OF_NEW_MESSAGES || item === LOAD_MORE_POSTS) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return item.id;
|
||||
};
|
||||
|
||||
loadMorePosts = () => {
|
||||
const {loadMore, isLoadingMore} = this.props;
|
||||
if (typeof loadMore === 'function' && !isLoadingMore) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
|
|
@ -95,18 +77,18 @@ export default class PostList extends Component {
|
|||
const {actions, channel} = this.props;
|
||||
|
||||
if (Object.keys(channel).length) {
|
||||
actions.setChannelRefreshing(true);
|
||||
actions.getPosts(channel.id);
|
||||
actions.refreshChannel(channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
renderChannelIntro = () => {
|
||||
const {channel, channelIsLoading, navigator, posts} = this.props;
|
||||
|
||||
// Check the webapp for atEnd, and replace it here
|
||||
if (channel.hasOwnProperty('id')) {
|
||||
const firstPostHasRendered = channel.total_msg_count ? posts.length > 0 : true;
|
||||
const messageCount = channel.total_msg_count - posts.length;
|
||||
if (channelIsLoading || !firstPostHasRendered || messageCount > Posts.POST_CHUNK_SIZE) {
|
||||
if (channelIsLoading || !firstPostHasRendered || messageCount > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -120,18 +102,27 @@ export default class PostList extends Component {
|
|||
return null;
|
||||
};
|
||||
|
||||
renderRow = (row) => {
|
||||
if (row instanceof Date) {
|
||||
return this.renderDateHeader(row);
|
||||
renderDateHeader = (date) => {
|
||||
return (
|
||||
<DateHeader
|
||||
theme={this.props.theme}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
if (item instanceof Date) {
|
||||
return this.renderDateHeader(item);
|
||||
}
|
||||
if (row === General.START_OF_NEW_MESSAGES) {
|
||||
if (item === General.START_OF_NEW_MESSAGES) {
|
||||
return (
|
||||
<NewMessagesDivider
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (row === LOAD_MORE_POSTS) {
|
||||
if (item === LOAD_MORE_POSTS) {
|
||||
return (
|
||||
<LoadMorePosts
|
||||
loading={this.props.isLoadingMore}
|
||||
|
|
@ -140,16 +131,7 @@ export default class PostList extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
return this.renderPost(row);
|
||||
};
|
||||
|
||||
renderDateHeader = (date) => {
|
||||
return (
|
||||
<DateHeader
|
||||
theme={this.props.theme}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
return this.renderPost(item);
|
||||
};
|
||||
|
||||
renderPost = (post) => {
|
||||
|
|
@ -166,44 +148,29 @@ export default class PostList extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
renderRefreshControl = () => {
|
||||
const {theme, refreshing} = this.props;
|
||||
|
||||
return (
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={this.onRefresh}
|
||||
tintColor={theme.centerChannelColor}
|
||||
colors={[theme.centerChannelColor]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderScrollComponent = (props) => {
|
||||
return (
|
||||
<InvertibleScrollView
|
||||
{...props}
|
||||
inverted={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {dataSource} = this.state;
|
||||
const {channel, refreshing, theme} = this.props;
|
||||
|
||||
const refreshControl = {
|
||||
refreshing
|
||||
};
|
||||
|
||||
if (Object.keys(channel).length) {
|
||||
refreshControl.onRefresh = this.onRefresh;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListView
|
||||
renderScrollComponent={this.renderScrollComponent}
|
||||
dataSource={dataSource.cloneWithRows(this.getPostsWithLoadMore())}
|
||||
renderFooter={this.renderChannelIntro}
|
||||
renderRow={this.renderRow}
|
||||
onEndReached={this.loadMore}
|
||||
enableEmptySections={true}
|
||||
showsVerticalScrollIndicator={true}
|
||||
initialListSize={30}
|
||||
onEndReachedThreshold={200}
|
||||
pageSize={10}
|
||||
refreshControl={this.renderRefreshControl()}
|
||||
<FlatList
|
||||
data={this.getPostsWithDates()}
|
||||
initialNumToRender={20}
|
||||
inverted={true}
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.renderChannelIntro}
|
||||
onEndReached={this.loadMorePosts}
|
||||
onEndReachedThreshold={700}
|
||||
{...refreshControl}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import keyMirror from 'mattermost-redux/utils/key_mirror';
|
||||
|
||||
const ViewTypes = keyMirror({
|
||||
|
|
@ -42,7 +43,14 @@ const ViewTypes = keyMirror({
|
|||
SET_LAST_CHANNEL_FOR_TEAM: null,
|
||||
|
||||
GITLAB: null,
|
||||
SAML: null
|
||||
SAML: null,
|
||||
|
||||
INCREASE_POST_VISIBILITY: null,
|
||||
RECEIVED_FOCUSED_POST: null,
|
||||
LOADING_POSTS: null
|
||||
});
|
||||
|
||||
export default ViewTypes;
|
||||
export default {
|
||||
...ViewTypes,
|
||||
POST_VISIBILITY_CHUNK_SIZE: Posts.POST_CHUNK_SIZE / 2
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {ChannelTypes, FileTypes} from 'mattermost-redux/action_types';
|
||||
import {ChannelTypes, FileTypes, PostTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
|
@ -199,10 +199,54 @@ function tooltipVisible(state = false, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function postVisibility(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ChannelTypes.SELECT_CHANNEL: {
|
||||
const nextState = {...state};
|
||||
nextState[action.data] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
return nextState;
|
||||
}
|
||||
case ViewTypes.INCREASE_POST_VISIBILITY: {
|
||||
const nextState = {...state};
|
||||
nextState[action.data] += action.amount;
|
||||
return nextState;
|
||||
}
|
||||
case ViewTypes.RECEIVED_FOCUSED_POST: {
|
||||
const nextState = {...state};
|
||||
nextState[action.channelId] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
return nextState;
|
||||
}
|
||||
case PostTypes.RECEIVED_POST: {
|
||||
if (action.data && state[action.data.channel_id]) {
|
||||
const nextState = {...state};
|
||||
nextState[action.data.channel_id] += 1;
|
||||
return nextState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function loadingPosts(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.LOADING_POSTS: {
|
||||
const nextState = {...state};
|
||||
nextState[action.channelId] = action.data;
|
||||
return nextState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
displayName,
|
||||
drafts,
|
||||
loading,
|
||||
refreshing,
|
||||
tooltipVisible
|
||||
tooltipVisible,
|
||||
postVisibility,
|
||||
loadingPosts
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
|||
|
||||
import ChannelDrawerButton from './channel_drawer_button';
|
||||
import ChannelTitle from './channel_title';
|
||||
import ChannelPostList from './channel_post_list/index';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
class Channel extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {General, Posts, RequestStatus} from 'mattermost-redux/constants';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import PostList from 'app/components/post_list';
|
||||
|
|
@ -24,90 +24,59 @@ class ChannelPostList extends PureComponent {
|
|||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadPostsIfNecessary: PropTypes.func.isRequired,
|
||||
getPostsBefore: PropTypes.func.isRequired,
|
||||
increasePostVisibility: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
applicationInitializing: PropTypes.bool.isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
channelIsLoading: PropTypes.bool,
|
||||
channelIsRefreshing: PropTypes.bool,
|
||||
channelName: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
loadingPosts: PropTypes.bool,
|
||||
myMember: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
postsRequests: PropTypes.shape({
|
||||
getPosts: PropTypes.object.isRequired,
|
||||
getPostsBefore: PropTypes.object.isRequired,
|
||||
getPostsSince: PropTypes.object.isRequired
|
||||
}).isRequired,
|
||||
posts: PropTypes.array.isRequired,
|
||||
postVisibility: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
networkOnline: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
didInitialPostsLoad: false,
|
||||
hasFirstPost: false,
|
||||
lastViewedAt: this.props.myMember.last_viewed_at,
|
||||
loaderOpacity: new Animated.Value(1)
|
||||
static defaultProps = {
|
||||
loadingPosts: false,
|
||||
postVisibility: 0
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loaderOpacity: new Animated.Value(1)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.actions.loadPostsIfNecessary(this.props.channel);
|
||||
this.loadPosts(this.props.channel.id);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.channel.id === nextProps.channel.id) {
|
||||
const didInitialPostsLoad = this.didPostsLoad(nextProps, 'getPosts') ||
|
||||
this.didPostsLoad(nextProps, 'getPostsSince');
|
||||
if (didInitialPostsLoad) {
|
||||
this.setState({didInitialPostsLoad});
|
||||
}
|
||||
const didMorePostsLoad = this.didPostsLoad(nextProps, 'getPostsBefore');
|
||||
let hasFirstPost = false;
|
||||
if (didInitialPostsLoad) {
|
||||
hasFirstPost = nextProps.posts.length < Posts.POST_CHUNK_SIZE;
|
||||
} else if (didMorePostsLoad) {
|
||||
hasFirstPost = (nextProps.posts.length - this.props.posts.length) < General.POST_CHUNK_SIZE;
|
||||
}
|
||||
if (hasFirstPost) {
|
||||
this.setState({hasFirstPost});
|
||||
}
|
||||
if (!nextProps.applicationInitializing) {
|
||||
this.loaderAnimationRunner();
|
||||
}
|
||||
} else {
|
||||
// Show the loader if the channel names change
|
||||
if (this.props.channelName !== nextProps.channelName) {
|
||||
this.setState({
|
||||
didInitialPostsLoad: false,
|
||||
hasFirstPost: false,
|
||||
lastViewedAt: nextProps.myMember.last_viewed_at,
|
||||
loaderOpacity: new Animated.Value(1)
|
||||
});
|
||||
this.props.actions.loadPostsIfNecessary(nextProps.channel);
|
||||
}
|
||||
|
||||
if (this.props.channel.id !== nextProps.channel.id) {
|
||||
// Load the posts when the channel actually changes
|
||||
this.loadPosts(nextProps.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
loaderAnimationRunner = () => {
|
||||
channelLoaded = () => {
|
||||
Animated.timing(this.state.loaderOpacity, {
|
||||
toValue: 0,
|
||||
duration: 500
|
||||
}).start();
|
||||
};
|
||||
|
||||
didPostsLoad(nextProps, postsRequest) {
|
||||
const nextGetPostsStatus = nextProps.postsRequests[postsRequest].status;
|
||||
const getPostsStatus = this.props.postsRequests[postsRequest].status;
|
||||
return getPostsStatus === RequestStatus.STARTED && nextGetPostsStatus === RequestStatus.SUCCESS;
|
||||
}
|
||||
|
||||
loadMorePosts = () => {
|
||||
const {channel, posts, postsRequests} = this.props;
|
||||
const {id: channelId} = channel;
|
||||
const oldestPost = posts[posts.length - 1];
|
||||
const {didInitialPostsLoad, hasFirstPost} = this.state;
|
||||
if (didInitialPostsLoad && !hasFirstPost && oldestPost && postsRequests.getPostsBefore.status !== RequestStatus.STARTED) {
|
||||
return this.props.actions.getPostsBefore(channelId, oldestPost.id);
|
||||
}
|
||||
return null;
|
||||
}).start(() => this.setState({channelLoaded: true}));
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
|
|
@ -149,46 +118,60 @@ class ChannelPostList extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
loadMorePosts = () => {
|
||||
const {actions, channel} = this.props;
|
||||
actions.increasePostVisibility(channel.id);
|
||||
};
|
||||
|
||||
loadPosts = async (channelId) => {
|
||||
this.setState({channelLoaded: false});
|
||||
await this.props.actions.loadPostsIfNecessary(channelId);
|
||||
this.channelLoaded();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
applicationInitializing,
|
||||
channel,
|
||||
channelIsLoading,
|
||||
channelIsRefreshing,
|
||||
loadingPosts,
|
||||
myMember,
|
||||
navigator,
|
||||
networkOnline,
|
||||
posts,
|
||||
postsRequests,
|
||||
theme,
|
||||
networkOnline
|
||||
postVisibility,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const {channelLoaded, loaderOpacity} = this.state;
|
||||
|
||||
let component;
|
||||
if (!posts.length && channel.total_msg_count > 0 && (postsRequests.getPosts.status === RequestStatus.FAILURE || !networkOnline)) {
|
||||
if (!posts.length && channel.total_msg_count > 0 && !networkOnline) {
|
||||
// If no posts has been loaded and we are offline
|
||||
component = (
|
||||
<PostListRetry
|
||||
retry={() => actions.loadPostsIfNecessary(channel)}
|
||||
retry={() => this.loadPosts(channel.id)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (!applicationInitializing && !channelIsLoading && posts && (postsRequests.getPosts.status !== RequestStatus.STARTED || channelIsRefreshing || !this.state.didInitialPostsLoad)) {
|
||||
} else if ((channelIsLoading || !channelLoaded) && !channelIsRefreshing && !loadingPosts) {
|
||||
component = <ChannelLoader theme={theme}/>;
|
||||
} else {
|
||||
component = (
|
||||
<PostList
|
||||
posts={posts}
|
||||
posts={posts.slice(0, postVisibility)}
|
||||
loadMore={this.loadMorePosts}
|
||||
isLoadingMore={postsRequests.getPostsBefore.status === RequestStatus.STARTED}
|
||||
showLoadMore={posts.length > 0 && !this.state.hasFirstPost}
|
||||
isLoadingMore={loadingPosts}
|
||||
showLoadMore={(channel.total_msg_count - posts.length) > 0}
|
||||
onPostPress={this.goToThread}
|
||||
renderReplies={true}
|
||||
indicateNewMessages={true}
|
||||
currentUserId={this.props.myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
currentUserId={myMember.user_id}
|
||||
lastViewedAt={myMember.last_viewed_at}
|
||||
channel={channel}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = <ChannelLoader theme={theme}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -202,7 +185,7 @@ class ChannelPostList extends PureComponent {
|
|||
width: deviceWidth,
|
||||
top: 0,
|
||||
left: 0,
|
||||
opacity: this.state.loaderOpacity
|
||||
opacity: loaderOpacity
|
||||
}}
|
||||
>
|
||||
<ChannelLoader theme={theme}/>
|
||||
|
|
|
|||
|
|
@ -4,31 +4,37 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectPost, getPostsBefore} from 'mattermost-redux/actions/posts';
|
||||
import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {loadPostsIfNecessary} from 'app/actions/views/channel';
|
||||
import {loadPostsIfNecessary, increasePostVisibility} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
const getPostsInCurrentChannelWithReplyProps = makeGetPostsInChannel();
|
||||
function makeMapStateToProps() {
|
||||
const getPostsInChannel = makeGetPostsInChannel();
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {loading, refreshing} = state.views.channel;
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const channelId = ownProps.channel.id;
|
||||
const {displayName, refreshing} = state.views.channel;
|
||||
const {getPosts} = state.requests.posts;
|
||||
const posts = getPostsInChannel(state, channelId) || [];
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
applicationInitializing: state.views.root.appInitializing,
|
||||
channelIsLoading: loading,
|
||||
channelIsRefreshing: refreshing,
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
postsRequests: state.requests.posts,
|
||||
posts: getPostsInCurrentChannelWithReplyProps(state, currentChannelId) || [],
|
||||
theme: getTheme(state),
|
||||
networkOnline: state.offline.online
|
||||
return {
|
||||
channelIsLoading: (getPosts.status === RequestStatus.STARTED),
|
||||
channelIsRefreshing: refreshing,
|
||||
channelName: displayName,
|
||||
posts,
|
||||
postVisibility: state.views.channel.postVisibility[channelId],
|
||||
loadingPosts: state.views.channel.loadingPosts[channelId],
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
networkOnline: state.offline.online,
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -36,10 +42,10 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
actions: bindActionCreators({
|
||||
loadPostsIfNecessary,
|
||||
getPostsBefore,
|
||||
increasePostVisibility,
|
||||
selectPost
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelPostList);
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelPostList);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
"react-native-device-info": "0.10.2",
|
||||
"react-native-drawer": "2.3.0",
|
||||
"react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4",
|
||||
"react-native-invertible-scroll-view": "1.0.0",
|
||||
"react-native-keyboard-aware-scroll-view": "0.2.8",
|
||||
"react-native-linear-gradient": "2.0.0",
|
||||
"react-native-navigation": "1.1.88",
|
||||
|
|
|
|||
11
yarn.lock
11
yarn.lock
|
|
@ -4480,13 +4480,6 @@ react-native-image-picker@jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f58
|
|||
version "0.26.2"
|
||||
resolved "https://codeload.github.com/jp928/react-native-image-picker/tar.gz/6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4"
|
||||
|
||||
react-native-invertible-scroll-view@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-invertible-scroll-view/-/react-native-invertible-scroll-view-1.0.0.tgz#60ceb384dc950c34eba3a5aeda39f97cdc7d4e23"
|
||||
dependencies:
|
||||
react-clone-referenced-element "^1.0.1"
|
||||
react-native-scrollable-mixin "^1.0.1"
|
||||
|
||||
react-native-keyboard-aware-scroll-view@0.2.8:
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.2.8.tgz#e9843c0d7467a2e6a2a737883bd9c2b7c7e38e35"
|
||||
|
|
@ -4548,10 +4541,6 @@ react-native-orientation@enahum/react-native-orientation.git:
|
|||
version "1.17.0"
|
||||
resolved "https://codeload.github.com/enahum/react-native-orientation/tar.gz/75ba7da814b99a949324846ce3a63fcbaeee9fdc"
|
||||
|
||||
react-native-scrollable-mixin@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-scrollable-mixin/-/react-native-scrollable-mixin-1.0.1.tgz#34a32167b64248594154fd0d6a8b03f22740548e"
|
||||
|
||||
react-native-search-bar@enahum/react-native-search-bar.git:
|
||||
version "2.16.0"
|
||||
resolved "https://codeload.github.com/enahum/react-native-search-bar/tar.gz/88e6f5ba68012440ec21bde8388ca9e3124921c0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue