Feature/search after before on (#2028)

* initial partial implementation of search date flags UI (on: before: after:)

* Make the calendar show correctly

* dismiss keyboard when showing the date suggestion since this type of input will primarily be entered through the calendar and later edited if needed

* fixed code style errors

* updates as per pull request comments

* fixed eslint errors

* changes as per pull request comments, removing unnecessary code

* added handling for hiding the date filter feature if the server version doesn't support it

* fixed eslint errors

* removed unnecessary property defaults and server version checks, as per PR suggestions

* this default is needed for the initial load of the component

* changed the way we capture the flag from the search string, this handles scenarios where multiple flags might be present in the search string that are handled by the same suggestion component as is the case here with date based flags

* need property declared to pass jslint rules

* eslint style fix, moved regex out into constants

* removed unused code and references

* updated as per pull request feedback

* updated to latest redux version

* changed to new search posts api that supports passing in of time zone utc offset to support date based search flags

* updated to the upcoming release version in preparation for merging to master

* Properly memoize DateSuggestion mapStateToProps

* Remove moment-timezone
This commit is contained in:
Dmitry Samuylov
2018-08-28 18:01:11 -04:00
committed by Harrison Healey
parent 19efaf92ca
commit 1ecb209712
11 changed files with 310 additions and 24 deletions

View File

@@ -16,6 +16,7 @@ import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import SlashSuggestion from './slash_suggestion';
import DateSuggestion from './date_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
@@ -26,11 +27,13 @@ export default class Autocomplete extends PureComponent {
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
};
static defaultProps = {
isSearch: false,
cursorPosition: 0,
enableDateSuggestion: false,
};
state = {
@@ -38,6 +41,7 @@ export default class Autocomplete extends PureComponent {
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
};
@@ -57,6 +61,10 @@ export default class Autocomplete extends PureComponent {
this.setState({commandCount});
};
handleIsDateFilterChange = (dateCount) => {
this.setState({dateCount});
};
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
@@ -97,8 +105,8 @@ export default class Autocomplete extends PureComponent {
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount > 0) {
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
} else {
@@ -128,6 +136,12 @@ export default class Autocomplete extends PureComponent {
onResultCountChange={this.handleCommandCountChange}
{...this.props}
/>
{(this.props.isSearch && this.props.enableDateSuggestion) &&
<DateSuggestion
onResultCountChange={this.handleIsDateFilterChange}
{...this.props}
/>
}
</View>
</View>
);

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Keyboard, StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import {CalendarList} from 'react-native-calendars';
import {DATE_MENTION_SEARCH_REGEX, ALL_SEARCH_FLAGS_REGEX} from 'app/constants/autocomplete';
export default class DateSuggestion extends PureComponent {
static propTypes = {
cursorPosition: PropTypes.number.isRequired,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
};
static defaultProps = {
value: '',
};
constructor(props) {
super(props);
this.state = {
mentionComplete: false,
sections: [],
};
}
componentWillReceiveProps(nextProps) {
const {matchTerm} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: [],
});
this.props.onResultCountChange(0);
}
}
completeMention = (day) => {
const mention = day.dateString;
const {cursorPosition, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const flags = mentionPart.match(ALL_SEARCH_FLAGS_REGEX);
const currentFlag = flags[flags.length - 1];
let completedDraft = mentionPart.replace(DATE_MENTION_SEARCH_REGEX, `${currentFlag} ${mention} `);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft, true);
this.props.onResultCountChange(1);
this.setState({mentionComplete: true});
};
render() {
const {mentionComplete} = this.state;
const {matchTerm, enableDateSuggestion} = this.props;
if (matchTerm === null || mentionComplete || !enableDateSuggestion) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.
return null;
}
const currentDate = (new Date()).toDateString();
Keyboard.dismiss();
return (
<CalendarList
style={styles.calList}
current={currentDate}
pastScrollRange={24}
futureScrollRange={0}
scrollingEnabled={true}
pagingEnabled={true}
hideArrows={true}
horizontal={true}
showScrollIndicator={true}
onDayPress={this.completeMention}
showWeekNumbers={true}
/>
);
}
}
const styles = StyleSheet.create({
calList: {
height: 1700,
},
});

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {makeGetMatchTermForDateMention} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import DateSuggestion from './date_suggestion';
function makeMapStateToProps() {
const getMatchTermForDateMention = makeGetMatchTermForDateMention();
return (state, ownProps) => {
const {cursorPosition, value} = ownProps;
const newValue = value.substring(0, cursorPosition);
const matchTerm = getMatchTermForDateMention(newValue);
return {
matchTerm,
theme: getTheme(state),
};
};
}
export default connect(makeMapStateToProps)(DateSuggestion);

