@atlaskit/emoji 65.0.0 → 65.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/cjs/components/common/CachingEmoji.js +84 -151
  3. package/dist/cjs/components/common/Emoji.js +2 -2
  4. package/dist/cjs/components/common/EmojiActions.js +129 -175
  5. package/dist/cjs/components/common/EmojiErrorMessage.js +23 -59
  6. package/dist/cjs/components/common/EmojiPreviewComponent.js +1 -0
  7. package/dist/cjs/components/common/EmojiUploadPicker.js +235 -293
  8. package/dist/cjs/components/common/FileChooser.js +34 -71
  9. package/dist/cjs/components/common/Popup.js +105 -154
  10. package/dist/cjs/components/common/ResourcedEmojiComponent.js +10 -5
  11. package/dist/cjs/components/common/RetryableButton.js +43 -64
  12. package/dist/cjs/components/common/ToneSelector.js +50 -89
  13. package/dist/cjs/components/common/styles.js +14 -16
  14. package/dist/cjs/components/hooks.js +16 -0
  15. package/dist/cjs/components/picker/EmojiPickerCategoryHeading.js +16 -48
  16. package/dist/cjs/components/picker/EmojiPickerComponent.js +496 -508
  17. package/dist/cjs/components/picker/EmojiPickerEmojiRow.js +33 -60
  18. package/dist/cjs/components/picker/EmojiPickerFooter.js +13 -46
  19. package/dist/cjs/components/picker/styles.js +16 -13
  20. package/dist/cjs/components/uploader/EmojiUploadComponent.js +124 -109
  21. package/dist/cjs/hooks/useEmojiContext.js +16 -0
  22. package/dist/cjs/hooks/usePrevious.js +16 -0
  23. package/dist/cjs/index.js +16 -0
  24. package/dist/cjs/util/constants.js +3 -1
  25. package/dist/cjs/version.json +1 -1
  26. package/dist/es2019/components/common/CachingEmoji.js +65 -112
  27. package/dist/es2019/components/common/Emoji.js +2 -2
  28. package/dist/es2019/components/common/EmojiActions.js +124 -150
  29. package/dist/es2019/components/common/EmojiErrorMessage.js +22 -26
  30. package/dist/es2019/components/common/EmojiPreviewComponent.js +1 -0
  31. package/dist/es2019/components/common/EmojiUploadPicker.js +190 -253
  32. package/dist/es2019/components/common/FileChooser.js +37 -40
  33. package/dist/es2019/components/common/Popup.js +89 -109
  34. package/dist/es2019/components/common/ResourcedEmojiComponent.js +5 -4
  35. package/dist/es2019/components/common/RetryableButton.js +43 -34
  36. package/dist/es2019/components/common/ToneSelector.js +46 -59
  37. package/dist/es2019/components/common/styles.js +9 -9
  38. package/dist/es2019/components/hooks.js +8 -0
  39. package/dist/es2019/components/picker/EmojiPickerCategoryHeading.js +13 -17
  40. package/dist/es2019/components/picker/EmojiPickerComponent.js +417 -497
  41. package/dist/es2019/components/picker/EmojiPickerEmojiRow.js +32 -35
  42. package/dist/es2019/components/picker/EmojiPickerFooter.js +11 -19
  43. package/dist/es2019/components/picker/styles.js +16 -14
  44. package/dist/es2019/components/uploader/EmojiUploadComponent.js +81 -91
  45. package/dist/es2019/hooks/useEmojiContext.js +3 -0
  46. package/dist/es2019/hooks/usePrevious.js +8 -0
  47. package/dist/es2019/index.js +4 -1
  48. package/dist/es2019/util/constants.js +1 -0
  49. package/dist/es2019/version.json +1 -1
  50. package/dist/esm/components/common/CachingEmoji.js +86 -156
  51. package/dist/esm/components/common/Emoji.js +2 -2
  52. package/dist/esm/components/common/EmojiActions.js +129 -176
  53. package/dist/esm/components/common/EmojiErrorMessage.js +21 -56
  54. package/dist/esm/components/common/EmojiPreviewComponent.js +1 -0
  55. package/dist/esm/components/common/EmojiUploadPicker.js +233 -299
  56. package/dist/esm/components/common/FileChooser.js +34 -70
  57. package/dist/esm/components/common/Popup.js +104 -155
  58. package/dist/esm/components/common/ResourcedEmojiComponent.js +8 -4
  59. package/dist/esm/components/common/RetryableButton.js +40 -60
  60. package/dist/esm/components/common/ToneSelector.js +50 -87
  61. package/dist/esm/components/common/styles.js +10 -10
  62. package/dist/esm/components/hooks.js +8 -0
  63. package/dist/esm/components/picker/EmojiPickerCategoryHeading.js +14 -43
  64. package/dist/esm/components/picker/EmojiPickerComponent.js +486 -526
  65. package/dist/esm/components/picker/EmojiPickerEmojiRow.js +31 -59
  66. package/dist/esm/components/picker/EmojiPickerFooter.js +14 -47
  67. package/dist/esm/components/picker/styles.js +16 -14
  68. package/dist/esm/components/uploader/EmojiUploadComponent.js +119 -113
  69. package/dist/esm/hooks/useEmojiContext.js +5 -0
  70. package/dist/esm/hooks/usePrevious.js +8 -0
  71. package/dist/esm/index.js +4 -1
  72. package/dist/esm/util/constants.js +1 -0
  73. package/dist/esm/version.json +1 -1
  74. package/dist/types/api/EmojiResource.d.ts +2 -3
  75. package/dist/types/components/common/CachingEmoji.d.ts +3 -13
  76. package/dist/types/components/common/Emoji.d.ts +1 -2
  77. package/dist/types/components/common/EmojiActions.d.ts +6 -17
  78. package/dist/types/components/common/EmojiErrorMessage.d.ts +3 -6
  79. package/dist/types/components/common/EmojiPreviewComponent.d.ts +2 -2
  80. package/dist/types/components/common/EmojiUploadPicker.d.ts +3 -27
  81. package/dist/types/components/common/FileChooser.d.ts +3 -5
  82. package/dist/types/components/common/LoadingEmojiComponent.d.ts +3 -0
  83. package/dist/types/components/common/Popup.d.ts +3 -20
  84. package/dist/types/components/common/ResourcedEmojiComponent.d.ts +23 -11
  85. package/dist/types/components/common/RetryableButton.d.ts +3 -7
  86. package/dist/types/components/common/ToneSelector.d.ts +4 -10
  87. package/dist/types/components/common/setSkinToneAriaLabelText.d.ts +1 -1
  88. package/dist/types/components/common/styles.d.ts +1 -3
  89. package/dist/types/components/hooks.d.ts +1 -0
  90. package/dist/types/components/picker/CategorySelector.d.ts +1 -1
  91. package/dist/types/components/picker/EmojiPicker.d.ts +12 -3
  92. package/dist/types/components/picker/EmojiPickerCategoryHeading.d.ts +3 -4
  93. package/dist/types/components/picker/EmojiPickerComponent.d.ts +14 -76
  94. package/dist/types/components/picker/EmojiPickerEmojiRow.d.ts +3 -4
  95. package/dist/types/components/picker/EmojiPickerFooter.d.ts +3 -5
  96. package/dist/types/components/picker/styles.d.ts +1 -1
  97. package/dist/types/components/typeahead/EmojiTypeAheadComponent.d.ts +18 -0
  98. package/dist/types/components/uploader/EmojiUploadComponent.d.ts +3 -17
  99. package/dist/types/components/uploader/EmojiUploader.d.ts +5 -7
  100. package/dist/types/hooks/useEmojiContext.d.ts +1 -0
  101. package/dist/types/hooks/usePrevious.d.ts +1 -0
  102. package/dist/types/index.d.ts +3 -1
  103. package/dist/types/types.d.ts +2 -1
  104. package/dist/types/util/constants.d.ts +1 -0
  105. package/docs/0-intro.tsx +35 -27
  106. package/docs/1-resourced-emoji.tsx +74 -0
  107. package/docs/2-emoji-picker.tsx +56 -0
  108. package/docs/3-typeahead.tsx +20 -0
  109. package/docs/4-emoji-provider.tsx +98 -0
  110. package/local-config-example.ts +3 -1
  111. package/package.json +19 -6
  112. package/dist/cjs/components/common/EmojiPreview.js +0 -194
  113. package/dist/es2019/components/common/EmojiPreview.js +0 -152
  114. package/dist/esm/components/common/EmojiPreview.js +0 -170
  115. package/dist/types/components/common/EmojiPreview.d.ts +0 -31
