mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Added emoji search
This commit is contained in:
		| @@ -6,7 +6,7 @@ import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice' | ||||
| import { chunk, forEach, groupBy, sortBy } from 'lodash' | ||||
| import React, { PropsWithChildren, RefObject, useEffect, useReducer, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Keyboard, KeyboardAvoidingView, Text, TextInput, View } from 'react-native' | ||||
| import { Keyboard, KeyboardAvoidingView, TextInput, View } from 'react-native' | ||||
| import FastImage from 'react-native-fast-image' | ||||
| import { ScrollView } from 'react-native-gesture-handler' | ||||
| import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| @@ -66,10 +66,7 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ | ||||
|   const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true) | ||||
|   useEffect(() => { | ||||
|     if (data && data.length) { | ||||
|       let sortedEmojis: { | ||||
|         title: string | ||||
|         data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][] | ||||
|       }[] = [] | ||||
|       let sortedEmojis: EmojisState['emojis'] = [] | ||||
|       forEach(groupBy(sortBy(data, ['category', 'shortcode']), 'category'), (value, key) => | ||||
|         sortedEmojis.push({ title: key, data: chunk(value, 5) }) | ||||
|       ) | ||||
| @@ -79,7 +76,8 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ | ||||
|           data: chunk( | ||||
|             frequentEmojis.map(e => e.emoji), | ||||
|             5 | ||||
|           ) | ||||
|           ), | ||||
|           type: 'frequent' | ||||
|         }) | ||||
|       } | ||||
|       emojisDispatch({ type: 'load', payload: sortedEmojis }) | ||||
| @@ -91,7 +89,10 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ | ||||
|   const [keyboardShown, setKeyboardShown] = useState(false) | ||||
|   useEffect(() => { | ||||
|     const showSubscription = Keyboard.addListener('keyboardWillShow', () => { | ||||
|       emojisDispatch({ type: 'target', payload: null }) | ||||
|       const anyInputHasFocus = inputProps.filter(props => props.ref.current?.isFocused()).length | ||||
|       if (anyInputHasFocus) { | ||||
|         emojisDispatch({ type: 'target', payload: null }) | ||||
|       } | ||||
|       setKeyboardShown(true) | ||||
|     }) | ||||
|     const hideSubscription = Keyboard.addListener('keyboardWillHide', () => { | ||||
| @@ -102,7 +103,7 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ | ||||
|       showSubscription.remove() | ||||
|       hideSubscription.remove() | ||||
|     } | ||||
|   }, []) | ||||
|   }, [inputProps]) | ||||
|   useEffect(() => { | ||||
|     if (focusRef) { | ||||
|       setTimeout(() => focusRef.current?.focus(), 500) | ||||
| @@ -117,16 +118,10 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ | ||||
|             <ScrollView keyboardShouldPersistTaps='always' children={children} /> | ||||
|             <View | ||||
|               style={[ | ||||
|                 keyboardShown | ||||
|                   ? { | ||||
|                       position: 'absolute', | ||||
|                       bottom: 0, | ||||
|                       width: '100%' | ||||
|                     } | ||||
|                   : null, | ||||
|                 keyboardShown ? { position: 'absolute', bottom: 0, width: '100%' } : null, | ||||
|                 { marginBottom: keyboardShown ? insets.bottom : 0 } | ||||
|               ]} | ||||
|               children={keyboardShown ? <EmojisButton /> : <EmojisList />} | ||||
|               children={emojisState.targetProps ? <EmojisList /> : <EmojisButton />} | ||||
|             /> | ||||
|           </EmojisContext.Provider> | ||||
|         </View> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import Icon from '@components/Icon' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useContext } from 'react' | ||||
| import { Keyboard, Pressable } from 'react-native' | ||||
| import { Keyboard, Pressable, View } from 'react-native' | ||||
| import EmojisContext from './helpers/EmojisContext' | ||||
|  | ||||
| const EmojisButton: React.FC = () => { | ||||
| @@ -23,17 +23,29 @@ const EmojisButton: React.FC = () => { | ||||
|         emojisDispatch({ type: 'target', payload: targetProps }) | ||||
|       }} | ||||
|       hitSlop={StyleConstants.Spacing.S} | ||||
|       style={{ alignSelf: 'flex-end', padding: StyleConstants.Spacing.Global.PagePadding }} | ||||
|       style={{ | ||||
|         alignSelf: 'flex-end', | ||||
|         padding: StyleConstants.Spacing.Global.PagePadding / 2 | ||||
|       }} | ||||
|       children={ | ||||
|         <Icon | ||||
|           name={emojisState.emojis && emojisState.emojis.length ? 'Smile' : 'Meh'} | ||||
|           size={24} | ||||
|           color={ | ||||
|             emojisState.emojis && emojisState.emojis.length | ||||
|               ? colors.primaryDefault | ||||
|               : colors.disabled | ||||
|           } | ||||
|         /> | ||||
|         <View | ||||
|           style={{ | ||||
|             borderWidth: 2, | ||||
|             borderColor: colors.primaryDefault, | ||||
|             padding: StyleConstants.Spacing.Global.PagePadding / 2, | ||||
|             borderRadius: 100 | ||||
|           }} | ||||
|         > | ||||
|           <Icon | ||||
|             name={emojisState.emojis && emojisState.emojis.length ? 'Smile' : 'Meh'} | ||||
|             size={24} | ||||
|             color={ | ||||
|               emojisState.emojis && emojisState.emojis.length | ||||
|                 ? colors.primaryDefault | ||||
|                 : colors.disabled | ||||
|             } | ||||
|           /> | ||||
|         </View> | ||||
|       } | ||||
|     /> | ||||
|   ) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import Icon from '@components/Icon' | ||||
| import CustomText from '@components/Text' | ||||
| import { useAppDispatch } from '@root/store' | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| @@ -5,9 +6,17 @@ import { countInstanceEmoji } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useContext, useEffect, useRef } from 'react' | ||||
| import { chunk } from 'lodash' | ||||
| import React, { useCallback, useContext, useEffect, useRef, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { AccessibilityInfo, findNodeHandle, Pressable, SectionList, View } from 'react-native' | ||||
| import { | ||||
|   AccessibilityInfo, | ||||
|   findNodeHandle, | ||||
|   Pressable, | ||||
|   SectionList, | ||||
|   TextInput, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import FastImage from 'react-native-fast-image' | ||||
| import validUrl from 'valid-url' | ||||
| import EmojisContext from './helpers/EmojisContext' | ||||
| @@ -18,8 +27,8 @@ const EmojisList = React.memo( | ||||
|     const { reduceMotionEnabled } = useAccessibility() | ||||
|     const { t } = useTranslation() | ||||
|  | ||||
|     const { emojisState, emojisDispatch } = useContext(EmojisContext) | ||||
|     const { colors } = useTheme() | ||||
|     const { emojisState } = useContext(EmojisContext) | ||||
|     const { colors, mode } = useTheme() | ||||
|  | ||||
|     const addEmoji = (shortcode: string) => { | ||||
|       if (!emojisState.targetProps) { | ||||
| @@ -70,6 +79,7 @@ const EmojisList = React.memo( | ||||
|                       addEmoji(`:${emoji.shortcode}:`) | ||||
|                       dispatch(countInstanceEmoji(emoji)) | ||||
|                     }} | ||||
|                     style={{ padding: StyleConstants.Spacing.S }} | ||||
|                   > | ||||
|                     <FastImage | ||||
|                       accessibilityLabel={t('common:customEmoji.accessibilityLabel', { | ||||
| @@ -79,12 +89,7 @@ const EmojisList = React.memo( | ||||
|                         'screenCompose:content.root.footer.emojis.accessibilityHint' | ||||
|                       )} | ||||
|                       source={{ uri }} | ||||
|                       style={{ | ||||
|                         width: 32, | ||||
|                         height: 32, | ||||
|                         padding: StyleConstants.Spacing.S, | ||||
|                         margin: StyleConstants.Spacing.S | ||||
|                       }} | ||||
|                       style={{ width: 32, height: 32 }} | ||||
|                     /> | ||||
|                   </Pressable> | ||||
|                 ) | ||||
| @@ -107,23 +112,97 @@ const EmojisList = React.memo( | ||||
|       } | ||||
|     }, [emojisState.targetProps]) | ||||
|  | ||||
|     const [search, setSearch] = useState('') | ||||
|     const searchLength = useRef(0) | ||||
|     useEffect(() => { | ||||
|       if ( | ||||
|         (search.length === 0 && searchLength.current === 1) || | ||||
|         (search.length === 1 && searchLength.current === 0) | ||||
|       ) { | ||||
|         layoutAnimation() | ||||
|       } | ||||
|       searchLength.current = search.length | ||||
|     }, [search.length, searchLength.current]) | ||||
|  | ||||
|     return emojisState.targetProps ? ( | ||||
|       <SectionList | ||||
|         accessible | ||||
|         ref={listRef} | ||||
|         horizontal | ||||
|         keyboardShouldPersistTaps='always' | ||||
|         sections={emojisState.emojis} | ||||
|         keyExtractor={item => item[0].shortcode} | ||||
|         renderSectionHeader={({ section: { title } }) => ( | ||||
|           <CustomText fontStyle='S' style={{ position: 'absolute', color: colors.secondary }}> | ||||
|             {title} | ||||
|           </CustomText> | ||||
|         )} | ||||
|         renderItem={listItem} | ||||
|         windowSize={4} | ||||
|         contentContainerStyle={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }} | ||||
|       /> | ||||
|       <View | ||||
|         style={{ | ||||
|           paddingBottom: StyleConstants.Spacing.Global.PagePadding, | ||||
|           backgroundColor: colors.backgroundDefault | ||||
|         }} | ||||
|       > | ||||
|         <View | ||||
|           style={{ | ||||
|             flexDirection: 'row', | ||||
|             alignItems: 'center', | ||||
|             padding: StyleConstants.Spacing.Global.PagePadding, | ||||
|             paddingBottom: StyleConstants.Spacing.S | ||||
|           }} | ||||
|         > | ||||
|           <View | ||||
|             style={{ | ||||
|               borderBottomWidth: 1, | ||||
|               borderBottomColor: colors.border, | ||||
|               alignSelf: 'stretch', | ||||
|               justifyContent: 'center', | ||||
|               paddingRight: StyleConstants.Spacing.S | ||||
|             }} | ||||
|           > | ||||
|             <Icon name='Search' size={StyleConstants.Font.Size.L} color={colors.secondary} /> | ||||
|           </View> | ||||
|           <TextInput | ||||
|             style={{ | ||||
|               flex: 1, | ||||
|               borderBottomWidth: 1, | ||||
|               borderBottomColor: colors.border, | ||||
|               ...StyleConstants.FontStyle.M, | ||||
|               color: colors.primaryDefault, | ||||
|               paddingVertical: StyleConstants.Spacing.S | ||||
|             }} | ||||
|             onChangeText={setSearch} | ||||
|             autoCapitalize='none' | ||||
|             clearButtonMode='always' | ||||
|             keyboardAppearance={mode} | ||||
|             autoCorrect={false} | ||||
|             spellCheck={false} | ||||
|           /> | ||||
|         </View> | ||||
|         <SectionList | ||||
|           accessible | ||||
|           ref={listRef} | ||||
|           horizontal | ||||
|           keyboardShouldPersistTaps='always' | ||||
|           sections={ | ||||
|             search.length | ||||
|               ? [ | ||||
|                   { | ||||
|                     title: 'Search result', | ||||
|                     data: chunk( | ||||
|                       emojisState.emojis | ||||
|                         .filter(e => e.type !== 'frequent') | ||||
|                         .flatMap(e => | ||||
|                           e.data.flatMap(e => e).filter(emoji => emoji.shortcode.includes(search)) | ||||
|                         ), | ||||
|                       2 | ||||
|                     ) | ||||
|                   } | ||||
|                 ] | ||||
|               : emojisState.emojis | ||||
|           } | ||||
|           keyExtractor={item => item[0]?.shortcode} | ||||
|           renderSectionHeader={({ section: { title } }) => ( | ||||
|             <CustomText fontStyle='S' style={{ position: 'absolute', color: colors.secondary }}> | ||||
|               {title} | ||||
|             </CustomText> | ||||
|           )} | ||||
|           renderItem={listItem} | ||||
|           windowSize={4} | ||||
|           contentContainerStyle={{ | ||||
|             paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, | ||||
|             minHeight: 32 * 2 + StyleConstants.Spacing.M * 3 | ||||
|           }} | ||||
|         /> | ||||
|       </View> | ||||
|     ) : null | ||||
|   }, | ||||
|   () => true | ||||
|   | ||||
| @@ -13,6 +13,7 @@ export type EmojisState = { | ||||
|   emojis: { | ||||
|     title: string | ||||
|     data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][] | ||||
|     type?: 'frequent' | ||||
|   }[] | ||||
|   targetProps: inputProps | null | ||||
|   inputProps: inputProps[] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user