diff --git a/app/components/autocomplete/autocomplete.js b/app/components/autocomplete/autocomplete.js
index 7daf48e53..24776a66e 100644
--- a/app/components/autocomplete/autocomplete.js
+++ b/app/components/autocomplete/autocomplete.js
@@ -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) &&
+
+ }
);
diff --git a/app/components/autocomplete/date_suggestion/date_suggestion.js b/app/components/autocomplete/date_suggestion/date_suggestion.js
new file mode 100644
index 000000000..acd8101fa
--- /dev/null
+++ b/app/components/autocomplete/date_suggestion/date_suggestion.js
@@ -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 (
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ calList: {
+ height: 1700,
+ },
+});
diff --git a/app/components/autocomplete/date_suggestion/index.js b/app/components/autocomplete/date_suggestion/index.js
new file mode 100644
index 000000000..1e3a28453
--- /dev/null
+++ b/app/components/autocomplete/date_suggestion/index.js
@@ -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);
diff --git a/app/constants/autocomplete.js b/app/constants/autocomplete.js
index 220a269cb..4f9c13f0c 100644
--- a/app/constants/autocomplete.js
+++ b/app/constants/autocomplete.js
@@ -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;
\ No newline at end of file
diff --git a/app/screens/search/index.js b/app/screens/search/index.js
index 46e4727b9..fd0a060b1 100644
--- a/app/screens/search/index.js
+++ b/app/screens/search/index.js
@@ -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),
};
diff --git a/app/screens/search/search.js b/app/screens/search/search.js
index c398e120a..58929854f 100644
--- a/app/screens/search/search.js
+++ b/app/screens/search/search.js
@@ -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}
/>
diff --git a/app/selectors/autocomplete.js b/app/selectors/autocomplete.js
index 11e9d20df..8bc92d477 100644
--- a/app/selectors/autocomplete.js
+++ b/app/selectors/autocomplete.js
@@ -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;
+ };
+};
diff --git a/app/utils/timezone.js b/app/utils/timezone.js
index df8da9520..8498efecf 100644
--- a/app/utils/timezone.js
+++ b/app/utils/timezone.js
@@ -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;
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 9312a9d73..9dd8a4ee3 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/package-lock.json b/package-lock.json
index bf43cb80b..fa5f64bdd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d68688d6e..f7b8ff064 100644
--- a/package.json
+++ b/package.json
@@ -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",