From 9294dec68a90e5275fa8c4948bb1b606b4ddd115 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 21 Nov 2018 13:04:26 -0300 Subject: [PATCH] Properly scroll to search results (#2348) * Properly scroll to search results * Add search recent_item test * Create a new component for the search modifiers --- .../__snapshots__/modifier.test.js.snap | 82 ++++++++++ .../__snapshots__/recent_item.test.js.snap | 54 +++++++ app/screens/search/modifier.js | 96 +++++++++++ app/screens/search/modifier.test.js | 34 ++++ app/screens/search/recent_item.js | 88 ++++++++++ app/screens/search/recent_item.test.js | 36 +++++ app/screens/search/search.js | 151 ++++++------------ 7 files changed, 438 insertions(+), 103 deletions(-) create mode 100644 app/screens/search/__snapshots__/modifier.test.js.snap create mode 100644 app/screens/search/__snapshots__/recent_item.test.js.snap create mode 100644 app/screens/search/modifier.js create mode 100644 app/screens/search/modifier.test.js create mode 100644 app/screens/search/recent_item.js create mode 100644 app/screens/search/recent_item.test.js diff --git a/app/screens/search/__snapshots__/modifier.test.js.snap b/app/screens/search/__snapshots__/modifier.test.js.snap new file mode 100644 index 000000000..7593d5e62 --- /dev/null +++ b/app/screens/search/__snapshots__/modifier.test.js.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Search RecentItem should match snapshot and respond to events 1`] = ` + + + + + + + + + channel-name + + + + to find posts in specific channels + + + + +`; diff --git a/app/screens/search/__snapshots__/recent_item.test.js.snap b/app/screens/search/__snapshots__/recent_item.test.js.snap new file mode 100644 index 000000000..f93e66303 --- /dev/null +++ b/app/screens/search/__snapshots__/recent_item.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Search RecentItem should match snapshot and respond to events 1`] = ` + + + + test + + + + + + +`; diff --git a/app/screens/search/modifier.js b/app/screens/search/modifier.js new file mode 100644 index 000000000..bd7a58501 --- /dev/null +++ b/app/screens/search/modifier.js @@ -0,0 +1,96 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Text, TouchableHighlight, View} from 'react-native'; +import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export const MODIFIER_LABEL_HEIGHT = 58; + +export default class Modifier extends PureComponent { + static propTypes = { + item: PropTypes.object.isRequired, + setModifierValue: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired, + }; + + handlePress = () => { + const {item, setModifierValue} = this.props; + + setModifierValue(item.value); + }; + + render() { + const {item, theme} = this.props; + const style = getStyleFromTheme(theme); + + return ( + + + + + + + + + {item.modifier} + + + + {item.description} + + + + + ); + } +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + 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, + }, + }; +}); diff --git a/app/screens/search/modifier.test.js b/app/screens/search/modifier.test.js new file mode 100644 index 000000000..3de89a480 --- /dev/null +++ b/app/screens/search/modifier.test.js @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import Preferences from 'mattermost-redux/constants/preferences'; + +import Modifier from './modifier'; + +describe('Search RecentItem', () => { + const item = { + value: 'in:', + modifier: 'channel-name', + description: 'to find posts in specific channels', + }; + + const baseProps = { + item, + setModifierValue: jest.fn(), + theme: Preferences.THEMES.default, + }; + + test('should match snapshot and respond to events', () => { + const wrapper = shallow( + + ); + + expect(wrapper.getElement()).toMatchSnapshot(); + wrapper.find('TouchableHighlight').first().props().onPress(); + expect(baseProps.setModifierValue).toHaveBeenCalledTimes(1); + expect(baseProps.setModifierValue).toHaveBeenCalledWith(item.value); + }); +}); diff --git a/app/screens/search/recent_item.js b/app/screens/search/recent_item.js new file mode 100644 index 000000000..7e0574842 --- /dev/null +++ b/app/screens/search/recent_item.js @@ -0,0 +1,88 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Text, TouchableHighlight, TouchableOpacity, View} from 'react-native'; +import IonIcon from 'react-native-vector-icons/Ionicons'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export const RECENT_LABEL_HEIGHT = 42; + +export default class RecentItem extends PureComponent { + static propTypes = { + item: PropTypes.object.isRequired, + removeSearchTerms: PropTypes.func.isRequired, + setRecentValue: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired, + }; + + handlePress = () => { + const {item, setRecentValue} = this.props; + + setRecentValue(item); + }; + + handleRemove = () => { + const {item, removeSearchTerms} = this.props; + removeSearchTerms(item); + }; + + render() { + const {item, theme} = this.props; + const style = getStyleFromTheme(theme); + + return ( + + + + {item.terms} + + + + + + + ); + } +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + recentItemContainer: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + height: RECENT_LABEL_HEIGHT, + }, + recentItemLabel: { + color: theme.centerChannelColor, + fontSize: 14, + height: 20, + flex: 1, + paddingHorizontal: 16, + }, + recentRemove: { + alignItems: 'center', + height: RECENT_LABEL_HEIGHT, + justifyContent: 'center', + width: 50, + }, + }; +}); diff --git a/app/screens/search/recent_item.test.js b/app/screens/search/recent_item.test.js new file mode 100644 index 000000000..2639bec96 --- /dev/null +++ b/app/screens/search/recent_item.test.js @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import Preferences from 'mattermost-redux/constants/preferences'; + +import RecentItem from './recent_item'; + +describe('Search RecentItem', () => { + const item = { + terms: 'test', + }; + + const baseProps = { + item, + removeSearchTerms: jest.fn(), + setRecentValue: jest.fn(), + theme: Preferences.THEMES.default, + }; + + test('should match snapshot and respond to events', () => { + const wrapper = shallow( + + ); + + expect(wrapper.getElement()).toMatchSnapshot(); + wrapper.find('TouchableHighlight').first().props().onPress(); + expect(baseProps.setRecentValue).toHaveBeenCalledTimes(1); + expect(baseProps.setRecentValue).toHaveBeenCalledWith(item); + wrapper.find('TouchableOpacity').first().props().onPress(); + expect(baseProps.setRecentValue).toHaveBeenCalledTimes(1); + expect(baseProps.setRecentValue).toHaveBeenCalledWith(item); + }); +}); diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 318cfa81b..0985b2515 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -9,11 +9,8 @@ import { Platform, SectionList, Text, - TouchableHighlight, - TouchableOpacity, View, } from 'react-native'; -import IonIcon from 'react-native-vector-icons/Ionicons'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -37,12 +34,12 @@ import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDisplayName from './channel_display_name'; +import Modifier, {MODIFIER_LABEL_HEIGHT} from './modifier'; +import RecentItem, {RECENT_LABEL_HEIGHT} from './recent_item'; import SearchResultPost from './search_result_post'; const SECTION_HEIGHT = 20; -const RECENT_LABEL_HEIGHT = 42; const RECENT_SEPARATOR_HEIGHT = 3; -const MODIFIER_LABEL_HEIGHT = 58; const SCROLL_UP_MULTIPLIER = 6; const SEARCHING = 'searching'; const NO_RESULTS = 'no results'; @@ -97,6 +94,7 @@ export default class Search extends PureComponent { cursorPosition: 0, value: props.initialValue, managedConfig: {}, + recent: props.recent, }; } @@ -118,12 +116,13 @@ export default class Search extends PureComponent { }, 150); } - componentDidUpdate(prevProps) { - const {searchingStatus: status, recent, enableDateSuggestion} = this.props; + componentDidUpdate(prevProps, prevState) { + const {searchingStatus: status, enableDateSuggestion} = this.props; + const {recent} = this.state; const {searchingStatus: prevStatus} = prevProps; const shouldScroll = prevStatus !== status && - (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED) && - !this.props.isSearchGettingMore && !prevProps.isSearchGettingMore; + (status === RequestStatus.SUCCESS || status === RequestStatus.FAILURE) && + !this.props.isSearchGettingMore && !prevProps.isSearchGettingMore && prevState.recent.length === recent.length; if (this.props.isLandscape !== prevProps.isLandscape) { this.refs.searchBar.blur(); @@ -131,17 +130,21 @@ export default class Search extends PureComponent { if (shouldScroll) { setTimeout(() => { - const recentLabelsHeight = (recent.length + (Platform.OS === 'ios' ? 1 : 0)) * RECENT_LABEL_HEIGHT; - const recentSeparatorsHeight = (recent.length + (Platform.OS === 'ios' ? 0 : 2)) * RECENT_SEPARATOR_HEIGHT; + const recentLabelsHeight = recent.length * RECENT_LABEL_HEIGHT; + const recentSeparatorsHeight = (recent.length - 1) * RECENT_SEPARATOR_HEIGHT; const modifiersCount = enableDateSuggestion ? 5 : 2; + const modifiersHeight = modifiersCount * MODIFIER_LABEL_HEIGHT; + const modifiersSeparatorHeight = (modifiersCount - 1) * RECENT_SEPARATOR_HEIGHT; + const offset = modifiersHeight + modifiersSeparatorHeight + SECTION_HEIGHT + recentLabelsHeight + recentSeparatorsHeight; + + Keyboard.dismiss(); if (this.refs.list) { this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle animated: true, - offset: SECTION_HEIGHT + (modifiersCount * MODIFIER_LABEL_HEIGHT) + - recentLabelsHeight + recentSeparatorsHeight, + offset, }); } - }, 100); + }, 250); } } @@ -325,6 +328,14 @@ export default class Search extends PureComponent { removeSearchTerms = preventDoubleTap((item) => { const {actions, currentTeamId} = this.props; + const recent = [...this.state.recent]; + const index = recent.indexOf(item); + + if (index !== -1) { + recent.splice(index, 1); + this.setState({recent}); + } + actions.removeSearchTerms(currentTeamId, item.terms); }); @@ -343,35 +354,13 @@ export default class Search extends PureComponent { renderModifiers = ({item}) => { const {theme} = this.props; - const style = getStyleFromTheme(theme); return ( - this.setModifierValue(item.value)} - > - - - - - - - - {item.modifier} - - - - {item.description} - - - - + ); }; @@ -455,34 +444,14 @@ export default class Search extends PureComponent { renderRecentItem = ({item}) => { const {theme} = this.props; - const style = getStyleFromTheme(theme); return ( - this.setRecentValue(item)} - > - - - {item.terms} - - this.removeSearchTerms(item)} - style={style.recentRemove} - > - - - - + ); }; @@ -537,10 +506,12 @@ export default class Search extends PureComponent { } }; - search = (terms, isOrSearch) => { + search = (text, isOrSearch) => { const {actions, currentTeamId, viewArchivedChannels} = this.props; + const recent = [...this.state.recent]; + const terms = text.trim(); - this.handleTextChanged(`${terms.trim()} `); + this.handleTextChanged(`${terms} `); // Trigger onSelectionChanged Manually when submitting this.handleSelectionChange({ @@ -553,7 +524,7 @@ export default class Search extends PureComponent { // timezone offset in seconds const params = { - terms: terms.trim(), + terms, is_or_search: isOrSearch, time_zone_offset: this.props.timezoneOffsetInSeconds, page: 0, @@ -561,6 +532,12 @@ export default class Search extends PureComponent { include_deleted_channels: viewArchivedChannels, }; actions.searchPostsWithParams(currentTeamId, params, true); + if (!recent.find((r) => r.terms === terms)) { + recent.push({ + terms, + }); + this.setState({recent}); + } }; setModifierValue = preventDoubleTap((modifier) => { @@ -593,7 +570,6 @@ export default class Search extends PureComponent { const { isLandscape, postIds, - recent, searchingStatus, theme, isSearchGettingMore, @@ -602,6 +578,7 @@ export default class Search extends PureComponent { const {intl} = this.context; const { cursorPosition, + recent, value, } = this.state; const style = getStyleFromTheme(theme); @@ -825,38 +802,6 @@ 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,