diff --git a/native/chat/message-list-types.js b/native/chat/message-list-types.js
index c5f5ffa0a..a26aed24d 100644
--- a/native/chat/message-list-types.js
+++ b/native/chat/message-list-types.js
@@ -1,136 +1,137 @@
// @flow
+import type { TabAction } from '@react-navigation/core';
import { useNavigation, useNavigationState } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { useThreadChatMentionCandidates } from 'lib/hooks/chat-mention-hooks.js';
import type { MinimallyEncodedThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { type UserInfo } from 'lib/types/user-types.js';
import { ChatContext } from './chat-context.js';
import type { ChatRouterNavigationAction } from './chat-router.js';
import type { MarkdownRules } from '../markdown/rules.react.js';
import { useTextMessageRulesFunc } from '../markdown/rules.react.js';
import {
MessageListRouteName,
TextMessageTooltipModalRouteName,
} from '../navigation/route-names.js';
export type MessageListParams = {
+threadInfo: ThreadInfo | MinimallyEncodedThreadInfo,
+pendingPersonalThreadUserInfo?: UserInfo,
+searching?: boolean,
+removeEditMode?: ?RemoveEditMode,
};
export type RemoveEditMode = (
- action: ChatRouterNavigationAction,
+ action: TabAction | ChatRouterNavigationAction,
) => 'ignore_action' | 'reduce_action';
export type MessageListContextType = {
+getTextMessageMarkdownRules: (useDarkStyle: boolean) => MarkdownRules,
};
const MessageListContext: React.Context =
React.createContext();
function useMessageListContext(
threadInfo: ThreadInfo | MinimallyEncodedThreadInfo,
) {
const chatMentionCandidates = useThreadChatMentionCandidates(threadInfo);
const getTextMessageMarkdownRules = useTextMessageRulesFunc(
threadInfo,
chatMentionCandidates,
);
return React.useMemo(
() => ({
getTextMessageMarkdownRules,
}),
[getTextMessageMarkdownRules],
);
}
type Props = {
+children: React.Node,
+threadInfo: ThreadInfo | MinimallyEncodedThreadInfo,
};
function MessageListContextProvider(props: Props): React.Node {
const context = useMessageListContext(props.threadInfo);
return (
{props.children}
);
}
type NavigateToThreadAction = {
+name: typeof MessageListRouteName,
+params: MessageListParams,
+key: string,
};
function createNavigateToThreadAction(
params: MessageListParams,
): NavigateToThreadAction {
return {
name: MessageListRouteName,
params,
key: `${MessageListRouteName}${params.threadInfo.id}`,
};
}
function useNavigateToThread(): (params: MessageListParams) => void {
const { navigate } = useNavigation();
return React.useCallback(
(params: MessageListParams) => {
navigate<'MessageList'>(createNavigateToThreadAction(params));
},
[navigate],
);
}
function useTextMessageMarkdownRules(useDarkStyle: boolean): MarkdownRules {
const messageListContext = React.useContext(MessageListContext);
invariant(messageListContext, 'DummyTextNode should have MessageListContext');
return messageListContext.getTextMessageMarkdownRules(useDarkStyle);
}
function useNavigateToThreadWithFadeAnimation(
threadInfo: ThreadInfo,
messageKey: ?string,
): () => mixed {
const chatContext = React.useContext(ChatContext);
invariant(chatContext, 'ChatContext should be set');
const {
setCurrentTransitionSidebarSourceID: setSidebarSourceID,
setSidebarAnimationType,
} = chatContext;
const navigateToThread = useNavigateToThread();
const navigationStack = useNavigationState(state => state.routes);
return React.useCallback(() => {
if (
navigationStack[navigationStack.length - 1].name ===
TextMessageTooltipModalRouteName
) {
setSidebarSourceID(messageKey);
setSidebarAnimationType('fade_source_message');
}
navigateToThread({ threadInfo });
}, [
messageKey,
navigateToThread,
navigationStack,
setSidebarAnimationType,
setSidebarSourceID,
threadInfo,
]);
}
export {
MessageListContextProvider,
createNavigateToThreadAction,
useNavigateToThread,
useTextMessageMarkdownRules,
useNavigateToThreadWithFadeAnimation,
};
diff --git a/native/navigation/nav-selectors.js b/native/navigation/nav-selectors.js
index b835a62bb..eedabf149 100644
--- a/native/navigation/nav-selectors.js
+++ b/native/navigation/nav-selectors.js
@@ -1,446 +1,446 @@
// @flow
import type { PossiblyStaleNavigationState } from '@react-navigation/core';
import { useRoute } from '@react-navigation/native';
import _memoize from 'lodash/memoize.js';
import * as React from 'react';
import { createSelector } from 'reselect';
import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js';
import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js';
import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import type { CalendarFilter } from 'lib/types/filter-types.js';
import type {
ComposableMessageInfo,
RobotextMessageInfo,
} from 'lib/types/message-types.js';
import type { MinimallyEncodedThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { GlobalTheme } from 'lib/types/theme-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { NavContextType } from './navigation-context.js';
import { NavContext } from './navigation-context.js';
import {
getStateFromNavigatorRoute,
getThreadIDFromRoute,
currentLeafRoute,
} from './navigation-utils.js';
import {
AppRouteName,
TabNavigatorRouteName,
MessageListRouteName,
ChatRouteName,
CalendarRouteName,
ThreadPickerModalRouteName,
ActionResultModalRouteName,
accountModals,
scrollBlockingModals,
chatRootModals,
threadRoutes,
CommunityDrawerNavigatorRouteName,
MessageResultsScreenRouteName,
MessageSearchRouteName,
} from './route-names.js';
-import type { RemoveEditMode } from '../chat/message-list-types';
+import type { RemoveEditMode } from '../chat/message-list-types.js';
import { useSelector } from '../redux/redux-utils.js';
import type { NavPlusRedux } from '../types/selector-types.js';
const baseCreateIsForegroundSelector: (
routeName: string,
) => (context: ?NavContextType) => boolean = (routeName: string) =>
createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState) => {
if (!navigationState) {
return false;
}
return navigationState.routes[navigationState.index].name === routeName;
},
);
const createIsForegroundSelector: (
routeName: string,
) => (context: ?NavContextType) => boolean = _memoize(
baseCreateIsForegroundSelector,
);
function useIsAppLoggedIn(): boolean {
const navContext = React.useContext(NavContext);
return React.useMemo(() => {
if (!navContext) {
return false;
}
const { state } = navContext;
return !accountModals.includes(state.routes[state.index].name);
}, [navContext]);
}
const baseCreateActiveTabSelector: (
routeName: string,
) => (context: ?NavContextType) => boolean = (routeName: string) =>
createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState) => {
if (!navigationState) {
return false;
}
const currentRootSubroute = navigationState.routes[navigationState.index];
if (currentRootSubroute.name !== AppRouteName) {
return false;
}
const appState = getStateFromNavigatorRoute(currentRootSubroute);
const [firstAppSubroute] = appState.routes;
if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) {
return false;
}
const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute);
const [firstCommunityDrawerSubroute] = communityDrawerState.routes;
if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) {
return false;
}
const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute);
return tabState.routes[tabState.index].name === routeName;
},
);
const createActiveTabSelector: (
routeName: string,
) => (context: ?NavContextType) => boolean = _memoize(
baseCreateActiveTabSelector,
);
const scrollBlockingModalsClosedSelector: (
context: ?NavContextType,
) => boolean = createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState) => {
if (!navigationState) {
return false;
}
const currentRootSubroute = navigationState.routes[navigationState.index];
if (currentRootSubroute.name !== AppRouteName) {
return true;
}
const appState = getStateFromNavigatorRoute(currentRootSubroute);
for (let i = appState.index; i >= 0; i--) {
const route = appState.routes[i];
if (scrollBlockingModals.includes(route.name)) {
return false;
}
}
return true;
},
);
function selectBackgroundIsDark(
navigationState: ?PossiblyStaleNavigationState,
theme: ?GlobalTheme,
): boolean {
if (!navigationState) {
return false;
}
const currentRootSubroute = navigationState.routes[navigationState.index];
if (currentRootSubroute.name !== AppRouteName) {
// Very bright... we'll call it non-dark. Doesn't matter right now since
// we only use this selector for determining ActionResultModal appearance
return false;
}
const appState = getStateFromNavigatorRoute(currentRootSubroute);
let appIndex = appState.index;
let currentAppSubroute = appState.routes[appIndex];
while (currentAppSubroute.name === ActionResultModalRouteName) {
currentAppSubroute = appState.routes[--appIndex];
}
if (scrollBlockingModals.includes(currentAppSubroute.name)) {
// All the scroll-blocking chat modals have a dark background
return true;
}
return theme === 'dark';
}
function activeThread(
navigationState: ?PossiblyStaleNavigationState,
validRouteNames: $ReadOnlyArray,
): ?string {
if (!navigationState) {
return null;
}
let rootIndex = navigationState.index;
let currentRootSubroute = navigationState.routes[rootIndex];
while (currentRootSubroute.name !== AppRouteName) {
if (!chatRootModals.includes(currentRootSubroute.name)) {
return null;
}
if (rootIndex === 0) {
return null;
}
currentRootSubroute = navigationState.routes[--rootIndex];
}
const appState = getStateFromNavigatorRoute(currentRootSubroute);
const [firstAppSubroute] = appState.routes;
if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) {
return null;
}
const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute);
const [firstCommunityDrawerSubroute] = communityDrawerState.routes;
if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) {
return null;
}
const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute);
const currentTabSubroute = tabState.routes[tabState.index];
if (currentTabSubroute.name !== ChatRouteName) {
return null;
}
const chatState = getStateFromNavigatorRoute(currentTabSubroute);
const currentChatSubroute = chatState.routes[chatState.index];
return getThreadIDFromRoute(currentChatSubroute, validRouteNames);
}
const activeThreadSelector: (context: ?NavContextType) => ?string =
createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState): ?string =>
activeThread(navigationState, threadRoutes),
);
const messageListRouteNames = [MessageListRouteName];
const activeMessageListSelector: (context: ?NavContextType) => ?string =
createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState): ?string =>
activeThread(navigationState, messageListRouteNames),
);
function useActiveThread(): ?string {
const navContext = React.useContext(NavContext);
return React.useMemo(() => {
if (!navContext) {
return null;
}
const { state } = navContext;
return activeThread(state, threadRoutes);
}, [navContext]);
}
function useActiveMessageList(): ?string {
const navContext = React.useContext(NavContext);
return React.useMemo(() => {
if (!navContext) {
return null;
}
const { state } = navContext;
return activeThread(state, messageListRouteNames);
}, [navContext]);
}
const calendarTabActiveSelector = createActiveTabSelector(CalendarRouteName);
const threadPickerActiveSelector = createIsForegroundSelector(
ThreadPickerModalRouteName,
);
const calendarActiveSelector: (context: ?NavContextType) => boolean =
createSelector(
calendarTabActiveSelector,
threadPickerActiveSelector,
(calendarTabActive: boolean, threadPickerActive: boolean) =>
calendarTabActive || threadPickerActive,
);
const nativeCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery =
createSelector(
(input: NavPlusRedux) => currentCalendarQuery(input.redux),
(input: NavPlusRedux) => calendarActiveSelector(input.navContext),
(
calendarQuery: (calendarActive: boolean) => CalendarQuery,
calendarActive: boolean,
) =>
() =>
calendarQuery(calendarActive),
);
const nonThreadCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery =
createSelector(
nativeCalendarQuery,
(input: NavPlusRedux) => nonThreadCalendarFiltersSelector(input.redux),
(
calendarQuery: () => CalendarQuery,
filters: $ReadOnlyArray,
) => {
return (): CalendarQuery => {
const query = calendarQuery();
return {
startDate: query.startDate,
endDate: query.endDate,
filters,
};
};
},
);
function useCalendarQuery(): () => CalendarQuery {
const navContext = React.useContext(NavContext);
return useSelector(state =>
nonThreadCalendarQuery({
redux: state,
navContext,
}),
);
}
const drawerSwipeEnabledSelector: (context: ?NavContextType) => boolean =
createSelector(
(context: ?NavContextType) => context && context.state,
(navigationState: ?PossiblyStaleNavigationState) => {
if (!navigationState) {
return true;
}
// First, we recurse into the navigation state until we find the tab route
// The tab route should always be accessible by recursing through the
// first routes of each subsequent nested navigation state
const [firstRootSubroute] = navigationState.routes;
if (firstRootSubroute.name !== AppRouteName) {
return true;
}
const appState = getStateFromNavigatorRoute(firstRootSubroute);
const [firstAppSubroute] = appState.routes;
if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) {
return true;
}
const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute);
const [firstCommunityDrawerSubroute] = communityDrawerState.routes;
if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) {
return true;
}
const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute);
// Once we have the tab state, we want to figure out if we currently have
// an active StackNavigator
const currentTabSubroute = tabState.routes[tabState.index];
if (!currentTabSubroute.state) {
return true;
}
const currentTabSubrouteState =
getStateFromNavigatorRoute(currentTabSubroute);
if (currentTabSubrouteState.type !== 'stack') {
return true;
}
// Finally, we want to disable the swipe gesture if there is a stack with
// more than one subroute, since then the stack will have its own swipe
// gesture that will conflict with the drawer's
return currentTabSubrouteState.routes.length < 2;
},
);
function getTabNavState(
navigationState: ?PossiblyStaleNavigationState,
): ?PossiblyStaleNavigationState {
if (!navigationState) {
return null;
}
const [firstAppSubroute] = navigationState.routes;
if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) {
return null;
}
const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute);
const [firstCommunityDrawerSubroute] = communityDrawerState.routes;
if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) {
return null;
}
const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute);
return tabState;
}
function getChatNavStateFromTabNavState(
tabState: ?PossiblyStaleNavigationState,
): ?PossiblyStaleNavigationState {
if (!tabState) {
return null;
}
let chatRoute;
for (const route of tabState.routes) {
if (route.name === ChatRouteName) {
chatRoute = route;
break;
}
}
if (!chatRoute || !chatRoute.state) {
return null;
}
const chatRouteState = getStateFromNavigatorRoute(chatRoute);
if (chatRouteState.type !== 'stack') {
return null;
}
return chatRouteState;
}
function getRemoveEditMode(
chatRouteState: ?PossiblyStaleNavigationState,
): ?RemoveEditMode {
if (!chatRouteState) {
return null;
}
const messageListRoute =
chatRouteState.routes[chatRouteState.routes.length - 1];
if (messageListRoute.name !== MessageListRouteName) {
return null;
}
if (!messageListRoute || !messageListRoute.params) {
return null;
}
const removeEditMode: Function = messageListRoute.params.removeEditMode;
return removeEditMode;
}
function useCurrentLeafRouteName(): ?string {
const navContext = React.useContext(NavContext);
return React.useMemo(() => {
if (!navContext) {
return undefined;
}
return currentLeafRoute(navContext.state).name;
}, [navContext]);
}
function useCanEditMessageNative(
threadInfo: ThreadInfo | MinimallyEncodedThreadInfo,
targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo,
): boolean {
const route = useRoute();
const screenKey = route.key;
const threadCreationTime = threadInfo.creationTime;
const messageCreationTime = targetMessageInfo.time;
const canEditInThisScreen =
!screenKey.startsWith(MessageSearchRouteName) &&
!screenKey.startsWith(MessageResultsScreenRouteName) &&
messageCreationTime >= threadCreationTime;
return (
useCanEditMessage(threadInfo, targetMessageInfo) && canEditInThisScreen
);
}
export {
createIsForegroundSelector,
useIsAppLoggedIn,
createActiveTabSelector,
scrollBlockingModalsClosedSelector,
selectBackgroundIsDark,
activeThreadSelector,
activeMessageListSelector,
useActiveThread,
useActiveMessageList,
calendarActiveSelector,
nativeCalendarQuery,
nonThreadCalendarQuery,
useCalendarQuery,
drawerSwipeEnabledSelector,
useCurrentLeafRouteName,
getRemoveEditMode,
getTabNavState,
getChatNavStateFromTabNavState,
useCanEditMessageNative,
};