diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js --- a/native/chat/text-message-tooltip-modal.react.js +++ b/native/chat/text-message-tooltip-modal.react.js @@ -46,18 +46,18 @@ const spec = { entries: [ - { id: 'copy', text: 'Copy', onPress: onPressCopy }, { id: 'reply', text: 'Reply', onPress: onPressReply }, - { - id: 'report', - text: 'Report', - onPress: onPressReport, - }, { id: 'sidebar', text: 'Thread', onPress: navigateToSidebar, }, + { id: 'copy', text: 'Copy', onPress: onPressCopy }, + { + id: 'report', + text: 'Report', + onPress: onPressReport, + }, ], }; diff --git a/native/navigation/tooltip.react.js b/native/navigation/tooltip.react.js --- a/native/navigation/tooltip.react.js +++ b/native/navigation/tooltip.react.js @@ -1,5 +1,9 @@ // @flow +import { + useActionSheet, + type ShowActionSheetWithOptions, +} from '@expo/react-native-action-sheet'; import type { RouteProp } from '@react-navigation/native'; import * as Haptics from 'expo-haptics'; import invariant from 'invariant'; @@ -10,6 +14,7 @@ TouchableWithoutFeedback, Platform, TouchableOpacity, + Keyboard, } from 'react-native'; import Animated from 'react-native-reanimated'; import { useDispatch } from 'react-redux'; @@ -111,7 +116,12 @@ // withInputState +inputState: ?InputState, +chatContext: ?ChatContextType, + +showActionSheetWithOptions: ShowActionSheetWithOptions, +}; +type State = { + +actionSheetShown: boolean, }; + function createTooltip< RouteName: $Keys, BaseTooltipPropsType: BaseTooltipProps = BaseTooltipProps, @@ -138,6 +148,10 @@ size={16} /> ); + } else if (this.props.spec.id === 'more') { + icon = ( + + ); } return ( @@ -159,6 +173,7 @@ } class Tooltip extends React.PureComponent< TooltipProps, + State, > { backdropOpacity: Node; tooltipContainerOpacity: Node; @@ -171,6 +186,10 @@ constructor(props: TooltipProps) { super(props); + this.state = { + actionSheetShown: false, + }; + const { overlayContext } = props; invariant(overlayContext, 'Tooltip should have OverlayContext'); const { position } = overlayContext; @@ -363,6 +382,7 @@ overlayContext, inputState, chatContext, + showActionSheetWithOptions, ...navAndRouteForFlow } = this.props; @@ -391,6 +411,27 @@ ); }); + if (this.location === 'fixed' && entries.length > 3) { + items.splice(3); + + const moreSpec = { + id: 'more', + text: 'More', + onPress: this.onPressMore, + }; + + const moreTooltipItem = ( + + ); + + items.push(moreTooltipItem); + } + let triangleStyle; const { route } = this.props; const { initialCoordinates } = route.params; @@ -434,6 +475,11 @@ itemsStyle.push(styles.itemsFixed); } + let tooltip = {items}; + if (this.state.actionSheetShown) { + tooltip = null; + } + return ( @@ -448,7 +494,7 @@ onLayout={this.onTooltipContainerLayout} > {triangleUp} - {items} + {tooltip} {triangleDown} @@ -477,6 +523,86 @@ ); }; + onPressMore = () => { + Keyboard.dismiss(); + this.setState({ actionSheetShown: true }); + + const { entries } = this; + const options = entries.map(entry => entry.text); + + const { + destructiveButtonIndex, + cancelButtonIndex, + } = this.handlePlatformSpecificActionSheetOptions(options); + + // reversing because we want the order of the normal tooltip, + // of left to right to now be from bottom to up + options.reverse(); + + const containerStyle = { + paddingBottom: 24, + }; + + const icons = [ + , + , + , + , + ]; + + this.props.showActionSheetWithOptions( + { + options, + cancelButtonIndex, + destructiveButtonIndex, + containerStyle, + icons, + }, + (selectedIndex?: number) => { + if (selectedIndex === cancelButtonIndex) { + this.props.navigation.goBackOnce(); + return; + } + const index = entries.length - (selectedIndex ?? 0); + const entry = entries[Platform.OS === 'ios' ? index : index - 1]; + this.onPressEntry(entry); + }, + ); + }; + + // A cancel action button and colors with the action text are almost + // always iOS specific + handlePlatformSpecificActionSheetOptions = (options: Array) => { + const destructiveButtonIndex = Platform.OS === 'ios' ? 1 : undefined; + const cancelButtonIndex = Platform.OS === 'ios' ? 0 : -1; + + if (Platform.OS === 'ios') { + options.push('Cancel'); + } + + return { destructiveButtonIndex, cancelButtonIndex }; + }; + bindServerCall = (serverCall: ActionFunc): F => { const { cookie, @@ -515,6 +641,8 @@ return React.memo(function ConnectedTooltip( props: BaseTooltipPropsType, ) { + const { showActionSheetWithOptions } = useActionSheet(); + const dimensions = useSelector(state => state.dimensions); const serverCallState = useSelector(serverCallStateSelector); const viewerID = useSelector( @@ -536,6 +664,7 @@ overlayContext={overlayContext} inputState={inputState} chatContext={chatContext} + showActionSheetWithOptions={showActionSheetWithOptions} /> ); }); @@ -550,6 +679,9 @@ right: 0, top: 0, }, + bottomSheetIcon: { + color: '#000000', + }, container: { flex: 1, }, diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -1,5 +1,6 @@ // @flow +import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; @@ -248,23 +249,25 @@ - - - - - {gated} - - - - - {navigation} - - - + + + + + + {gated} + + + + + {navigation} + + + +