diff --git a/native/chat/chat-tab-bar.react.js b/native/chat/chat-tab-bar.react.js index 6e183ea48..c39a9cf15 100644 --- a/native/chat/chat-tab-bar.react.js +++ b/native/chat/chat-tab-bar.react.js @@ -1,73 +1,70 @@ // @flow import type { MaterialTopTabBarProps, Route, TabBarItemProps, } from '@react-navigation/core'; import { MaterialTopTabBar } from '@react-navigation/material-top-tabs'; import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; +import type { MeasureOnSuccessCallback } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; import { TabBarItem } from 'react-native-tab-view'; +import type { ReactRefSetter } from 'lib/types/react-types.js'; + import { nuxTip, NUXTipsContext, } from '../components/nux-tips-context.react.js'; import { HomeChatThreadListRouteName, BackgroundChatThreadListRouteName, } from '../navigation/route-names.js'; const ButtonTitleToTip = Object.freeze({ [BackgroundChatThreadListRouteName]: nuxTip.MUTED, [HomeChatThreadListRouteName]: nuxTip.HOME, }); +const onLayout = () => {}; + function TabBarButton(props: TabBarItemProps>) { const tipsContext = React.useContext(NUXTipsContext); invariant(tipsContext, 'NUXTipsContext should be defined'); + const { registerTipButton } = tipsContext; - const viewRef = React.useRef>(); - const onLayout = React.useCallback(() => { - const button = viewRef.current; - if (!button) { - return; - } - - const tipType = ButtonTitleToTip[props.route.name]; - if (!tipType) { - return; - } - button.measure((x, y, width, height, pageX, pageY) => { - tipsContext.registerTipButton(tipType, { - x, - y, - width, - height, - pageX, - pageY, - }); - }); - }, [props.route.name, tipsContext]); + const registerRef: ReactRefSetter> = + React.useCallback( + element => { + const tipType = ButtonTitleToTip[props.route.name]; + if (!tipType) { + return; + } + const measure = (callback: MeasureOnSuccessCallback) => + element?.measure(callback); + registerTipButton(tipType, measure); + }, + [props.route.name, registerTipButton], + ); return ( - + ); } export default function TabBarTop( props: MaterialTopTabBarProps>, ): React.Node { const renderTabBarItem = React.useCallback( ( innerProps: $ReadOnly<{ ...TabBarItemProps>, +key: string, ... }>, ) => , [], ); return ; } diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js index beb24ac48..25a58d77f 100644 --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -1,529 +1,527 @@ // @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 type { MeasureOnSuccessCallback } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; 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 type { ReactRefSetter } from 'lib/types/react-types.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, >(); +const communityDrawerButtonOnLayout = () => {}; + 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 communityDrawerButtonRegisterRef: ReactRefSetter< + React.ElementRef, + > = React.useCallback( + element => { + const measure = (callback: MeasureOnSuccessCallback) => + element?.measure(callback); + + registerTipButton(nuxTip.COMMUNITY_DRAWER, measure); + }, + [registerTipButton], + ); const headerLeftButton = React.useCallback( (headerProps: StackHeaderLeftButtonProps) => { if (headerProps.canGoBack) { return ; } return ( ); }, - [communityDrawerButtonOnLayout, props.navigation], + [communityDrawerButtonRegisterRef, 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-tips-context.react.js b/native/components/nux-tips-context.react.js index 92f998654..3f84388e0 100644 --- a/native/components/nux-tips-context.react.js +++ b/native/components/nux-tips-context.react.js @@ -1,137 +1,131 @@ // @flow import * as React from 'react'; +import type { MeasureOnSuccessCallback } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; import { values } from 'lib/utils/objects.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import type { NUXTipRouteNames } from '../navigation/route-names.js'; import { CommunityDrawerTipRouteName, MutedTabTipRouteName, HomeTabTipRouteName, IntroTipRouteName, } from '../navigation/route-names.js'; const nuxTip = Object.freeze({ INTRO: 'intro', COMMUNITY_DRAWER: 'community_drawer', HOME: 'home', MUTED: 'muted', }); export type NUXTip = $Values; type NUXTipParams = { +nextTip: ?NUXTip, +tooltipLocation: 'below' | 'above' | 'absolute', +routeName: NUXTipRouteNames, +exitingCallback?: ( navigation: AppNavigationProp, ) => void, }; const firstNUXTipKey = nuxTip.INTRO; const nuxTipParams: { +[NUXTip]: NUXTipParams } = { [nuxTip.INTRO]: { nextTip: nuxTip.COMMUNITY_DRAWER, tooltipLocation: 'absolute', routeName: IntroTipRouteName, }, [nuxTip.COMMUNITY_DRAWER]: { nextTip: nuxTip.HOME, tooltipLocation: 'below', routeName: CommunityDrawerTipRouteName, }, [nuxTip.HOME]: { nextTip: nuxTip.MUTED, tooltipLocation: 'below', routeName: HomeTabTipRouteName, }, [nuxTip.MUTED]: { nextTip: undefined, routeName: MutedTabTipRouteName, tooltipLocation: 'below', exitingCallback: navigation => navigation.goBack(), }, }; function getNUXTipParams(currentTipKey: NUXTip): NUXTipParams { return nuxTipParams[currentTipKey]; } -type TipProps = { - +x: number, - +y: number, - +width: number, - +height: number, - +pageX: number, - +pageY: number, -}; +type TipProps = (callback: MeasureOnSuccessCallback) => void; export type NUXTipsContextType = { +registerTipButton: (type: NUXTip, tipProps: ?TipProps) => void, +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 [tipsPropsState, setTipsPropsState] = React.useState<{ +[tip: NUXTip]: ?TipProps, }>(() => ({})); const registerTipButton = React.useCallback( (type: NUXTip, tipProps: ?TipProps) => { setTipsPropsState(currenttipsPropsState => { const newtipsPropsState = { ...currenttipsPropsState }; newtipsPropsState[type] = tipProps; return newtipsPropsState; }); }, [], ); const tipsProps = React.useMemo(() => { const result: { [tip: NUXTip]: TipProps } = {}; for (const type of values(nuxTip)) { if (nuxTipParams[type].tooltipLocation === 'absolute') { continue; } if (!tipsPropsState[type]) { return null; } result[type] = tipsPropsState[type]; } return result; }, [tipsPropsState]); const value = React.useMemo( () => ({ registerTipButton, tipsProps, }), [tipsProps, registerTipButton], ); return ( {children} ); } 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 b50b31445..c4c28a2f6 100644 --- a/native/tooltip/nux-tips-overlay.react.js +++ b/native/tooltip/nux-tips-overlay.react.js @@ -1,464 +1,498 @@ // @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 Button from '../components/button.react.js'; import { getNUXTipParams, NUXTipsContext, type NUXTip, } from '../components/nux-tips-context.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 = { container: { flex: 1, }, contentContainer: { flex: 1, overflow: 'hidden', }, items: { backgroundColor: 'tooltipBackground', borderRadius: 5, overflow: 'hidden', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 18, }, 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: 18, marginBottom: 10, }, buttonContainer: { alignSelf: 'flex-end', }, okButtonText: { fontSize: 18, color: 'panelForegroundLabel', textAlign: 'center', paddingVertical: 10, paddingHorizontal: 20, }, okButton: { backgroundColor: 'purpleButton', borderRadius: 8, }, }; export type NUXTipsOverlayParams = { +tipKey: NUXTip, }; export type NUXTipsOverlayProps = { +navigation: AppNavigationProp, +route: NavigationRoute, }; const marginVertical: number = 20; const marginHorizontal: number = 10; function createNUXTipsOverlay( ButtonComponent: ?React.ComponentType>, tipText: string, ): React.ComponentType> { function NUXTipsOverlay(props: NUXTipsOverlayProps) { const nuxTipContext = React.useContext(NUXTipsContext); const { navigation, route } = props; const dimensions = useSelector(state => state.dimensions); - const { initialCoordinates, verticalBounds } = React.useMemo(() => { + const [coordinates, setCoordinates] = React.useState(null); + + React.useEffect(() => { if (!ButtonComponent) { - const y = (dimensions.height * 2) / 5; - const x = dimensions.width / 2; - return { - initialCoordinates: { height: 0, width: 0, x, y }, - verticalBounds: { height: 0, y }, - }; + const yInitial = (dimensions.height * 2) / 5; + const xInitial = dimensions.width / 2; + setCoordinates({ + initialCoordinates: { height: 0, width: 0, x: xInitial, y: yInitial }, + verticalBounds: { height: 0, y: yInitial }, + }); + return; } - const tipProps = nuxTipContext?.tipsProps?.[route.params.tipKey]; - invariant(tipProps, 'button should be registered with nuxTipContext'); - const { pageX, pageY, width, height } = tipProps; - - return { - initialCoordinates: { height, width, x: pageX, y: pageY }, - verticalBounds: { height, y: pageY }, - }; + const measure = nuxTipContext?.tipsProps?.[route.params.tipKey]; + invariant(measure, 'button should be registered with nuxTipContext'); + + measure((x, y, width, height, pageX, pageY) => + setCoordinates({ + initialCoordinates: { height, width, x: pageX, y: pageY }, + verticalBounds: { height, y: pageY }, + }), + ); }, [dimensions, nuxTipContext?.tipsProps, route.params.tipKey]); 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(() => { + if (!coordinates) { + return {}; + } const fullScreenHeight = dimensions.height; - const top = verticalBounds.y; + const top = coordinates.verticalBounds.y; const bottom = - fullScreenHeight - verticalBounds.y - verticalBounds.height; + fullScreenHeight - + coordinates.verticalBounds.y - + coordinates.verticalBounds.height; return { ...styles.contentContainer, marginTop: top, marginBottom: bottom, }; - }, [ - dimensions.height, - styles.contentContainer, - verticalBounds.height, - verticalBounds.y, - ]); + }, [dimensions.height, styles.contentContainer, coordinates]); const buttonStyle = React.useMemo(() => { - const { x, y, width, height } = initialCoordinates; + if (!coordinates) { + return {}; + } + const { x, y, width, height } = coordinates.initialCoordinates; return { width: Math.ceil(width), height: Math.ceil(height), - marginTop: y - verticalBounds.y, + marginTop: y - coordinates.verticalBounds.y, marginLeft: x, }; - }, [initialCoordinates, verticalBounds]); + }, [coordinates]); const tipHorizontalOffsetRef = React.useRef(new Value(0)); const tipHorizontalOffset = tipHorizontalOffsetRef.current; const onTipContainerLayout = React.useCallback( (event: LayoutEvent) => { - const { x, width } = initialCoordinates; + if (!coordinates) { + return; + } + const { x, width } = coordinates.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], + [coordinates, dimensions.width, tipHorizontalOffset], ); const tipParams = getNUXTipParams(route.params.tipKey); const { tooltipLocation } = tipParams; const baseTipContainerStyle = React.useMemo(() => { + if (!coordinates) { + return {}; + } + const { initialCoordinates, verticalBounds } = coordinates; 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) + marginVertical; } else { style.bottom = dimensions.height - Math.max(y, verticalBounds.y) + marginVertical; } const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (tooltipLocation === 'absolute') { style.left = marginHorizontal; style.right = marginHorizontal; } else if (extraLeftSpace < extraRightSpace) { style.left = marginHorizontal; style.minWidth = width + 2 * extraLeftSpace; style.marginRight = 2 * marginHorizontal; } else { style.right = marginHorizontal; style.minWidth = width + 2 * extraRightSpace; style.marginLeft = 2 * marginHorizontal; } return style; - }, [ - dimensions.height, - dimensions.width, - initialCoordinates, - tooltipLocation, - verticalBounds.height, - verticalBounds.y, - ]); + }, [coordinates, dimensions.height, dimensions.width, tooltipLocation]); const triangleStyle = React.useMemo(() => { - const { x, width } = initialCoordinates; + if (!coordinates) { + return {}; + } + const { x, width } = coordinates.initialCoordinates; const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (extraLeftSpace < extraRightSpace) { return { alignSelf: 'flex-start', left: extraLeftSpace + (4 / 10) * width - marginHorizontal, }; } else { return { alignSelf: 'flex-end', right: extraRightSpace + (4 / 10) * width - marginHorizontal, }; } - }, [dimensions.width, initialCoordinates]); + }, [coordinates, dimensions.width]); // prettier-ignore const tipContainerEnteringAnimation = React.useCallback( (values/*: EntryAnimationsValues*/) => { 'worklet'; + if (!coordinates) { + return { + animations: {}, + initialValues:{}, + }; + } + if(tooltipLocation === 'absolute'){ return { animations: { opacity: withTiming(1, { duration: animationDuration }), transform: [ { scale: withTiming(1, { duration: animationDuration }) }, ], }, initialValues: { opacity: 0, transform: [ { scale: 0 }, ], }, }; } const initialX = (-values.targetWidth + - initialCoordinates.width + - initialCoordinates.x) / + coordinates.initialCoordinates.width + + coordinates.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], + [coordinates, tooltipLocation], ); // prettier-ignore const tipContainerExitingAnimation = React.useCallback( (values/*: ExitAnimationsValues*/) => { 'worklet'; + if (!coordinates) { + return { + animations: {}, + initialValues:{}, + }; + } + if (tooltipLocation === 'absolute') { return { animations: { opacity: withTiming(0, { duration: animationDuration }), transform: [ { scale: withTiming(0, { duration: animationDuration }) }, ], }, initialValues: { opacity: 1, transform: [{ scale: 1 }], }, callback: onExitFinish, }; } const toValueX = (-values.currentWidth + - initialCoordinates.width + - initialCoordinates.x) / + coordinates.initialCoordinates.width + + coordinates.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 }], }, callback: onExitFinish, }; }, - [ - initialCoordinates.width, - initialCoordinates.x, - onExitFinish, - tooltipLocation, - ], + [coordinates, onExitFinish, tooltipLocation], ); let triangleDown = null; let triangleUp = null; if (tooltipLocation === 'above') { triangleDown = ; } else if (tooltipLocation === 'below') { triangleUp = ; } const onPressOk = React.useCallback(() => { const { nextTip, exitingCallback } = tipParams; goBackOnce(); if (exitingCallback) { exitingCallback?.(navigation); } if (!nextTip) { return; } const { routeName } = getNUXTipParams(nextTip); navigation.navigate({ name: routeName, params: { tipKey: nextTip, }, }); }, [goBackOnce, navigation, tipParams]); const button = React.useMemo( () => ButtonComponent ? ( ) : undefined, [buttonStyle, contentContainerStyle, props.navigation, route], ); + if (!coordinates) { + return null; + } + return ( {button} {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, animationDuration };