View File

@@ -8,3 +8,7 @@ export const AT_MENTION_SEARCH_REGEX = /\bfrom:\s*(\S*)$/i;
export const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
export const CHANNEL_MENTION_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
export const DATE_MENTION_SEARCH_REGEX = /\b(?:on|before|after):\s*(\S*)$/i;
export const ALL_SEARCH_FLAGS_REGEX = /\b\w+:/g;

View File

@@ -5,10 +5,11 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
import {clearSearch, removeSearchTerms, searchPosts} from 'mattermost-redux/actions/search';
import {clearSearch, removeSearchTerms, searchPostsWithParams} from 'mattermost-redux/actions/search';
import {getCurrentChannelId, filterPostIds} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
@@ -29,6 +30,9 @@ function makeMapStateToProps() {
const {recent} = state.entities.search;
const {searchPosts: searchRequest} = state.requests.search;
const serverVersion = state.entities.general.serverVersion;
const enableDateSuggestion = isMinimumServerVersion(serverVersion, 5, 3);
return {
currentTeamId,
currentChannelId,
@@ -38,6 +42,7 @@ function makeMapStateToProps() {
recent: recent[currentTeamId],
searchingStatus: searchRequest.status,
theme: getTheme(state),
enableDateSuggestion,
};
};
}
@@ -51,7 +56,7 @@ function mapDispatchToProps(dispatch) {
loadThreadIfNecessary,
removeSearchTerms,
selectFocusedPostId,
searchPosts,
searchPostsWithParams,
selectPost,
}, dispatch),
};

View File

@@ -31,6 +31,7 @@ import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
import {getDeviceUtcOffset} from 'app/utils/timezone';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDisplayName from './channel_display_name';
@@ -51,7 +52,7 @@ export default class Search extends PureComponent {
loadChannelsByTeamName: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
removeSearchTerms: PropTypes.func.isRequired,
searchPosts: PropTypes.func.isRequired,
searchPostsWithParams: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired,
}).isRequired,
@@ -63,6 +64,7 @@ export default class Search extends PureComponent {
recent: PropTypes.array.isRequired,
searchingStatus: PropTypes.string,
theme: PropTypes.object.isRequired,
enableDateSuggestion: PropTypes.bool,
};
static defaultProps = {
@@ -466,7 +468,9 @@ export default class Search extends PureComponent {
},
});
actions.searchPosts(currentTeamId, terms.trim(), isOrSearch, true);
// timezone offset in seconds
const timeZoneOffset = getDeviceUtcOffset() * 60;
actions.searchPostsWithParams(currentTeamId, {terms: terms.trim(), is_or_search: isOrSearch, time_zone_offset: timeZoneOffset}, true);
};
handleSearchButtonPress = preventDoubleTap((text) => {
@@ -514,22 +518,53 @@ export default class Search extends PureComponent {
value,
} = this.state;
const style = getStyleFromTheme(theme);
const sectionsData = [{
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',
}),
}];
// if search by date filters supported
if (this.props.enableDateSuggestion) {
sectionsData.push({
value: 'on:',
modifier: 'on: YYYY-MM-DD',
description: intl.formatMessage({
id: 'mobile.search.on_modifier_description',
defaultMessage: 'to find posts on a specific date',
}),
});
sectionsData.push({
value: 'after:',
modifier: 'after: YYYY-MM-DD',
description: intl.formatMessage({
id: 'mobile.search.after_modifier_description',
defaultMessage: 'to find posts after a specific date',
}),
});
sectionsData.push({
value: 'before:',
modifier: 'before: YYYY-MM-DD',
description: intl.formatMessage({
id: 'mobile.search.before_modifier_description',
defaultMessage: 'to find posts before a specific date',
}),
});
}
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',
}),
}],
data: sectionsData,
key: 'modifiers',
title: '',
renderItem: this.renderModifiers,
@@ -651,6 +686,7 @@ export default class Search extends PureComponent {
onChangeText={this.handleTextChanged}
isSearch={true}
value={value}
enableDateSuggestion={this.props.enableDateSuggestion}
/>
</View>
</SafeAreaView>

View File

@@ -224,3 +224,21 @@ export const filterPrivateChannels = createSelector(
return channels.map((c) => c.id);
}
);
export const makeGetMatchTermForDateMention = () => {
let lastMatchTerm = null;
let lastValue;
return (value) => {
if (value !== lastValue) {
const regex = Autocomplete.DATE_MENTION_SEARCH_REGEX;
const match = value.match(regex);
lastValue = value;
if (match) {
lastMatchTerm = match[1];
} else {
lastMatchTerm = null;
}
}
return lastMatchTerm;
};
};

