Fixed Search bugs and avoid re-renders (#1047)

* Fixed Search and avoid re-renders

* Feedback review

* Feedback review
This commit is contained in:
enahum 2017-10-24 18:02:09 -03:00 committed by Harrison Healey
parent 53f0576672
commit 27713322e3
14 changed files with 316 additions and 191 deletions

View file

@ -181,11 +181,11 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const posts = await action(dispatch, getState);
const {data} = await action(dispatch, getState);
if (posts) {
if (data) {
dispatch(setChannelRetryFailed(false));
return posts;
return data;
}
}
@ -413,13 +413,14 @@ export function increasePostVisibility(channelId, focusedPostId) {
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let posts;
let result;
if (focusedPostId) {
posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
result = 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);
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results

View file

@ -12,7 +12,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
const state = getState();
const currentUserId = getCurrentUserId(state);
const teamId = getCurrentTeamId(state);
let channel = {
const channel = {
team_id: teamId,
name: cleanUpUrlable(displayName),
display_name: displayName,
@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
type
};
channel = await createChannel(channel, currentUserId)(dispatch, getState);
if (channel && channel.id) {
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
if (data && data.id) {
dispatch(setChannelDisplayName(displayName));
handleSelectChannel(channel.id)(dispatch, getState);
handleSelectChannel(data.id)(dispatch, getState);
}
};
}

View file

@ -40,6 +40,11 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
}
componentDidMount() {
this.mounted = true;
this.getBestImageUrl(this.props.openGraphData);
}
componentWillMount() {
this.fetchData(this.props.link, this.props.openGraphData);
}
@ -49,6 +54,14 @@ export default class PostAttachmentOpenGraph extends PureComponent {
this.setState({imageLoaded: false});
this.fetchData(nextProps.link, nextProps.openGraphData);
}
if (this.props.openGraphData !== nextProps.openGraphData) {
this.getBestImageUrl(nextProps.openGraphData);
}
}
componentWillUnmount() {
this.mounted = false;
}
calculateLargeImageDimensions = (width, height) => {
@ -100,8 +113,8 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}
getBestImageUrl(data) {
if (!data.images) {
return null;
if (!data || !data.images) {
return;
}
const bestDimensions = {
@ -109,7 +122,11 @@ export default class PostAttachmentOpenGraph extends PureComponent {
height: MAX_IMAGE_HEIGHT
};
const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height');
return bestImage.secure_url || bestImage.url;
const imageUrl = bestImage.secure_url || bestImage.url;
if (imageUrl) {
this.getImageSize(imageUrl);
}
}
getImageSize = (imageUrl) => {
@ -126,11 +143,14 @@ export default class PostAttachmentOpenGraph extends PureComponent {
} else {
dimensions = this.calculateSmallImageDimensions(width, height);
}
this.setState({
...dimensions,
hasLargeImage: isLarge,
imageLoaded: true
});
if (this.mounted) {
this.setState({
...dimensions,
hasLargeImage: isLarge,
imageLoaded: true,
imageUrl
});
}
}, () => null);
}
};
@ -141,18 +161,14 @@ export default class PostAttachmentOpenGraph extends PureComponent {
render() {
const {openGraphData, theme} = this.props;
const {hasLargeImage, height, imageLoaded, offset, width} = this.state;
const {hasLargeImage, height, imageLoaded, imageUrl, offset, width} = this.state;
if (!openGraphData || !openGraphData.description) {
return null;
}
const style = getStyleSheet(theme);
const imageUrl = this.getBestImageUrl(openGraphData);
const isThumbnail = !hasLargeImage && imageLoaded;
if (imageUrl) {
this.getImageSize(imageUrl);
}
return (
<View style={style.container}>

View file

@ -1,26 +1,42 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getPost, makeGetPostIdsAroundPost} from 'mattermost-redux/selectors/entities/posts';
import {getPostsAfter, getPostsBefore, getPostThread} from 'mattermost-redux/actions/posts';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import SearchPreview from './search_preview';
function makeMapStateToProps() {
const getPostIdsAroundPost = makeGetPostIdsAroundPost();
const getChannel = makeGetChannel();
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.focusedPostId);
const channel = getChannel(state, {id: post.channel_id});
const postIds = getPostIdsAroundPost(state, post.id, post.channel_id, {postsBeforeCount: 5, postsAfterCount: 5});
return {
channelId: post.channel_id,
channelName: channel.display_name,
currentUserId: getCurrentUserId(state),
postIds
};
};
}
export default connect(makeMapStateToProps, null, null, {withRef: true})(SearchPreview);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getPostsAfter,
getPostsBefore,
getPostThread
}, dispatch)
};
}
export default connect(makeMapStateToProps, mapDispatchToProps, null, {withRef: true})(SearchPreview);

