diff --git a/app/components/emoji_picker/__snapshots__/emoji_picker.test.js.snap b/app/components/emoji_picker/__snapshots__/emoji_picker.test.js.snap new file mode 100644 index 000000000..a054758a7 --- /dev/null +++ b/app/components/emoji_picker/__snapshots__/emoji_picker.test.js.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = ` + + + + + + + + + + + + + +`; diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js index c3565b04c..0b1bafedb 100644 --- a/app/components/emoji_picker/emoji_picker.js +++ b/app/components/emoji_picker/emoji_picker.js @@ -35,6 +35,10 @@ const SECTION_MARGIN = 15; const SECTION_HEADER_HEIGHT = 28; const EMOJIS_PER_PAGE = 200; +export function filterEmojiSearchInput(searchText) { + return searchText.toLowerCase().replace(/^:|:$/g, ''); +} + export default class EmojiPicker extends PureComponent { static propTypes = { customEmojisEnabled: PropTypes.bool.isRequired, @@ -87,9 +91,9 @@ export default class EmojiPicker extends PureComponent { } componentWillReceiveProps(nextProps) { - let rebuildEmojis = false; + this.rebuildEmojis = false; if (this.props.deviceWidth !== nextProps.deviceWidth) { - rebuildEmojis = true; + this.rebuildEmojis = true; if (this.refs.search_bar) { this.refs.search_bar.blur(); @@ -97,14 +101,16 @@ export default class EmojiPicker extends PureComponent { } if (this.props.emojis !== nextProps.emojis) { - rebuildEmojis = true; + this.rebuildEmojis = true; + this.deviceWidth = nextProps.deviceWidth; } + } - if (rebuildEmojis) { - const emojis = this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth); - this.setState({ - emojis, - }); + setRebuiltEmojis = (searchBarAnimationComplete) => { + if (this.rebuildEmojis && searchBarAnimationComplete) { + this.rebuildEmojis = false; + const emojis = this.renderableEmojis(this.props.emojisBySection, this.deviceWidth); + this.setState({emojis}); } } @@ -160,24 +166,25 @@ export default class EmojiPicker extends PureComponent { }); }; - changeSearchTerm = (text) => { + changeSearchTerm = (rawText) => { + const searchTerm = filterEmojiSearchInput(rawText); const nextState = { - searchTerm: text, + searchTerm: rawText, }; - - if (!text) { - nextState.currentSectionIndex = 0; - } - this.setState(nextState); + if (!searchTerm) { + nextState.currentSectionIndex = 0; + return; + } + clearTimeout(this.searchTermTimeout); - const timeout = text ? 100 : 0; + const timeout = searchTerm ? 100 : 0; this.searchTermTimeout = setTimeout(async () => { if (isMinimumServerVersion(this.props.serverVersion, 4, 7)) { - await this.props.actions.searchCustomEmojis(text); + await this.props.actions.searchCustomEmojis(searchTerm); } - const filteredEmojis = this.searchEmojis(text); + const filteredEmojis = this.searchEmojis(searchTerm); this.setState({ filteredEmojis, }); @@ -476,6 +483,7 @@ export default class EmojiPicker extends PureComponent { onCancelButtonPress={this.cancelSearch} autoCapitalize='none' value={searchTerm} + onAnimationComplete={this.setRebuiltEmojis} /> diff --git a/app/components/emoji_picker/emoji_picker.test.js b/app/components/emoji_picker/emoji_picker.test.js new file mode 100644 index 000000000..3f8511a13 --- /dev/null +++ b/app/components/emoji_picker/emoji_picker.test.js @@ -0,0 +1,117 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import Preferences from 'mattermost-redux/constants/preferences'; + +import {shallowWithIntl} from 'test/intl-test-helper'; +import EmojiPicker, {filterEmojiSearchInput} from './emoji_picker.js'; + +describe('components/emoji_picker/EmojiPicker', () => { + const baseProps = { + actions: { + getCustomEmojis: jest.fn(), + incrementEmojiPickerPage: jest.fn(), + searchCustomEmojis: jest.fn(), + }, + customEmojisEnabled: false, + customEmojiPage: 200, + deviceWidth: 400, + emojis: [], + emojisBySection: [], + fuse: {}, + isLandscape: false, + theme: Preferences.THEMES.default, + }; + + const testCases = [ + {input: 'smile', output: 'smile'}, + {input: 'SMILE', output: 'smile'}, + {input: ':smile', output: 'smile'}, + {input: ':SMILE', output: 'smile'}, + {input: 'smile:', output: 'smile'}, + {input: 'SMILE:', output: 'smile'}, + {input: ':smile:', output: 'smile'}, + {input: ':SMILE:', output: 'smile'}, + ]; + + testCases.forEach((testCase) => { + test(`'${testCase.input}' should return '${testCase.output}'`, () => { + expect(filterEmojiSearchInput(testCase.input)).toEqual(testCase.output); + }); + }); + + test('should match snapshot', () => { + const wrapper = shallowWithIntl(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should set rebuildEmojis to true when deviceWidth changes', () => { + const wrapper = shallowWithIntl(); + const instance = wrapper.instance(); + + expect(instance.rebuildEmojis).toBe(undefined); + + const newDeviceWidth = baseProps.deviceWidth * 2; + wrapper.setProps({deviceWidth: newDeviceWidth}); + + expect(instance.rebuildEmojis).toBe(true); + }); + + test('should set rebuildEmojis to true and new deviceWidth when emojis change', () => { + const wrapper = shallowWithIntl(); + const instance = wrapper.instance(); + + expect(instance.rebuildEmojis).toBe(undefined); + expect(instance.deviceWidth).toBe(undefined); + + const newDeviceWidth = baseProps.deviceWidth * 2; + const newEmojis = [{}]; + wrapper.setProps({deviceWidth: newDeviceWidth, emojis: newEmojis}); + + expect(instance.rebuildEmojis).toBe(true); + expect(instance.deviceWidth).toBe(newDeviceWidth); + }); + + test('should set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is true', () => { + const wrapper = shallowWithIntl(); + const instance = wrapper.instance(); + instance.setState = jest.fn(); + instance.renderableEmojis = jest.spyOn(instance, 'renderableEmojis'); + + instance.rebuildEmojis = true; + const searchBarAnimationComplete = true; + const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis'); + setRebuiltEmojis(searchBarAnimationComplete); + + expect(instance.setState).toHaveBeenCalledWith({emojis: []}); + expect(instance.rebuildEmojis).toBe(false); + }); + + test('should not set rebuilt emojis when rebuildEmojis is false and searchBarAnimationComplete is true', () => { + const wrapper = shallowWithIntl(); + const instance = wrapper.instance(); + instance.setState = jest.fn(); + + instance.rebuildEmojis = false; + const searchBarAnimationComplete = true; + const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis'); + setRebuiltEmojis(searchBarAnimationComplete); + + expect(instance.setState).not.toHaveBeenCalled(); + }); + + test('should not set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is false', () => { + const wrapper = shallowWithIntl(); + const instance = wrapper.instance(); + instance.setState = jest.fn(); + + instance.rebuildEmojis = true; + const searchBarAnimationComplete = false; + const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis'); + setRebuiltEmojis(searchBarAnimationComplete); + + expect(instance.setState).not.toHaveBeenCalled(); + }); +}); diff --git a/app/components/search_bar/search_box.js b/app/components/search_bar/search_box.js index 7360e5afd..dd926299e 100644 --- a/app/components/search_bar/search_box.js +++ b/app/components/search_bar/search_box.js @@ -18,6 +18,7 @@ import EvilIcon from 'react-native-vector-icons/EvilIcons'; import IonIcon from 'react-native-vector-icons/Ionicons'; import CustomPropTypes from 'app/constants/custom_prop_types'; +import {emptyFunction} from 'app/utils/general'; const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon); @@ -77,6 +78,7 @@ export default class Search extends Component { shadowVisible: PropTypes.bool, leftComponent: PropTypes.element, inputCollapsedMargin: PropTypes.number, + onAnimationComplete: PropTypes.func, }; static defaultProps = { @@ -101,6 +103,7 @@ export default class Search extends Component { value: '', leftComponent: null, inputCollapsedMargin: 10, + onAnimationComplete: emptyFunction, }; constructor(props) { @@ -369,7 +372,7 @@ export default class Search extends Component { useNativeDriver: true, } ), - ]).start(); + ]).start(({finished}) => this.props.onAnimationComplete(finished)); this.shadowHeight = this.props.shadowOffsetHeightCollapsed; resolve(); });