diff --git a/native/components/nux-handler.react.js b/native/components/nux-handler.react.js
index ba4b192d8..2a14ab866 100644
--- a/native/components/nux-handler.react.js
+++ b/native/components/nux-handler.react.js
@@ -1,52 +1,39 @@
// @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 { NUXTipsContext } from './nux-tips-context.react.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.navigate<'NUXTipOverlayBackdrop'>({
+ name: 'NUXTipOverlayBackdrop',
});
}, [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 38e4a270c..2f6acd21b 100644
--- a/native/components/nux-tips-context.react.js
+++ b/native/components/nux-tips-context.react.js
@@ -1,122 +1,127 @@
// @flow
import * as React from 'react';
import { values } from 'lib/utils/objects.js';
+import type { AppNavigationProp } from '../navigation/app-navigator.react.js';
import {
CommunityDrawerTipRouteName,
MutedTabTipRouteName,
} from '../navigation/route-names.js';
import type { NUXTipRouteNames } 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,
+ +exitingCallback?: (
+ navigation: AppNavigationProp,
+ ) => void,
};
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',
+ exitingCallback: navigation => navigation.goBack(),
},
};
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,
+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 (!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/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js
index fb2248a39..ef246e3f0 100644
--- a/native/navigation/app-navigator.react.js
+++ b/native/navigation/app-navigator.react.js
@@ -1,174 +1,180 @@
// @flow
import * as SplashScreen from 'expo-splash-screen';
import * as React from 'react';
import { PersistGate } from 'redux-persist/es/integration/react.js';
import ActionResultModal from './action-result-modal.react.js';
import { CommunityDrawerNavigator } from './community-drawer-navigator.react.js';
import CommunityDrawerTip from './community-drawer-tip.react.js';
import MutedTabTip from './muted-tab-tip.react.js';
+import NUXTipOverlayBackdrop from './nux-tip-overlay-backdrop.react.js';
import { createOverlayNavigator } from './overlay-navigator.react.js';
import type {
OverlayNavigationProp,
OverlayNavigationHelpers,
} from './overlay-navigator.react.js';
import type { RootNavigationProp } from './root-navigator.react.js';
import {
CommunityDrawerTipRouteName,
MutedTabTipRouteName,
+ NUXTipOverlayBackdropRouteName,
} from './route-names.js';
import {
UserAvatarCameraModalRouteName,
ThreadAvatarCameraModalRouteName,
ImageModalRouteName,
MultimediaMessageTooltipModalRouteName,
ActionResultModalRouteName,
TextMessageTooltipModalRouteName,
ThreadSettingsMemberTooltipModalRouteName,
UserRelationshipTooltipModalRouteName,
RobotextMessageTooltipModalRouteName,
ChatCameraModalRouteName,
VideoPlaybackModalRouteName,
CommunityDrawerNavigatorRouteName,
type ScreenParamList,
type OverlayParamList,
TogglePinModalRouteName,
} from './route-names.js';
import MultimediaMessageTooltipModal from '../chat/multimedia-message-tooltip-modal.react.js';
import RobotextMessageTooltipModal from '../chat/robotext-message-tooltip-modal.react.js';
import ThreadSettingsMemberTooltipModal from '../chat/settings/thread-settings-member-tooltip-modal.react.js';
import TextMessageTooltipModal from '../chat/text-message-tooltip-modal.react.js';
import TogglePinModal from '../chat/toggle-pin-modal.react.js';
import KeyboardStateContainer from '../keyboard/keyboard-state-container.react.js';
import ChatCameraModal from '../media/chat-camera-modal.react.js';
import ImageModal from '../media/image-modal.react.js';
import ThreadAvatarCameraModal from '../media/thread-avatar-camera-modal.react.js';
import UserAvatarCameraModal from '../media/user-avatar-camera-modal.react.js';
import VideoPlaybackModal from '../media/video-playback-modal.react.js';
import UserRelationshipTooltipModal from '../profile/user-relationship-tooltip-modal.react.js';
import PushHandler from '../push/push-handler.react.js';
import { getPersistor } from '../redux/persist.js';
import { useSelector } from '../redux/redux-utils.js';
import { RootContext } from '../root-context.js';
import { useLoadCommFonts } from '../themes/fonts.js';
import { waitForInteractions } from '../utils/timers.js';
let splashScreenHasHidden = false;
export type AppNavigationProp<
RouteName: $Keys = $Keys,
> = OverlayNavigationProp;
const App = createOverlayNavigator<
ScreenParamList,
OverlayParamList,
OverlayNavigationHelpers,
>();
type AppNavigatorProps = {
navigation: RootNavigationProp<'App'>,
...
};
function AppNavigator(props: AppNavigatorProps): React.Node {
const { navigation } = props;
const fontsLoaded = useLoadCommFonts();
const rootContext = React.useContext(RootContext);
const storeLoadedFromLocalDatabase = useSelector(state => state.storeLoaded);
const setNavStateInitialized =
rootContext && rootContext.setNavStateInitialized;
React.useEffect(() => {
setNavStateInitialized && setNavStateInitialized();
}, [setNavStateInitialized]);
const [localSplashScreenHasHidden, setLocalSplashScreenHasHidden] =
React.useState(splashScreenHasHidden);
React.useEffect(() => {
if (localSplashScreenHasHidden || !fontsLoaded) {
return;
}
splashScreenHasHidden = true;
void (async () => {
await waitForInteractions();
try {
await SplashScreen.hideAsync();
} finally {
setLocalSplashScreenHasHidden(true);
}
})();
}, [localSplashScreenHasHidden, fontsLoaded]);
let pushHandler;
if (localSplashScreenHasHidden) {
pushHandler = (
);
}
if (!storeLoadedFromLocalDatabase) {
return null;
}
return (
+
{pushHandler}
);
}
export default AppNavigator;
diff --git a/native/navigation/nux-tip-overlay-backdrop.react.js b/native/navigation/nux-tip-overlay-backdrop.react.js
new file mode 100644
index 000000000..b98da3193
--- /dev/null
+++ b/native/navigation/nux-tip-overlay-backdrop.react.js
@@ -0,0 +1,105 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+import { withTiming } from 'react-native-reanimated';
+
+import type { AppNavigationProp } from './app-navigator.react.js';
+import { OverlayContext } from './overlay-context.js';
+import type { NUXTipRouteNames, NavigationRoute } from './route-names';
+import {
+ firstNUXTipKey,
+ getNUXTipParams,
+} from '../components/nux-tips-context.react.js';
+import { useStyles } from '../themes/colors.js';
+import { animationDuration } from '../tooltip/nux-tips-overlay.react.js';
+import { AnimatedView } from '../types/styles.js';
+
+type Props = {
+ +navigation: AppNavigationProp<'NUXTipOverlayBackdrop'>,
+ +route: NavigationRoute<'NUXTipOverlayBackdrop'>,
+};
+
+function NUXTipOverlayBackdrop(props: Props): React.Node {
+ const overlayContext = React.useContext(OverlayContext);
+ invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext');
+ const { shouldRenderScreenContent } = overlayContext;
+
+ return shouldRenderScreenContent ? (
+
+ ) : null;
+}
+
+function opacityEnteringAnimation() {
+ 'worklet';
+
+ return {
+ animations: {
+ opacity: withTiming(0.7, { duration: animationDuration }),
+ },
+ initialValues: {
+ opacity: 0,
+ },
+ };
+}
+
+function NUXTipOverlayBackdropInner(props: Props): React.Node {
+ const overlayContext = React.useContext(OverlayContext);
+ invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext');
+ const { onExitFinish } = overlayContext;
+
+ const styles = useStyles(unboundStyles);
+
+ const opacityExitingAnimation = React.useCallback(() => {
+ 'worklet';
+
+ return {
+ animations: {
+ opacity: withTiming(0, { duration: animationDuration }),
+ },
+ initialValues: {
+ opacity: 0.7,
+ },
+ callback: onExitFinish,
+ };
+ }, [onExitFinish]);
+
+ const { nextTip, tooltipLocation, nextRouteName } =
+ getNUXTipParams(firstNUXTipKey);
+ invariant(nextRouteName && nextTip, 'first nux tip should be defined');
+
+ React.useEffect(
+ () =>
+ props.navigation.navigate({
+ name: nextRouteName,
+ params: {
+ tipKey: nextTip,
+ tooltipLocation,
+ },
+ }),
+ // We want this effect to run exactly once, when this component is mounted
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ return (
+
+ );
+}
+
+const unboundStyles = {
+ backdrop: {
+ backgroundColor: 'black',
+ bottom: 0,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+};
+
+export default NUXTipOverlayBackdrop;
diff --git a/native/navigation/overlay-navigator.react.js b/native/navigation/overlay-navigator.react.js
index e602c5514..2b1928817 100644
--- a/native/navigation/overlay-navigator.react.js
+++ b/native/navigation/overlay-navigator.react.js
@@ -1,575 +1,577 @@
// @flow
import type {
StackNavigationState,
NavigatorPropsBase,
ExtraNavigatorPropsBase,
CreateNavigator,
StackNavigationProp,
ParamListBase,
StackNavigationHelpers,
ScreenListeners,
StackRouterOptions,
Descriptor,
Route,
} from '@react-navigation/core';
import {
useNavigationBuilder,
createNavigatorFactory,
NavigationHelpersContext,
} from '@react-navigation/native';
import { TransitionPresets } from '@react-navigation/stack';
import invariant from 'invariant';
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import Animated, { EasingNode } from 'react-native-reanimated';
import { values } from 'lib/utils/objects.js';
import {
OverlayContext,
type VisibleOverlay,
type ScrollBlockingModalStatus,
} from './overlay-context.js';
import OverlayRouter from './overlay-router.js';
import type {
OverlayRouterExtraNavigationHelpers,
OverlayRouterNavigationAction,
} from './overlay-router.js';
import {
scrollBlockingModals,
TabNavigatorRouteName,
CommunityDrawerTipRouteName,
MutedTabTipRouteName,
+ NUXTipOverlayBackdropRouteName,
} from './route-names.js';
import { isMessageTooltipKey } from '../chat/utils.js';
const newReanimatedRoutes = new Set([
CommunityDrawerTipRouteName,
MutedTabTipRouteName,
+ NUXTipOverlayBackdropRouteName,
]);
export type OverlayNavigationHelpers =
{
...$Exact>,
...OverlayRouterExtraNavigationHelpers,
...
};
export type OverlayNavigationProp<
ParamList: ParamListBase = ParamListBase,
RouteName: $Keys = $Keys,
> = {
...StackNavigationProp,
...OverlayRouterExtraNavigationHelpers,
};
const { Value, timing, cond, call, lessOrEq, block } = Animated;
type Scene = {
+route: Route<>,
+descriptor: Descriptor, {}>,
+context: {
+position: ?Value,
+shouldRenderScreenContent: boolean,
+onExitFinish?: () => void,
+isDismissing: boolean,
},
+ordering: {
+routeIndex: number,
},
};
type SceneData = $ReadOnly<{
...Scene,
+context: $ReadOnly<{
...$PropertyType,
+visibleOverlays: $ReadOnlyArray,
+scrollBlockingModalStatus: ScrollBlockingModalStatus,
+setScrollBlockingModalStatus: ScrollBlockingModalStatus => void,
+resetScrollBlockingModalStatus: () => void,
}>,
+ordering: $ReadOnly<{
...$PropertyType,
+creationTime: number,
}>,
+listeners: $ReadOnlyArray,
}>;
type Props = $Exact<
NavigatorPropsBase<
{},
ScreenListeners,
OverlayNavigationHelpers<>,
>,
>;
const OverlayNavigator = React.memo(
({ initialRouteName, children, screenOptions, screenListeners }: Props) => {
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState,
OverlayRouterNavigationAction,
{},
StackRouterOptions,
OverlayNavigationHelpers<>,
{},
ExtraNavigatorPropsBase,
>(OverlayRouter, {
children,
screenOptions,
screenListeners,
initialRouteName,
});
const curIndex = state.index;
const positionRefs = React.useRef<{ [string]: Animated.Value }>({});
const positions = positionRefs.current;
const firstRenderRef = React.useRef(true);
React.useEffect(() => {
firstRenderRef.current = false;
}, [firstRenderRef]);
const firstRender = firstRenderRef.current;
const { routes } = state;
const scenes = React.useMemo(
() =>
routes.map((route, routeIndex) => {
const descriptor = descriptors[route.key];
invariant(
descriptor,
`OverlayNavigator could not find descriptor for ${route.key}`,
);
const shouldUseLegacyAnimation = !newReanimatedRoutes.has(route.name);
if (!positions[route.key] && shouldUseLegacyAnimation) {
positions[route.key] = new Value(firstRender ? 1 : 0);
}
return {
route,
descriptor,
context: {
position: positions[route.key],
isDismissing: curIndex < routeIndex,
shouldRenderScreenContent: true,
},
ordering: {
routeIndex,
},
};
}),
// We don't include descriptors here because they can change on every
// render. We know that they should only substantially change if something
// about the underlying route has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[positions, routes, curIndex],
);
const prevScenesRef = React.useRef$ReadOnlyArray>();
const prevScenes = prevScenesRef.current;
const visibleOverlayEntryForNewScene = (scene: Scene) => {
const { route } = scene;
if (route.name === TabNavigatorRouteName) {
// We don't consider the TabNavigator at the bottom to be an overlay
return undefined;
}
const presentedFrom =
typeof route.params?.presentedFrom === 'string'
? route.params.presentedFrom
: undefined;
return {
routeKey: route.key,
routeName: route.name,
position: positions[route.key],
shouldRenderScreenContent: true,
presentedFrom,
};
};
const visibleOverlaysRef = React.useRef$ReadOnlyArray>();
if (!visibleOverlaysRef.current) {
visibleOverlaysRef.current = scenes
.map(visibleOverlayEntryForNewScene)
.filter(Boolean);
}
let visibleOverlays = visibleOverlaysRef.current;
// The scrollBlockingModalStatus state gets incorporated into the
// OverlayContext, but it's global to the navigator rather than local to
// each screen. Note that we also include the setter in OverlayContext. We
// do this so that screens can freeze ScrollViews as quickly as possible to
// avoid drags after onLongPress is triggered
const getScrollBlockingModalStatus = (
data: $ReadOnlyArray,
) => {
let status = 'closed';
for (const scene of data) {
if (!scrollBlockingModals.includes(scene.route.name)) {
continue;
}
if (!scene.context.isDismissing) {
status = 'open';
break;
}
status = 'closing';
}
return status;
};
const [scrollBlockingModalStatus, setScrollBlockingModalStatus] =
React.useState(() => getScrollBlockingModalStatus(scenes));
const resetScrollBlockingModalStatus = React.useCallback(() => {
setScrollBlockingModalStatus(
getScrollBlockingModalStatus(prevScenesRef.current ?? []),
);
}, []);
const sceneDataForNewScene = (scene: Scene) => ({
...scene,
context: {
...scene.context,
visibleOverlays,
scrollBlockingModalStatus,
setScrollBlockingModalStatus,
resetScrollBlockingModalStatus,
},
ordering: {
...scene.ordering,
creationTime: Date.now(),
},
listeners: [],
});
// We track two previous states of scrollBlockingModalStatus via refs. We
// need two because we expose setScrollBlockingModalStatus to screens. We
// track the previous sceneData-determined value separately so that we only
// overwrite the screen-determined value with the sceneData-determined value
// when the latter actually changes
const prevScrollBlockingModalStatusRef = React.useRef(
scrollBlockingModalStatus,
);
const prevScrollBlockingModalStatus =
prevScrollBlockingModalStatusRef.current;
const prevScrollBlockingModalStatusFromSceneDataRef = React.useRef(
scrollBlockingModalStatus,
);
const prevScrollBlockingModalStatusFromSceneData =
prevScrollBlockingModalStatusFromSceneDataRef.current;
// We need state to continue rendering screens while they are dismissing
const [sceneData, setSceneData] = React.useState(() => {
const newSceneData: { [string]: SceneData } = {};
for (const scene of scenes) {
const { key } = scene.route;
newSceneData[key] = sceneDataForNewScene(scene);
}
return newSceneData;
});
const prevSceneDataRef = React.useRef(sceneData);
const prevSceneData = prevSceneDataRef.current;
// We need to initiate animations in useEffect blocks, but because we
// setState within render we might have multiple renders before the
// useEffect triggers. So we cache whether or not new animations should be
// started in this ref
const pendingAnimationsRef = React.useRef<{ [key: string]: number }>({});
const queueAnimation = (key: string, toValue: number) => {
pendingAnimationsRef.current = {
...pendingAnimationsRef.current,
[key]: toValue,
};
};
// This block keeps sceneData updated when our props change. It's the
// hook equivalent of getDerivedStateFromProps
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
const updatedSceneData = { ...sceneData };
let sceneDataChanged = false;
if (prevScenes && scenes !== prevScenes) {
const currentKeys = new Set();
for (const scene of scenes) {
const { key } = scene.route;
currentKeys.add(key);
let data = updatedSceneData[key];
if (!data) {
// A new route has been pushed
const newVisibleOverlayEntry = visibleOverlayEntryForNewScene(scene);
if (newVisibleOverlayEntry) {
visibleOverlays = [...visibleOverlays, newVisibleOverlayEntry];
}
updatedSceneData[key] = sceneDataForNewScene(scene);
sceneDataChanged = true;
queueAnimation(key, 1);
continue;
}
let dataChanged = false;
if (scene.route !== data.route) {
data = { ...data, route: scene.route };
dataChanged = true;
}
if (scene.descriptor !== data.descriptor) {
data = { ...data, descriptor: scene.descriptor };
// We don't set dataChanged here because descriptors get recomputed on
// every render, which means we could get an infinite loop. However,
// we want to update the descriptor whenever anything else changes, so
// that if and when our scene is dismissed, the sceneData has the most
// recent descriptor
}
if (scene.context.isDismissing !== data.context.isDismissing) {
data = { ...data, context: { ...data.context, ...scene.context } };
dataChanged = true;
}
if (scene.ordering.routeIndex !== data.ordering.routeIndex) {
data = { ...data, ordering: { ...data.ordering, ...scene.ordering } };
dataChanged = true;
}
if (dataChanged) {
// Something about an existing route has changed
updatedSceneData[key] = data;
sceneDataChanged = true;
}
}
for (let i = 0; i < prevScenes.length; i++) {
const scene = prevScenes[i];
const { key } = scene.route;
if (currentKeys.has(key)) {
continue;
}
currentKeys.add(key);
const data = updatedSceneData[key];
invariant(data, `should have sceneData for dismissed key ${key}`);
if (!visibleOverlayEntryForNewScene(scene)) {
// This should only happen if TabNavigator gets dismissed
// TabNavigator doesn't normally ever get dismissed, but hot reload
// can cause that to happen. We don't need to animate TabNavigator
// closed, and in fact we would crash if we tried. So we short-circuit
// the logic below
delete updatedSceneData[key];
sceneDataChanged = true;
continue;
}
// A route just got dismissed
// We'll watch the animation to determine when to clear the screen
const { position } = data.context;
const removeScreen = () => {
// This gets called when the scene is no longer visible and
// handles cleaning up our data structures to remove it
const curVisibleOverlays = visibleOverlaysRef.current;
invariant(curVisibleOverlays, 'visibleOverlaysRef should be set');
const newVisibleOverlays = curVisibleOverlays.filter(
(overlay: VisibleOverlay) => overlay.routeKey !== key,
);
if (newVisibleOverlays.length === curVisibleOverlays.length) {
return;
}
visibleOverlaysRef.current = newVisibleOverlays;
setSceneData(curSceneData => {
const newSceneData: { [string]: SceneData } = {};
for (const sceneKey in curSceneData) {
if (sceneKey === key) {
continue;
}
newSceneData[sceneKey] = {
...curSceneData[sceneKey],
context: {
...curSceneData[sceneKey].context,
visibleOverlays: newVisibleOverlays,
},
};
}
return newSceneData;
});
};
const listeners = position
? [cond(lessOrEq(position, 0), call([], removeScreen))]
: [];
updatedSceneData[key] = {
...data,
context: {
...data.context,
isDismissing: true,
shouldRenderScreenContent: false,
onExitFinish: removeScreen,
},
listeners,
};
sceneDataChanged = true;
queueAnimation(key, 0);
}
}
if (visibleOverlays !== visibleOverlaysRef.current) {
// This indicates we have pushed a new route. Let's make sure every
// sceneData has the updated visibleOverlays
for (const sceneKey in updatedSceneData) {
updatedSceneData[sceneKey] = {
...updatedSceneData[sceneKey],
context: {
...updatedSceneData[sceneKey].context,
visibleOverlays,
},
};
}
visibleOverlaysRef.current = visibleOverlays;
sceneDataChanged = true;
}
const pendingAnimations = pendingAnimationsRef.current;
React.useEffect(() => {
if (Object.keys(pendingAnimations).length === 0) {
return;
}
for (const key in pendingAnimations) {
const position = positions[key];
if (!position) {
continue;
}
const toValue = pendingAnimations[key];
let duration = 150;
if (isMessageTooltipKey(key)) {
const navigationTransitionSpec =
toValue === 0
? TransitionPresets.DefaultTransition.transitionSpec.close
: TransitionPresets.DefaultTransition.transitionSpec.open;
duration =
(navigationTransitionSpec.animation === 'timing' &&
navigationTransitionSpec.config.duration) ||
400;
}
timing(position, {
duration,
easing: EasingNode.inOut(EasingNode.ease),
toValue,
}).start();
}
pendingAnimationsRef.current = {};
}, [positions, pendingAnimations]);
// If sceneData changes, we update scrollBlockingModalStatus based on it,
// both in state and within the individual sceneData contexts.
// If sceneData doesn't change,
// it's still possible for scrollBlockingModalStatus to change via the
// setScrollBlockingModalStatus callback we expose via context
let newScrollBlockingModalStatus;
if (sceneDataChanged || sceneData !== prevSceneData) {
const statusFromSceneData = getScrollBlockingModalStatus(
values(updatedSceneData),
);
if (
statusFromSceneData !== scrollBlockingModalStatus &&
statusFromSceneData !== prevScrollBlockingModalStatusFromSceneData
) {
newScrollBlockingModalStatus = statusFromSceneData;
}
prevScrollBlockingModalStatusFromSceneDataRef.current =
statusFromSceneData;
}
if (
!newScrollBlockingModalStatus &&
scrollBlockingModalStatus !== prevScrollBlockingModalStatus
) {
newScrollBlockingModalStatus = scrollBlockingModalStatus;
}
if (newScrollBlockingModalStatus) {
if (newScrollBlockingModalStatus !== scrollBlockingModalStatus) {
setScrollBlockingModalStatus(newScrollBlockingModalStatus);
}
for (const key in updatedSceneData) {
const data = updatedSceneData[key];
updatedSceneData[key] = {
...data,
context: {
...data.context,
scrollBlockingModalStatus: newScrollBlockingModalStatus,
},
};
}
sceneDataChanged = true;
}
if (sceneDataChanged) {
setSceneData(updatedSceneData);
}
// Usually this would be done in an effect,
// but calling setState from the body
// of a hook causes the hook to rerender before triggering effects. To avoid
// infinite loops we make sure to set our prev values after we finish
// comparing them
prevScenesRef.current = scenes;
prevSceneDataRef.current = sceneDataChanged ? updatedSceneData : sceneData;
prevScrollBlockingModalStatusRef.current = newScrollBlockingModalStatus
? newScrollBlockingModalStatus
: scrollBlockingModalStatus;
const sceneList = values(updatedSceneData).sort((a, b) => {
const routeIndexDifference =
a.ordering.routeIndex - b.ordering.routeIndex;
if (routeIndexDifference) {
return routeIndexDifference;
}
return a.ordering.creationTime - b.ordering.creationTime;
});
const screens = sceneList.map(scene => {
const { route, descriptor, context, listeners } = scene;
const { render } = descriptor;
const pressable = !context.isDismissing && !route.params?.preventPresses;
const pointerEvents = pressable ? 'auto' : 'none';
// These listeners are used to clear routes after they finish dismissing
const listenerCode =
listeners.length > 0 ? : null;
return (
{render()}
{listenerCode}
);
});
return (
{screens}
);
},
);
OverlayNavigator.displayName = 'OverlayNavigator';
const createOverlayNavigator: CreateNavigator<
StackNavigationState,
{},
{},
ExtraNavigatorPropsBase,
> = createNavigatorFactory<
StackNavigationState,
{},
{},
OverlayNavigationHelpers<>,
ExtraNavigatorPropsBase,
>(OverlayNavigator);
const styles = StyleSheet.create({
container: {
flex: 1,
},
scene: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});
export { createOverlayNavigator };
diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js
index 623993525..186ab7747 100644
--- a/native/navigation/route-names.js
+++ b/native/navigation/route-names.js
@@ -1,391 +1,393 @@
// @flow
import type { RouteProp } from '@react-navigation/core';
import type { ActionResultModalParams } from './action-result-modal.react.js';
import type { InviteLinkModalParams } from './invite-link-modal.react';
import type { AvatarSelectionParams } from '../account/registration/avatar-selection.react.js';
import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js';
import type { ConnectFarcasterParams } from '../account/registration/connect-farcaster.react.js';
import type { EmojiAvatarSelectionParams } from '../account/registration/emoji-avatar-selection.react.js';
import type { ExistingEthereumAccountParams } from '../account/registration/existing-ethereum-account.react.js';
import type { KeyserverSelectionParams } from '../account/registration/keyserver-selection.react.js';
import type { PasswordSelectionParams } from '../account/registration/password-selection.react.js';
import type { RegistrationTermsParams } from '../account/registration/registration-terms.react.js';
import type { CreateSIWEBackupMessageParams } from '../account/registration/siwe-backup-message-creation.react.js';
import type { UsernameSelectionParams } from '../account/registration/username-selection.react.js';
import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react.js';
import type { RestoreSIWEBackupParams } from '../backup/restore-siwe-backup.react.js';
import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react.js';
import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react.js';
import type { FullScreenThreadMediaGalleryParams } from '../chat/fullscreen-thread-media-gallery.react.js';
import type { ImagePasteModalParams } from '../chat/image-paste-modal.react.js';
import type { MessageListParams } from '../chat/message-list-types.js';
import type { MessageReactionsModalParams } from '../chat/message-reactions-modal.react.js';
import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react.js';
import type { PinnedMessagesScreenParams } from '../chat/pinned-messages-screen.react.js';
import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react.js';
import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react.js';
import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react.js';
import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react.js';
import type { DeleteThreadParams } from '../chat/settings/delete-thread.react.js';
import type { EmojiThreadAvatarCreationParams } from '../chat/settings/emoji-thread-avatar-creation.react.js';
import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react.js';
import type { ThreadSettingsNotificationsParams } from '../chat/settings/thread-settings-notifications.react.js';
import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react.js';
import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react.js';
import type { SubchannelListModalParams } from '../chat/subchannels-list-modal.react.js';
import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react.js';
import type { TogglePinModalParams } from '../chat/toggle-pin-modal.react.js';
import type { TagFarcasterChannelByNameParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js';
import type { TagFarcasterChannelParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js';
import type { InviteLinksNavigatorParams } from '../invite-links/invite-links-navigator.react.js';
import type { ManagePublicLinkScreenParams } from '../invite-links/manage-public-link-screen.react.js';
import type { ViewInviteLinksScreenParams } from '../invite-links/view-invite-links-screen.react.js';
import type { ChatCameraModalParams } from '../media/chat-camera-modal.react.js';
import type { ImageModalParams } from '../media/image-modal.react.js';
import type { ThreadAvatarCameraModalParams } from '../media/thread-avatar-camera-modal.react.js';
import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js';
import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js';
import type { KeyserverSelectionBottomSheetParams } from '../profile/keyserver-selection-bottom-sheet.react.js';
import type { UserRelationshipTooltipModalParams } from '../profile/user-relationship-tooltip-modal.react.js';
import type { ChangeRolesScreenParams } from '../roles/change-roles-screen.react.js';
import type { CommunityRolesScreenParams } from '../roles/community-roles-screen.react.js';
import type { CreateRolesScreenParams } from '../roles/create-roles-screen.react.js';
import type { MessageSearchParams } from '../search/message-search.react.js';
import type { NUXTipsOverlayParams } from '../tooltip/nux-tips-overlay.react.js';
import type { UserProfileAvatarModalParams } from '../user-profile/user-profile-avatar-modal.react.js';
import type { UserProfileBottomSheetParams } from '../user-profile/user-profile-bottom-sheet.react.js';
export const ActionResultModalRouteName = 'ActionResultModal';
export const AddUsersModalRouteName = 'AddUsersModal';
export const AppearancePreferencesRouteName = 'AppearancePreferences';
export const AppRouteName = 'App';
export const AppsRouteName = 'Apps';
export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList';
export const BackupMenuRouteName = 'BackupMenu';
export const BlockListRouteName = 'BlockList';
export const BuildInfoRouteName = 'BuildInfo';
export const CalendarRouteName = 'Calendar';
export const CalendarScreenRouteName = 'CalendarScreen';
export const ChangeRolesScreenRouteName = 'ChangeRolesScreen';
export const ChatCameraModalRouteName = 'ChatCameraModal';
export const ChatRouteName = 'Chat';
export const ChatThreadListRouteName = 'ChatThreadList';
export const ColorSelectorModalRouteName = 'ColorSelectorModal';
export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal';
export const ComposeSubchannelRouteName = 'ComposeSubchannel';
export const CommunityDrawerNavigatorRouteName = 'CommunityDrawerNavigator';
export const CustomServerModalRouteName = 'CustomServerModal';
export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications';
export const DeleteAccountRouteName = 'DeleteAccount';
export const DeleteThreadRouteName = 'DeleteThread';
export const DevToolsRouteName = 'DevTools';
export const EditPasswordRouteName = 'EditPassword';
export const EmojiThreadAvatarCreationRouteName = 'EmojiThreadAvatarCreation';
export const EmojiUserAvatarCreationRouteName = 'EmojiUserAvatarCreation';
export const FriendListRouteName = 'FriendList';
export const FullScreenThreadMediaGalleryRouteName =
'FullScreenThreadMediaGallery';
export const HomeChatThreadListRouteName = 'HomeChatThreadList';
export const ImageModalRouteName = 'ImageModal';
export const ImagePasteModalRouteName = 'ImagePasteModal';
export const InviteLinkModalRouteName = 'InviteLinkModal';
export const InviteLinkNavigatorRouteName = 'InviteLinkNavigator';
export const LinkedDevicesRouteName = 'LinkedDevices';
export const LinkedDevicesBottomSheetRouteName = 'LinkedDevicesBottomSheet';
export const LoggedOutModalRouteName = 'LoggedOutModal';
export const ManagePublicLinkRouteName = 'ManagePublicLink';
export const MessageListRouteName = 'MessageList';
export const MessageReactionsModalRouteName = 'MessageReactionsModal';
export const PinnedMessagesScreenRouteName = 'PinnedMessagesScreen';
export const MultimediaMessageTooltipModalRouteName =
'MultimediaMessageTooltipModal';
export const PrivacyPreferencesRouteName = 'PrivacyPreferences';
export const ProfileRouteName = 'Profile';
export const ProfileScreenRouteName = 'ProfileScreen';
export const UserRelationshipTooltipModalRouteName =
'UserRelationshipTooltipModal';
export const RobotextMessageTooltipModalRouteName =
'RobotextMessageTooltipModal';
export const SecondaryDeviceQRCodeScannerRouteName =
'SecondaryDeviceQRCodeScanner';
export const SidebarListModalRouteName = 'SidebarListModal';
export const SubchannelsListModalRouteName = 'SubchannelsListModal';
export const TabNavigatorRouteName = 'TabNavigator';
export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal';
export const ThreadAvatarCameraModalRouteName = 'ThreadAvatarCameraModal';
export const ThreadPickerModalRouteName = 'ThreadPickerModal';
export const ThreadSettingsMemberTooltipModalRouteName =
'ThreadSettingsMemberTooltipModal';
export const ThreadSettingsRouteName = 'ThreadSettings';
export const TunnelbrokerMenuRouteName = 'TunnelbrokerMenu';
export const UserAvatarCameraModalRouteName = 'UserAvatarCameraModal';
export const TogglePinModalRouteName = 'TogglePinModal';
export const VideoPlaybackModalRouteName = 'VideoPlaybackModal';
export const ViewInviteLinksRouteName = 'ViewInviteLinks';
export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal';
export const RegistrationRouteName = 'Registration';
export const KeyserverSelectionRouteName = 'KeyserverSelection';
export const CoolOrNerdModeSelectionRouteName = 'CoolOrNerdModeSelection';
export const ConnectEthereumRouteName = 'ConnectEthereum';
export const CreateSIWEBackupMessageRouteName = 'CreateSIWEBackupMessage';
export const CreateMissingSIWEBackupMessageRouteName =
'CreateMissingSIWEBackupMessage';
export const RestoreSIWEBackupRouteName = 'RestoreSIWEBackup';
export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount';
export const ConnectFarcasterRouteName = 'ConnectFarcaster';
export const UsernameSelectionRouteName = 'UsernameSelection';
export const CommunityCreationRouteName = 'CommunityCreation';
export const CommunityConfigurationRouteName = 'CommunityConfiguration';
export const MessageSearchRouteName = 'MessageSearch';
export const PasswordSelectionRouteName = 'PasswordSelection';
export const AvatarSelectionRouteName = 'AvatarSelection';
export const EmojiAvatarSelectionRouteName = 'EmojiAvatarSelection';
export const RegistrationUserAvatarCameraModalRouteName =
'RegistrationUserAvatarCameraModal';
export const RegistrationTermsRouteName = 'RegistrationTerms';
export const RolesNavigatorRouteName = 'RolesNavigator';
export const CommunityRolesScreenRouteName = 'CommunityRolesScreen';
export const CreateRolesScreenRouteName = 'CreateRolesScreen';
export const QRCodeSignInNavigatorRouteName = 'QRCodeSignInNavigator';
export const QRCodeScreenRouteName = 'QRCodeScreen';
export const UserProfileBottomSheetNavigatorRouteName =
'UserProfileBottomSheetNavigator';
export const UserProfileBottomSheetRouteName = 'UserProfileBottomSheet';
export const UserProfileAvatarModalRouteName = 'UserProfileAvatarModal';
export const KeyserverSelectionListRouteName = 'KeyserverSelectionList';
export const AddKeyserverRouteName = 'AddKeyserver';
export const KeyserverSelectionBottomSheetRouteName =
'KeyserverSelectionBottomSheet';
export const AccountDoesNotExistRouteName = 'AccountDoesNotExist';
export const FarcasterAccountSettingsRouteName = 'FarcasterAccountSettings';
export const ConnectFarcasterBottomSheetRouteName =
'ConnectFarcasterBottomSheet';
export const TagFarcasterChannelNavigatorRouteName =
'TagFarcasterChannelNavigator';
export const TagFarcasterChannelRouteName = 'TagFarcasterChannel';
export const TagFarcasterChannelByNameRouteName = 'TagFarcasterChannelByName';
export const ThreadSettingsNotificationsRouteName =
'ThreadSettingsNotifications';
export const CommunityDrawerTipRouteName = 'CommunityDrawerTip';
export const MutedTabTipRouteName = 'MutedTabTip';
+export const NUXTipOverlayBackdropRouteName = 'NUXTipOverlayBackdrop';
export type RootParamList = {
+LoggedOutModal: void,
+App: void,
+ThreadPickerModal: ThreadPickerModalParams,
+AddUsersModal: AddUsersModalParams,
+CustomServerModal: CustomServerModalParams,
+ColorSelectorModal: ColorSelectorModalParams,
+ComposeSubchannelModal: ComposeSubchannelModalParams,
+SidebarListModal: SidebarListModalParams,
+ImagePasteModal: ImagePasteModalParams,
+TermsAndPrivacyModal: TermsAndPrivacyModalParams,
+SubchannelsListModal: SubchannelListModalParams,
+MessageReactionsModal: MessageReactionsModalParams,
+Registration: void,
+CommunityCreation: void,
+InviteLinkModal: InviteLinkModalParams,
+InviteLinkNavigator: InviteLinksNavigatorParams,
+RolesNavigator: void,
+QRCodeSignInNavigator: void,
+UserProfileBottomSheetNavigator: void,
+TunnelbrokerMenu: void,
+KeyserverSelectionBottomSheet: KeyserverSelectionBottomSheetParams,
+LinkedDevicesBottomSheet: void,
+ConnectFarcasterBottomSheet: void,
+TagFarcasterChannelNavigator: void,
+CreateMissingSIWEBackupMessage: void,
+RestoreSIWEBackup: RestoreSIWEBackupParams,
};
export type NUXTipRouteNames =
| typeof CommunityDrawerTipRouteName
| typeof MutedTabTipRouteName;
export type MessageTooltipRouteNames =
| typeof RobotextMessageTooltipModalRouteName
| typeof MultimediaMessageTooltipModalRouteName
| typeof TextMessageTooltipModalRouteName;
export const PinnableMessageTooltipRouteNames = [
TextMessageTooltipModalRouteName,
MultimediaMessageTooltipModalRouteName,
];
export type TooltipModalParamList = {
+MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams,
+TextMessageTooltipModal: TextMessageTooltipModalParams,
+ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams,
+UserRelationshipTooltipModal: UserRelationshipTooltipModalParams,
+RobotextMessageTooltipModal: RobotextMessageTooltipModalParams,
};
export type OverlayParamList = {
+CommunityDrawerNavigator: void,
+ImageModal: ImageModalParams,
+ActionResultModal: ActionResultModalParams,
+ChatCameraModal: ChatCameraModalParams,
+UserAvatarCameraModal: void,
+ThreadAvatarCameraModal: ThreadAvatarCameraModalParams,
+VideoPlaybackModal: VideoPlaybackModalParams,
+TogglePinModal: TogglePinModalParams,
+CommunityDrawerTip: NUXTipsOverlayParams,
+MutedTabTip: NUXTipsOverlayParams,
+ +NUXTipOverlayBackdrop: void,
...TooltipModalParamList,
};
export type TabParamList = {
+Calendar: void,
+Chat: void,
+Profile: void,
+Apps: void,
};
export type ChatParamList = {
+ChatThreadList: void,
+MessageList: MessageListParams,
+ComposeSubchannel: ComposeSubchannelParams,
+ThreadSettings: ThreadSettingsParams,
+EmojiThreadAvatarCreation: EmojiThreadAvatarCreationParams,
+DeleteThread: DeleteThreadParams,
+FullScreenThreadMediaGallery: FullScreenThreadMediaGalleryParams,
+PinnedMessagesScreen: PinnedMessagesScreenParams,
+MessageSearch: MessageSearchParams,
+ChangeRolesScreen: ChangeRolesScreenParams,
+ThreadSettingsNotifications: ThreadSettingsNotificationsParams,
};
export type ChatTopTabsParamList = {
+HomeChatThreadList: void,
+BackgroundChatThreadList: void,
};
export type ProfileParamList = {
+ProfileScreen: void,
+EmojiUserAvatarCreation: void,
+EditPassword: void,
+DeleteAccount: void,
+BuildInfo: void,
+DevTools: void,
+AppearancePreferences: void,
+PrivacyPreferences: void,
+DefaultNotifications: void,
+FriendList: void,
+BlockList: void,
+LinkedDevices: void,
+SecondaryDeviceQRCodeScanner: void,
+BackupMenu: void,
+TunnelbrokerMenu: void,
+KeyserverSelectionList: void,
+AddKeyserver: void,
+FarcasterAccountSettings: void,
};
export type CalendarParamList = {
+CalendarScreen: void,
};
export type CommunityDrawerParamList = { +TabNavigator: void };
export type RegistrationParamList = {
+CoolOrNerdModeSelection: void,
+KeyserverSelection: KeyserverSelectionParams,
+ConnectEthereum: ConnectEthereumParams,
+ExistingEthereumAccount: ExistingEthereumAccountParams,
+ConnectFarcaster: ConnectFarcasterParams,
+CreateSIWEBackupMessage: CreateSIWEBackupMessageParams,
+UsernameSelection: UsernameSelectionParams,
+PasswordSelection: PasswordSelectionParams,
+AvatarSelection: AvatarSelectionParams,
+EmojiAvatarSelection: EmojiAvatarSelectionParams,
+RegistrationUserAvatarCameraModal: void,
+RegistrationTerms: RegistrationTermsParams,
+AccountDoesNotExist: void,
};
export type InviteLinkParamList = {
+ViewInviteLinks: ViewInviteLinksScreenParams,
+ManagePublicLink: ManagePublicLinkScreenParams,
};
export type CommunityCreationParamList = {
+CommunityConfiguration: void,
};
export type RolesParamList = {
+CommunityRolesScreen: CommunityRolesScreenParams,
+CreateRolesScreen: CreateRolesScreenParams,
};
export type TagFarcasterChannelParamList = {
+TagFarcasterChannel: TagFarcasterChannelParams,
+TagFarcasterChannelByName: TagFarcasterChannelByNameParams,
};
export type QRCodeSignInParamList = {
+QRCodeScreen: void,
};
export type UserProfileBottomSheetParamList = {
+UserProfileBottomSheet: UserProfileBottomSheetParams,
+UserProfileAvatarModal: UserProfileAvatarModalParams,
+UserRelationshipTooltipModal: UserRelationshipTooltipModalParams,
};
export type ScreenParamList = {
...RootParamList,
...OverlayParamList,
...TabParamList,
...ChatParamList,
...ChatTopTabsParamList,
...ProfileParamList,
...CalendarParamList,
...CommunityDrawerParamList,
...RegistrationParamList,
...InviteLinkParamList,
...CommunityCreationParamList,
...RolesParamList,
...QRCodeSignInParamList,
...UserProfileBottomSheetParamList,
...TagFarcasterChannelParamList,
};
export type NavigationRoute> =
RouteProp;
export const accountModals = [
LoggedOutModalRouteName,
RegistrationRouteName,
QRCodeSignInNavigatorRouteName,
];
export const scrollBlockingModals = [
ImageModalRouteName,
MultimediaMessageTooltipModalRouteName,
TextMessageTooltipModalRouteName,
ThreadSettingsMemberTooltipModalRouteName,
UserRelationshipTooltipModalRouteName,
RobotextMessageTooltipModalRouteName,
VideoPlaybackModalRouteName,
];
export const chatRootModals = [
AddUsersModalRouteName,
ColorSelectorModalRouteName,
ComposeSubchannelModalRouteName,
];
export const threadRoutes = [
MessageListRouteName,
ThreadSettingsRouteName,
DeleteThreadRouteName,
ComposeSubchannelRouteName,
FullScreenThreadMediaGalleryRouteName,
PinnedMessagesScreenRouteName,
MessageSearchRouteName,
EmojiThreadAvatarCreationRouteName,
CommunityRolesScreenRouteName,
ThreadSettingsNotificationsRouteName,
];
diff --git a/native/tooltip/nux-tips-overlay.react.js b/native/tooltip/nux-tips-overlay.react.js
index 13b89a4b4..aaddbaa43 100644
--- a/native/tooltip/nux-tips-overlay.react.js
+++ b/native/tooltip/nux-tips-overlay.react.js
@@ -1,446 +1,412 @@
// @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 = {
- 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',
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,
+tooltipLocation: 'above' | 'below',
};
export type NUXTipsOverlayProps = {
+navigation: AppNavigationProp,
+route: NavigationRoute,
};
const marginVertical: number = 20;
const marginHorizontal: number = 10;
-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?.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) +
marginVertical;
} else {
style.bottom =
dimensions.height - Math.max(y, verticalBounds.y) + marginVertical;
}
const extraLeftSpace = x;
const extraRightSpace = dimensions.width - width - x;
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,
]);
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 - marginHorizontal,
};
} else {
return {
alignSelf: 'flex-end',
right: extraRightSpace + (4 / 10) * width - marginHorizontal,
};
}
}, [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 }],
},
+ callback: onExitFinish,
};
},
- [initialCoordinates.width, initialCoordinates.x, tooltipLocation],
+ [initialCoordinates.width, initialCoordinates.x, onExitFinish, 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 callbackParams = getNUXTipParams(route.params.tipKey);
+
const {
nextTip,
tooltipLocation: nextLocation,
nextRouteName,
+ exitingCallback,
} = callbackParams;
goBackOnce();
+ if (exitingCallback) {
+ exitingCallback?.(navigation);
+ }
+
if (!nextTip || !nextRouteName) {
return;
}
navigation.navigate({
name: nextRouteName,
params: {
tipKey: nextTip,
tooltipLocation: nextLocation,
},
});
- }, [callbackParams, goBackOnce, navigation]);
+ }, [goBackOnce, navigation, route.params.tipKey]);
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 };
+export { createNUXTipsOverlay, animationDuration };