diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js
deleted file mode 100644
index 5393f0dc8..000000000
--- a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import Overlay from 'react-overlays/lib/Overlay';
-
-import Icon from 'soapbox/components/icon';
-
-import IconPickerMenu from './icon-picker-menu';
-
-const messages = defineMessages({
- emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
-});
-
-class IconPickerDropdown extends React.PureComponent {
-
- static propTypes = {
- frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
- intl: PropTypes.object.isRequired,
- onPickEmoji: PropTypes.func.isRequired,
- value: PropTypes.string,
- };
-
- state = {
- active: false,
- loading: false,
- };
-
- setRef = (c) => {
- this.dropdown = c;
- }
-
- onShowDropdown = ({ target }) => {
- this.setState({ active: true });
-
- const { top } = target.getBoundingClientRect();
- this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
- }
-
- onHideDropdown = () => {
- this.setState({ active: false });
- }
-
- onToggle = (e) => {
- if (!this.state.loading && (!e.key || e.key === 'Enter')) {
- if (this.state.active) {
- this.onHideDropdown();
- } else {
- this.onShowDropdown(e);
- }
- }
- }
-
- handleKeyDown = e => {
- if (e.key === 'Escape') {
- this.onHideDropdown();
- }
- }
-
- setTargetRef = c => {
- this.target = c;
- }
-
- findTarget = () => {
- return this.target;
- }
-
- render() {
- const { intl, onPickEmoji, value } = this.props;
- const title = intl.formatMessage(messages.emoji);
- const { active, loading, placement } = this.state;
- const forkAwesomeIcons = require('../forkawesome.json');
-
- return (
-
- );
- }
-
-}
-
-export default injectIntl(IconPickerDropdown);
\ No newline at end of file
diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx
new file mode 100644
index 000000000..d23e9f004
--- /dev/null
+++ b/app/soapbox/features/soapbox-config/components/icon-picker-dropdown.tsx
@@ -0,0 +1,85 @@
+import React, { useRef, useState } from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+// @ts-ignore
+import Overlay from 'react-overlays/lib/Overlay';
+
+import Icon from 'soapbox/components/icon';
+
+import IconPickerMenu from './icon-picker-menu';
+
+const messages = defineMessages({
+ emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
+});
+
+interface IIconPickerDropdown {
+ value: string,
+ onPickEmoji: React.ChangeEventHandler,
+}
+
+const IconPickerDropdown: React.FC = ({ value, onPickEmoji }) => {
+ const intl = useIntl();
+
+ const [active, setActive] = useState(false);
+ const [placement, setPlacement] = useState<'bottom' | 'top'>();
+
+ const target = useRef(null);
+
+ const onShowDropdown: React.KeyboardEventHandler = ({ target }) => {
+ setActive(true);
+
+ const { top } = (target as any).getBoundingClientRect();
+ setPlacement(top * 2 < innerHeight ? 'bottom' : 'top');
+ };
+
+ const onHideDropdown = () => {
+ setActive(false);
+ };
+
+ const onToggle: React.KeyboardEventHandler = (e) => {
+ e.stopPropagation();
+ if (!e.key || e.key === 'Enter') {
+ if (active) {
+ onHideDropdown();
+ } else {
+ onShowDropdown(e);
+ }
+ }
+ };
+
+ const handleKeyDown: React.KeyboardEventHandler = (e) => {
+ if (e.key === 'Escape') {
+ onHideDropdown();
+ }
+ };
+
+ const title = intl.formatMessage(messages.emoji);
+ const forkAwesomeIcons = require('../forkawesome.json');
+
+ return (
+
+
}
+ onKeyDown={onToggle}
+ tabIndex={0}
+ >
+
+
+
+
+
+
+
+ );
+};
+
+export default IconPickerDropdown;
diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-menu.js b/app/soapbox/features/soapbox-config/components/icon-picker-menu.js
deleted file mode 100644
index a9b6be2f2..000000000
--- a/app/soapbox/features/soapbox-config/components/icon-picker-menu.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import classNames from 'clsx';
-import { supportsPassiveEvents } from 'detect-passive-events';
-import Picker from 'emoji-mart/dist-es/components/picker/picker';
-import PropTypes from 'prop-types';
-import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-
-const messages = defineMessages({
- emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
- emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
- emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' },
- custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
- search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
-});
-
-const backgroundImageFn = () => '';
-const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
-
-const categoriesSort = ['custom'];
-
-class IconPickerMenu extends React.PureComponent {
-
- static propTypes = {
- custom_emojis: PropTypes.object,
- loading: PropTypes.bool,
- onClose: PropTypes.func.isRequired,
- onPick: PropTypes.func.isRequired,
- style: PropTypes.object,
- placement: PropTypes.string,
- arrowOffsetLeft: PropTypes.string,
- arrowOffsetTop: PropTypes.string,
- intl: PropTypes.object.isRequired,
- };
-
- static defaultProps = {
- style: {},
- loading: true,
- };
-
- state = {
- modifierOpen: false,
- placement: null,
- };
-
- handleDocumentClick = e => {
- if (this.node && !this.node.contains(e.target)) {
- this.props.onClose();
- }
- }
-
- componentDidMount() {
- document.addEventListener('click', this.handleDocumentClick, false);
- document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
- }
-
- componentWillUnmount() {
- document.removeEventListener('click', this.handleDocumentClick, false);
- document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
- }
-
- setRef = c => {
- this.node = c;
-
- if (!c) return;
-
- // Nice and dirty hack to display the icons
- c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
- const newIcon = document.createElement('span');
- newIcon.innerHTML = ``;
- elem.parentNode.replaceChild(newIcon, elem);
- });
- }
-
- getI18n = () => {
- const { intl } = this.props;
-
- return {
- search: intl.formatMessage(messages.emoji_search),
- notfound: intl.formatMessage(messages.emoji_not_found),
- categories: {
- search: intl.formatMessage(messages.search_results),
- custom: intl.formatMessage(messages.custom),
- },
- };
- }
-
- handleClick = emoji => {
- emoji.native = emoji.colons;
-
- this.props.onClose();
- this.props.onPick(emoji);
- }
-
- buildIcons = (customEmojis, autoplay = false) => {
- const emojis = [];
-
- Object.values(customEmojis).forEach(category => {
- category.forEach(function(icon) {
- const name = icon.replace('fa fa-', '');
- if (icon !== 'email' && icon !== 'memo') {
- emojis.push({
- id: name,
- name,
- short_names: [name],
- emoticons: [],
- keywords: [name],
- imageUrl: '',
- });
- }
- });
- });
-
- return emojis;
- };
-
- render() {
- const { loading, style, intl, custom_emojis } = this.props;
-
- if (loading) {
- return ;
- }
-
- const data = { compressed: true, categories: [], aliases: [], emojis: [] };
- const title = intl.formatMessage(messages.emoji);
- const { modifierOpen } = this.state;
-
- return (
-
- );
- }
-
-}
-
-export default injectIntl(IconPickerMenu);
diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx
new file mode 100644
index 000000000..c88c1afd7
--- /dev/null
+++ b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx
@@ -0,0 +1,131 @@
+import classNames from 'clsx';
+import { supportsPassiveEvents } from 'detect-passive-events';
+// @ts-ignore
+import Picker from 'emoji-mart/dist-es/components/picker/picker';
+import React, { useCallback, useEffect, useRef } from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+
+const messages = defineMessages({
+ emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
+ emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
+ emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' },
+ custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
+ search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
+});
+
+const backgroundImageFn = () => '';
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
+
+const categoriesSort = ['custom'];
+
+interface IIconPickerMenu {
+ customEmojis: Record>,
+ onClose: () => void,
+ onPick: any,
+ style?: React.CSSProperties,
+}
+
+const IconPickerMenu: React.FC = ({ customEmojis, onClose, onPick, style }) => {
+ const intl = useIntl();
+
+ const node = useRef(null);
+
+ const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
+ if (node.current && !node.current.contains(e.target as Node)) {
+ onClose();
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('click', handleDocumentClick, false);
+ document.addEventListener('touchend', handleDocumentClick, listenerOptions);
+
+ return () => {
+ document.removeEventListener('click', handleDocumentClick, false);
+ document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any);
+
+ };
+ }, []);
+
+ const setRef = (c: HTMLDivElement) => {
+ node.current = c;
+
+ if (!c) return;
+
+ // Nice and dirty hack to display the icons
+ c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
+ const newIcon = document.createElement('span');
+ newIcon.innerHTML = ``;
+ (elem.parentNode as any).replaceChild(newIcon, elem);
+ });
+ };
+
+ const getI18n = () => {
+
+ return {
+ search: intl.formatMessage(messages.emoji_search),
+ notfound: intl.formatMessage(messages.emoji_not_found),
+ categories: {
+ search: intl.formatMessage(messages.search_results),
+ custom: intl.formatMessage(messages.custom),
+ },
+ };
+ };
+
+ const handleClick = (emoji: Record) => {
+ emoji.native = emoji.colons;
+
+ onClose();
+ onPick(emoji);
+ };
+
+ const buildIcons = () => {
+ const emojis: Record = [];
+
+ Object.values(customEmojis).forEach((category) => {
+ category.forEach((icon) => {
+ const name = icon.replace('fa fa-', '');
+ if (icon !== 'email' && icon !== 'memo') {
+ emojis.push({
+ id: name,
+ name,
+ short_names: [name],
+ emoticons: [],
+ keywords: [name],
+ imageUrl: '',
+ });
+ }
+ });
+ });
+
+ return emojis;
+ };
+
+ const data = { compressed: true, categories: [], aliases: [], emojis: [] };
+ const title = intl.formatMessage(messages.emoji);
+
+ return (
+
+ );
+};
+
+export default IconPickerMenu;