View File

@@ -9,6 +9,11 @@ export function getDeviceTimezone() {
return DeviceInfo.getTimezone();
}
export function getDeviceUtcOffset() {
const reverseOffsetInMinutes = new Date().getTimezoneOffset();
return -reverseOffsetInMinutes;
}
export function isTimezoneEnabled(state) {
const {config} = state.entities.general;
const serverVersion = state.entities.general.serverVersion;

View File

@@ -2598,6 +2598,12 @@
"mobile.search.from_modifier_title": "username",
"mobile.search.in_modifier_description": "to find posts in specific channels",
"mobile.search.in_modifier_title": "channel-name",
"mobile.search.on_modifier_description": "to find posts on a specific date",
"mobile.search.on_modifier_title": "YYYY-MM-DD",
"mobile.search.after_modifier_description": "to find posts after a specific date",
"mobile.search.after_modifier_title": "YYYY-MM-DD",
"mobile.search.before_modifier_description": "to find posts before a specific date",
"mobile.search.before_modifier_title": "YYYY-MM-DD",
"mobile.search.jump": "Jump to recent messages",
"mobile.search.no_results": "No Results Found",
"mobile.search.recent_title": "Recent Searches",

70
package-lock.json generated
View File

@@ -2750,6 +2750,46 @@
"integrity": "sha512-NOLEgsT6UiDTjnWG5Hd2Mg25LRyz/oe8ql3wbjzgSFeRzRROhPmtlsvIrei4B46UjERF0td9SZ1ZXPLOdcrBHg==",
"dev": true
},
"@types/prop-types": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.5.tgz",
"integrity": "sha512-mOrlCEdwX3seT3n0AXNt4KNPAZZxcsABUHwBgFXOt+nvFUXkxCAO6UBJHPrDxWEa2KDMil86355fjo8jbZ+K0Q==",
"requires": {
"@types/react": "*"
}
},
"@types/react": {
"version": "16.4.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.11.tgz",
"integrity": "sha512-1DQnmwO8u8N3ucvRX2ZLDEjQ2VctkAvL/rpbm2ev4uaZA0z4ysU+I0tk+K8ZLblC6p7MCgFyF+cQlSNIPUHzeQ==",
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
}
},
"@types/react-native": {
"version": "0.56.10",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.56.10.tgz",
"integrity": "sha512-xbut9LMHg773sJX+0PJuGoG7+sB7VL24djp/41rrwht9Kg+3TPcqTdP4k3V6Gqi+Jpv/YbmL0Wlxdlu+vVxORA==",
"requires": {
"@types/react": "*"
}
},
"@types/react-native-calendars": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/react-native-calendars/-/react-native-calendars-1.20.2.tgz",
"integrity": "sha512-mb7H4yRug06STg40SnqGM9c6kvAXcPuImvQjKfr8v8DVrgFpTS0RO2Ykyk+B1s3bpsK3eop8ExLLKmMM4bz5fA==",
"requires": {
"@types/react": "*",
"@types/react-native": "*",
"@types/xdate": "*"
}
},
"@types/xdate": {
"version": "0.8.29",
"resolved": "https://registry.npmjs.org/@types/xdate/-/xdate-0.8.29.tgz",
"integrity": "sha512-vhjumeX678c1aA5TDdxeLdWjqazQ4qDvgK57pRr5l3Y0H5+ZZYckNtLkJiW4tJ3YVZq43ZFZFVResfAM4UMvTw=="
},
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -5413,6 +5453,11 @@
"cssom": "0.3.x"
}
},
"csstype": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.6.tgz",
"integrity": "sha512-tKPyhy0FmfYD2KQYXD5GzkvAYLYj96cMLXr648CKGd3wBe0QqoPipImjGiLze9c8leJK8J3n7ap90tpk3E6HGQ=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -9993,6 +10038,11 @@
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
"integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -10128,8 +10178,8 @@
}
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#11902b0d530e5b3303e7fffe74ef8102d25a1893",
"from": "github:mattermost/mattermost-redux#11902b0d530e5b3303e7fffe74ef8102d25a1893",
"version": "github:mattermost/mattermost-redux#45ae4849c2810b56cb5a420fb7d49d2621145969",
"from": "github:mattermost/mattermost-redux#45ae4849c2810b56cb5a420fb7d49d2621145969",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "1.2.0",
@@ -14654,6 +14704,17 @@
"prop-types": "^15.5.10"
}
},
"react-native-calendars": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/react-native-calendars/-/react-native-calendars-1.20.0.tgz",
"integrity": "sha512-VlRoDcnEAWYE1JBPBh/Bie6baLQCmtuOGhw7V5yk09Y4j7Hy8BtuZIHh2+LU/TFYso+wEHJAFdj6D0QFttDOlg==",
"requires": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"prop-types": "^15.5.10",
"xdate": "^0.8.0"
}
},
"react-native-circular-progress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/react-native-circular-progress/-/react-native-circular-progress-0.2.0.tgz",
@@ -18284,6 +18345,11 @@
}
}
},
"xdate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/xdate/-/xdate-0.8.2.tgz",
"integrity": "sha1-17AzwASF0CaVuvAET06s2j/JYaM="
},
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",

