From 2bbbcd625eede562e8613c05771fa015900928fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 25 Feb 2023 23:30:24 +0100 Subject: [PATCH] WIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/events.ts | 2 +- .../announcements/reactions-bar.tsx | 3 +- .../ui/emoji-selector/emoji-selector.tsx | 13 +- .../account-gallery/components/media-item.tsx | 2 +- .../components/emoji-picker-dropdown.tsx | 112 ++++++++++++++--- .../emoji-picker-dropdown-container.jsx | 119 ------------------ .../emoji-picker-dropdown-container.tsx | 37 ++++++ app/soapbox/features/emoji/mapping.ts | 4 +- app/soapbox/features/emoji/search.ts | 6 +- .../reducers/__tests__/statuses.test.ts | 2 +- app/soapbox/reducers/custom-emojis.ts | 2 +- types/emoji-mart/index.d.ts | 102 +++++++-------- 12 files changed, 202 insertions(+), 202 deletions(-) delete mode 100644 app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.jsx create mode 100644 app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx diff --git a/app/soapbox/actions/events.ts b/app/soapbox/actions/events.ts index d4ec49491..44a9ae207 100644 --- a/app/soapbox/actions/events.ts +++ b/app/soapbox/actions/events.ts @@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro }); const fetchEventIcs = (id: string) => - (dispatch: any, getState: () => RootState) => + (dispatch: AppDispatch, getState: () => RootState) => api(getState).get(`/api/v1/pleroma/events/${id}/ics`); const cancelEventCompose = () => ({ diff --git a/app/soapbox/components/announcements/reactions-bar.tsx b/app/soapbox/components/announcements/reactions-bar.tsx index b74fba9e0..55b72c59a 100644 --- a/app/soapbox/components/announcements/reactions-bar.tsx +++ b/app/soapbox/components/announcements/reactions-bar.tsx @@ -2,7 +2,6 @@ import clsx from 'clsx'; import React from 'react'; import { TransitionMotion, spring } from 'react-motion'; -import { Icon } from 'soapbox/components/ui'; import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container'; import { useSettings } from 'soapbox/hooks'; @@ -55,7 +54,7 @@ const ReactionsBar: React.FC = ({ announcementId, reactions, addR /> ))} - {visibleReactions.size < 8 && } />} + {visibleReactions.size < 8 && } )} diff --git a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx index 36eb25e68..549031cf7 100644 --- a/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx +++ b/app/soapbox/components/ui/emoji-selector/emoji-selector.tsx @@ -6,8 +6,7 @@ import { usePopper } from 'react-popper'; import { changeSetting } from 'soapbox/actions/settings'; import { Emoji, HStack, IconButton } from 'soapbox/components/ui'; -import { messages } from 'soapbox/features/emoji/components/emoji-picker-dropdown'; -import { getFrequentlyUsedEmojis } from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container'; +import { getFrequentlyUsedEmojis, messages } from 'soapbox/features/emoji/components/emoji-picker-dropdown'; import { EmojiPicker as EmojiPickerAsync } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch, useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks'; @@ -88,10 +87,14 @@ const EmojiSelector: React.FC = ({ }; const handleClickOutside = (event: MouseEvent) => { - if ([referenceElement, popperElement, document.querySelector('em-emoji-picker')].some(el => el?.contains(event.target as Node))) { + if ([referenceElement, popperElement].some(el => el?.contains(event.target as Node))) { return; } + if (document.querySelector('em-emoji-picker')) { + return setExpanded(false); + } + if (onClose) { onClose(); } @@ -145,6 +148,10 @@ const EmojiSelector: React.FC = ({ }; }; + useEffect(() => () => { + document.body.style.overflow = ''; + }, []); + useEffect(() => { setExpanded(false); }, [visible]); diff --git a/app/soapbox/features/account-gallery/components/media-item.tsx b/app/soapbox/features/account-gallery/components/media-item.tsx index b568f31bf..ef1aa1382 100644 --- a/app/soapbox/features/account-gallery/components/media-item.tsx +++ b/app/soapbox/features/account-gallery/components/media-item.tsx @@ -103,7 +103,7 @@ const MediaItem: React.FC = ({ attachment, onOpenMedia }) => { } else if (attachment.type === 'audio') { const remoteURL = attachment.remote_url || ''; const fileExtensionLastIndex = remoteURL.lastIndexOf('.'); - const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase(); + const fileExtension = remoteURL.slice(fileExtensionLastIndex + 1).toUpperCase(); thumbnail = (
diff --git a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx b/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx index 22f44e6ce..cacac1914 100644 --- a/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx +++ b/app/soapbox/features/emoji/components/emoji-picker-dropdown.tsx @@ -1,17 +1,21 @@ import { supportsPassiveEvents } from 'detect-passive-events'; +import { Map as ImmutableMap } from 'immutable'; import React, { useEffect, useState, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { defineMessages, useIntl } from 'react-intl'; import { usePopper } from 'react-popper'; +import { createSelector } from 'reselect'; -import { useSettings } from 'soapbox/hooks'; +import { useEmoji } from 'soapbox/actions/emojis'; +import { changeSetting } from 'soapbox/actions/settings'; +import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import { isMobile } from 'soapbox/is-mobile'; +import { RootState } from 'soapbox/store'; import { buildCustomEmojis } from '../../emoji'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; import type { EmojiPick } from 'emoji-mart'; -import type { List } from 'immutable'; import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji'; let EmojiPicker: any; // load asynchronously @@ -43,17 +47,73 @@ export const messages = defineMessages({ skins_6: { id: 'emoji_button.skins_6', defaultMessage: 'Dark' }, }); -// TODO: fix types -interface IEmojiPickerDropdown { - custom_emojis: List - frequentlyUsedEmojis: string[] - intl: any - onPickEmoji: (emoji: Emoji) => void - onSkinTone: () => void - condensed: boolean - render: any +export interface IEmojiPickerDropdown { + onPickEmoji?: (emoji: Emoji) => void + condensed?: boolean + render: React.FC<{ + setPopperReference: React.Ref + title?: string + visible?: boolean + loading?: boolean + handleToggle: (e: Event) => void + }> } +const perLine = 8; +const lines = 2; + +const DEFAULTS = [ + '+1', + 'grinning', + 'kissing_heart', + 'heart_eyes', + 'laughing', + 'stuck_out_tongue_winking_eye', + 'sweat_smile', + 'joy', + 'yum', + 'disappointed', + 'thinking_face', + 'weary', + 'sob', + 'sunglasses', + 'heart', + 'ok_hand', +]; + +export const getFrequentlyUsedEmojis = createSelector([ + (state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()), +], (emojiCounters: ImmutableMap) => { + let emojis = emojiCounters + .keySeq() + .sort((a, b) => emojiCounters.get(a)! - emojiCounters.get(b)!) + .reverse() + .slice(0, perLine * lines) + .toArray(); + + if (emojis.length < DEFAULTS.length) { + const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); + emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length)); + } + + return emojis; +}); + +const getCustomEmojis = createSelector([ + (state: RootState) => state.custom_emojis, +], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { + const aShort = a.get('shortcode')!.toLowerCase(); + const bShort = b.get('shortcode')!.toLowerCase(); + + if (aShort < bShort) { + return -1; + } else if (aShort > bShort) { + return 1; + } else { + return 0; + } +})); + // Fixes render bug where popover has a delayed position update const RenderAfter = ({ children, update }: any) => { const [nextTick, setNextTick] = useState(false); @@ -75,13 +135,17 @@ const RenderAfter = ({ children, update }: any) => { const listenerOptions = supportsPassiveEvents ? { passive: true } : false; -const EmojiPickerDropdown: React.FC = ({ custom_emojis, frequentlyUsedEmojis, onPickEmoji, onSkinTone, condensed, render: Render }) => { +const EmojiPickerDropdown: React.FC = ({ onPickEmoji, condensed, render: Render }) => { const intl = useIntl(); + const dispatch = useAppDispatch(); const settings = useSettings(); const title = intl.formatMessage(messages.emoji); const userTheme = settings.get('themeMode'); const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto'; + const customEmojis = useAppSelector((state) => getCustomEmojis(state)); + const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state)); + const [popperElement, setPopperElement] = useState(null); const [popperReference, setPopperReference] = useState(null); const [containerElement, setContainerElement] = useState(null); @@ -108,24 +172,36 @@ const EmojiPickerDropdown: React.FC = ({ custom_emojis, fr const handlePick = (emoji: EmojiPick) => { setVisible(false); + let pickedEmoji: Emoji; + if (emoji.native) { - onPickEmoji({ + pickedEmoji = { id: emoji.id, colons: emoji.shortcodes, custom: false, native: emoji.native, unified: emoji.unified, - } as NativeEmoji); + } as NativeEmoji; } else { - onPickEmoji({ + pickedEmoji = { id: emoji.id, colons: emoji.shortcodes, custom: true, imageUrl: emoji.src, - } as CustomEmoji); + } as CustomEmoji; + + dispatch(useEmoji(pickedEmoji)); // eslint-disable-line react-hooks/rules-of-hooks + + if (onPickEmoji) { + onPickEmoji(pickedEmoji); + } } }; + const handleSkinTone = (skinTone: string) => { + dispatch(changeSetting(['skinTone'], skinTone)); + }; + const getI18n = () => { return { search: intl.formatMessage(messages.emoji_search), @@ -216,12 +292,12 @@ const EmojiPickerDropdown: React.FC = ({ custom_emojis, fr {!loading && ( state.settings.get('frequentlyUsedEmojis', ImmutableMap()), -], emojiCounters => { - let emojis = emojiCounters - .keySeq() - .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) - .reverse() - .slice(0, perLine * lines) - .toArray(); - - if (emojis.length < DEFAULTS.length) { - const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); - emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length)); - } - - return emojis; -}); - -const getCustomEmojis = createSelector([ - state => state.custom_emojis, -], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { - const aShort = a.get('shortcode').toLowerCase(); - const bShort = b.get('shortcode').toLowerCase(); - - if (aShort < bShort) { - return -1; - } else if (aShort > bShort) { - return 1; - } else { - return 0; - } -})); - -const mapStateToProps = state => ({ - custom_emojis: getCustomEmojis(state), - skinTone: getSettings(state).get('skinTone'), - frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), -}); - -const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ - onSkinTone: skinTone => { - dispatch(changeSetting(['skinTone'], skinTone)); - }, - - onPickEmoji: emoji => { - dispatch(useEmoji(emoji)); // eslint-disable-line react-hooks/rules-of-hooks - - if (onPickEmoji) { - onPickEmoji(emoji); - } - }, -}); - -const Container = connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); - -const EmojiPickerDropdownWrapper = (props) => { - return ( - ( - - ) - } - - {...props} - /> - ); -}; - - -export default EmojiPickerDropdownWrapper; diff --git a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx new file mode 100644 index 000000000..cb9d509ed --- /dev/null +++ b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx @@ -0,0 +1,37 @@ +import clsx from 'clsx'; +import React from 'react'; + +import { IconButton } from 'soapbox/components/ui'; + +import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown'; + +const EmojiPickerDropdownWrapper = (props: Omit) => { + return ( + ( + + ) + } + + {...props} + /> + ); +}; + + +export default EmojiPickerDropdownWrapper; diff --git a/app/soapbox/features/emoji/mapping.ts b/app/soapbox/features/emoji/mapping.ts index baf16590d..5259c2dc2 100644 --- a/app/soapbox/features/emoji/mapping.ts +++ b/app/soapbox/features/emoji/mapping.ts @@ -8,8 +8,8 @@ function replaceAll(str: string, find: string, replace: string) { interface UnicodeMap { [s: string]: { - unified: string, - shortcode: string, + unified: string + shortcode: string } } diff --git a/app/soapbox/features/emoji/search.ts b/app/soapbox/features/emoji/search.ts index 1a2d56b73..815f1fd36 100644 --- a/app/soapbox/features/emoji/search.ts +++ b/app/soapbox/features/emoji/search.ts @@ -41,7 +41,7 @@ const search = (str: string, { maxResults = 5, custom }: searchOptions = {}, cus .flatMap(id => { // @ts-ignore if (id[0] === 'c') { - const { shortcode, static_url } = custom_emojis.get((id as string).substr(1)).toJS(); + const { shortcode, static_url } = custom_emojis.get((id as string).slice(1)).toJS(); return { id: shortcode, @@ -51,10 +51,10 @@ const search = (str: string, { maxResults = 5, custom }: searchOptions = {}, cus }; } - const { skins } = data.emojis[(id as string).substr(1)]; + const { skins } = data.emojis[(id as string).slice(1)]; return { - id: (id as string).substr(1), + id: (id as string).slice(1), colons: ':' + id + ':', unified: skins[0].unified, native: skins[0].native, diff --git a/app/soapbox/reducers/__tests__/statuses.test.ts b/app/soapbox/reducers/__tests__/statuses.test.ts index 1db00ec64..06d82c871 100644 --- a/app/soapbox/reducers/__tests__/statuses.test.ts +++ b/app/soapbox/reducers/__tests__/statuses.test.ts @@ -118,7 +118,7 @@ describe('statuses reducer', () => { const status = require('soapbox/__fixtures__/status-custom-emoji.json'); const action = { type: STATUS_IMPORT, status }; - const expected = 'Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:'; + const expected = 'Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:'; const result = reducer(undefined, action).getIn(['AGm7uC9DaAIGUa4KYK', 'contentHtml']); expect(result).toBe(expected); diff --git a/app/soapbox/reducers/custom-emojis.ts b/app/soapbox/reducers/custom-emojis.ts index bce0cf1c8..e63348580 100644 --- a/app/soapbox/reducers/custom-emojis.ts +++ b/app/soapbox/reducers/custom-emojis.ts @@ -9,7 +9,7 @@ import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom-emojis'; import type { AnyAction } from 'redux'; import type { APIEntity } from 'soapbox/types/entities'; -const initialState = ImmutableList(); +const initialState = ImmutableList>(); // Populate custom emojis for composer autosuggest const autosuggestPopulate = (emojis: ImmutableList>) => { diff --git a/types/emoji-mart/index.d.ts b/types/emoji-mart/index.d.ts index 5726c54f5..39ce0a0d0 100644 --- a/types/emoji-mart/index.d.ts +++ b/types/emoji-mart/index.d.ts @@ -1,10 +1,10 @@ declare module 'emoji-mart' { export interface NativeEmoji { - unified: string, - native: string, - x: number, - y: number, + unified: string + native: string + x: number + y: number } export interface CustomEmoji { @@ -12,40 +12,40 @@ declare module 'emoji-mart' { } export interface Emoji { - id: string, - name: string, - keywords: string[], - skins: T[], - version?: number, + id: string + name: string + keywords: string[] + skins: T[] + version?: number } export interface EmojiPick { - id: string, - name: string, - native?: string, - unified?: string, - keywords: string[], - shortcodes: string, - emoticons: string[], - src?: string, + id: string + name: string + native?: string + unified?: string + keywords: string[] + shortcodes: string + emoticons: string[] + src?: string } export interface PickerProps { - custom?: { emojis: Emoji }[], - set?: string, - title?: string, - theme?: string, - onEmojiSelect?: (emoji: EmojiPick) => void, - recent?: any, - skin?: any, - perLine?: number, - emojiSize?: number, - emojiButtonSize?: number, - navPosition?: string, - autoFocus?: boolean, - i18n?: any, - getImageURL: (set: string, name: string) => string, - getSpritesheetURL: (set: string) => string, + custom?: { emojis: Emoji }[] + set?: string + title?: string + theme?: string + onEmojiSelect?: (emoji: EmojiPick) => void + recent?: any + skin?: any + perLine?: number + emojiSize?: number + emojiButtonSize?: number + navPosition?: string + autoFocus?: boolean + i18n?: any + getImageURL: (set: string, name: string) => string + getSpritesheetURL: (set: string) => string } export class Picker { @@ -58,10 +58,10 @@ declare module 'emoji-mart' { declare module '@emoji-mart/data/sets/14/twitter.json' { export interface NativeEmoji { - unified: string, - native: string, - x: number, - y: number, + unified: string + native: string + x: number + y: number } export interface CustomEmoji { @@ -69,36 +69,36 @@ declare module '@emoji-mart/data/sets/14/twitter.json' { } export interface Emoji { - id: string, - name: string, - keywords: string[], - skins: T[], - version?: number, + id: string + name: string + keywords: string[] + skins: T[] + version?: number } export interface EmojiCategory { - id: string, - emojis: string[], + id: string + emojis: string[] } export interface EmojiMap { - [s: string]: Emoji, + [s: string]: Emoji } export interface EmojiAlias { - [s: string]: string, + [s: string]: string } export interface EmojiSheet { - cols: number, - rows: number, + cols: number + rows: number } export interface EmojiData { - categories: EmojiCategory[], - emojis: EmojiMap, - aliases: EmojiAlias, - sheet: EmojiSheet, + categories: EmojiCategory[] + emojis: EmojiMap + aliases: EmojiAlias + sheet: EmojiSheet } const data: EmojiData;