diff --git a/components/EmojiPicker/EmojiRowList.tsx b/components/EmojiPicker/EmojiRowList.tsx index d268de9fc..cdbb6b54a 100644 --- a/components/EmojiPicker/EmojiRowList.tsx +++ b/components/EmojiPicker/EmojiRowList.tsx @@ -1,5 +1,5 @@ import { ListRenderItem as FlashListRenderItem } from "@shopify/flash-list"; -import { ReanimatedView, ReanimatedFlashList } from "@utils/animations"; +import { ReanimatedView } from "@utils/animations"; import { CategorizedEmojisRecord } from "@utils/emojis/interfaces"; import React, { FC, useCallback, useEffect } from "react"; import { @@ -9,7 +9,6 @@ import { useWindowDimensions, View, } from "react-native"; -import { FlatList } from "react-native-gesture-handler"; import { useAnimatedStyle, useSharedValue, @@ -18,17 +17,20 @@ import { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { EmojiRow } from "./EmojiRow"; +import { BottomSheetFlashList } from "@design-system/BottomSheet/BottomSheetFlashList"; +import { BottomSheetFlatList } from "@design-system/BottomSheet/BottomSheetFlatList"; -interface EmojiRowListProps { +type EmojiRowListProps = { emojis: CategorizedEmojisRecord[]; ListHeader?: React.ReactNode; onPress: (emoji: string) => void; -} +}; const keyExtractor = (_: unknown, index: number) => String(index); // Works around issue with Android not picking up scrolls -const ListRenderer = Platform.OS === "ios" ? ReanimatedFlashList : FlatList; +const ListRenderer = + Platform.OS === "ios" ? BottomSheetFlashList : BottomSheetFlatList; export const EmojiRowList: FC = ({ emojis, @@ -62,17 +64,21 @@ export const EmojiRowList: FC = ({ }; }); + const ListHeaderComponent = useCallback(() => { + return ListHeader; + }, [ListHeader]); + return ( ListHeader} + ListHeaderComponent={ListHeaderComponent} showsVerticalScrollIndicator={false} - estimatedItemSize={50} data={emojis} scrollEnabled={emojis.length > 1} keyExtractor={keyExtractor} renderItem={renderItem} keyboardShouldPersistTaps="handled" + estimatedItemSize={49} ListFooterComponent={() => } /> diff --git a/components/EmojiPicker/EmojiSearchBar.tsx b/components/EmojiPicker/EmojiSearchBar.tsx deleted file mode 100644 index 01d12a26f..000000000 --- a/components/EmojiPicker/EmojiSearchBar.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Picto from "@components/Picto/Picto"; -import { translate } from "@i18n"; -import { - textSecondaryColor, - textPrimaryColor, - backgroundColor, - clickedItemBackgroundColor, -} from "@styles/colors"; -import { PictoSizes } from "@styles/sizes"; -import React, { useCallback } from "react"; -import { - Platform, - StyleSheet, - TextInput, - useColorScheme, - View, -} from "react-native"; -import { Searchbar, IconButton } from "react-native-paper"; - -export const EmojiSearchBar = ({ - value, - setValue, -}: { - value: string; - setValue: (value: string) => void; -}) => { - const styles = useStyles(); - const colorScheme = useColorScheme(); - - const MaterialPicto = useCallback( - ({ color }: { color: string }) => { - return ( - - ); - }, - [styles.searchIcon] - ); - - const Right = useCallback(() => { - if (!value) return null; - return ( - ( - - )} - onPress={() => { - setValue(""); - }} - /> - ); - }, [setValue, value]); - - return ( - - {Platform.OS === "ios" && ( - - - - - )} - {Platform.OS === "android" && ( - null} - /> - )} - - ); -}; - -const useStyles = () => { - const colorScheme = useColorScheme(); - return StyleSheet.create({ - inputContainer: { - marginVertical: 8, - borderBottomWidth: Platform.OS === "android" ? 1 : 0, - backgroundColor: - Platform.OS === "ios" - ? clickedItemBackgroundColor(colorScheme) - : backgroundColor(colorScheme), - borderRadius: 8, - paddingLeft: 8, - }, - input: { - height: 32, - paddingLeft: 16, - paddingRight: 8, - marginRight: 8, - fontSize: 17, - color: textPrimaryColor(colorScheme), - flex: 1, - }, - iosContainer: { - flexDirection: "row", - flex: 1, - }, - iosPicto: { - top: 1, - marginLeft: 8, - }, - searchIcon: { - top: 1, - }, - searchbar: { - backgroundColor: backgroundColor(colorScheme), - }, - }); -}; diff --git a/containers/EmojiPicker.tsx b/containers/EmojiPicker.tsx index 0dec7fd94..3be2823fa 100644 --- a/containers/EmojiPicker.tsx +++ b/containers/EmojiPicker.tsx @@ -1,9 +1,15 @@ -import { Drawer, DrawerRef } from "@components/Drawer"; import { EmojiRowList } from "@components/EmojiPicker/EmojiRowList"; -import { EmojiSearchBar } from "@components/EmojiPicker/EmojiSearchBar"; import { useChatStore, useCurrentAccount } from "@data/store/accountsStore"; import { useSelect } from "@data/store/storeHelpers"; -import { textSecondaryColor } from "@styles/colors"; +import { useBottomSheetModalRef } from "@design-system/BottomSheet/BottomSheet.utils"; +import { BottomSheetContentContainer } from "@design-system/BottomSheet/BottomSheetContentContainer"; +import { BottomSheetHeader } from "@design-system/BottomSheet/BottomSheetHeader"; +import { BottomSheetModal } from "@design-system/BottomSheet/BottomSheetModal"; +import { Text } from "@design-system/Text"; +import { TextField } from "@design-system/TextField/TextField"; +import { VStack } from "@design-system/VStack"; +import { translate } from "@i18n"; +import { ThemedStyle, useAppTheme } from "@theme/useAppTheme"; import { emojis } from "@utils/emojis/emojis"; import { CategorizedEmojisRecord, Emoji } from "@utils/emojis/interfaces"; import { @@ -13,9 +19,10 @@ import { removeReactionFromMessage, } from "@utils/reactions"; import { matchSorter } from "match-sorter"; -import { useCallback, useMemo, useRef, useState } from "react"; -import { View, StyleSheet, useColorScheme } from "react-native"; -import { Text } from "react-native-paper"; +import { debounce } from "perfect-debounce"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { TextInput, ViewStyle, TextStyle } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; const flatEmojis = emojis.flatMap((category) => category.data); @@ -44,6 +51,20 @@ const sliceEmojis = (emojis: Emoji[]) => { return slicedEmojis; }; +const filterEmojis = (text: string) => { + const cleanedSearch = text.toLowerCase().trim(); + if (cleanedSearch.length === 0) { + return sliceEmojis(flatEmojis); + } + return sliceEmojis( + matchSorter(flatEmojis, cleanedSearch, { + keys: ["keywords", "name", "emoji"], + }) + ); +}; + +const defaultEmojis = sliceEmojis(flatEmojis); + // const myEmojis = sliceEmojis( // flatEmojis.filter((emoji) => favoritedEmojis.isFavorite(emoji.emoji)) // ); @@ -53,18 +74,36 @@ export const EmojiPicker = () => { const { reactingToMessage, setReactingToMessage } = useChatStore( useSelect(["reactingToMessage", "setReactingToMessage"]) ); - const drawerRef = useRef(null); - const [searchInput, setSearchInput] = useState(""); - const visible = !!reactingToMessage; - const styles = useStyles(); + + const bottomSheetRef = useBottomSheetModalRef(); + const textInputRef = useRef(null); + + const insets = useSafeAreaInsets(); + const { themed } = useAppTheme(); + + const [filteredReactions, setFilteredReactions] = useState(defaultEmojis); + const [hasInput, setHasInput] = useState(false); + + useEffect(() => { + if (reactingToMessage) { + bottomSheetRef.current?.present(); + } else { + bottomSheetRef.current?.dismiss(); + setHasInput(false); + setFilteredReactions(defaultEmojis); + } + }, [reactingToMessage, bottomSheetRef]); + const conversation = useChatStore((s) => reactingToMessage ? s.conversations[reactingToMessage.topic] : undefined ); + const message = useChatStore((s) => reactingToMessage && conversation ? conversation.messages.get(reactingToMessage.messageId) : undefined ); + const reactions = useMemo(() => { return message ? getMessageReactions(message) : {}; }, [message]); @@ -84,21 +123,11 @@ export const EmojiPicker = () => { return emojiSet; }, [reactions, currentUser]); - const filteredReactions = useMemo(() => { - const cleanedSearch = searchInput.toLowerCase().trim(); - if (cleanedSearch.length === 0) { - return sliceEmojis(flatEmojis); - } - return sliceEmojis( - matchSorter(flatEmojis, cleanedSearch, { - keys: ["keywords", "name", "emoji"], - }) - ); - }, [searchInput]); const closeMenu = useCallback(() => { setReactingToMessage(null); - setSearchInput(""); + textInputRef.current?.blur(); }, [setReactingToMessage]); + const handleReaction = useCallback( (emoji: string) => { if (!conversation || !message) return; @@ -108,49 +137,81 @@ export const EmojiPicker = () => { } else { addReactionToMessage(currentUser, message, emoji); } - drawerRef?.current?.closeDrawer(closeMenu); + bottomSheetRef?.current?.dismiss(); + closeMenu(); }, - [closeMenu, conversation, currentUser, currentUserEmojiMap, message] + [ + conversation, + currentUser, + currentUserEmojiMap, + message, + bottomSheetRef, + closeMenu, + ] + ); + + const debouncedFilter = useMemo( + () => + debounce((value: string) => { + setFilteredReactions(filterEmojis(value)); + setHasInput(value.length > 0); + }, 150), + [] + ); + + const onTextInputChange = useCallback( + (value: string) => { + debouncedFilter(value); + }, + [debouncedFilter] ); return ( - - - - {searchInput.length > 0 ? ( + + + + + + + + {hasInput ? ( ) : ( - <> - - {/* Removing until customization is ready */} - {/* Your Reactions - */} - All - - } - /> - + + {translate("emoji_picker_all")} + + } + /> )} - - + + ); }; -const useStyles = () => { - const colorScheme = useColorScheme(); - return StyleSheet.create({ - container: { - flex: 1, - height: "auto", - }, - headerText: { - fontSize: 13, - padding: 8, - color: textSecondaryColor(colorScheme), - }, - }); -}; +const $inputContainer: ThemedStyle = ({ spacing }) => ({ + marginVertical: spacing.xxs, + marginHorizontal: spacing.xxs, +}); + +const $container: ThemedStyle = ({ spacing }) => ({ + marginHorizontal: spacing.xxs, +}); + +const $headerText: ThemedStyle = ({ spacing }) => ({ + marginLeft: spacing.sm, +}); diff --git a/design-system/BottomSheet/BottomSheetFlashList.tsx b/design-system/BottomSheet/BottomSheetFlashList.tsx new file mode 100644 index 000000000..5354494e8 --- /dev/null +++ b/design-system/BottomSheet/BottomSheetFlashList.tsx @@ -0,0 +1,3 @@ +import { BottomSheetFlashList as GhoromBottomSheetFlashList } from "@gorhom/bottom-sheet"; + +export const BottomSheetFlashList = GhoromBottomSheetFlashList; diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts index 6637718f4..acea4007b 100644 --- a/i18n/translations/en.ts +++ b/i18n/translations/en.ts @@ -333,7 +333,9 @@ export const en = { "Connect the wallet associated with your XMTP account: {{wallet}}", // Emoji Picker + choose_reaction: "Choose a reaction", search_emojis: "Search emojis", + emoji_picker_all: "All", // Chat null state connectWithYourNetwork: "Find your frens",