1- import React from 'react' ;
2- import { FlatList , StyleSheet , View } from 'react-native' ;
1+ import React , { useCallback , useMemo } from 'react' ;
2+ import { Pressable , StyleSheet , Text , View } from 'react-native' ;
3+ import { FlatList } from 'react-native-gesture-handler' ;
34
5+ import { emojis } from './emojis' ;
46import { ReactionButton } from './ReactionButton' ;
57
68import { scheduleActionOnClose } from '../../contexts' ;
@@ -12,9 +14,12 @@ import {
1214
1315import { useOwnCapabilitiesContext } from '../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext' ;
1416import { useTheme } from '../../contexts/themeContext/ThemeContext' ;
17+ import { useStableCallback } from '../../hooks' ;
18+ import { Attach } from '../../icons' ;
1519import { NativeHandlers } from '../../native' ;
1620
1721import { ReactionData } from '../../utils/utils' ;
22+ import { BottomSheetModal } from '../UIComponents' ;
1823
1924export type MessageReactionPickerProps = Pick < MessagesContextValue , 'supportedReactions' > &
2025 Pick < MessageContextValue , 'handleReaction' | 'dismissOverlay' > & {
@@ -29,6 +34,8 @@ export type ReactionPickerItemType = ReactionData & {
2934 ownReactionTypes : string [ ] ;
3035} ;
3136
37+ const keyExtractor = ( item : ReactionPickerItemType ) => item . type ;
38+
3239const renderItem = ( { index, item } : { index : number ; item : ReactionPickerItemType } ) => (
3340 < ReactionButton
3441 Icon = { item . Icon }
@@ -39,10 +46,21 @@ const renderItem = ({ index, item }: { index: number; item: ReactionPickerItemTy
3946 />
4047) ;
4148
49+ const emojiKeyExtractor = ( item : string ) => `unicode-${ item } ` ;
50+
51+ // TODO: V9: Move this to utils and also clean it up a bit.
52+ // This was done quickly and in a bit of a hurry.
53+ export const toUnicodeScalarString = ( emoji : string ) : string => {
54+ const out : number [ ] = [ ] ;
55+ for ( const ch of emoji ) out . push ( ch . codePointAt ( 0 ) ! ) ;
56+ return out . map ( ( cp ) => `U+${ cp . toString ( 16 ) . toUpperCase ( ) . padStart ( 4 , '0' ) } ` ) . join ( '-' ) ;
57+ } ;
58+
4259/**
4360 * MessageReactionPicker - A high level component which implements all the logic required for a message overlay reaction list
4461 */
4562export const MessageReactionPicker = ( props : MessageReactionPickerProps ) => {
63+ const [ emojiViewerOpened , setEmojiViewerOpened ] = React . useState < boolean | null > ( null ) ;
4664 const {
4765 dismissOverlay,
4866 handleReaction,
@@ -52,7 +70,7 @@ export const MessageReactionPicker = (props: MessageReactionPickerProps) => {
5270 const { supportedReactions : contextSupportedReactions } = useMessagesContext ( ) ;
5371 const {
5472 theme : {
55- colors : { white } ,
73+ colors : { white, grey } ,
5674 messageMenu : {
5775 reactionPicker : { container, contentContainer } ,
5876 } ,
@@ -62,25 +80,63 @@ export const MessageReactionPicker = (props: MessageReactionPickerProps) => {
6280
6381 const supportedReactions = propSupportedReactions || contextSupportedReactions ;
6482
65- const onSelectReaction = ( type : string ) => {
83+ const onSelectReaction = useStableCallback ( ( type : string ) => {
6684 NativeHandlers . triggerHaptic ( 'impactLight' ) ;
85+ setEmojiViewerOpened ( false ) ;
6786 dismissOverlay ( ) ;
6887 if ( handleReaction ) {
6988 scheduleActionOnClose ( ( ) => handleReaction ( type ) ) ;
7089 }
71- } ;
90+ } ) ;
91+
92+ const onOpenEmojiViewer = useStableCallback ( ( ) => {
93+ NativeHandlers . triggerHaptic ( 'impactLight' ) ;
94+ setEmojiViewerOpened ( true ) ;
95+ } ) ;
96+
97+ const EmojiViewerButton = useCallback (
98+ ( ) => (
99+ < Pressable onPress = { onOpenEmojiViewer } style = { styles . emojiViewerButton } >
100+ < Attach fill = { grey } size = { 32 } />
101+ </ Pressable >
102+ ) ,
103+ [ grey , onOpenEmojiViewer ] ,
104+ ) ;
105+
106+ const reactions : ReactionPickerItemType [ ] = useMemo (
107+ ( ) =>
108+ supportedReactions
109+ ?. filter ( ( reaction ) => ! reaction . isUnicode )
110+ ?. map ( ( reaction ) => ( {
111+ ...reaction ,
112+ onSelectReaction,
113+ ownReactionTypes,
114+ } ) ) ?? [ ] ,
115+ [ onSelectReaction , ownReactionTypes , supportedReactions ] ,
116+ ) ;
117+
118+ const selectEmoji = useStableCallback ( ( emoji : string ) => {
119+ const scalarString = toUnicodeScalarString ( emoji ) ;
120+ onSelectReaction ( scalarString ) ;
121+ } ) ;
122+
123+ const closeModal = useStableCallback ( ( ) => setEmojiViewerOpened ( false ) ) ;
124+
125+ const renderEmoji = useCallback (
126+ ( { item } : { item : string } ) => {
127+ return (
128+ < Pressable onPress = { ( ) => selectEmoji ( item ) } style = { styles . emojiContainer } >
129+ < Text style = { styles . emojiText } > { item } </ Text >
130+ </ Pressable >
131+ ) ;
132+ } ,
133+ [ selectEmoji ] ,
134+ ) ;
72135
73136 if ( ! own_capabilities . sendReaction ) {
74137 return null ;
75138 }
76139
77- const reactions : ReactionPickerItemType [ ] =
78- supportedReactions ?. map ( ( reaction ) => ( {
79- ...reaction ,
80- onSelectReaction,
81- ownReactionTypes,
82- } ) ) ?? [ ] ;
83-
84140 return (
85141 < View
86142 accessibilityLabel = 'Reaction Selector on long pressing message'
@@ -94,14 +150,35 @@ export const MessageReactionPicker = (props: MessageReactionPickerProps) => {
94150 ] }
95151 data = { reactions }
96152 horizontal
97- keyExtractor = { ( item ) => item . type }
153+ keyExtractor = { keyExtractor }
154+ ListFooterComponent = { EmojiViewerButton }
98155 renderItem = { renderItem }
99156 />
157+ { emojiViewerOpened ? (
158+ < BottomSheetModal height = { 300 } onClose = { closeModal } visible = { true } >
159+ < FlatList
160+ columnWrapperStyle = { styles . bottomSheetColumnWrapper }
161+ contentContainerStyle = { styles . bottomSheetContentContainer }
162+ data = { emojis }
163+ keyExtractor = { emojiKeyExtractor }
164+ numColumns = { 4 }
165+ renderItem = { renderEmoji }
166+ style = { styles . bottomSheet }
167+ />
168+ </ BottomSheetModal >
169+ ) : null }
100170 </ View >
101171 ) ;
102172} ;
103173
104174const styles = StyleSheet . create ( {
175+ bottomSheet : { height : 300 } ,
176+ bottomSheetColumnWrapper : {
177+ alignItems : 'center' ,
178+ justifyContent : 'space-evenly' ,
179+ width : '100%' ,
180+ } ,
181+ bottomSheetContentContainer : { paddingVertical : 16 } ,
105182 container : {
106183 alignSelf : 'stretch' ,
107184 } ,
@@ -112,4 +189,7 @@ const styles = StyleSheet.create({
112189 marginVertical : 8 ,
113190 paddingHorizontal : 5 ,
114191 } ,
192+ emojiContainer : { height : 30 } ,
193+ emojiText : { fontSize : 20 , padding : 2 } ,
194+ emojiViewerButton : { alignItems : 'flex-start' , justifyContent : 'flex-start' , paddingTop : 4 } ,
115195} ) ;
0 commit comments