View File

@@ -7,6 +7,7 @@
"license": "Apache 2.0",
"private": true,
"dependencies": {
"@types/react-native-calendars": "1.20.2",
"analytics-react-native": "1.2.0",
"commonmark": "github:mattermost/commonmark.js#e5c34706cafa2924370772ff47937b833648a6de",
"commonmark-react-renderer": "mattermost/commonmark-react-renderer#86fa63f898802953842526c2030f3b63c5d1ae7a",
@@ -15,7 +16,7 @@
"intl": "1.2.5",
"jail-monkey": "1.0.0",
"jsc-android": "216113.0.3",
"mattermost-redux": "github:mattermost/mattermost-redux#11902b0d530e5b3303e7fffe74ef8102d25a1893",
"mattermost-redux": "github:mattermost/mattermost-redux#45ae4849c2810b56cb5a420fb7d49d2621145969",
"mime-db": "1.33.0",
"prop-types": "15.6.1",
"react": "16.3.2",
@@ -24,6 +25,7 @@
"react-native-animatable": "1.2.4",
"react-native-bottom-sheet": "1.0.3",
"react-native-button": "2.3.0",
"react-native-calendars": "1.20.0",
"react-native-circular-progress": "0.2.0",
"react-native-cookies": "3.2.0",
"react-native-device-info": "enahum/react-native-device-info.git#a7bb3cff1086780b2c791a3e43e5b826fdf3ab11",