diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 68f70b7a7..6b8ff2e3d 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -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 diff --git a/app/actions/views/create_channel.js b/app/actions/views/create_channel.js index 58c233a3d..66ca49c4f 100644 --- a/app/actions/views/create_channel.js +++ b/app/actions/views/create_channel.js @@ -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); } }; } diff --git a/app/components/post_attachment_opengraph/post_attachment_opengraph.js b/app/components/post_attachment_opengraph/post_attachment_opengraph.js index a20aac6b4..8bd6c482e 100644 --- a/app/components/post_attachment_opengraph/post_attachment_opengraph.js +++ b/app/components/post_attachment_opengraph/post_attachment_opengraph.js @@ -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 ( diff --git a/app/components/search_preview/index.js b/app/components/search_preview/index.js index d936e13b4..8192eed3b 100644 --- a/app/components/search_preview/index.js +++ b/app/components/search_preview/index.js @@ -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); diff --git a/app/components/search_preview/search_preview.js b/app/components/search_preview/search_preview.js index 69d35117f..fa0be7614 100644 --- a/app/components/search_preview/search_preview.js +++ b/app/components/search_preview/search_preview.js @@ -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 = ( + + ); + } else if (this.state.show) { postList = ( { }, 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 } }) }, diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 432e5083c..54a396d61 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -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: { diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index fad65652c..2cb9bcfe7 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -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, diff --git a/app/screens/search/channel_display_name/channel_display_name.js b/app/screens/search/channel_display_name/channel_display_name.js new file mode 100644 index 000000000..500eb8747 --- /dev/null +++ b/app/screens/search/channel_display_name/channel_display_name.js @@ -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 ( + {displayName} + ); + } +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + channelName: { + color: changeOpacity(theme.centerChannelColor, 0.8), + fontSize: 14, + fontWeight: '600', + marginTop: 5, + paddingHorizontal: 16 + } + }; +}); diff --git a/app/screens/search/channel_display_name/index.js b/app/screens/search/channel_display_name/index.js new file mode 100644 index 000000000..55a3bddd1 --- /dev/null +++ b/app/screens/search/channel_display_name/index.js @@ -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); diff --git a/app/screens/search/index.js b/app/screens/search/index.js index 0da996058..f57557ac7 100644 --- a/app/screens/search/index.js +++ b/app/screens/search/index.js @@ -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, diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 71bced0a5..4a56f3036 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -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 ( {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 ( - - {displayName} - + { + 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 { ) }]; - } 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: ( + + + + ) + }]; + break; } if (results) { @@ -566,7 +552,6 @@ class Search extends Component { previewComponent = ( { }, 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 } }; }); diff --git a/app/screens/thread/index.js b/app/screens/thread/index.js index fac1fe1d0..6fa23b2dc 100644 --- a/app/screens/thread/index.js +++ b/app/screens/thread/index.js @@ -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), diff --git a/app/screens/thread/thread.js b/app/screens/thread/thread.js index 7b526cebc..9c9d90e78 100644 --- a/app/screens/thread/thread.js +++ b/app/screens/thread/thread.js @@ -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); diff --git a/yarn.lock b/yarn.lock index db712ac84..79424a06b 100644 --- a/yarn.lock +++ b/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"