RN-220 Add "in:" and "from:" search modifiers (#869)

* Fix search and search preview

* Add "in:" and "from:" search modifiers
This commit is contained in:
enahum 2017-08-24 18:07:07 -03:00 committed by GitHub
parent 18b3d6eec9
commit 553f3796b1
6 changed files with 147 additions and 25 deletions

View file

@ -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);
}
};
}

View file

@ -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});
}

View file

@ -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;

View file

@ -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 (
<TouchableHighlight
key={item.modifier}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => preventDoubleTap(this.setModifierValue, this, item.value)}
>
<View style={style.modifierItemContainer}>
<View style={style.modifierItemWrapper}>
<View style={style.modifierItemLabelContainer}>
<View style={style.modifierLabelIconContainer}>
<AwesomeIcon
style={style.modifierLabelIcon}
name='plus-square-o'
/>
</View>
<Text
style={style.modifierItemLabel}
>
{item.modifier}
</Text>
</View>
<Text style={style.modifierItemDescription}>
{item.description}
</Text>
</View>
</View>
</TouchableHighlight>
);
};
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 (
<View style={style.sectionWrapper}>
<View style={style.sectionContainer}>
<Text style={style.sectionLabel}>
{title}
</Text>
if (title) {
return (
<View style={style.sectionWrapper}>
<View style={style.sectionContainer}>
<Text style={style.sectionLabel}>
{title}
</Text>
</View>
</View>
</View>
);
);
}
return <View/>;
};
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,

View file

@ -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",

View file

@ -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"