diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index 461298b20..faf383678 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,214 +1,247 @@ // @flow +import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Text, View } from 'react-native'; import Animated, { Extrapolate, interpolateNode, } from 'react-native-reanimated'; import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react'; import type { MessageReactionInfo } from 'lib/selectors/chat-selectors'; import { stringForReactionList } from 'lib/shared/reaction-utils'; import type { ThreadInfo } from 'lib/types/thread-types'; import CommIcon from '../components/comm-icon.react'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react'; +import { MessageReactionsModalRouteName } from '../navigation/route-names'; import { useStyles } from '../themes/colors'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types'; import { inlineEngagementStyle, inlineEngagementCenterStyle, inlineEngagementRightStyle, composedMessageStyle, } from './chat-constants'; import { useNavigateToThread } from './message-list-types'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: $ReadOnlyMap, +disabled?: boolean, }; function InlineEngagement(props: Props): React.Node { const { disabled = false, reactions, threadInfo } = props; const repliesText = useInlineEngagementText(threadInfo); const navigateToThread = useNavigateToThread(); - const onPress = React.useCallback(() => { + const { navigate } = useNavigation(); + + const styles = useStyles(unboundStyles); + + const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null; + const repliesStyles = React.useMemo(() => [styles.repliesText, unreadStyle], [ + styles.repliesText, + unreadStyle, + ]); + + const onPressThread = React.useCallback(() => { if (threadInfo && !disabled) { navigateToThread({ threadInfo }); } }, [disabled, navigateToThread, threadInfo]); - const styles = useStyles(unboundStyles); + const sidebarItem = React.useMemo(() => { + if (!threadInfo) { + return null; + } + return ( + + + {repliesText} + + ); + }, [ + threadInfo, + onPressThread, + styles.sidebar, + styles.icon, + repliesStyles, + repliesText, + ]); + + const onPressReactions = React.useCallback(() => { + navigate<'MessageReactionsModal'>({ + name: MessageReactionsModalRouteName, + params: { reactions }, + }); + }, [navigate, reactions]); + + const marginLeft = React.useMemo( + () => (sidebarItem ? styles.reactionMarginLeft : null), + [sidebarItem, styles.reactionMarginLeft], + ); const reactionList = React.useMemo(() => { if (!reactions || reactions.size === 0) { return null; } const reactionText = stringForReactionList(reactions); const reactionItems = {reactionText}; - return {reactionItems}; - }, [reactions, styles.reaction, styles.reactionsContainer]); - - const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null; - const marginRight = reactionList ? styles.repliesMarginRight : null; - const repliesStyles = React.useMemo( - () => [marginRight, styles.repliesText, unreadStyle], - [marginRight, styles.repliesText, unreadStyle], - ); - const noThreadInfo = !threadInfo; - const sidebarInfo = React.useMemo(() => { - if (noThreadInfo) { - return null; - } return ( - <> - - {repliesText} - - ); - }, [noThreadInfo, styles.icon, repliesStyles, repliesText]); - return ( - - {sidebarInfo} - {reactionList} + {reactionItems} + ); + }, [ + marginLeft, + onPressReactions, + reactions, + styles.reaction, + styles.reactionsContainer, + ]); + + return ( + + {sidebarItem} + {reactionList} ); } const unboundStyles = { container: { flexDirection: 'row', height: inlineEngagementStyle.height, - display: 'flex', borderRadius: 16, + backgroundColor: 'inlineEngagementBackground', + alignSelf: 'baseline', + alignItems: 'center', + padding: 8, }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, sidebar: { flexDirection: 'row', - display: 'flex', alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'inlineEngagementBackground', - padding: 8, - borderRadius: 16, - height: inlineEngagementStyle.height, }, icon: { color: 'inlineEngagementLabel', marginRight: 4, }, repliesText: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, - repliesMarginRight: { - marginRight: 12, - }, reaction: { - marginLeft: 4, color: 'inlineEngagementLabel', + fontSize: 14, + lineHeight: 22, + }, + reactionMarginLeft: { + marginLeft: 12, }, reactionsContainer: { display: 'flex', flexDirection: 'row', - marginLeft: -4, + alignItems: 'center', }, }; type TooltipInlineEngagementProps = { +item: ChatMessageInfoItemWithHeight, +isOpeningSidebar: boolean, +progress: Animated.Node, +windowWidth: number, +positioning: 'left' | 'right' | 'center', +initialCoordinates: { +x: number, +y: number, +width: number, +height: number, }, }; function TooltipInlineEngagement( props: TooltipInlineEngagementProps, ): React.Node { const { item, isOpeningSidebar, progress, windowWidth, initialCoordinates, positioning, } = props; const inlineEngagementStyles = React.useMemo(() => { if (positioning === 'left') { return { position: 'absolute', top: inlineEngagementStyle.marginTop + inlineEngagementRightStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: inlineEngagementRightStyle.marginRight + composedMessageStyle.marginRight, top: inlineEngagementStyle.marginTop + inlineEngagementRightStyle.topOffset, }; } else if (positioning === 'center') { return { alignSelf: 'center', top: inlineEngagementCenterStyle.topOffset, }; } }, [positioning]); const inlineEngagementContainer = React.useMemo(() => { const opacity = isOpeningSidebar ? 0 : interpolateNode(progress, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }); return { position: 'absolute', width: windowWidth, top: initialCoordinates.height, left: -initialCoordinates.x, opacity, }; }, [ initialCoordinates.height, initialCoordinates.x, isOpeningSidebar, progress, windowWidth, ]); return ( ); } export { InlineEngagement, TooltipInlineEngagement }; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js index c544fc340..498947064 100644 --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -1,242 +1,249 @@ // @flow import { createNavigatorFactory, useNavigationBuilder, type StackNavigationState, type StackOptions, type StackNavigationEventMap, type StackNavigatorProps, type ExtraStackNavigatorProps, type ParamListBase, type StackNavigationHelpers, type StackNavigationProp, } from '@react-navigation/native'; import { StackView, TransitionPresets } from '@react-navigation/stack'; import * as React from 'react'; import { Platform } from 'react-native'; import { enableScreens } from 'react-native-screens'; import LoggedOutModal from '../account/logged-out-modal.react'; import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react'; import ThreadPickerModal from '../calendar/thread-picker-modal.react'; import ImagePasteModal from '../chat/image-paste-modal.react'; +import MessageReactionsModal from '../chat/message-reactions-modal.react'; import AddUsersModal from '../chat/settings/add-users-modal.react'; import ColorSelectorModal from '../chat/settings/color-selector-modal.react'; import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react'; import SidebarListModal from '../chat/sidebar-list-modal.react'; import SubchannelsListModal from '../chat/subchannels-list-modal.react'; import CustomServerModal from '../profile/custom-server-modal.react'; import AppNavigator from './app-navigator.react'; import { defaultStackScreenOptions } from './options'; import { RootNavigatorContext } from './root-navigator-context'; import RootRouter, { type RootRouterExtraNavigationHelpers, } from './root-router'; import { LoggedOutModalRouteName, AppRouteName, ThreadPickerModalRouteName, ImagePasteModalRouteName, AddUsersModalRouteName, CustomServerModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, SidebarListModalRouteName, SubchannelsListModalRouteName, + MessageReactionsModalRouteName, type ScreenParamList, type RootParamList, TermsAndPrivacyRouteName, } from './route-names'; enableScreens(); export type RootNavigationHelpers = { ...$Exact>, ...RootRouterExtraNavigationHelpers, ... }; type RootNavigatorProps = StackNavigatorProps>; function RootNavigator({ initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, id, ...rest }: RootNavigatorProps) { const [keyboardHandlingEnabled, setKeyboardHandlingEnabled] = React.useState( true, ); const mergedScreenOptions = React.useMemo(() => { if (typeof screenOptions === 'function') { return input => ({ ...screenOptions(input), keyboardHandlingEnabled, }); } return { ...screenOptions, keyboardHandlingEnabled, }; }, [screenOptions, keyboardHandlingEnabled]); const { state, descriptors, navigation } = useNavigationBuilder(RootRouter, { id, initialRouteName, children, screenOptions: mergedScreenOptions, defaultScreenOptions, screenListeners, }); const rootNavigationContext = React.useMemo( () => ({ setKeyboardHandlingEnabled }), [setKeyboardHandlingEnabled], ); return ( ); } const createRootNavigator = createNavigatorFactory< StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationHelpers<>, ExtraStackNavigatorProps, >(RootNavigator); const baseTransitionPreset = Platform.select({ ios: TransitionPresets.ModalSlideFromBottomIOS, default: TransitionPresets.FadeFromBottomAndroid, }); const transitionPreset = { ...baseTransitionPreset, cardStyleInterpolator: interpolatorProps => { const baseCardStyleInterpolator = baseTransitionPreset.cardStyleInterpolator( interpolatorProps, ); const overlayOpacity = interpolatorProps.current.progress.interpolate({ inputRange: [0, 1], outputRange: ([0, 0.7]: number[]), // Flow... extrapolate: 'clamp', }); return { ...baseCardStyleInterpolator, overlayStyle: [ baseCardStyleInterpolator.overlayStyle, { opacity: overlayOpacity }, ], }; }, }; const defaultScreenOptions = { ...defaultStackScreenOptions, ...transitionPreset, cardStyle: { backgroundColor: 'transparent' }, presentation: 'modal', headerShown: false, }; const disableGesturesScreenOptions = { gestureEnabled: false, }; const modalOverlayScreenOptions = { cardOverlayEnabled: true, presentation: 'transparentModal', }; const termsAndPrivacyModalScreenOptions = { gestureEnabled: false, cardOverlayEnabled: true, presentation: 'transparentModal', }; export type RootRouterNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; export type RootNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; const Root = createRootNavigator< ScreenParamList, RootParamList, RootNavigationHelpers, >(); function RootComponent(): React.Node { return ( + ); } export default RootComponent;