View file

@ -15,6 +15,7 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
Animatable.initializeRegistryWithDefinitions({
@ -36,6 +37,11 @@ Animatable.initializeRegistryWithDefinitions({
export default class SearchPreview extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getPostsAfter: PropTypes.func.isRequired,
getPostsBefore: PropTypes.func.isRequired,
getPostThread: PropTypes.func.isRequired
}).isRequired,
channelId: PropTypes.string,
channelName: PropTypes.string,
currentUserId: PropTypes.string.isRequired,
@ -51,41 +57,65 @@ export default class SearchPreview extends PureComponent {
postIds: []
};
state = {
showPosts: false,
animationEnded: false
};
constructor(props) {
super(props);
componentWillReceiveProps(nextProps) {
const {animationEnded, showPosts} = this.state;
if (animationEnded && !showPosts && nextProps.postIds.length) {
this.setState({showPosts: true});
const {postIds} = props;
let show = false;
if (postIds && postIds.length >= 10) {
show = true;
}
this.state = {
show,
error: false
};
}
componentDidMount() {
if (!this.state.show) {
this.loadPosts();
}
}
handleClose = () => {
this.refs.view.zoomOut().then(() => {
if (this.props.onClose) {
this.props.onClose();
}
});
return true;
if (this.refs.view) {
this.refs.view.zoomOut().then(() => {
if (this.props.onClose) {
this.props.onClose();
}
});
}
};
handlePress = () => {
const {channelId, onPress} = this.props;
this.refs.view.growOut().then(() => {
if (onPress) {
onPress(channelId);
}
});
const {channelId, channelName, onPress} = this.props;
if (this.refs.view) {
this.refs.view.growOut().then(() => {
if (onPress) {
onPress(channelId, channelName);
}
});
}
};
showPostList = () => {
this.setState({animationEnded: true});
if (!this.state.showPosts && this.props.postIds.length) {
this.setState({showPosts: true});
}
loadPosts = async () => {
const {actions, channelId, focusedPostId} = this.props;
const result = await Promise.all([
actions.getPostThread(focusedPostId, false),
actions.getPostsBefore(channelId, focusedPostId, 0, 5),
actions.getPostsAfter(channelId, focusedPostId, 0, 5)
]);
const error = result.some((res) => Boolean(res.error));
this.setState({show: true, error});
};
retry = () => {
this.setState({show: false, error: false});
this.loadPosts();
};
render() {
@ -93,13 +123,21 @@ export default class SearchPreview extends PureComponent {
channelName,
currentUserId,
focusedPostId,
navigator,
postIds,
theme
} = this.props;
const style = getStyleSheet(theme);
let postList;
if (this.state.showPosts) {
if (this.state.error) {
postList = (
<PostListRetry
retry={this.retry}
theme={theme}
/>
);
} else if (this.state.show) {
postList = (
<PostList
highlightPostId={focusedPostId}
@ -124,10 +162,10 @@ export default class SearchPreview extends PureComponent {
<Animatable.View
ref='view'
animation='zoomIn'
duration={500}
duration={200}
delay={0}
style={style.wrapper}
onAnimationEnd={this.showPostList}
useNativeDriver={true}
>
<View
style={style.header}
@ -184,16 +222,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
wrapper: {
flex: 1,
marginBottom: 10,
marginHorizontal: 10,
opacity: 0,
...Platform.select({
android: {
marginTop: 10,
marginBottom: 35
marginTop: 10
},
ios: {
marginTop: 20,
marginBottom: 10
marginTop: 20
}
})
},

View file

@ -10,8 +10,6 @@ import {
View
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
import RetryBarIndicator from 'app/components/retry_bar_indicator';
@ -25,10 +23,8 @@ class ChannelPostList extends PureComponent {
selectPost: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired
}).isRequired,
channelDisplayName: PropTypes.string,
channelId: PropTypes.string.isRequired,
channelRefreshingFailed: PropTypes.bool,
channelType: PropTypes.string,
currentUserId: PropTypes.string,
intl: intlShape.isRequired,
lastViewedAt: PropTypes.number,
@ -73,22 +69,14 @@ class ChannelPostList extends PureComponent {
};
goToThread = (post) => {
const {actions, channelId, channelDisplayName, channelType, intl, navigator, theme} = this.props;
const {actions, channelId, navigator, theme} = this.props;
const rootId = (post.root_id || post.id);
actions.loadThreadIfNecessary(post.root_id, channelId);
actions.selectPost(rootId);
let title;
if (channelType === General.DM_CHANNEL) {
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
} else {
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: channelDisplayName});
}
const options = {
screen: 'Thread',
title,
animated: true,
backButtonTitle: '',
navigatorStyle: {

View file

@ -25,8 +25,6 @@ function makeMapStateToProps() {
channelId,
channelRefreshingFailed,
currentUserId: getCurrentUserId(state),
channelType: channel.type,
channelDisplayName: channel.display_name,
postIds: getPostIdsInCurrentChannel(state),
postVisibility: state.views.channel.postVisibility[channelId],
lastViewedAt: getMyCurrentChannelMembership(state).last_viewed_at,

View file

@ -0,0 +1,36 @@
// 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 {Text} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelDisplayName extends PureComponent {
static propTypes = {
displayName: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
render() {
const {displayName, theme} = this.props;
const styles = getStyleFromTheme(theme);
return (
<Text style={styles.channelName}>{displayName}</Text>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
channelName: {
color: changeOpacity(theme.centerChannelColor, 0.8),
fontSize: 14,
fontWeight: '600',
marginTop: 5,
paddingHorizontal: 16
}
};
});

View file

@ -0,0 +1,25 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelDisplayName from './channel_display_name';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const post = getPost(state, ownProps.postId);
const channel = getChannel(state, {id: post.channel_id});
return {
displayName: channel.display_name,
theme: getTheme(state)
};
};
}
export default connect(makeMapStateToProps)(ChannelDisplayName);

View file

@ -5,10 +5,9 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getPostsAfter, getPostsBefore, getPostThread, selectPost} from 'mattermost-redux/actions/posts';
import {selectPost} from 'mattermost-redux/actions/posts';
import {clearSearch, removeSearchTerms, searchPosts} from 'mattermost-redux/actions/search';
import {getCurrentChannelId, getMyChannels} from 'mattermost-redux/selectors/entities/channels';
import {getSearchResults} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@ -21,19 +20,17 @@ import {handleSearchDraftChanged} from 'app/actions/views/search';
import Search from './search';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
const {recent} = state.entities.search;
const {searchPosts: searchRequest} = state.requests.search;
return {
...ownProps,
currentTeamId,
currentChannelId,
posts: getSearchResults(state),
recent: recent[currentTeamId] || [],
channels: getMyChannels(state),
postIds: state.entities.search.results,
recent: recent[currentTeamId],
searchingStatus: searchRequest.status
};
}
@ -42,9 +39,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearSearch,
getPostsAfter,
getPostsBefore,
getPostThread,
handleSearchDraftChanged,
handleSelectChannel,
loadThreadIfNecessary,

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016-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 {injectIntl, intlShape} from 'react-intl';
import {
@ -9,7 +9,6 @@ import {
InteractionManager,
Platform,
SectionList,
StyleSheet,
Text,
TouchableHighlight,
TouchableOpacity,
@ -18,53 +17,51 @@ import {
import IonIcon from 'react-native-vector-icons/Ionicons';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {RequestStatus} from 'mattermost-redux/constants';
import Autocomplete from 'app/components/autocomplete';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import Post from 'app/components/post';
import PostListRetry from 'app/components/post_list_retry';
import SearchBar from 'app/components/search_bar';
import SearchPreview from 'app/components/search_preview';
import StatusBar from 'app/components/status_bar';
import {ViewTypes} from 'app/constants';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDisplayName from './channel_display_name';
const SECTION_HEIGHT = 20;
const RECENT_LABEL_HEIGHT = 42;
const RECENT_SEPARATOR_HEIGHT = 3;
const MODIFIER_LABEL_HEIGHT = 58;
const POSTS_PER_PAGE = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
const SEARCHING = 'searching';
const NO_RESULTS = 'no results';
class Search extends Component {
class Search extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearSearch: PropTypes.func.isRequired,
getPostsAfter: PropTypes.func.isRequired,
getPostsBefore: PropTypes.func.isRequired,
getPostThread: PropTypes.func.isRequired,
handleSearchDraftChanged: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
removeSearchTerms: PropTypes.func.isRequired,
searchPosts: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired
}).isRequired,
channels: PropTypes.array.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
posts: PropTypes.array,
postIds: PropTypes.array,
recent: PropTypes.array.isRequired,
searchingStatus: PropTypes.string,
theme: PropTypes.object.isRequired
};
static defaultProps = {
posts: []
postIds: [],
recent: []
};
constructor(props) {
@ -81,28 +78,22 @@ class Search extends Component {
}
componentDidMount() {
this.refs.searchBar.focus();
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.props.recent !== nextProps.recent ||
this.props.posts !== nextProps.posts ||
this.state !== nextState
);
if (this.refs.searchBar) {
this.refs.searchBar.focus();
}
}
componentDidUpdate(prevProps) {
const {searchingStatus: status, recent} = this.props;
const {searchingStatus: prevStatus} = prevProps;
const recentLenght = recent.length;
const recentLength = recent.length;
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED);
if (shouldScroll) {
requestAnimationFrame(() => {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
animated: true,
offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght + 1) * RECENT_SEPARATOR_HEIGHT)
offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLength * RECENT_LABEL_HEIGHT) + ((recentLength + 1) * RECENT_SEPARATOR_HEIGHT)
});
});
}
@ -119,26 +110,16 @@ class Search extends Component {
};
goToThread = (post) => {
const {actions, channels, intl, navigator, theme} = this.props;
const {actions, navigator, theme} = this.props;
const channelId = post.channel_id;
const channel = channels.find((c) => c.id === channelId);
const rootId = (post.root_id || post.id);
Keyboard.dismiss();
actions.loadThreadIfNecessary(rootId, channelId);
actions.selectPost(rootId);
let title;
if (channel.type === General.DM_CHANNEL) {
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
} else {
const channelName = channel.display_name;
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName});
}
const options = {
screen: 'Thread',
title,
animated: true,
backButtonTitle: '',
navigatorStyle: {
@ -194,7 +175,7 @@ class Search extends Component {
};
keyPostExtractor = (item) => {
return `result-${item.id}`;
return item.id || item;
};
onBlur = () => {
@ -202,7 +183,9 @@ class Search extends Component {
};
onFocus = () => {
this.setState({isFocused: true});
if (!this.state.isFocused) {
this.setState({isFocused: true});
}
this.scrollToTop();
};
@ -217,23 +200,10 @@ class Search extends Component {
};
previewPost = (post) => {
const {actions, channels} = this.props;
const focusedPostId = post.id;
const channelId = post.channel_id;
Keyboard.dismiss();
actions.getPostThread(focusedPostId, false);
actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE);
actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE);
const channel = channels.find((c) => c.id === channelId);
let displayName = '';
if (channel) {
displayName = channel.display_name;
}
this.setState({preview: true, postId: focusedPostId, channelName: displayName});
this.setState({preview: true, postId: focusedPostId});
};
removeSearchTerms = (item) => {
@ -276,10 +246,10 @@ class Search extends Component {
};
renderPost = ({item, index}) => {
const {channels, posts, theme} = this.props;
const {postIds, theme} = this.props;
const style = getStyleFromTheme(theme);
if (item.id === SEARCHING || item.id === NO_RESULTS) {
if (item.id) {
return (
<View style={style.customItem}>
{item.component}
@ -287,25 +257,16 @@ class Search extends Component {
);
}
const channel = channels.find((c) => c.id === item.channel_id);
let displayName = '';
if (channel) {
displayName = channel.display_name;
}
let separator;
if (index === posts.length - 1) {
if (index === postIds.length - 1) {
separator = this.renderPostSeparator();
}
return (
<View>
<Text style={style.channelName}>
{displayName}
</Text>
<ChannelDisplayName postId={item}/>
<Post
postId={item.id}
postId={item}
renderReplies={true}
onPress={this.previewPost}
onReply={this.goToThread}
@ -394,18 +355,23 @@ class Search extends Component {
);
};
retry = () => {
this.search(this.state.value.trim());
};
scrollToTop = () => {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
animated: false,
offset: 0
});
if (this.refs.list) {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
animated: false,
offset: 0
});
}
};
search = (terms, isOrSearch) => {
const {actions, currentTeamId} = this.props;
actions.searchPosts(currentTeamId, terms.trim(), isOrSearch);
this.handleTextChanged(`${terms} `);
this.handleTextChanged(`${terms.trim()} `);
// Trigger onSelectionChanged Manually when submitting
this.handleSelectionChange({
@ -415,6 +381,8 @@ class Search extends Component {
}
}
});
actions.searchPosts(currentTeamId, terms.trim(), isOrSearch);
};
setModifierValue = (modifier) => {
@ -431,24 +399,28 @@ class Search extends Component {
this.handleTextChanged(newValue, true);
this.refs.searchBar.focus();
if (this.refs.searchBar) {
this.refs.searchBar.focus();
}
};
setRecentValue = (recent) => {
const {terms, isOrSearch} = recent;
this.handleTextChanged(terms);
this.search(terms, isOrSearch);
this.refs.searchBar.blur();
if (this.refs.searchBar) {
this.refs.searchBar.blur();
}
};
handleClosePreview = () => {
// console.warn('close preview');
this.setState({preview: false, postId: null});
};
handleJumpToChannel = (channelId) => {
handleJumpToChannel = (channelId, channelDisplayName) => {
if (channelId) {
const {actions, channels, currentChannelId} = this.props;
const {actions, currentChannelId} = this.props;
const {
handleSelectChannel,
markChannelAsRead,
@ -457,22 +429,20 @@ class Search extends Component {
viewChannel
} = actions;
setChannelLoading();
setChannelLoading(channelId !== currentChannelId);
setChannelDisplayName(channelDisplayName);
const channel = channels.find((c) => c.id === channelId);
let displayName = '';
if (channel) {
displayName = channel.display_name;
}
this.props.navigator.dismissModal({animationType: 'none'});
markChannelAsRead(channelId, currentChannelId);
viewChannel(channelId, currentChannelId);
setChannelDisplayName(displayName);
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channelId);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channelId, currentChannelId);
if (channelId !== currentChannelId) {
viewChannel(currentChannelId);
}
});
this.props.navigator.dismissModal({animationType: 'slide-down'});
});
}
};
@ -481,13 +451,13 @@ class Search extends Component {
const {
intl,
navigator,
posts,
postIds,
recent,
searchingStatus,
theme
} = this.props;
const {channelName, postId, preview, value} = this.state;
const {postId, preview, value} = this.state;
const style = getStyleFromTheme(theme);
const sections = [{
data: [{
@ -524,7 +494,8 @@ class Search extends Component {
}
let results;
if (searchingStatus === RequestStatus.STARTED) {
switch (searchingStatus) {
case RequestStatus.STARTED:
results = [{
id: SEARCHING,
component: (
@ -533,9 +504,10 @@ class Search extends Component {
</View>
)
}];
} else if (searchingStatus === RequestStatus.SUCCESS) {
if (posts.length) {
results = posts;
break;
case RequestStatus.SUCCESS:
if (postIds.length) {
results = postIds;
} else if (this.state.value) {
results = [{
id: NO_RESULTS,
@ -548,6 +520,20 @@ class Search extends Component {
)
}];
}
break;
case RequestStatus.FAILURE:
results = [{
id: RequestStatus.FAILURE,
component: (
<View style={style.searching}>
<PostListRetry
retry={this.retry}
theme={theme}
/>
</View>
)
}];
break;
}
if (results) {
@ -566,7 +552,6 @@ class Search extends Component {
previewComponent = (
<SearchPreview
ref='preview'
channelName={channelName}
focusedPostId={postId}
navigator={navigator}
onClose={this.handleClosePreview}
@ -720,14 +705,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: StyleSheet.hairlineWidth
},
channelName: {
color: changeOpacity(theme.centerChannelColor, 0.8),
fontSize: 14,
fontWeight: '600',
marginTop: 5,
paddingHorizontal: 16
height: 1
},
sectionList: {
flex: 1,
@ -747,7 +725,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
textAlignVertical: 'center'
},
searching: {
marginTop: 25
marginTop: 65
}
};
});

View file

@ -7,17 +7,22 @@ import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {selectPost} from 'mattermost-redux/actions/posts';
import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import Thread from './thread';
function makeMapStateToProps() {
const getPostIdsForThread = makeGetPostIdsForThread();
const getChannel = makeGetChannel();
return function mapStateToProps(state, ownProps) {
const channel = getChannel(state, {id: ownProps.channelId});
return {
channelId: ownProps.channelId,
channelType: channel.type,
displayName: channel.display_name,
myMember: getMyCurrentChannelMembership(state),
rootId: ownProps.rootId,
postIds: getPostIdsForThread(state, ownProps.rootId),

View file

@ -1,8 +1,11 @@
// 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 {injectIntl, intlShape} from 'react-intl';
import {General} from 'mattermost-redux/constants';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import PostList from 'app/components/post_list';
@ -10,12 +13,15 @@ import PostTextbox from 'app/components/post_textbox';
import StatusBar from 'app/components/status_bar';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class Thread extends Component {
class Thread extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
selectPost: PropTypes.func.isRequired
}).isRequired,
channelId: PropTypes.string.isRequired,
channelType: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
myMember: PropTypes.object.isRequired,
rootId: PropTypes.string.isRequired,
@ -25,6 +31,21 @@ export default class Thread extends Component {
state = {};
componentWillMount() {
const {channelType, displayName, intl} = this.props;
let title;
if (channelType === General.DM_CHANNEL) {
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
} else {
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: displayName});
}
this.props.navigator.setTitle({
title
});
}
componentWillReceiveProps(nextProps) {
if (!this.state.lastViewedAt) {
this.setState({lastViewedAt: nextProps.myMember.last_viewed_at});
@ -78,3 +99,5 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
}
};
});
export default injectIntl(Thread);

View file

@ -3882,7 +3882,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/f6d97c8a49e9402fcb5d72604751c9a5ae6d7e5a"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/14b5f58c9be349c0f9514bc9e457ea4ac6bf2d03"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"
@ -5264,7 +5264,7 @@ redux-persist-transform-filter@0.0.15:
lodash.set "^4.3.2"
lodash.unset "^4.5.2"
redux-persist@4.9.1, redux-persist@^4.5.0:
redux-persist@4.9.1:
version "4.9.1"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-4.9.1.tgz#271fa31d1c782ebf9082fb5174e829db24faf59e"
dependencies:
@ -5272,6 +5272,14 @@ redux-persist@4.9.1, redux-persist@^4.5.0:
lodash "^4.17.4"
lodash-es "^4.17.4"
redux-persist@^4.5.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-4.10.1.tgz#4fb2b789942f10f56d51cc7ad068d9d6beb46124"
dependencies:
json-stringify-safe "^5.0.1"
lodash "^4.17.4"
lodash-es "^4.17.4"
redux-thunk@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"