diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index fe087cba1..f36b9d35d 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -167,13 +167,14 @@ export function loadFilesForPostIfNecessary(postId) { }; } -export function loadThreadIfNecessary(rootId) { +export function loadThreadIfNecessary(rootId, channelId) { return async (dispatch, getState) => { const state = getState(); - const {posts} = state.entities.posts; + const {posts, postsInChannel} = state.entities.posts; + const channelPosts = postsInChannel[channelId]; - if (rootId && !posts[rootId]) { - getPostThread(rootId)(dispatch, getState); + if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) { + getPostThread(rootId, false)(dispatch, getState); } }; } diff --git a/app/components/search_preview/search_preview.js b/app/components/search_preview/search_preview.js index 849b9a06f..09eb9d0a1 100644 --- a/app/components/search_preview/search_preview.js +++ b/app/components/search_preview/search_preview.js @@ -52,9 +52,17 @@ export default class SearchPreview extends PureComponent { }; state = { - showPosts: false + showPosts: false, + animationEnded: false }; + componentWillReceiveProps(nextProps) { + const {animationEnded, showPosts} = this.state; + if (animationEnded && !showPosts && nextProps.posts.length) { + this.setState({showPosts: true}); + } + } + handleClose = () => { this.refs.view.zoomOut().then(() => { if (this.props.onClose) { @@ -74,6 +82,7 @@ export default class SearchPreview extends PureComponent { }; showPostList = () => { + this.setState({animationEnded: true}); if (!this.state.showPosts && this.props.posts.length) { this.setState({showPosts: true}); } 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 4b2b09f5d..0416aa4cc 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -133,7 +133,7 @@ class ChannelPostList extends PureComponent { const channelId = post.channel_id; const rootId = (post.root_id || post.id); - actions.loadThreadIfNecessary(post.root_id); + actions.loadThreadIfNecessary(post.root_id, channelId); actions.selectPost(rootId); let title; diff --git a/app/screens/search/search.js b/app/screens/search/search.js index dc63baebe..5034f4253 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import { Dimensions, + Keyboard, Platform, StyleSheet, Text, @@ -14,6 +15,7 @@ import { View } from 'react-native'; import IonIcon from 'react-native-vector-icons/Ionicons'; +import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {General, RequestStatus} from 'mattermost-redux/constants'; @@ -32,6 +34,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; 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'; @@ -99,7 +102,7 @@ class Search extends Component { setTimeout(() => { this.refs.list.getWrapperRef().getListRef().scrollToOffset({ animated: true, - offset: SECTION_HEIGHT + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght - 1) * RECENT_SEPARATOR_HEIGHT) + offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght + 1) * RECENT_SEPARATOR_HEIGHT) }); }, 200); } @@ -121,7 +124,8 @@ class Search extends Component { const channel = channels.find((c) => c.id === channelId); const rootId = (post.root_id || post.id); - actions.loadThreadIfNecessary(post.root_id); + Keyboard.dismiss(); + actions.loadThreadIfNecessary(rootId, channelId); actions.selectPost(rootId); let title; @@ -149,11 +153,7 @@ class Search extends Component { } }; - if (Platform.OS === 'android') { - navigator.showModal(options); - } else { - navigator.push(options); - } + navigator.push(options); }; handleSelectionChange = (event) => { @@ -173,6 +173,10 @@ class Search extends Component { } }; + keyModifierExtractor = (item) => { + return `modifier-${item.value}`; + }; + keyRecentExtractor = (item) => { return `recent-${item.terms}`; }; @@ -205,7 +209,8 @@ class Search extends Component { const focusedPostId = post.id; const channelId = post.channel_id; - actions.getPostThread(focusedPostId); + Keyboard.dismiss(); + actions.getPostThread(focusedPostId, false); actions.getPostsBefore(channelId, focusedPostId, 0, POSTS_PER_PAGE); actions.getPostsAfter(channelId, focusedPostId, 0, POSTS_PER_PAGE); @@ -224,6 +229,40 @@ class Search extends Component { actions.removeSearchTerms(currentTeamId, item.terms); }; + renderModifiers = ({item}) => { + const {theme} = this.props; + const style = getStyleFromTheme(theme); + + return ( + preventDoubleTap(this.setModifierValue, this, item.value)} + > + + + + + + + + {item.modifier} + + + + {item.description} + + + + + ); + }; + renderPost = ({item, index}) => { const {channels, posts, theme} = this.props; const style = getStyleFromTheme(theme); @@ -297,15 +336,19 @@ class Search extends Component { const {title} = section; const style = getStyleFromTheme(theme); - return ( - - - - {title} - + if (title) { + return ( + + + + {title} + + - - ); + ); + } + + return ; }; renderRecentItem = ({item}) => { @@ -353,6 +396,17 @@ class Search extends Component { actions.searchPosts(currentTeamId, terms, isOrSearch); }; + setModifierValue = (modifier) => { + const {value} = this.state; + if (!value) { + this.handleTextChanged(modifier); + } else if (value.endsWith(' ')) { + this.handleTextChanged(`${value}${modifier}`); + } else { + this.handleTextChanged(`${value} ${modifier}`); + } + }; + setRecentValue = (recent) => { const {terms, isOrSearch} = recent; this.handleTextChanged(terms); @@ -405,7 +459,28 @@ class Search extends Component { const {channelName, postId, preview, value} = this.state; const style = getStyleFromTheme(theme); - const sections = []; + const sections = [{ + data: [{ + value: 'from:', + modifier: `from:${intl.formatMessage({id: 'mobile.search.from_modifier_title', defaultMessage: 'username'})}`, + description: intl.formatMessage({ + id: 'mobile.search.from_modifier_description', + defaultMessage: 'to find posts from specific users' + }) + }, { + value: 'in:', + modifier: `in:${intl.formatMessage({id: 'mobile.search.in_modifier_title', defaultMessage: 'channel-name'})}`, + description: intl.formatMessage({ + id: 'mobile.search.in_modifier_description', + defaultMessage: 'to find posts in specific channels' + }) + }], + key: 'modifiers', + title: '', + renderItem: this.renderModifiers, + keyExtractor: this.keyModifierExtractor, + ItemSeparatorComponent: this.renderRecentSeparator + }]; if (recent.length) { sections.push({ @@ -520,7 +595,8 @@ class Search extends Component { style={style.sectionList} renderSectionHeader={this.renderSectionHeader} sections={sections} - keyboardShouldPersistTaps='handled' + keyboardShouldPersistTaps='always' + keyboardDismissMode='interactive' stickySectionHeadersEnabled={Platform.OS === 'ios'} /> {previewComponent} @@ -559,6 +635,38 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { fontSize: 12, fontWeight: '600' }, + modifierItemContainer: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + height: MODIFIER_LABEL_HEIGHT + }, + modifierItemWrapper: { + flex: 1, + flexDirection: 'column', + paddingHorizontal: 16 + }, + modifierItemLabelContainer: { + alignItems: 'center', + flexDirection: 'row' + }, + modifierLabelIconContainer: { + alignItems: 'center', + marginRight: 5 + }, + modifierLabelIcon: { + fontSize: 16, + color: changeOpacity(theme.centerChannelColor, 0.5) + }, + modifierItemLabel: { + fontSize: 14, + color: theme.centerChannelColor + }, + modifierItemDescription: { + fontSize: 12, + color: changeOpacity(theme.centerChannelColor, 0.5), + marginTop: 5 + }, recentItemContainer: { alignItems: 'center', flex: 1, diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 6ace3ca1d..5b2c1a9b1 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1906,7 +1906,11 @@ "mobile.routes.thread_dm": "Direct Message Thread", "mobile.routes.user_profile": "Profile", "mobile.routes.user_profile.send_message": "Send Message", + "mobile.search.from_modifier_description": "to find posts from specific users", + "mobile.search.from_modifier_title": "username", "mobile.search.jump": "JUMP", + "mobile.search.in_modifier_description": "to find posts in specific channels", + "mobile.search.in_modifier_title": "channel-name", "mobile.search.no_results": "No Results Found", "mobile.select_team.choose": "Your teams:", "mobile.select_team.join_open": "Open teams you can join", diff --git a/yarn.lock b/yarn.lock index ebc7873cf..032f7197f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3656,7 +3656,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/09be668d27afd13ea9f3618f4c368c0d0a0db485" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/1afaf513717108b609c4dc677cf49d474624e0ca" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1"