Fixed Search bugs and avoid re-renders (#1047)
* Fixed Search and avoid re-renders * Feedback review * Feedback review
This commit is contained in:
parent
53f0576672
commit
27713322e3
14 changed files with 316 additions and 191 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
25
app/screens/search/channel_display_name/index.js
Normal file
25
app/screens/search/channel_display_name/index.js
Normal 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);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue