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