Changed to pass IDs to PostList instead of posts (#1036)
* Moved PostList to take a list of postIds instead of posts * Removed usage of selectors that return posts from ChannelPostList and Thread * Fixed search and switched to use getPostIdsAroundPost selector * Removed use of selectors that returned posts from emoji reactions * Updated makePreparePostIdsForPostList to be better memoized * Fixed filter of join/leave messages * Added unit tests for makePrepaprePostIdsForPostList * Check if post edit/delete should be enabled more often * Updated mattermost-redux version in yarn.lock
This commit is contained in:
parent
e84af2bfa7
commit
0e4f9d5825
15 changed files with 532 additions and 201 deletions
|
|
@ -2,16 +2,16 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import {addReaction} from 'mattermost-redux/actions/posts';
|
||||
import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
const getPostsForThread = makeGetPostsForThread();
|
||||
const getPostIdsForThread = makeGetPostIdsForThread();
|
||||
|
||||
export function addReactionToLatestPost(emoji, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state);
|
||||
const lastPost = posts[0];
|
||||
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
|
||||
const lastPostId = postIds[0];
|
||||
|
||||
dispatch(addReaction(lastPost.id, emoji));
|
||||
dispatch(addReaction(lastPostId, emoji));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,38 @@ function mapStateToProps(state, ownProps) {
|
|||
const {config, license} = state.entities.general;
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
|
||||
let isFirstReply = true;
|
||||
let isLastReply = true;
|
||||
let commentedOnPost = null;
|
||||
if (ownProps.renderReplies && post && post.root_id) {
|
||||
if (ownProps.previousPostId) {
|
||||
const previousPost = getPost(state, ownProps.previousPostId);
|
||||
|
||||
if (previousPost && (previousPost.id === post.root_id || previousPost.root_id === post.root_id)) {
|
||||
// Previous post is root post or previous post is in same thread
|
||||
isFirstReply = false;
|
||||
} else {
|
||||
// Last post is not a comment on the same message
|
||||
commentedOnPost = getPost(state, post.root_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (ownProps.nextPostId) {
|
||||
const nextPost = getPost(state, ownProps.nextPostId);
|
||||
|
||||
if (nextPost && nextPost.root_id === post.root_id) {
|
||||
isLastReply = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
post,
|
||||
config,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
highlight: ownProps.highlight,
|
||||
post,
|
||||
isFirstReply,
|
||||
isLastReply,
|
||||
commentedOnPost,
|
||||
license,
|
||||
roles,
|
||||
theme: getTheme(state)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class Post extends PureComponent {
|
|||
intl: intlShape.isRequired,
|
||||
style: ViewPropTypes.style,
|
||||
post: PropTypes.object,
|
||||
postId: PropTypes.string.isRequired, // Used by container // eslint-disable-line no-unused-prop-types
|
||||
renderReplies: PropTypes.bool,
|
||||
isFirstReply: PropTypes.bool,
|
||||
isLastReply: PropTypes.bool,
|
||||
|
|
@ -65,15 +66,27 @@ class Post extends PureComponent {
|
|||
|
||||
const {config, license, currentUserId, roles, post} = props;
|
||||
this.editDisableAction = new DelayedAction(this.handleEditDisable);
|
||||
this.state = {
|
||||
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
|
||||
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
|
||||
};
|
||||
if (post) {
|
||||
this.state = {
|
||||
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
|
||||
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
canEdit: false,
|
||||
canDelete: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {config, license, currentUserId, roles, post} = nextProps;
|
||||
if (post) {
|
||||
if (nextProps.config !== this.props.config ||
|
||||
nextProps.license !== this.props.license ||
|
||||
nextProps.currentUserId !== this.props.currentUserId ||
|
||||
nextProps.post !== this.props.post ||
|
||||
nextProps.roles !== this.props.roles) {
|
||||
const {config, license, currentUserId, roles, post} = nextProps;
|
||||
|
||||
this.setState({
|
||||
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
|
||||
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
|
||||
|
|
|
|||
|
|
@ -5,13 +5,20 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {makePreparePostIdsForPostList} from 'app/selectors/post_list';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
function makeMapStateToProps() {
|
||||
const preparePostIds = makePreparePostIdsForPostList();
|
||||
|
||||
return (state, ownProps) => {
|
||||
return {
|
||||
postIds: preparePostIds(state, ownProps),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -23,4 +30,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(PostList);
|
||||
|
|
|
|||
|
|
@ -7,19 +7,16 @@ import {
|
|||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import FlatList from 'app/components/inverted_flat_list';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {addDatesToPostList} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import ChannelIntro from 'app/components/channel_intro';
|
||||
import FlatList from 'app/components/inverted_flat_list';
|
||||
import Post from 'app/components/post';
|
||||
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
|
||||
|
||||
import DateHeader from './date_header';
|
||||
import LoadMorePosts from './load_more_posts';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
|
||||
const LOAD_MORE_POSTS = 'load-more-posts';
|
||||
|
||||
export default class PostList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
|
|
@ -27,14 +24,15 @@ export default class PostList extends PureComponent {
|
|||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
currentUserId: PropTypes.string,
|
||||
highlightPostId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
lastViewedAt: PropTypes.number,
|
||||
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
|
||||
loadMore: PropTypes.func,
|
||||
navigator: PropTypes.object,
|
||||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
posts: PropTypes.array.isRequired,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
renderReplies: PropTypes.bool,
|
||||
showLoadMore: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
|
|
@ -52,22 +50,13 @@ export default class PostList extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
getPostsWithDates = () => {
|
||||
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
|
||||
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
|
||||
if (showLoadMore) {
|
||||
return [...list, LOAD_MORE_POSTS];
|
||||
}
|
||||
getItem = (data, index) => data[index];
|
||||
|
||||
return list;
|
||||
};
|
||||
getItemCount = (data) => data.length;
|
||||
|
||||
keyExtractor = (item) => {
|
||||
if (item instanceof Date) {
|
||||
return item.getTime();
|
||||
}
|
||||
|
||||
return item.id || item;
|
||||
// All keys are strings (either post IDs or special keys)
|
||||
return item;
|
||||
};
|
||||
|
||||
onRefresh = () => {
|
||||
|
|
@ -86,19 +75,26 @@ export default class PostList extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
renderChannelIntro = () => {
|
||||
const {channelId, navigator, showLoadMore} = this.props;
|
||||
|
||||
// FIXME: Only show the channel intro when we are at the very start of the channel
|
||||
if (channelId && !showLoadMore) {
|
||||
renderItem = ({item, index}) => {
|
||||
if (item === START_OF_NEW_MESSAGES) {
|
||||
return (
|
||||
<View>
|
||||
<ChannelIntro navigator={navigator}/>
|
||||
</View>
|
||||
<NewMessagesDivider
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
} else if (item.indexOf(DATE_LINE) === 0) {
|
||||
const date = item.substring(DATE_LINE.length);
|
||||
return this.renderDateHeader(new Date(date));
|
||||
}
|
||||
|
||||
return null;
|
||||
const postId = item;
|
||||
|
||||
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
|
||||
// comes after this one in the list
|
||||
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
|
||||
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
|
||||
|
||||
return this.renderPost(postId, previousPostId, nextPostId);
|
||||
};
|
||||
|
||||
renderDateHeader = (date) => {
|
||||
|
|
@ -110,32 +106,9 @@ export default class PostList extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
if (item instanceof Date) {
|
||||
return this.renderDateHeader(item);
|
||||
}
|
||||
if (item === General.START_OF_NEW_MESSAGES) {
|
||||
return (
|
||||
<NewMessagesDivider
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (item === LOAD_MORE_POSTS && this.props.showLoadMore) {
|
||||
return (
|
||||
<LoadMorePosts theme={this.props.theme}/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderPost(item);
|
||||
};
|
||||
|
||||
getItem = (data, index) => data[index];
|
||||
|
||||
getItemCount = (data) => data.length;
|
||||
|
||||
renderPost = (post) => {
|
||||
renderPost = (postId, previousPostId, nextPostId) => {
|
||||
const {
|
||||
highlightPostId,
|
||||
isSearchResult,
|
||||
navigator,
|
||||
onPostPress,
|
||||
|
|
@ -145,22 +118,42 @@ export default class PostList extends PureComponent {
|
|||
|
||||
return (
|
||||
<Post
|
||||
highlight={post.highlight}
|
||||
postId={post.id}
|
||||
postId={postId}
|
||||
previousPostId={previousPostId}
|
||||
nextPostId={nextPostId}
|
||||
highlight={highlightPostId && highlightPostId === postId}
|
||||
renderReplies={renderReplies}
|
||||
isFirstReply={post.isFirstReply}
|
||||
isLastReply={post.isLastReply}
|
||||
isSearchResult={isSearchResult}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
commentedOnPost={post.commentedOnPost}
|
||||
onPress={onPostPress}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
if (this.props.showLoadMore) {
|
||||
return <LoadMorePosts theme={this.props.theme}/>;
|
||||
} else if (this.props.channelId) {
|
||||
// FIXME: Only show the channel intro when we are at the very start of the channel
|
||||
return (
|
||||
<View>
|
||||
<ChannelIntro navigator={this.props.navigator}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {channelId, loadMore, theme} = this.props;
|
||||
const {
|
||||
channelId,
|
||||
highlightPostId,
|
||||
loadMore,
|
||||
postIds,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const refreshControl = {
|
||||
refreshing: false
|
||||
|
|
@ -170,15 +163,15 @@ export default class PostList extends PureComponent {
|
|||
refreshControl.onRefresh = this.onRefresh;
|
||||
}
|
||||
|
||||
const data = this.getPostsWithDates();
|
||||
return (
|
||||
<FlatList
|
||||
ref='list'
|
||||
data={data}
|
||||
data={postIds}
|
||||
extraData={highlightPostId}
|
||||
initialNumToRender={15}
|
||||
inverted={true}
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.renderChannelIntro}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0}
|
||||
{...refreshControl}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,22 @@
|
|||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost, makeGetPostsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getPost, makeGetPostIdsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import SearchPreview from './search_preview';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getPostsAroundPost = makeGetPostsAroundPost();
|
||||
const getPostIdsAroundPost = makeGetPostIdsAroundPost();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.focusedPostId);
|
||||
const postsAroundPost = getPostsAroundPost(state, post.id, post.channel_id);
|
||||
const focusedPostIndex = postsAroundPost ? postsAroundPost.findIndex((p) => p.id === ownProps.focusedPostId) : -1;
|
||||
let posts = [];
|
||||
|
||||
if (focusedPostIndex !== -1) {
|
||||
const desiredPostIndexBefore = focusedPostIndex - 5;
|
||||
const minPostIndex = desiredPostIndexBefore < 0 ? 0 : desiredPostIndexBefore;
|
||||
posts = [...postsAroundPost].splice(minPostIndex, 10);
|
||||
}
|
||||
const postIds = getPostIdsAroundPost(state, post.id, post.channel_id, {postsBeforeCount: 5, postsAfterCount: 5});
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelId: post.channel_id,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
posts
|
||||
postIds
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,15 +39,16 @@ export default class SearchPreview extends PureComponent {
|
|||
channelId: PropTypes.string,
|
||||
channelName: PropTypes.string,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
focusedPostId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
posts: PropTypes.array,
|
||||
postIds: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
posts: []
|
||||
postIds: []
|
||||
};
|
||||
|
||||
state = {
|
||||
|
|
@ -57,7 +58,7 @@ export default class SearchPreview extends PureComponent {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {animationEnded, showPosts} = this.state;
|
||||
if (animationEnded && !showPosts && nextProps.posts.length) {
|
||||
if (animationEnded && !showPosts && nextProps.postIds.length) {
|
||||
this.setState({showPosts: true});
|
||||
}
|
||||
}
|
||||
|
|
@ -82,24 +83,31 @@ export default class SearchPreview extends PureComponent {
|
|||
|
||||
showPostList = () => {
|
||||
this.setState({animationEnded: true});
|
||||
if (!this.state.showPosts && this.props.posts.length) {
|
||||
if (!this.state.showPosts && this.props.postIds.length) {
|
||||
this.setState({showPosts: true});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {channelName, currentUserId, posts, theme} = this.props;
|
||||
const {
|
||||
channelName,
|
||||
currentUserId,
|
||||
focusedPostId,
|
||||
postIds,
|
||||
theme
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let postList;
|
||||
if (this.state.showPosts) {
|
||||
postList = (
|
||||
<PostList
|
||||
highlightPostId={focusedPostId}
|
||||
indicateNewMessages={false}
|
||||
isSearchResult={true}
|
||||
shouldRenderReplyButton={false}
|
||||
renderReplies={false}
|
||||
posts={posts}
|
||||
postIds={postIds}
|
||||
currentUserId={currentUserId}
|
||||
lastViewedAt={0}
|
||||
navigator={navigator}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ChannelPostList extends PureComponent {
|
|||
intl: intlShape.isRequired,
|
||||
lastViewedAt: PropTypes.number,
|
||||
navigator: PropTypes.object,
|
||||
posts: PropTypes.array.isRequired,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
postVisibility: PropTypes.number,
|
||||
totalMessageCount: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired
|
||||
|
|
@ -47,29 +47,29 @@ class ChannelPostList extends PureComponent {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
visiblePosts: this.getVisiblePosts(props),
|
||||
showLoadMore: props.posts.length >= props.postVisibility
|
||||
visiblePostIds: this.getVisiblePostIds(props),
|
||||
showLoadMore: props.postIds.length >= props.postVisibility
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {posts: nextPosts} = nextProps;
|
||||
const {postIds: nextPostIds} = nextProps;
|
||||
|
||||
const showLoadMore = nextPosts.length >= nextProps.postVisibility;
|
||||
let visiblePosts = this.state.visiblePosts;
|
||||
const showLoadMore = nextPostIds.length >= nextProps.postVisibility;
|
||||
let visiblePostIds = this.state.visiblePostIds;
|
||||
|
||||
if (nextPosts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) {
|
||||
visiblePosts = this.getVisiblePosts(nextProps);
|
||||
if (nextPostIds !== this.props.postIds || nextProps.postVisibility !== this.props.postVisibility) {
|
||||
visiblePostIds = this.getVisiblePostIds(nextProps);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showLoadMore,
|
||||
visiblePosts
|
||||
visiblePostIds
|
||||
});
|
||||
}
|
||||
|
||||
getVisiblePosts = (props) => {
|
||||
return props.posts.slice(0, props.postVisibility);
|
||||
getVisiblePostIds = (props) => {
|
||||
return props.postIds.slice(0, props.postVisibility);
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
|
|
@ -130,17 +130,17 @@ class ChannelPostList extends PureComponent {
|
|||
currentUserId,
|
||||
lastViewedAt,
|
||||
navigator,
|
||||
posts,
|
||||
postIds,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showLoadMore,
|
||||
visiblePosts
|
||||
visiblePostIds
|
||||
} = this.state;
|
||||
|
||||
let component;
|
||||
if (!posts.length && channelRefreshingFailed) {
|
||||
if (!postIds.length && channelRefreshingFailed) {
|
||||
component = (
|
||||
<PostListRetry
|
||||
retry={this.loadPostsRetry}
|
||||
|
|
@ -150,7 +150,7 @@ class ChannelPostList extends PureComponent {
|
|||
} else {
|
||||
component = (
|
||||
<PostList
|
||||
posts={visiblePosts}
|
||||
postIds={visiblePostIds}
|
||||
loadMore={this.loadMorePosts}
|
||||
showLoadMore={showLoadMore}
|
||||
onPostPress={this.goToThread}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getPostIdsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentChannelId, getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
|
|
@ -16,11 +15,9 @@ import ChannelPostList from './channel_post_list';
|
|||
|
||||
function makeMapStateToProps() {
|
||||
const getChannel = makeGetChannel();
|
||||
const getPostsInChannel = makeGetPostsInChannel();
|
||||
|
||||
return function mapStateToProps(state) {
|
||||
const channelId = getCurrentChannelId(state);
|
||||
const posts = getPostsInChannel(state, channelId) || [];
|
||||
const channelRefreshingFailed = state.views.channel.retryFailed;
|
||||
const channel = getChannel(state, {id: channelId}) || {};
|
||||
|
||||
|
|
@ -30,7 +27,7 @@ function makeMapStateToProps() {
|
|||
currentUserId: getCurrentUserId(state),
|
||||
channelType: channel.type,
|
||||
channelDisplayName: channel.display_name,
|
||||
posts,
|
||||
postIds: getPostIdsInCurrentChannel(state),
|
||||
postVisibility: state.views.channel.postVisibility[channelId],
|
||||
lastViewedAt: getMyCurrentChannelMembership(state).last_viewed_at,
|
||||
totalMessageCount: channel.total_msg_count,
|
||||
|
|
@ -51,54 +48,4 @@ function mapDispatchToProps(dispatch) {
|
|||
};
|
||||
}
|
||||
|
||||
function areStatesEqual(next, prev) {
|
||||
const nextChannelId = next.entities.channels.currentChannelId;
|
||||
const prevChannelId = prev.entities.channels.currentChannelId;
|
||||
if (prevChannelId !== nextChannelId) {
|
||||
return false;
|
||||
}
|
||||
const nextVisible = next.views.channel.postVisibility[nextChannelId];
|
||||
const prevVisible = prev.views.channel.postVisibility[nextChannelId];
|
||||
const nextPostsIds = next.entities.posts.postsInChannel[nextChannelId];
|
||||
const prevPostsIds = prev.entities.posts.postsInChannel[nextChannelId];
|
||||
|
||||
// When we don't have posts and we failed to get the post after tha max retry attempts
|
||||
const prevRetryFailed = prev.views.channel.retryFailed;
|
||||
const nextRetryFailed = next.views.channel.retryFailed;
|
||||
if (prevRetryFailed !== nextRetryFailed && nextRetryFailed && !nextPostsIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if we don't have post and visibility is not set we don't need to re-render
|
||||
if (!nextPostsIds || !nextVisible || !prevVisible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nextVisiblePostsIds = nextPostsIds.slice(0, nextVisible);
|
||||
const prevVisiblePostsIds = prevPostsIds ? prevPostsIds.slice(0, prevVisible) : [];
|
||||
|
||||
// if we have a different amount of post we should re-render
|
||||
if (nextVisiblePostsIds.length !== prevVisiblePostsIds.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {status: nextStatus} = next.requests.posts.getPosts;
|
||||
const {status: prevStatus} = prev.requests.posts.getPosts;
|
||||
|
||||
// if we are requesting post for the first time we should re-render
|
||||
if (prevStatus === RequestStatus.STARTED && nextStatus === RequestStatus.SUCCESS &&
|
||||
nextPostsIds.length !== prevPostsIds.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if at least one post id changed we need to re-render
|
||||
for (let i = 0; i <= nextVisiblePostsIds.length; i++) {
|
||||
if (nextVisiblePostsIds[i] && nextVisiblePostsIds[i] !== prevVisiblePostsIds[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(ChannelPostList);
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelPostList);
|
||||
|
|
|
|||
|
|
@ -305,11 +305,8 @@ class Search extends Component {
|
|||
{displayName}
|
||||
</Text>
|
||||
<Post
|
||||
post={item}
|
||||
postId={item.id}
|
||||
renderReplies={true}
|
||||
isFirstReply={false}
|
||||
isLastReply={false}
|
||||
commentedOnPost={null}
|
||||
onPress={this.previewPost}
|
||||
onReply={this.goToThread}
|
||||
isSearchResult={true}
|
||||
|
|
|
|||
|
|
@ -7,24 +7,20 @@ import {connect} from 'react-redux';
|
|||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import Thread from './thread';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
// Create a getPostsForThread selector for each instance of Thread so that each Thread
|
||||
// is memoized correctly based on its own props
|
||||
const getPostsForThread = makeGetPostsForThread();
|
||||
const getPostIdsForThread = makeGetPostIdsForThread();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const posts = getPostsForThread(state, ownProps);
|
||||
|
||||
return {
|
||||
channelId: ownProps.channelId,
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
rootId: ownProps.rootId,
|
||||
posts,
|
||||
postIds: getPostIdsForThread(state, ownProps.rootId),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,26 +20,11 @@ export default class Thread extends Component {
|
|||
myMember: PropTypes.object.isRequired,
|
||||
rootId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
posts: PropTypes.array.isRequired
|
||||
postIds: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.posts.length !== this.props.posts.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const length = nextProps.posts.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (nextProps.posts[i].id !== this.props.posts[i].id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.state.lastViewedAt) {
|
||||
this.setState({lastViewedAt: nextProps.myMember.last_viewed_at});
|
||||
|
|
@ -55,7 +40,7 @@ export default class Thread extends Component {
|
|||
channelId,
|
||||
myMember,
|
||||
navigator,
|
||||
posts,
|
||||
postIds,
|
||||
rootId,
|
||||
theme
|
||||
} = this.props;
|
||||
|
|
@ -70,7 +55,7 @@ export default class Thread extends Component {
|
|||
<StatusBar/>
|
||||
<PostList
|
||||
indicateNewMessages={true}
|
||||
posts={posts}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
navigator={navigator}
|
||||
|
|
|
|||
79
app/selectors/post_list.js
Normal file
79
app/selectors/post_list.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Posts, Preferences} from 'mattermost-redux/constants';
|
||||
import {makeGetPostsForIds} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {createIdsSelector} from 'mattermost-redux/utils/helpers';
|
||||
import {shouldFilterPost} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
export const DATE_LINE = 'date-';
|
||||
export const START_OF_NEW_MESSAGES = 'start-of-new-messages';
|
||||
|
||||
function shouldShowJoinLeaveMessages(state) {
|
||||
// This setting is true or not set if join/leave messages are to be displayed
|
||||
return getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true);
|
||||
}
|
||||
|
||||
// Returns a selector that, given the state and an object containing an array of postIds and an optional
|
||||
// timestamp of when the channel was last read, returns a memoized array of postIds interspersed with
|
||||
// day indicators and an optional new message indicator.
|
||||
export function makePreparePostIdsForPostList() {
|
||||
const getMyPosts = makeGetPostsForIds();
|
||||
|
||||
return createIdsSelector(
|
||||
(state, props) => getMyPosts(state, props.postIds),
|
||||
(state, props) => props.lastViewedAt,
|
||||
getCurrentUserId,
|
||||
shouldShowJoinLeaveMessages,
|
||||
(posts, lastViewedAt, currentUserId, showJoinLeave) => {
|
||||
const out = [];
|
||||
|
||||
let lastDate = null;
|
||||
let addedNewMessagesIndicator = false;
|
||||
|
||||
const filterOptions = {showJoinLeave};
|
||||
|
||||
// Remember that we're iterating through the posts from newest to oldest
|
||||
for (const post of posts) {
|
||||
if (post.state === Posts.POST_DELETED && post.user_id === currentUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out join/leave messages if necessary
|
||||
if (shouldFilterPost(post, filterOptions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only add the new messages line if a lastViewedAt time is set
|
||||
if (lastViewedAt && !addedNewMessagesIndicator) {
|
||||
const postIsUnread = post.create_at > lastViewedAt;
|
||||
|
||||
if (postIsUnread) {
|
||||
out.push(START_OF_NEW_MESSAGES);
|
||||
addedNewMessagesIndicator = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Push on a date header if the last post was on a different day than the current one
|
||||
const postDate = new Date(post.create_at);
|
||||
|
||||
if (lastDate && lastDate.toDateString() !== postDate.toDateString()) {
|
||||
out.push(DATE_LINE + lastDate.toDateString());
|
||||
}
|
||||
|
||||
lastDate = postDate;
|
||||
|
||||
out.push(post.id);
|
||||
}
|
||||
|
||||
// Push on the date header for the oldest post
|
||||
if (lastDate) {
|
||||
out.push(DATE_LINE + lastDate.toDateString());
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
);
|
||||
}
|
||||
288
test/app/selectors/post_list.test.js
Normal file
288
test/app/selectors/post_list.test.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import {
|
||||
DATE_LINE,
|
||||
makePreparePostIdsForPostList,
|
||||
START_OF_NEW_MESSAGES
|
||||
} from 'app/selectors/post_list';
|
||||
|
||||
import {Posts, Preferences} from 'mattermost-redux/constants';
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
describe('Selectors.PostList', () => {
|
||||
describe('makePreparePostIdsForPostList', () => {
|
||||
it('filter join/leave posts', () => {
|
||||
const preparePostIdsForPostList = makePreparePostIdsForPostList();
|
||||
|
||||
let state = {
|
||||
entities: {
|
||||
posts: {
|
||||
posts: {
|
||||
1001: {id: '1001', create_at: 0},
|
||||
1002: {id: '1002', create_at: 1, type: Posts.POST_TYPES.JOIN_CHANNEL}
|
||||
}
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {}
|
||||
},
|
||||
users: {
|
||||
currentUserId: '1234'
|
||||
}
|
||||
}
|
||||
};
|
||||
const lastViewedAt = Number.POSITIVE_INFINITY;
|
||||
const postIds = ['1001', '1002'];
|
||||
|
||||
// Defaults to show post
|
||||
let now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') !== -1);
|
||||
|
||||
// Show join/leave posts
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
preferences: {
|
||||
...state.entities.preferences,
|
||||
myPreferences: {
|
||||
...state.entities.preferences.myPreferences,
|
||||
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
|
||||
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
value: 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') !== -1);
|
||||
|
||||
// Hide join/leave posts
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
preferences: {
|
||||
...state.entities.preferences,
|
||||
myPreferences: {
|
||||
...state.entities.preferences.myPreferences,
|
||||
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
|
||||
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
value: 'false'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
});
|
||||
|
||||
it('memoization', () => {
|
||||
const preparePostIdsForPostList = makePreparePostIdsForPostList();
|
||||
|
||||
// Posts 7 hours apart so they should appear on multiple days
|
||||
const initialPosts = {
|
||||
1001: {id: '1001', create_at: 1 * 60 * 60 * 1000},
|
||||
1002: {id: '1002', create_at: (1 * 60 * 60 * 1000) + 5},
|
||||
1003: {id: '1003', create_at: (1 * 60 * 60 * 1000) + 10},
|
||||
1004: {id: '1004', create_at: 25 * 60 * 60 * 1000},
|
||||
1005: {id: '1005', create_at: (25 * 60 * 60 * 1000) + 5},
|
||||
1006: {id: '1006', create_at: (25 * 60 * 60 * 1000) + 10, type: Posts.POST_TYPES.JOIN_CHANNEL}
|
||||
};
|
||||
let state = {
|
||||
entities: {
|
||||
posts: {
|
||||
posts: initialPosts
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
|
||||
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
value: 'true'
|
||||
}
|
||||
}
|
||||
},
|
||||
users: {
|
||||
currentUserId: '1234'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let postIds = ['1001', '1003', '1004', '1006'];
|
||||
let lastViewedAt = initialPosts['1001'].create_at + 1;
|
||||
|
||||
let now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// No changes
|
||||
let prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// lastViewedAt changed slightly
|
||||
lastViewedAt = initialPosts['1001'].create_at + 2;
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// lastViewedAt changed a lot
|
||||
lastViewedAt += initialPosts['1003'].create_at + 1;
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.notEqual(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
|
||||
// postIds changed, but still shallowly equal
|
||||
postIds = [...postIds];
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// Post changed, not in postIds
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
posts: {
|
||||
...state.entities.posts,
|
||||
posts: {
|
||||
...state.entities.posts.posts,
|
||||
1007: {id: '1007', create_at: 7 * 60 * 60 * 7 * 1000}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// Post changed, in postIds
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
posts: {
|
||||
...state.entities.posts,
|
||||
posts: {
|
||||
...state.entities.posts.posts,
|
||||
1006: {...state.entities.posts.posts['1006'], message: 'abcd'}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') !== -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
// Filter changed
|
||||
state = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
preferences: {
|
||||
...state.entities.preferences,
|
||||
myPreferences: {
|
||||
...state.entities.preferences.myPreferences,
|
||||
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
|
||||
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
value: 'false'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.notEqual(now, prev);
|
||||
assert.ok(now.indexOf('1001') !== -1);
|
||||
assert.ok(now.indexOf('1002') === -1);
|
||||
assert.ok(now.indexOf('1003') !== -1);
|
||||
assert.ok(now.indexOf('1004') !== -1);
|
||||
assert.ok(now.indexOf('1005') === -1);
|
||||
assert.ok(now.indexOf('1006') === -1);
|
||||
assert.ok(now.findIndex((id) => id.startsWith(DATE_LINE)) !== -1);
|
||||
assert.ok(now.indexOf(START_OF_NEW_MESSAGES) !== -1);
|
||||
|
||||
prev = now;
|
||||
now = preparePostIdsForPostList(state, {postIds, lastViewedAt});
|
||||
assert.equal(now, prev);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3878,7 +3878,7 @@ makeerror@1.0.x:
|
|||
|
||||
mattermost-redux@mattermost/mattermost-redux#master:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/18fa0a2dc2d0648d0346e01f71f096aa835096ac"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/f6d97c8a49e9402fcb5d72604751c9a5ae6d7e5a"
|
||||
dependencies:
|
||||
deep-equal "1.0.1"
|
||||
harmony-reflect "1.5.1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue