diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js index 5c2248197..beb24ac48 100644 --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -1,527 +1,529 @@ // @flow import type { MaterialTopTabNavigationProp, StackNavigationState, StackOptions, StackNavigationEventMap, StackNavigatorProps, ExtraStackNavigatorProps, StackHeaderProps, StackNavigationProp, StackNavigationHelpers, ParamListBase, StackRouterOptions, MaterialTopTabNavigationHelpers, HeaderTitleInputProps, StackHeaderLeftButtonProps, } from '@react-navigation/core'; import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import { createNavigatorFactory, useNavigationBuilder, } from '@react-navigation/native'; import { StackView } from '@react-navigation/stack'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, View, useWindowDimensions } from 'react-native'; import MessageStorePruner from 'lib/components/message-store-pruner.react.js'; import ThreadDraftUpdater from 'lib/components/thread-draft-updater.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { threadSettingsNotificationsCopy } from 'lib/shared/thread-settings-notifications-utils.js'; import { threadIsPending, threadIsSidebar } from 'lib/shared/thread-utils.js'; import BackgroundChatThreadList from './background-chat-thread-list.react.js'; import ChatHeader from './chat-header.react.js'; import { backgroundChatThreadListOptions, homeChatThreadListOptions, } from './chat-options.js'; import ChatRouter, { type ChatRouterNavigationHelpers, type ChatRouterNavigationAction, } from './chat-router.js'; import TabBar from './chat-tab-bar.react.js'; import ComposeSubchannel from './compose-subchannel.react.js'; import ComposeThreadButton from './compose-thread-button.react.js'; import FullScreenThreadMediaGallery from './fullscreen-thread-media-gallery.react.js'; import HomeChatThreadList from './home-chat-thread-list.react.js'; import { MessageEditingContext } from './message-editing-context.react.js'; import MessageListContainer from './message-list-container.react.js'; import MessageListHeaderTitle from './message-list-header-title.react.js'; import PinnedMessagesScreen from './pinned-messages-screen.react.js'; import DeleteThread from './settings/delete-thread.react.js'; import EmojiThreadAvatarCreation from './settings/emoji-thread-avatar-creation.react.js'; import ThreadSettingsNotifications from './settings/thread-settings-notifications.react.js'; import ThreadSettings from './settings/thread-settings.react.js'; import ThreadScreenPruner from './thread-screen-pruner.react.js'; import ThreadSettingsButton from './thread-settings-button.react.js'; import ThreadSettingsHeaderTitle from './thread-settings-header-title.react.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; +import { NUXHandler } from '../components/nux-handler.react.js'; import { nuxTip, NUXTipsContext, } from '../components/nux-tips-context.react.js'; import { InputStateContext } from '../input/input-state.js'; import CommunityDrawerButton from '../navigation/community-drawer-button.react.js'; import HeaderBackButton from '../navigation/header-back-button.react.js'; import { activeThreadSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { defaultStackScreenOptions, transitionPreset, } from '../navigation/options.js'; import { ComposeSubchannelRouteName, DeleteThreadRouteName, ThreadSettingsRouteName, EmojiThreadAvatarCreationRouteName, FullScreenThreadMediaGalleryRouteName, PinnedMessagesScreenRouteName, MessageListRouteName, ChatThreadListRouteName, HomeChatThreadListRouteName, BackgroundChatThreadListRouteName, ThreadSettingsNotificationsRouteName, type ScreenParamList, type ChatParamList, type ChatTopTabsParamList, MessageSearchRouteName, ChangeRolesScreenRouteName, type NavigationRoute, } from '../navigation/route-names.js'; import type { TabNavigationProp } from '../navigation/tab-navigator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import ChangeRolesHeaderLeftButton from '../roles/change-roles-header-left-button.react.js'; import ChangeRolesScreen from '../roles/change-roles-screen.react.js'; import MessageSearch from '../search/message-search.react.js'; import SearchHeader from '../search/search-header.react.js'; import SearchMessagesButton from '../search/search-messages-button.react.js'; import { useColors, useStyles } from '../themes/colors.js'; const unboundStyles = { keyboardAvoidingView: { flex: 1, }, view: { flex: 1, backgroundColor: 'listBackground', }, threadListHeaderStyle: { elevation: 0, shadowOffset: { width: 0, height: 0 }, borderBottomWidth: 0, backgroundColor: 'tabBarBackground', }, }; export type ChatTopTabsNavigationProp< RouteName: $Keys = $Keys, > = MaterialTopTabNavigationProp; export type ChatTopTabsNavigationHelpers = MaterialTopTabNavigationHelpers; const ChatThreadsTopTab = createMaterialTopTabNavigator< ScreenParamList, ChatTopTabsParamList, ChatTopTabsNavigationHelpers, >(); function ChatThreadsComponent(): React.Node { const colors = useColors(); const { tabBarBackground, tabBarAccent } = colors; const screenOptions = React.useMemo( () => ({ tabBarShowIcon: true, tabBarStyle: { backgroundColor: tabBarBackground, }, tabBarItemStyle: { flexDirection: 'row', }, tabBarIndicatorStyle: { borderColor: tabBarAccent, borderBottomWidth: 2, }, }), [tabBarAccent, tabBarBackground], ); return ( ); } export type ChatNavigationHelpers = { ...$Exact>, ...ChatRouterNavigationHelpers, }; type ChatNavigatorProps = StackNavigatorProps>; function ChatNavigator({ initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, id, ...rest }: ChatNavigatorProps) { const { state, descriptors, navigation } = useNavigationBuilder< StackNavigationState, ChatRouterNavigationAction, StackOptions, StackRouterOptions, ChatNavigationHelpers<>, StackNavigationEventMap, ExtraStackNavigatorProps, >(ChatRouter, { id, initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, }); // Clear ComposeSubchannel screens after each message is sent. If a user goes // to ComposeSubchannel to create a new thread, but finds an existing one and // uses it instead, we can assume the intent behind opening ComposeSubchannel // is resolved const inputState = React.useContext(InputStateContext); invariant(inputState, 'InputState should be set in ChatNavigator'); const clearComposeScreensAfterMessageSend = React.useCallback(() => { navigation.clearScreens([ComposeSubchannelRouteName]); }, [navigation]); React.useEffect(() => { inputState.registerSendCallback(clearComposeScreensAfterMessageSend); return () => { inputState.unregisterSendCallback(clearComposeScreensAfterMessageSend); }; }, [inputState, clearComposeScreensAfterMessageSend]); return ( ); } const createChatNavigator = createNavigatorFactory< StackNavigationState, StackOptions, StackNavigationEventMap, ChatNavigationHelpers<>, ExtraStackNavigatorProps, >(ChatNavigator); const header = (props: StackHeaderProps) => { // Flow has trouble reconciling identical types between different libdefs, // and flow-typed has no way for one libdef to depend on another const castProps: StackHeaderProps = (props: any); return ; }; const headerRightStyle = { flexDirection: 'row' }; const messageListOptions = ({ navigation, route, }: { +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, }) => { const isSearchEmpty = !!route.params.searching && route.params.threadInfo.members.length === 1; const areSettingsEnabled = !threadIsPending(route.params.threadInfo.id) && !isSearchEmpty; return { headerTitle: (props: HeaderTitleInputProps) => ( ), headerRight: areSettingsEnabled ? () => ( ) : undefined, headerBackTitleVisible: false, headerTitleAlign: isSearchEmpty ? 'center' : 'left', headerLeftContainerStyle: { width: Platform.OS === 'ios' ? 32 : 40 }, headerTitleStyle: areSettingsEnabled ? { marginRight: 20 } : undefined, }; }; const composeThreadOptions = { headerTitle: 'Compose chat', headerBackTitleVisible: false, }; const threadSettingsOptions = ({ route, }: { +route: NavigationRoute<'ThreadSettings'>, ... }) => ({ headerTitle: (props: HeaderTitleInputProps) => ( ), headerBackTitleVisible: false, }); const emojiAvatarCreationOptions = { headerTitle: 'Emoji avatar selection', headerBackTitleVisible: false, }; const fullScreenThreadMediaGalleryOptions = { headerTitle: 'All Media', headerBackTitleVisible: false, }; const deleteThreadOptions = { headerTitle: 'Delete chat', headerBackTitleVisible: false, }; const messageSearchOptions = { headerTitle: () => , headerBackTitleVisible: false, headerTitleContainerStyle: { width: '100%', }, }; const pinnedMessagesScreenOptions = { headerTitle: 'Pinned Messages', headerBackTitleVisible: false, }; const threadSettingsNotificationsOptions = ({ route, }: { +route: NavigationRoute<'ThreadSettingsNotifications'>, ... }) => ({ headerTitle: threadIsSidebar(route.params.threadInfo) ? threadSettingsNotificationsCopy.SIDEBAR_TITLE : threadSettingsNotificationsCopy.CHANNEL_TITLE, headerBackTitleVisible: false, }); const changeRolesScreenOptions = ({ route, }: { +route: NavigationRoute<'ChangeRolesScreen'>, ... }) => ({ headerLeft: (headerLeftProps: StackHeaderLeftButtonProps) => ( ), headerTitle: 'Change Role', presentation: 'modal', ...transitionPreset, }); export type ChatNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...ChatRouterNavigationHelpers, }; const Chat = createChatNavigator< ScreenParamList, ChatParamList, ChatNavigationHelpers, >(); type Props = { +navigation: TabNavigationProp<'Chat'>, ... }; export default function ChatComponent(props: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const loggedIn = useSelector(isLoggedIn); let draftUpdater = null; if (loggedIn) { draftUpdater = ; } const communityDrawerButtonRef = React.useRef>(); const tipsContext = React.useContext(NUXTipsContext); invariant(tipsContext, 'NUXTipsContext should be defined'); const { registerTipButton } = tipsContext; const communityDrawerButtonOnLayout = React.useCallback(() => { communityDrawerButtonRef.current?.measure( (x, y, width, height, pageX, pageY) => { registerTipButton(nuxTip.COMMUNITY_DRAWER, { x, y, width, height, pageX, pageY, }); }, ); }, [registerTipButton]); const headerLeftButton = React.useCallback( (headerProps: StackHeaderLeftButtonProps) => { if (headerProps.canGoBack) { return ; } return ( ); }, [communityDrawerButtonOnLayout, props.navigation], ); const messageEditingContext = React.useContext(MessageEditingContext); const editState = messageEditingContext?.editState; const editMode = !!editState?.editedMessage; const { width: screenWidth } = useWindowDimensions(); const screenOptions = React.useMemo( () => ({ ...defaultStackScreenOptions, header, headerLeft: headerLeftButton, headerStyle: { backgroundColor: colors.tabBarBackground, borderBottomWidth: 1, }, gestureEnabled: true, gestureResponseDistance: editMode ? 0 : screenWidth, }), [colors.tabBarBackground, headerLeftButton, screenWidth, editMode], ); const chatThreadListOptions = React.useCallback( ({ navigation, }: { +navigation: ChatNavigationProp<'ChatThreadList'>, ... }) => ({ headerTitle: 'Inbox', headerRight: Platform.OS === 'ios' ? () => : undefined, headerBackTitleVisible: false, headerStyle: styles.threadListHeaderStyle, }), [styles.threadListHeaderStyle], ); const frozen = useSelector(state => state.frozen); const navContext = React.useContext(NavContext); const activeThreadID = activeThreadSelector(navContext); return ( + {draftUpdater} ); } diff --git a/native/components/nux-handler.react.js b/native/components/nux-handler.react.js new file mode 100644 index 000000000..ba4b192d8 --- /dev/null +++ b/native/components/nux-handler.react.js @@ -0,0 +1,52 @@ +// @flow + +import { useNavigation } from '@react-navigation/core'; +import invariant from 'invariant'; +import * as React from 'react'; + +import { isLoggedIn } from 'lib/selectors/user-selectors.js'; + +import { + firstNUXTipKey, + NUXTipsContext, + getNUXTipParams, +} from './nux-tips-context.react.js'; +import type { NUXTipRouteNames } from '../navigation/route-names.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { useOnFirstLaunchEffect } from '../utils/hooks.js'; + +function NUXHandler(): React.Node { + const nuxTipsContext = React.useContext(NUXTipsContext); + invariant(nuxTipsContext, 'nuxTipsContext should be defined'); + const { tipsProps } = nuxTipsContext; + + const loggedIn = useSelector(isLoggedIn); + + if (!tipsProps || !loggedIn) { + return null; + } + + return ; +} + +function NUXHandlerInner(): React.Node { + const navigation = useNavigation(); + + const effect = React.useCallback(() => { + const { nextTip, tooltipLocation, nextRouteName } = + getNUXTipParams(firstNUXTipKey); + invariant(nextRouteName && nextTip, 'first nux tip should be defined'); + + navigation.navigate({ + name: nextRouteName, + params: { + tipKey: nextTip, + tooltipLocation, + }, + }); + }, [navigation]); + + useOnFirstLaunchEffect('NUX_HANDLER', effect); +} + +export { NUXHandler }; diff --git a/native/components/nux-tips-context.react.js b/native/components/nux-tips-context.react.js index 504289a76..38e4a270c 100644 --- a/native/components/nux-tips-context.react.js +++ b/native/components/nux-tips-context.react.js @@ -1,99 +1,122 @@ // @flow import * as React from 'react'; import { values } from 'lib/utils/objects.js'; +import { + CommunityDrawerTipRouteName, + MutedTabTipRouteName, +} from '../navigation/route-names.js'; import type { NUXTipRouteNames } from '../navigation/route-names.js'; -import { MutedTabTipRouteName } from '../navigation/route-names.js'; const nuxTip = Object.freeze({ COMMUNITY_DRAWER: 'community_drawer', MUTED: 'muted', HOME: 'home', }); export type NUXTip = $Values; type NUXTipParams = { +nextTip: ?NUXTip, +tooltipLocation: 'below' | 'above', +nextRouteName: ?NUXTipRouteNames, }; -const nuxTipParams: { [NUXTip]: NUXTipParams } = { +const firstNUXTipKey = 'firstTip'; +type NUXTipParamsKeys = NUXTip | 'firstTip'; + +const nuxTipParams: { +[NUXTipParamsKeys]: NUXTipParams } = { + [firstNUXTipKey]: { + nextTip: nuxTip.COMMUNITY_DRAWER, + tooltipLocation: 'below', + nextRouteName: CommunityDrawerTipRouteName, + }, [nuxTip.COMMUNITY_DRAWER]: { nextTip: nuxTip.MUTED, tooltipLocation: 'below', nextRouteName: MutedTabTipRouteName, }, [nuxTip.MUTED]: { nextTip: undefined, nextRouteName: undefined, tooltipLocation: 'below', }, }; -function getNUXTipParams(currentTipKey: NUXTip): NUXTipParams { +function getNUXTipParams(currentTipKey: NUXTipParamsKeys): NUXTipParams { return nuxTipParams[currentTipKey]; } type TipProps = { +x: number, +y: number, +width: number, +height: number, +pageX: number, +pageY: number, }; export type NUXTipsContextType = { +registerTipButton: (type: NUXTip, tipProps: ?TipProps) => void, - +getTipsProps: () => ?{ +[type: NUXTip]: TipProps }, + +tipsProps: ?{ +[type: NUXTip]: TipProps }, }; const NUXTipsContext: React.Context = React.createContext(); type Props = { +children: React.Node, }; function NUXTipsContextProvider(props: Props): React.Node { const { children } = props; - const tipsProps = React.useRef<{ [tip: NUXTip]: ?TipProps }>({}); + const [tipsPropsState, setTipsPropsState] = React.useState<{ + +[tip: NUXTip]: ?TipProps, + }>(() => ({})); const registerTipButton = React.useCallback( (type: NUXTip, tipProps: ?TipProps) => { - tipsProps.current[type] = tipProps; + setTipsPropsState(currenttipsPropsState => { + const newtipsPropsState = { ...currenttipsPropsState }; + newtipsPropsState[type] = tipProps; + return newtipsPropsState; + }); }, [], ); - const getTipsProps = React.useCallback(() => { + const tipsProps = React.useMemo(() => { const result: { [tip: NUXTip]: TipProps } = {}; for (const type of values(nuxTip)) { - if (!tipsProps.current[type]) { + if (!tipsPropsState[type]) { return null; } - result[type] = tipsProps.current[type]; + result[type] = tipsPropsState[type]; } return result; - }, []); + }, [tipsPropsState]); const value = React.useMemo( () => ({ registerTipButton, - getTipsProps, + tipsProps, }), - [getTipsProps, registerTipButton], + [tipsProps, registerTipButton], ); return ( {children} ); } -export { NUXTipsContext, NUXTipsContextProvider, nuxTip, getNUXTipParams }; +export { + NUXTipsContext, + NUXTipsContextProvider, + nuxTip, + getNUXTipParams, + firstNUXTipKey, +}; diff --git a/native/tooltip/nux-tips-overlay.react.js b/native/tooltip/nux-tips-overlay.react.js index 090ffe9d1..fc9c8d2f3 100644 --- a/native/tooltip/nux-tips-overlay.react.js +++ b/native/tooltip/nux-tips-overlay.react.js @@ -1,429 +1,429 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, TouchableWithoutFeedback, Platform, Text } from 'react-native'; import Animated, { FadeOut, withTiming, // eslint-disable-next-line no-unused-vars type EntryAnimationsValues, // eslint-disable-next-line no-unused-vars type ExitAnimationsValues, } from 'react-native-reanimated'; import { getNUXTipParams, NUXTipsContext, type NUXTip, } from '../components/nux-tips-context.react.js'; import PrimaryButton from '../components/primary-button.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { OverlayContext } from '../navigation/overlay-context.js'; import type { NavigationRoute, NUXTipRouteNames, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import type { LayoutEvent } from '../types/react-native.js'; import { AnimatedView } from '../types/styles.js'; import type { WritableAnimatedStyleObj } from '../types/styles.js'; const { Value } = Animated; const animationDuration = 150; const unboundStyles = { backdrop: { backgroundColor: 'black', bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, container: { flex: 1, }, contentContainer: { flex: 1, overflow: 'hidden', }, items: { backgroundColor: 'tooltipBackground', borderRadius: 5, overflow: 'hidden', padding: 20, }, triangleUp: { borderBottomColor: 'tooltipBackground', borderBottomWidth: 10, borderLeftColor: 'transparent', borderLeftWidth: 10, borderRightColor: 'transparent', borderRightWidth: 10, borderStyle: 'solid', borderTopColor: 'transparent', borderTopWidth: 0, bottom: Platform.OS === 'android' ? -1 : 0, height: 10, width: 10, }, triangleDown: { borderBottomColor: 'transparent', borderBottomWidth: 0, borderLeftColor: 'transparent', borderLeftWidth: 10, borderRightColor: 'transparent', borderRightWidth: 10, borderStyle: 'solid', borderTopColor: 'tooltipBackground', borderTopWidth: 10, height: 10, top: Platform.OS === 'android' ? -1 : 0, width: 10, }, tipText: { color: 'panelForegroundLabel', fontSize: 20, marginBottom: 10, }, buttonContainer: { width: 100, alignSelf: 'flex-end', }, }; export type NUXTipsOverlayParams = { +tipKey: NUXTip, +tooltipLocation: 'above' | 'below', }; export type NUXTipsOverlayProps = { +navigation: AppNavigationProp, +route: NavigationRoute, }; const margin: number = 20; function opacityEnteringAnimation() { 'worklet'; return { animations: { opacity: withTiming(0.7, { duration: animationDuration }), }, initialValues: { opacity: 0, }, }; } function createNUXTipsOverlay( ButtonComponent: React.ComponentType>, tipText: string, ): React.ComponentType> { function NUXTipsOverlay(props: NUXTipsOverlayProps) { const nuxTipContext = React.useContext(NUXTipsContext); const { navigation, route } = props; const { initialCoordinates, verticalBounds } = React.useMemo(() => { - const tipsProps = nuxTipContext?.getTipsProps(); + const tipsProps = nuxTipContext?.tipsProps; invariant(tipsProps, 'tips props should be defined in nux tips overlay'); const { pageX, pageY, width, height } = tipsProps[route.params.tipKey]; return { initialCoordinates: { height, width, x: pageX, y: pageY }, verticalBounds: { height, y: pageY }, }; }, [nuxTipContext, route.params.tipKey]); const dimensions = useSelector(state => state.dimensions); const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext'); const { onExitFinish } = overlayContext; const { goBackOnce } = navigation; const styles = useStyles(unboundStyles); const contentContainerStyle = React.useMemo(() => { const fullScreenHeight = dimensions.height; const top = verticalBounds.y; const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height; return { ...styles.contentContainer, marginTop: top, marginBottom: bottom, }; }, [ dimensions.height, styles.contentContainer, verticalBounds.height, verticalBounds.y, ]); const buttonStyle = React.useMemo(() => { const { x, y, width, height } = initialCoordinates; return { width: Math.ceil(width), height: Math.ceil(height), marginTop: y - verticalBounds.y, marginLeft: x, }; }, [initialCoordinates, verticalBounds]); const tipHorizontalOffsetRef = React.useRef(new Value(0)); const tipHorizontalOffset = tipHorizontalOffsetRef.current; const onTipContainerLayout = React.useCallback( (event: LayoutEvent) => { const { x, width } = initialCoordinates; const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; const actualWidth = event.nativeEvent.layout.width; if (extraLeftSpace < extraRightSpace) { const minWidth = width + 2 * extraLeftSpace; tipHorizontalOffset.setValue((minWidth - actualWidth) / 2); } else { const minWidth = width + 2 * extraRightSpace; tipHorizontalOffset.setValue((actualWidth - minWidth) / 2); } }, [dimensions.width, initialCoordinates, tipHorizontalOffset], ); const { tooltipLocation } = route.params; const baseTipContainerStyle = React.useMemo(() => { const { y, x, height, width } = initialCoordinates; const style: WritableAnimatedStyleObj = { position: 'absolute', alignItems: 'center', }; if (tooltipLocation === 'below') { style.top = Math.min(y + height, verticalBounds.y + verticalBounds.height) + margin; } else { style.bottom = dimensions.height - Math.max(y, verticalBounds.y) + margin; } const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (extraLeftSpace < extraRightSpace) { style.left = 0; style.minWidth = width + 2 * extraLeftSpace; } else { style.right = 0; style.minWidth = width + 2 * extraRightSpace; } return style; }, [ dimensions.height, dimensions.width, initialCoordinates, tooltipLocation, verticalBounds.height, verticalBounds.y, ]); const triangleStyle = React.useMemo(() => { const { x, width } = initialCoordinates; const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (extraLeftSpace < extraRightSpace) { return { alignSelf: 'flex-start', left: extraLeftSpace + (4 / 10) * width, }; } else { return { alignSelf: 'flex-end', right: extraRightSpace + (4 / 10) * width, }; } }, [dimensions.width, initialCoordinates]); const opacityExitingAnimation = React.useCallback(() => { 'worklet'; return { animations: { opacity: withTiming(0, { duration: animationDuration }), }, initialValues: { opacity: 0.7, }, callback: onExitFinish, }; }, [onExitFinish]); // prettier-ignore const tipContainerEnteringAnimation = React.useCallback( (values/*: EntryAnimationsValues*/) => { 'worklet'; const initialX = (-values.targetWidth + initialCoordinates.width + initialCoordinates.x) / 2; const initialY = tooltipLocation === 'below' ? -values.targetHeight / 2 : values.targetHeight / 2; return { animations: { opacity: withTiming(1, { duration: animationDuration }), transform: [ { translateX: withTiming(0, { duration: animationDuration }) }, { translateY: withTiming(0, { duration: animationDuration }) }, { scale: withTiming(1, { duration: animationDuration }) }, ], }, initialValues: { opacity: 0, transform: [ { translateX: initialX }, { translateY: initialY }, { scale: 0 }, ], }, }; }, [initialCoordinates.width, initialCoordinates.x, tooltipLocation], ); // prettier-ignore const tipContainerExitingAnimation = React.useCallback( (values/*: ExitAnimationsValues*/) => { 'worklet'; const toValueX = (-values.currentWidth + initialCoordinates.width + initialCoordinates.x) / 2; const toValueY = tooltipLocation === 'below' ? -values.currentHeight / 2 : values.currentHeight / 2;; return { animations: { opacity: withTiming(0, { duration: animationDuration }), transform: [ { translateX: withTiming(toValueX, { duration: animationDuration, }), }, { translateY: withTiming(toValueY, { duration: animationDuration, }), }, { scale: withTiming(0, { duration: animationDuration }) }, ], }, initialValues: { opacity: 1, transform: [{ translateX: 0 }, { translateY: 0 }, { scale: 1 }], }, }; }, [initialCoordinates.width, initialCoordinates.x, tooltipLocation], ); let triangleDown = null; let triangleUp = null; if (tooltipLocation === 'above') { triangleDown = ; } else if (tooltipLocation === 'below') { triangleUp = ; } const callbackParams = getNUXTipParams(route.params.tipKey); const onPressOk = React.useCallback(() => { const { nextTip, tooltipLocation: nextLocation, nextRouteName, } = callbackParams; goBackOnce(); if (!nextTip || !nextRouteName) { return; } navigation.navigate({ name: nextRouteName, params: { tipKey: nextTip, tooltipLocation: nextLocation, }, }); }, [callbackParams, goBackOnce, navigation]); return ( {triangleUp} {tipText} {triangleDown} ); } function NUXTipsOverlayWrapper(props: NUXTipsOverlayProps) { const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext'); const { shouldRenderScreenContent } = overlayContext; return shouldRenderScreenContent ? : null; } return React.memo>(NUXTipsOverlayWrapper); } export { createNUXTipsOverlay };