@@ -1,8 +1,7 @@
1
- import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
-
3
1
  /** @jsx jsx */
4
2
  import { jsx } from '@emotion/core';
5
- import { PureComponent } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState, createRef, memo } from 'react';
4
+ import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
6
5
  import { FormattedMessage } from 'react-intl-next';
7
6
  import { getEmojiVariation } from '../../api/EmojiRepository';
8
7
  import { supportsUploadFeature } from '../../api/EmojiResource';
@@ -20,566 +19,487 @@ import EmojiPickerList from './EmojiPickerList';
20
19
  import { createAndFireEventInElementsChannel, categoryClickedEvent, closedPickerEvent, deleteBeginEvent, deleteCancelEvent, deleteConfirmEvent, openedPickerEvent, pickerClickedEvent, pickerSearchedEvent, selectedFileEvent, uploadBeginButton, uploadCancelButton, uploadConfirmButton, toneSelectorClosedEvent, ufoExperiences } from '../../util/analytics';
21
20
  import { emojiPicker } from './styles';
22
21
  import LegacyEmojiContextProvider from '../../context/LegacyEmojiContextProvider';
22
+ import { useDidMount } from '../hooks';
23
23
  const FREQUENTLY_USED_MAX = 16;
24
- export default class EmojiPickerComponent extends PureComponent {
25
- constructor(props) {
26
- super(props);
27
-
28
- _defineProperty(this, "onEmojiActive", (_emojiId, emoji) => {
29
- if (this.state.selectedEmoji !== emoji) {
30
- this.setState({
31
- selectedEmoji: emoji
32
- });
33
- }
34
- });
35
24
 
36
- _defineProperty(this, "onCategoryActivated", category => {
37
- if (this.state.activeCategory !== category) {
38
- this.setState({
39
- activeCategory: category
40
- });
41
- }
25
+ const EmojiPickerComponent = ({
26
+ emojiProvider,
27
+ onSelection,
28
+ onPickerRef,
29
+ hideToneSelector,
30
+ createAnalyticsEvent
31
+ }) => {
32
+ const [filteredEmojis, setFilteredEmojis] = useState([]);
33
+ const [searchEmojis, setSearchEmojis] = useState([]);
34
+ const [frequentlyUsedEmojis, setFrequentlyUsedEmojis] = useState([]);
35
+ const [query, setQuery] = useState('');
36
+ const [dynamicCategories, setDynamicCategories] = useState([]);
37
+ const [selectedTone, setSelectedTone] = useState(!hideToneSelector ? emojiProvider.getSelectedTone() : undefined);
38
+ const [loading, setLoading] = useState(true);
39
+ const [uploadSupported, setUploadSupported] = useState(false);
40
+ const [uploading, setUploading] = useState(false);
41
+ const [selectedEmoji, setSelectedEmoji] = useState();
42
+ const [activeCategory, setActiveCategory] = useState(null);
43
+ const [disableCategories, setDisableCategories] = useState(false);
44
+ const [uploadErrorMessage, setUploadErrorMessage] = useState();
45
+ const [emojiToDelete, setEmojiToDelete] = useState();
46
+ const [toneEmoji, setToneEmoji] = useState();
47
+ const emojiPickerList = useMemo(() => /*#__PURE__*/createRef(), []);
48
+ const openTime = useRef(0);
49
+ const isMounting = useRef(true);
50
+ const didMount = useDidMount();
51
+ const updateAfterDidMount = useRef(true);
52
+ const previousEmojiProvider = useRef(emojiProvider);
53
+ const currentUser = useMemo(() => {
54
+ return emojiProvider.getCurrentUser();
55
+ }, [emojiProvider]);
56
+ const fireAnalytics = useCallback(analyticsEvent => {
57
+ if (createAnalyticsEvent) {
58
+ createAndFireEventInElementsChannel(analyticsEvent)(createAnalyticsEvent);
59
+ }
60
+ }, [createAnalyticsEvent]);
61
+ const onEmojiActive = useCallback((emojiId, emoji) => {
62
+ if (!selectedEmoji || selectedEmoji.id !== (emojiId === null || emojiId === void 0 ? void 0 : emojiId.id)) {
63
+ setSelectedEmoji(emoji);
64
+ }
65
+ }, [selectedEmoji]);
66
+ const onCategoryActivated = useCallback(category => {
67
+ if (activeCategory !== category) {
68
+ setActiveCategory(category);
69
+ }
70
+ }, [activeCategory]);
71
+
72
+ const calculateElapsedTime = () => {
73
+ return Date.now() - openTime.current;
74
+ };
75
+
76
+ const onUploadSupported = useCallback(supported => {
77
+ setUploadSupported(supported);
78
+ }, []);
79
+ const onDynamicCategoryChange = useCallback(categories => {
80
+ setDynamicCategories(categories);
81
+ }, []);
82
+ const onUploadCancelled = useCallback(() => {
83
+ batchedUpdates(() => {
84
+ setUploading(false);
85
+ setUploadErrorMessage(undefined);
42
86
  });
87
+ fireAnalytics(uploadCancelButton());
88
+ }, [fireAnalytics]);
89
+ const getDynamicCategories = useCallback(() => {
90
+ if (!emojiProvider.calculateDynamicCategories) {
91
+ return Promise.resolve([]);
92
+ }
43
93
 
44
- _defineProperty(this, "onCategorySelected", categoryId => {
45
- const {
46
- emojiProvider
47
- } = this.props;
48
-
49
- if (!categoryId) {
50
- return;
51
- }
52
-
53
- emojiProvider.findInCategory(categoryId).then(emojisInCategory => {
54
- const {
55
- disableCategories
56
- } = this.state;
57
-
58
- if (!disableCategories) {
59
- let selectedEmoji;
60
-
61
- if (emojisInCategory && emojisInCategory.length > 0) {
62
- selectedEmoji = getEmojiVariation(emojisInCategory[0], {
63
- skinTone: this.state.selectedTone
64
- });
65
- }
66
-
67
- const emojiPickerList = this.refs.emojiPickerList;
68
-
69
- if (emojiPickerList) {
70
- emojiPickerList.reveal(categoryId);
71
- }
94
+ return emojiProvider.calculateDynamicCategories();
95
+ }, [emojiProvider]);
96
+ /**
97
+ * Calculate and set the new state of the component in response to the list of emoji changing for some reason (a search has returned
98
+ * or the frequently used emoji have updated.)
99
+ */
72
100
 
73
- this.setState({
74
- activeCategory: categoryId,
75
- selectedEmoji
76
- });
77
- this.fireAnalytics(categoryClickedEvent({
78
- category: categoryId
79
- }));
80
- }
101
+ const setStateAfterEmojiChange = useCallback(({
102
+ searchQuery,
103
+ emojiToRender,
104
+ searchEmoji,
105
+ frequentEmoji
106
+ }) => {
107
+ // Only enable categories for full emoji list (non-search)
108
+ const disableCategories = !!searchQuery;
109
+
110
+ if (!disableCategories && emojiToRender && emojiToRender.length !== filteredEmojis.length) {
111
+ getDynamicCategories().then(categories => {
112
+ onDynamicCategoryChange(categories);
81
113
  });
82
- });
83
-
84
- _defineProperty(this, "onFileChooserClicked", () => {
85
- this.fireAnalytics(selectedFileEvent());
86
- });
87
-
88
- _defineProperty(this, "fireAnalytics", analyticsEvent => {
89
- const {
90
- createAnalyticsEvent
91
- } = this.props;
92
-
93
- if (createAnalyticsEvent) {
94
- createAndFireEventInElementsChannel(analyticsEvent)(createAnalyticsEvent);
95
- }
96
- });
114
+ }
97
115
 
98
- _defineProperty(this, "calculateElapsedTime", () => {
99
- return Date.now() - this.openTime;
100
- });
116
+ if (emojiToRender && !containsEmojiId(emojiToRender, selectedEmoji)) {
117
+ batchedUpdates(() => {
118
+ setSelectedEmoji(undefined); // Only enable categories for full emoji list (non-search)
101
119
 
102
- _defineProperty(this, "onUploadSupported", supported => {
103
- this.setState({
104
- uploadSupported: supported
120
+ setActiveCategory(null);
105
121
  });
106
- });
107
-
108
- _defineProperty(this, "onSearch", query => {
109
- const options = {
110
- skinTone: this.state.selectedTone
111
- };
122
+ }
112
123
 
113
- if (query !== this.state.query) {
114
- ufoExperiences['emoji-searched'].start();
115
- ufoExperiences['emoji-searched'].addMetadata({
116
- queryLength: query.length,
117
- source: 'picker'
118
- });
124
+ batchedUpdates(() => {
125
+ if (emojiToRender) {
126
+ setFilteredEmojis(emojiToRender);
119
127
  }
120
128
 
121
- this.updateEmojis(query, options);
122
- });
123
-
124
- _defineProperty(this, "onSearchResult", searchResults => {
125
- const frequentlyUsedEmoji = this.state.frequentlyUsedEmojis || [];
126
- const searchQuery = searchResults.query || '';
127
- const emojiToRender = this.buildQuerySpecificEmojiList(searchQuery, searchResults.emojis, frequentlyUsedEmoji);
128
-
129
- if (searchQuery !== this.state.query) {
130
- this.fireAnalytics(pickerSearchedEvent({
131
- queryLength: searchQuery.length,
132
- numMatches: emojiToRender.length
133
- }));
134
- ufoExperiences['emoji-searched'].success({
135
- metadata: {
136
- emojisLength: emojiToRender.length
137
- }
138
- });
129
+ if (searchEmoji) {
130
+ setSearchEmojis(searchEmoji);
139
131
  }
140
132
 
141
- this.setStateAfterEmojiChange(searchQuery, emojiToRender, searchResults.emojis, frequentlyUsedEmoji);
142
- });
143
-
144
- _defineProperty(this, "onFrequentEmojiResult", frequentEmoji => {
145
- const {
146
- query,
147
- searchEmojis
148
- } = this.state; // change the category of each of the featured emoji
149
-
150
- const recategorised = frequentEmoji.map(emoji => {
151
- const clone = JSON.parse(JSON.stringify(emoji));
152
- clone.category = frequentCategory;
153
- return clone;
154
- });
155
- const emojiToRender = this.buildQuerySpecificEmojiList(query, searchEmojis, recategorised);
156
- this.setStateAfterEmojiChange(query, emojiToRender, searchEmojis, recategorised);
157
- });
133
+ if (frequentEmoji) {
134
+ setFrequentlyUsedEmojis(frequentEmoji);
135
+ }
158
136
 
159
- _defineProperty(this, "onDynamicCategoryChange", categories => {
160
- this.setState({
161
- dynamicCategories: categories
162
- });
137
+ setLoading(false);
138
+ setDisableCategories(disableCategories);
163
139
  });
164
-
165
- _defineProperty(this, "onProviderChange", {
166
- result: this.onSearchResult
140
+ }, [filteredEmojis.length, getDynamicCategories, onDynamicCategoryChange, selectedEmoji]);
141
+ const onFrequentEmojiResult = useCallback(frequentEmoji => {
142
+ // change the category of each of the featured emoji
143
+ const recategorised = frequentEmoji.map(emoji => {
144
+ const clone = JSON.parse(JSON.stringify(emoji));
145
+ clone.category = frequentCategory;
146
+ return clone;
167
147
  });
168
-
169
- _defineProperty(this, "onToneSelected", toneValue => {
170
- this.setState({
171
- selectedTone: toneValue
172
- });
173
- this.props.emojiProvider.setSelectedTone(toneValue);
174
- const {
175
- query = ''
176
- } = this.state;
177
- this.updateEmojis(query, {
178
- skinTone: toneValue
179
- });
148
+ setStateAfterEmojiChange({
149
+ frequentEmoji: recategorised
180
150
  });
151
+ }, [setStateAfterEmojiChange]);
152
+ const onSearchResult = useCallback(searchResults => {
153
+ const frequentlyUsedEmoji = frequentlyUsedEmojis || [];
154
+ const searchQuery = searchResults.query || '';
155
+ /**
156
+ * If there is no user search in the EmojiPicker then it should display all emoji received from the EmojiRepository and should
157
+ * also include a special category of most frequently used emoji (if there are any). This method decides if we are in this 'no search'
158
+ * state and appends the frequent emoji if necessary.
159
+ */
160
+
161
+ let emojiToRender;
162
+
163
+ if (!frequentlyUsedEmoji.length || query) {
164
+ emojiToRender = searchResults.emojis;
165
+ } else {
166
+ emojiToRender = [...searchResults.emojis, ...frequentlyUsedEmoji];
167
+ }
181
168
 
182
- _defineProperty(this, "onToneSelectorCancelled", () => {
183
- this.fireAnalytics(toneSelectorClosedEvent());
169
+ setStateAfterEmojiChange({
170
+ searchQuery,
171
+ emojiToRender,
172
+ searchEmoji: searchResults.emojis
184
173
  });
174
+ }, [frequentlyUsedEmojis, query, setStateAfterEmojiChange]);
175
+ const onProviderChange = useMemo(() => {
176
+ return {
177
+ result: onSearchResult
178
+ };
179
+ }, [onSearchResult]);
180
+ /**
181
+ * Updates the emoji displayed by the picker. If there is no query specified then we expect to retrieve all emoji for display,
182
+ * by category, in the picker. This differs from when there is a query in which case we expect to receive a sorted result matching
183
+ * the search.
184
+ */
185
185
 
186
- _defineProperty(this, "updateEmojis", (query, options) => {
187
- // if the query is empty then we want the emoji to be in service defined order, unless specified otherwise
188
- // and we want emoji for the 'frequently used' category to be refreshed as well.
189
- if (!query) {
190
- if (!options) {
191
- options = {};
192
- }
193
-
194
- if (!options.sort) {
195
- options.sort = SearchSort.None;
196
- } // take a copy of search options so that the frequently used can be limited to 16 without affecting the full emoji query
197
-
198
-
199
- const frequentOptions = { ...options,
200
- sort: SearchSort.None,
201
- limit: FREQUENTLY_USED_MAX
202
- };
203
- this.props.emojiProvider.getFrequentlyUsed(frequentOptions).then(this.onFrequentEmojiResult);
186
+ const updateEmojis = useCallback((query, options) => {
187
+ // if the query is empty then we want the emoji to be in service defined order, unless specified otherwise
188
+ // and we want emoji for the 'frequently used' category to be refreshed as well.
189
+ if (!query) {
190
+ if (!options) {
191
+ options = {};
204
192
  }
205
193
 
206
- this.props.emojiProvider.filter(query, options);
207
- });
194
+ if (!options.sort) {
195
+ options.sort = SearchSort.None;
196
+ } // take a copy of search options so that the frequently used can be limited to 16 without affecting the full emoji query
208
197
 
209
- _defineProperty(this, "onOpenUpload", () => {
210
- // Prime upload token so it's ready when the user adds
211
- const {
212
- emojiProvider
213
- } = this.props;
214
198
 
215
- if (supportsUploadFeature(emojiProvider)) {
216
- emojiProvider.prepareForUpload();
217
- }
199
+ const frequentOptions = { ...options,
200
+ sort: SearchSort.None,
201
+ limit: FREQUENTLY_USED_MAX
202
+ };
203
+ emojiProvider.getFrequentlyUsed(frequentOptions).then(onFrequentEmojiResult);
204
+ }
218
205
 
219
- this.setState({
220
- uploadErrorMessage: undefined,
221
- uploading: true
222
- });
223
- this.fireAnalytics(uploadBeginButton());
206
+ emojiProvider.filter(query, options);
207
+ }, [emojiProvider, onFrequentEmojiResult]);
208
+ const onToneSelected = useCallback(toneValue => {
209
+ emojiProvider.setSelectedTone(toneValue);
210
+ updateEmojis(query, {
211
+ skinTone: toneValue
224
212
  });
225
-
226
- _defineProperty(this, "onUploadEmoji", (upload, retry) => {
227
- const {
228
- emojiProvider
229
- } = this.props;
230
- this.fireAnalytics(uploadConfirmButton({
231
- retry
213
+ setSelectedTone(toneValue);
214
+ }, [emojiProvider, query, updateEmojis]);
215
+ const onToneSelectorCancelled = useCallback(() => {
216
+ fireAnalytics(toneSelectorClosedEvent());
217
+ }, [fireAnalytics]);
218
+ const onSelectWrapper = useCallback((emojiId, emoji, event) => {
219
+ if (onSelection) {
220
+ onSelection(emojiId, emoji, event);
221
+ fireAnalytics(pickerClickedEvent({
222
+ duration: calculateElapsedTime(),
223
+ emojiId: (emojiId === null || emojiId === void 0 ? void 0 : emojiId.id) || '',
224
+ category: emoji && emoji.category || '',
225
+ type: emoji && emoji.type || '',
226
+ queryLength: query && query.length || 0
232
227
  }));
228
+ }
229
+ }, [fireAnalytics, onSelection, query]);
230
+ const onCategorySelected = useCallback(categoryId => {
231
+ if (!categoryId) {
232
+ return;
233
+ }
233
234
 
234
- const errorSetter = message => {
235
- this.setState({
236
- uploadErrorMessage: message
237
- });
238
- };
235
+ emojiProvider.findInCategory(categoryId).then(emojisInCategory => {
236
+ if (!disableCategories) {
237
+ let newSelectedEmoji;
239
238
 
240
- const onSuccess = emojiDescription => {
241
- this.setState({
242
- activeCategory: customCategory,
243
- selectedEmoji: emojiDescription,
244
- uploading: false
245
- }); // this.loadEmoji(emojiProvider, emojiDescription);
239
+ if (emojisInCategory && emojisInCategory.length > 0) {
240
+ newSelectedEmoji = getEmojiVariation(emojisInCategory[0], {
241
+ skinTone: selectedTone
242
+ });
243
+ }
246
244
 
247
- this.scrollToEndOfList();
248
- };
245
+ if (emojiPickerList.current) {
246
+ emojiPickerList.current.reveal(categoryId);
247
+ }
249
248
 
250
- uploadEmoji(upload, emojiProvider, errorSetter, onSuccess, this.fireAnalytics, retry);
249
+ batchedUpdates(() => {
250
+ setActiveCategory(categoryId);
251
+ setSelectedEmoji(newSelectedEmoji);
252
+ });
253
+ fireAnalytics(categoryClickedEvent({
254
+ category: categoryId
255
+ }));
256
+ }
251
257
  });
258
+ }, [disableCategories, emojiPickerList, emojiProvider, fireAnalytics, selectedTone]);
259
+ const recordUsageOnSelection = useMemo(() => createRecordSelectionDefault(emojiProvider, onSelectWrapper, analytic => fireAnalytics(analytic('picker'))), [emojiProvider, fireAnalytics, onSelectWrapper]);
260
+ const formattedErrorMessage = useMemo(() => uploadErrorMessage ? jsx(FormattedMessage, uploadErrorMessage) : null, [uploadErrorMessage]);
261
+ const emojiContextValue = useMemo(() => ({
262
+ emoji: {
263
+ emojiProvider
264
+ }
265
+ }), [emojiProvider]);
266
+ const onFileChooserClicked = useCallback(() => {
267
+ fireAnalytics(selectedFileEvent());
268
+ }, [fireAnalytics]);
269
+ const onSearch = useCallback(searchQuery => {
270
+ const options = {
271
+ skinTone: selectedTone
272
+ };
252
273
 
253
- _defineProperty(this, "onTriggerDelete", (_emojiId, emoji) => {
254
- this.fireAnalytics(deleteBeginEvent({
255
- emojiId: _emojiId.id
256
- }));
257
- this.setState({
258
- emojiToDelete: emoji
274
+ if (query) {
275
+ ufoExperiences['emoji-searched'].start();
276
+ ufoExperiences['emoji-searched'].addMetadata({
277
+ queryLength: query.length,
278
+ source: 'picker'
259
279
  });
260
- });
280
+ }
261
281
 
262
- _defineProperty(this, "onCloseDelete", () => {
263
- const {
264
- emojiToDelete
265
- } = this.state;
266
- this.fireAnalytics(deleteCancelEvent({
267
- emojiId: emojiToDelete && emojiToDelete.id
268
- }));
269
- this.setState({
270
- emojiToDelete: undefined
271
- });
272
- });
282
+ if (searchQuery !== query) {
283
+ setQuery(searchQuery);
284
+ }
273
285
 
274
- _defineProperty(this, "onDeleteEmoji", emoji => {
275
- const {
276
- emojiToDelete,
277
- query,
278
- selectedTone
279
- } = this.state;
280
- this.fireAnalytics(deleteConfirmEvent({
281
- emojiId: emojiToDelete && emojiToDelete.id
282
- }));
283
- return this.props.emojiProvider.deleteSiteEmoji(emoji).then(success => {
284
- if (success) {
285
- this.updateEmojis(query, {
286
- skinTone: selectedTone
287
- });
288
- }
286
+ updateEmojis(query, options);
287
+ }, [query, selectedTone, updateEmojis]);
288
+ const onOpenUpload = useCallback(() => {
289
+ // Prime upload token so it's ready when the user adds
290
+ if (supportsUploadFeature(emojiProvider)) {
291
+ emojiProvider.prepareForUpload();
292
+ }
289
293
 
290
- return success;
291
- });
294
+ batchedUpdates(() => {
295
+ setUploadErrorMessage(undefined);
296
+ setUploading(true);
292
297
  });
298
+ fireAnalytics(uploadBeginButton());
299
+ }, [emojiProvider, fireAnalytics]);
300
+ const scrollToEndOfList = useCallback(() => {
301
+ if (typeof window === 'undefined') {
302
+ return;
303
+ }
293
304
 
294
- _defineProperty(this, "scrollToEndOfList", () => {
295
- const emojiPickerList = this.refs.emojiPickerList;
305
+ if (emojiPickerList.current) {
306
+ // Wait a tick to ensure repaint and updated height for picker list
307
+ window.setTimeout(() => {
308
+ var _emojiPickerList$curr;
296
309
 
297
- if (typeof window === 'undefined') {
298
- return;
299
- }
310
+ (_emojiPickerList$curr = emojiPickerList.current) === null || _emojiPickerList$curr === void 0 ? void 0 : _emojiPickerList$curr.scrollToBottom();
311
+ }, 0);
312
+ }
313
+ }, [emojiPickerList]);
314
+ const onUploadEmoji = useCallback((upload, retry) => {
315
+ fireAnalytics(uploadConfirmButton({
316
+ retry
317
+ }));
300
318
 
301
- if (emojiPickerList) {
302
- // Wait a tick to ensure repaint and updated height for picker list
303
- window.setTimeout(() => {
304
- emojiPickerList.scrollToBottom();
305
- }, 0);
306
- }
307
- });
319
+ const errorSetter = message => {
320
+ setUploadErrorMessage(message);
321
+ };
308
322
 
309
- _defineProperty(this, "onUploadCancelled", () => {
310
- this.setState({
311
- uploading: false,
312
- uploadErrorMessage: undefined
323
+ const onSuccess = emojiDescription => {
324
+ batchedUpdates(() => {
325
+ setActiveCategory(customCategory);
326
+ setSelectedEmoji(emojiDescription);
327
+ setUploading(false);
313
328
  });
314
- this.fireAnalytics(uploadCancelButton());
315
- });
329
+ scrollToEndOfList();
330
+ };
316
331
 
317
- _defineProperty(this, "handlePickerRef", ref => {
318
- if (this.props.onPickerRef) {
319
- this.props.onPickerRef(ref);
332
+ uploadEmoji(upload, emojiProvider, errorSetter, onSuccess, fireAnalytics, retry);
333
+ }, [emojiProvider, fireAnalytics, scrollToEndOfList]);
334
+ const onTriggerDelete = useCallback((_emojiId, emoji) => {
335
+ if (_emojiId) {
336
+ fireAnalytics(deleteBeginEvent({
337
+ emojiId: _emojiId.id
338
+ }));
339
+ setEmojiToDelete(emoji);
340
+ }
341
+ }, [fireAnalytics]);
342
+ const onCloseDelete = useCallback(() => {
343
+ fireAnalytics(deleteCancelEvent({
344
+ emojiId: emojiToDelete && emojiToDelete.id
345
+ }));
346
+ setEmojiToDelete(undefined);
347
+ }, [emojiToDelete, fireAnalytics]);
348
+ const onDeleteEmoji = useCallback(emoji => {
349
+ fireAnalytics(deleteConfirmEvent({
350
+ emojiId: emojiToDelete && emojiToDelete.id
351
+ }));
352
+ return emojiProvider.deleteSiteEmoji(emoji).then(success => {
353
+ if (success) {
354
+ updateEmojis(query, {
355
+ skinTone: selectedTone
356
+ });
320
357
  }
321
- });
322
358
 
323
- _defineProperty(this, "onSelectWrapper", (emojiId, emoji, event) => {
324
- const {
325
- onSelection
326
- } = this.props;
327
- const {
328
- query
329
- } = this.state;
330
-
331
- if (onSelection) {
332
- onSelection(emojiId, emoji, event);
333
- this.fireAnalytics(pickerClickedEvent({
334
- duration: this.calculateElapsedTime(),
335
- emojiId: emojiId.id || '',
336
- category: emoji && emoji.category || '',
337
- type: emoji && emoji.type || '',
338
- queryLength: query && query.length || 0
339
- }));
340
- }
359
+ return success;
341
360
  });
342
-
343
- const {
344
- emojiProvider: _emojiProvider,
345
- hideToneSelector
346
- } = props;
347
- this.state = {
348
- filteredEmojis: [],
349
- searchEmojis: [],
350
- frequentlyUsedEmojis: [],
351
- query: '',
352
- dynamicCategories: [],
353
- selectedTone: !hideToneSelector ? _emojiProvider.getSelectedTone() : undefined,
354
- loading: true,
355
- uploadSupported: false,
356
- uploading: false
357
- };
358
- this.openTime = 0;
359
- }
360
-
361
- UNSAFE_componentWillMount() {
362
- ufoExperiences['emoji-picker-opened'].success();
363
- this.openTime = Date.now();
364
- this.fireAnalytics(openedPickerEvent());
365
- }
366
-
367
- componentDidMount() {
368
- const {
369
- emojiProvider,
370
- hideToneSelector
371
- } = this.props;
372
- emojiProvider.subscribe(this.onProviderChange);
373
- this.onSearch(this.state.query);
361
+ }, [emojiProvider, emojiToDelete, fireAnalytics, query, selectedTone, updateEmojis]);
362
+ const onComponentDidMount = useCallback(() => {
363
+ emojiProvider.subscribe(onProviderChange);
364
+ onSearch(query);
374
365
 
375
366
  if (supportsUploadFeature(emojiProvider)) {
376
- emojiProvider.isUploadSupported().then(this.onUploadSupported);
367
+ emojiProvider.isUploadSupported().then(onUploadSupported);
377
368
  }
378
369
 
379
370
  if (!hideToneSelector) {
380
371
  const toneEmoji = getToneEmoji(emojiProvider);
381
372
 
382
373
  if (isPromise(toneEmoji)) {
383
- toneEmoji.then(emoji => this.setState({
384
- toneEmoji: emoji
385
- }));
374
+ toneEmoji.then(emoji => setToneEmoji(emoji));
386
375
  } else if (toneEmoji === undefined || isEmojiDescription(toneEmoji)) {
387
- this.setState({
388
- toneEmoji
389
- });
376
+ setToneEmoji(toneEmoji);
390
377
  }
391
378
  }
392
- }
379
+ }, [emojiProvider, hideToneSelector, onProviderChange, onSearch, onUploadSupported, query]);
393
380
 
394
- componentWillUnmount() {
395
- const {
396
- emojiProvider
397
- } = this.props;
398
- emojiProvider.unsubscribe(this.onProviderChange);
399
- this.fireAnalytics(closedPickerEvent({
400
- duration: this.calculateElapsedTime()
401
- }));
402
- ufoExperiences['emoji-picker-opened'].abort({
403
- metadata: {
404
- source: 'EmojiPickerComponent',
405
- reason: 'unmount'
406
- }
407
- });
408
- ufoExperiences['emoji-searched'].abort({
409
- metadata: {
410
- source: 'EmojiPickerComponent',
411
- reason: 'unmount'
412
- }
413
- });
414
- }
415
-
416
- UNSAFE_componentWillReceiveProps(nextProps) {
417
- const prevEmojiProvider = this.props.emojiProvider;
418
- const nextEmojiProvider = nextProps.emojiProvider;
419
-
420
- if (prevEmojiProvider !== nextEmojiProvider) {
421
- if (supportsUploadFeature(nextEmojiProvider)) {
422
- nextEmojiProvider.isUploadSupported().then(this.onUploadSupported);
423
- }
424
- }
425
- }
426
-
427
- componentDidUpdate(prevProps) {
428
- const prevEmojiProvider = prevProps.emojiProvider;
429
- const currentEmojiProvider = this.props.emojiProvider;
430
-
431
- if (prevEmojiProvider !== currentEmojiProvider) {
432
- prevEmojiProvider.unsubscribe(this.onProviderChange);
433
- currentEmojiProvider.subscribe(this.onProviderChange); // We changed provider which means we subscribed to filter results for a new subscriber.
434
- // So we refresh the emoji display with onSearch and we do it here, after the new props have
435
- // been set since onSearch leads to filter being called on the current emojiProvider.
436
- // (Calling onSearch in a '...Will...' lifecycle method would lead to filter being called on
437
- // an emojiProvider we have already unsubscribed from)
438
-
439
- this.onSearch(this.state.query);
440
- }
381
+ if (isMounting.current) {
382
+ // componentWillMount equivalent
383
+ ufoExperiences['emoji-picker-opened'].success();
384
+ openTime.current = Date.now();
385
+ fireAnalytics(openedPickerEvent());
386
+ isMounting.current = false;
441
387
  }
442
388
 
443
- /**
444
- * If there is no user search in the EmojiPicker then it should display all emoji received from the EmojiRepository and should
445
- * also include a special category of most frequently used emoji (if there are any). This method decides if we are in this 'no search'
446
- * state and appends the frequent emoji if necessary.
447
- *
448
- * @param searchEmoji the emoji last received from the EmojiRepository after a search (may be empty)
449
- * @param frequentEmoji the frequently used emoji last received from the EmojiRepository (may be empty)
450
- */
451
- buildQuerySpecificEmojiList(query, searchEmoji, frequentEmoji) {
452
- // If there are no frequent emoji, or if there was a search query then we want to take the search result exactly as is.
453
- if (!frequentEmoji.length || query) {
454
- return searchEmoji;
389
+ useEffect(() => {
390
+ // componentDidMount logic
391
+ if (didMount && updateAfterDidMount.current) {
392
+ onComponentDidMount();
393
+ updateAfterDidMount.current = false;
455
394
  }
395
+ }, [didMount, onComponentDidMount]);
396
+ useEffect(() => {
397
+ previousEmojiProvider.current.unsubscribe(onProviderChange);
398
+ previousEmojiProvider.current = emojiProvider;
399
+ emojiProvider.subscribe(onProviderChange);
456
400
 
457
- return [...searchEmoji, ...frequentEmoji];
458
- }
459
- /**
460
- * Calculate and set the new state of the component in response to the list of emoji changing for some reason (a search has returned
461
- * or the frequently used emoji have updated.)
462
- */
463
-
464
-
465
- setStateAfterEmojiChange(query, emojiToRender, searchEmoji, frequentEmoji) {
466
- const {
467
- filteredEmojis
468
- } = this.state; // Only enable categories for full emoji list (non-search)
469
-
470
- const disableCategories = !!query;
471
-
472
- if (!disableCategories && emojiToRender.length !== filteredEmojis.length) {
473
- this.getDynamicCategories().then(categories => {
474
- this.onDynamicCategoryChange(categories);
475
- });
476
- }
477
-
478
- let selectedEmoji;
479
- let activeCategory;
480
-
481
- if (containsEmojiId(emojiToRender, this.state.selectedEmoji)) {
482
- // Keep existing emoji selected if still in results
483
- selectedEmoji = this.state.selectedEmoji;
484
- activeCategory = this.state.activeCategory;
485
- } else {
486
- selectedEmoji = undefined; // Only enable categories for full emoji list (non-search)
487
-
488
- activeCategory = undefined;
401
+ if (supportsUploadFeature(emojiProvider)) {
402
+ emojiProvider.isUploadSupported().then(onUploadSupported);
489
403
  }
490
404
 
491
- this.setState({
492
- filteredEmojis: emojiToRender,
493
- searchEmojis: searchEmoji,
494
- frequentlyUsedEmojis: frequentEmoji,
495
- selectedEmoji,
496
- activeCategory,
497
- disableCategories,
498
- query,
499
- loading: false
405
+ return () => {
406
+ emojiProvider.unsubscribe(onProviderChange);
407
+ };
408
+ }, [emojiProvider, onProviderChange, onUploadSupported]);
409
+ useEffect(() => {
410
+ // We changed provider which means we subscribed to filter results for a new subscriber.
411
+ // So we refresh the emoji display with onSearch and we do it here, after the new props have
412
+ // been set since onSearch leads to filter being called on the current emojiProvider.
413
+ // (Calling onSearch in a '...Will...' lifecycle method would lead to filter being called on
414
+ // an emojiProvider we have already unsubscribed from)
415
+ onSearch(query);
416
+ }, [emojiProvider, onSearch, query]);
417
+ useEffect(() => {
418
+ // Fire analytics event whenever query changes
419
+ fireAnalytics(pickerSearchedEvent({
420
+ queryLength: query.length,
421
+ numMatches: filteredEmojis.length
422
+ }));
423
+ ufoExperiences['emoji-searched'].success({
424
+ metadata: {
425
+ emojisLength: filteredEmojis.length
426
+ }
500
427
  });
501
- }
502
-
503
- getDynamicCategories() {
504
- if (!this.props.emojiProvider.calculateDynamicCategories) {
505
- return Promise.resolve([]);
428
+ }, [filteredEmojis.length, fireAnalytics, query]);
429
+ useEffect(() => {
430
+ if (!frequentlyUsedEmojis.length || query) {
431
+ setFilteredEmojis(searchEmojis);
432
+ } else {
433
+ setFilteredEmojis([...searchEmojis, ...frequentlyUsedEmojis]);
506
434
  }
507
-
508
- return this.props.emojiProvider.calculateDynamicCategories();
509
- }
510
-
511
- render() {
512
- const {
513
- emojiProvider
514
- } = this.props;
515
- const {
516
- activeCategory,
517
- disableCategories,
518
- dynamicCategories,
519
- filteredEmojis,
520
- loading,
521
- query,
522
- selectedEmoji,
523
- selectedTone,
524
- toneEmoji,
525
- emojiToDelete,
526
- uploading,
527
- uploadErrorMessage,
528
- uploadSupported
529
- } = this.state;
530
- const recordUsageOnSelection = createRecordSelectionDefault(emojiProvider, this.onSelectWrapper, analytic => this.fireAnalytics(analytic('picker')));
531
- const formattedErrorMessage = uploadErrorMessage ? jsx(FormattedMessage, uploadErrorMessage) : null;
532
- const emojiContextValue = {
533
- emoji: {
534
- emojiProvider: this.props.emojiProvider
535
- }
435
+ }, [frequentlyUsedEmojis, query, searchEmojis]);
436
+ useEffect(() => {
437
+ // Fire analytics on component unmount
438
+ return () => {
439
+ fireAnalytics(closedPickerEvent({
440
+ duration: calculateElapsedTime()
441
+ }));
442
+ ufoExperiences['emoji-picker-opened'].abort({
443
+ metadata: {
444
+ source: 'EmojiPickerComponent',
445
+ reason: 'unmount'
446
+ }
447
+ });
448
+ ufoExperiences['emoji-searched'].abort({
449
+ metadata: {
450
+ source: 'EmojiPickerComponent',
451
+ reason: 'unmount'
452
+ }
453
+ });
536
454
  };
537
- const picker = jsx(LegacyEmojiContextProvider, {
538
- emojiContextValue: emojiContextValue
539
- }, jsx("div", {
540
- css: emojiPicker,
541
- ref: this.handlePickerRef,
542
- "data-emoji-picker-container": true
543
- }, jsx(CategorySelector, {
544
- activeCategoryId: activeCategory,
545
- dynamicCategories: dynamicCategories,
546
- disableCategories: disableCategories,
547
- onCategorySelected: this.onCategorySelected
548
- }), jsx(EmojiPickerList, {
549
- emojis: filteredEmojis,
550
- currentUser: emojiProvider.getCurrentUser(),
551
- onEmojiSelected: recordUsageOnSelection,
552
- onEmojiActive: this.onEmojiActive,
553
- onEmojiDelete: this.onTriggerDelete,
554
- onCategoryActivated: this.onCategoryActivated,
555
- onSearch: this.onSearch,
556
- query: query,
557
- selectedTone: selectedTone,
558
- loading: loading,
559
- ref: "emojiPickerList",
560
- initialUploadName: query,
561
- onToneSelected: this.onToneSelected,
562
- onToneSelectorCancelled: this.onToneSelectorCancelled,
563
- toneEmoji: toneEmoji,
564
- uploading: uploading,
565
- emojiToDelete: emojiToDelete,
566
- uploadErrorMessage: formattedErrorMessage,
567
- uploadEnabled: uploadSupported && !uploading,
568
- onUploadEmoji: this.onUploadEmoji,
569
- onUploadCancelled: this.onUploadCancelled,
570
- onDeleteEmoji: this.onDeleteEmoji,
571
- onCloseDelete: this.onCloseDelete,
572
- onFileChooserClicked: this.onFileChooserClicked,
573
- onOpenUpload: this.onOpenUpload
574
- }), jsx(EmojiPickerFooter, {
575
- selectedEmoji: selectedEmoji,
576
- isUploading: uploading
577
- })));
578
- return picker;
579
- }
580
-
581
- }
582
-
583
- _defineProperty(EmojiPickerComponent, "defaultProps", {
584
- onSelection: () => {}
585
- });
455
+ }, [fireAnalytics]);
456
+ useEffect(() => {
457
+ // Unsubscribe emojiProvider on component unmount
458
+ return () => {
459
+ emojiProvider.unsubscribe(onProviderChange);
460
+ };
461
+ }, [emojiProvider, onProviderChange]);
462
+ const showPreview = selectedEmoji && !uploading;
463
+ return jsx(LegacyEmojiContextProvider, {
464
+ emojiContextValue: emojiContextValue
465
+ }, jsx("div", {
466
+ css: emojiPicker(showPreview),
467
+ ref: onPickerRef,
468
+ "data-emoji-picker-container": true
469
+ }, jsx(CategorySelector, {
470
+ activeCategoryId: activeCategory,
471
+ dynamicCategories: dynamicCategories,
472
+ disableCategories: disableCategories,
473
+ onCategorySelected: onCategorySelected
474
+ }), jsx(EmojiPickerList, {
475
+ emojis: filteredEmojis,
476
+ currentUser: currentUser,
477
+ onEmojiSelected: recordUsageOnSelection,
478
+ onEmojiActive: onEmojiActive,
479
+ onEmojiDelete: onTriggerDelete,
480
+ onCategoryActivated: onCategoryActivated,
481
+ onSearch: onSearch,
482
+ query: query,
483
+ selectedTone: selectedTone,
484
+ loading: loading,
485
+ ref: emojiPickerList,
486
+ initialUploadName: query,
487
+ onToneSelected: onToneSelected,
488
+ onToneSelectorCancelled: onToneSelectorCancelled,
489
+ toneEmoji: toneEmoji,
490
+ uploading: uploading,
491
+ emojiToDelete: emojiToDelete,
492
+ uploadErrorMessage: formattedErrorMessage,
493
+ uploadEnabled: uploadSupported && !uploading,
494
+ onUploadEmoji: onUploadEmoji,
495
+ onUploadCancelled: onUploadCancelled,
496
+ onDeleteEmoji: onDeleteEmoji,
497
+ onCloseDelete: onCloseDelete,
498
+ onFileChooserClicked: onFileChooserClicked,
499
+ onOpenUpload: onOpenUpload
500
+ }), showPreview && jsx(EmojiPickerFooter, {
501
+ selectedEmoji: selectedEmoji
502
+ })));
503
+ };
504
+
505
+ export default /*#__PURE__*/memo(EmojiPickerComponent);