diff --git a/native/chat/chat-options.js b/native/chat/chat-options.js
new file mode 100644
index 000000000..cf8fdaf44
--- /dev/null
+++ b/native/chat/chat-options.js
@@ -0,0 +1,28 @@
+// @flow
+
+import * as React from 'react';
+
+import { threadSettingsNotificationsCopy } from 'lib/shared/thread-settings-notifications-utils.js';
+
+import SWMansionIcon from '../components/swmansion-icon.react.js';
+
+const homeChatThreadListOptions: {
+ +title: string,
+ +tabBarIcon: ({ +color: string, ... }) => React.Node,
+} = {
+ title: threadSettingsNotificationsCopy.HOME,
+ tabBarIcon: ({ color }) => (
+
+ ),
+};
+const backgroundChatThreadListOptions: {
+ +title: string,
+ +tabBarIcon: ({ +color: string, ... }) => React.Node,
+} = {
+ title: threadSettingsNotificationsCopy.MUTED,
+ tabBarIcon: ({ color }) => (
+
+ ),
+};
+
+export { backgroundChatThreadListOptions, homeChatThreadListOptions };
diff --git a/native/chat/chat-tab-bar.react.js b/native/chat/chat-tab-bar.react.js
new file mode 100644
index 000000000..3e6e95724
--- /dev/null
+++ b/native/chat/chat-tab-bar.react.js
@@ -0,0 +1,73 @@
+// @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 { TabBarItem } from 'react-native-tab-view';
+
+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,
+});
+
+function TabBarButton(props: TabBarItemProps>) {
+ const tipsContext = React.useContext(NUXTipsContext);
+ invariant(tipsContext, 'NUXTipsContext should be defined');
+
+ 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]);
+
+ 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 26e8bede6..5c2248197 100644
--- a/native/chat/chat.react.js
+++ b/native/chat/chat.react.js
@@ -1,535 +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 MessageStorePruner from 'lib/components/message-store-pruner.react.js';
import ThreadDraftUpdater from 'lib/components/thread-draft-updater.react.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { threadSettingsNotificationsCopy } from 'lib/shared/thread-settings-notifications-utils.js';
import { threadIsPending, threadIsSidebar } from 'lib/shared/thread-utils.js';
import BackgroundChatThreadList from './background-chat-thread-list.react.js';
import ChatHeader from './chat-header.react.js';
+import {
+ backgroundChatThreadListOptions,
+ homeChatThreadListOptions,
+} from './chat-options.js';
import ChatRouter, {
type ChatRouterNavigationHelpers,
type ChatRouterNavigationAction,
} from './chat-router.js';
+import TabBar from './chat-tab-bar.react.js';
import ComposeSubchannel from './compose-subchannel.react.js';
import ComposeThreadButton from './compose-thread-button.react.js';
import FullScreenThreadMediaGallery from './fullscreen-thread-media-gallery.react.js';
import HomeChatThreadList from './home-chat-thread-list.react.js';
import { MessageEditingContext } from './message-editing-context.react.js';
import MessageListContainer from './message-list-container.react.js';
import MessageListHeaderTitle from './message-list-header-title.react.js';
import PinnedMessagesScreen from './pinned-messages-screen.react.js';
import DeleteThread from './settings/delete-thread.react.js';
import EmojiThreadAvatarCreation from './settings/emoji-thread-avatar-creation.react.js';
import ThreadSettingsNotifications from './settings/thread-settings-notifications.react.js';
import ThreadSettings from './settings/thread-settings.react.js';
import ThreadScreenPruner from './thread-screen-pruner.react.js';
import ThreadSettingsButton from './thread-settings-button.react.js';
import ThreadSettingsHeaderTitle from './thread-settings-header-title.react.js';
import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js';
import {
nuxTip,
NUXTipsContext,
} from '../components/nux-tips-context.react.js';
-import SWMansionIcon from '../components/swmansion-icon.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 homeChatThreadListOptions = {
- title: threadSettingsNotificationsCopy.HOME,
- tabBarIcon: ({ color }: { +color: string, ... }) => (
-
- ),
-};
-const backgroundChatThreadListOptions = {
- title: threadSettingsNotificationsCopy.MUTED,
- tabBarIcon: ({ color }: { +color: string, ... }) => (
-
- ),
-};
-
const ChatThreadsTopTab = createMaterialTopTabNavigator<
ScreenParamList,
ChatTopTabsParamList,
ChatTopTabsNavigationHelpers,
>();
function ChatThreadsComponent(): React.Node {
const colors = useColors();
const { tabBarBackground, tabBarAccent } = colors;
const screenOptions = React.useMemo(
() => ({
tabBarShowIcon: true,
tabBarStyle: {
backgroundColor: tabBarBackground,
},
tabBarItemStyle: {
flexDirection: 'row',
},
tabBarIndicatorStyle: {
borderColor: tabBarAccent,
borderBottomWidth: 2,
},
}),
[tabBarAccent, tabBarBackground],
);
+
return (
-
+
);
}
export type ChatNavigationHelpers = {
...$Exact>,
...ChatRouterNavigationHelpers,
};
type ChatNavigatorProps = StackNavigatorProps>;
function ChatNavigator({
initialRouteName,
children,
screenOptions,
defaultScreenOptions,
screenListeners,
id,
...rest
}: ChatNavigatorProps) {
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState,
ChatRouterNavigationAction,
StackOptions,
StackRouterOptions,
ChatNavigationHelpers<>,
StackNavigationEventMap,
ExtraStackNavigatorProps,
>(ChatRouter, {
id,
initialRouteName,
children,
screenOptions,
defaultScreenOptions,
screenListeners,
});
// Clear ComposeSubchannel screens after each message is sent. If a user goes
// to ComposeSubchannel to create a new thread, but finds an existing one and
// uses it instead, we can assume the intent behind opening ComposeSubchannel
// is resolved
const inputState = React.useContext(InputStateContext);
invariant(inputState, 'InputState should be set in ChatNavigator');
const clearComposeScreensAfterMessageSend = React.useCallback(() => {
navigation.clearScreens([ComposeSubchannelRouteName]);
}, [navigation]);
React.useEffect(() => {
inputState.registerSendCallback(clearComposeScreensAfterMessageSend);
return () => {
inputState.unregisterSendCallback(clearComposeScreensAfterMessageSend);
};
}, [inputState, clearComposeScreensAfterMessageSend]);
return (
);
}
const createChatNavigator = createNavigatorFactory<
StackNavigationState,
StackOptions,
StackNavigationEventMap,
ChatNavigationHelpers<>,
ExtraStackNavigatorProps,
>(ChatNavigator);
const header = (props: StackHeaderProps) => {
// Flow has trouble reconciling identical types between different libdefs,
// and flow-typed has no way for one libdef to depend on another
const castProps: StackHeaderProps = (props: any);
return ;
};
const headerRightStyle = { flexDirection: 'row' };
const messageListOptions = ({
navigation,
route,
}: {
+navigation: ChatNavigationProp<'MessageList'>,
+route: NavigationRoute<'MessageList'>,
}) => {
const isSearchEmpty =
!!route.params.searching && route.params.threadInfo.members.length === 1;
const areSettingsEnabled =
!threadIsPending(route.params.threadInfo.id) && !isSearchEmpty;
return {
headerTitle: (props: HeaderTitleInputProps) => (
),
headerRight: areSettingsEnabled
? () => (
)
: undefined,
headerBackTitleVisible: false,
headerTitleAlign: isSearchEmpty ? 'center' : 'left',
headerLeftContainerStyle: { width: Platform.OS === 'ios' ? 32 : 40 },
headerTitleStyle: areSettingsEnabled ? { marginRight: 20 } : undefined,
};
};
const composeThreadOptions = {
headerTitle: 'Compose chat',
headerBackTitleVisible: false,
};
const threadSettingsOptions = ({
route,
}: {
+route: NavigationRoute<'ThreadSettings'>,
...
}) => ({
headerTitle: (props: HeaderTitleInputProps) => (
),
headerBackTitleVisible: false,
});
const emojiAvatarCreationOptions = {
headerTitle: 'Emoji avatar selection',
headerBackTitleVisible: false,
};
const fullScreenThreadMediaGalleryOptions = {
headerTitle: 'All Media',
headerBackTitleVisible: false,
};
const deleteThreadOptions = {
headerTitle: 'Delete chat',
headerBackTitleVisible: false,
};
const messageSearchOptions = {
headerTitle: () => ,
headerBackTitleVisible: false,
headerTitleContainerStyle: {
width: '100%',
},
};
const pinnedMessagesScreenOptions = {
headerTitle: 'Pinned Messages',
headerBackTitleVisible: false,
};
const threadSettingsNotificationsOptions = ({
route,
}: {
+route: NavigationRoute<'ThreadSettingsNotifications'>,
...
}) => ({
headerTitle: threadIsSidebar(route.params.threadInfo)
? threadSettingsNotificationsCopy.SIDEBAR_TITLE
: threadSettingsNotificationsCopy.CHANNEL_TITLE,
headerBackTitleVisible: false,
});
const changeRolesScreenOptions = ({
route,
}: {
+route: NavigationRoute<'ChangeRolesScreen'>,
...
}) => ({
headerLeft: (headerLeftProps: StackHeaderLeftButtonProps) => (
),
headerTitle: 'Change Role',
presentation: 'modal',
...transitionPreset,
});
export type ChatNavigationProp<
RouteName: $Keys = $Keys,
> = {
...StackNavigationProp,
...ChatRouterNavigationHelpers,
};
const Chat = createChatNavigator<
ScreenParamList,
ChatParamList,
ChatNavigationHelpers,
>();
type Props = {
+navigation: TabNavigationProp<'Chat'>,
...
};
export default function ChatComponent(props: Props): React.Node {
const styles = useStyles(unboundStyles);
const colors = useColors();
const loggedIn = useSelector(isLoggedIn);
let draftUpdater = null;
if (loggedIn) {
draftUpdater = ;
}
const communityDrawerButtonRef =
React.useRef>();
const tipsContext = React.useContext(NUXTipsContext);
invariant(tipsContext, 'NUXTipsContext should be defined');
const { registerTipButton } = tipsContext;
const communityDrawerButtonOnLayout = React.useCallback(() => {
communityDrawerButtonRef.current?.measure(
(x, y, width, height, pageX, pageY) => {
registerTipButton(nuxTip.COMMUNITY_DRAWER, {
x,
y,
width,
height,
pageX,
pageY,
});
},
);
}, [registerTipButton]);
const headerLeftButton = React.useCallback(
(headerProps: StackHeaderLeftButtonProps) => {
if (headerProps.canGoBack) {
return ;
}
return (
);
},
[communityDrawerButtonOnLayout, props.navigation],
);
const messageEditingContext = React.useContext(MessageEditingContext);
const editState = messageEditingContext?.editState;
const editMode = !!editState?.editedMessage;
const { width: screenWidth } = useWindowDimensions();
const screenOptions = React.useMemo(
() => ({
...defaultStackScreenOptions,
header,
headerLeft: headerLeftButton,
headerStyle: {
backgroundColor: colors.tabBarBackground,
borderBottomWidth: 1,
},
gestureEnabled: true,
gestureResponseDistance: editMode ? 0 : screenWidth,
}),
[colors.tabBarBackground, headerLeftButton, screenWidth, editMode],
);
const chatThreadListOptions = React.useCallback(
({
navigation,
}: {
+navigation: ChatNavigationProp<'ChatThreadList'>,
...
}) => ({
headerTitle: 'Inbox',
headerRight:
Platform.OS === 'ios'
? () =>
: undefined,
headerBackTitleVisible: false,
headerStyle: styles.threadListHeaderStyle,
}),
[styles.threadListHeaderStyle],
);
const frozen = useSelector(state => state.frozen);
const navContext = React.useContext(NavContext);
const activeThreadID = activeThreadSelector(navContext);
return (
{draftUpdater}
);
}
diff --git a/native/components/nux-tips-context.react.js b/native/components/nux-tips-context.react.js
index 8ef520e6e..4a714f115 100644
--- a/native/components/nux-tips-context.react.js
+++ b/native/components/nux-tips-context.react.js
@@ -1,97 +1,98 @@
// @flow
import * as React from 'react';
import { values } from 'lib/utils/objects.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,
};
const nuxTipParams: { [NUXTip]: NUXTipParams } = {
[nuxTip.COMMUNITY_DRAWER]: {
nextTip: nuxTip.MUTED,
tooltipLocation: 'below',
nextRouteName: undefined, //TODO: update to the next screens name
},
[nuxTip.MUTED]: {
nextTip: undefined,
nextRouteName: undefined,
tooltipLocation: 'below',
},
};
function getNUXTipParams(currentTipKey: NUXTip): NUXTipParams {
return nuxTipParams[currentTipKey];
}
type TipProps = {
+x: number,
+y: number,
+width: number,
+height: number,
+pageX: number,
+pageY: number,
};
export type NUXTipsContextType = {
+registerTipButton: (type: NUXTip, tipProps: ?TipProps) => void,
+getTipsProps: () => ?{ +[type: NUXTip]: TipProps },
};
const NUXTipsContext: React.Context =
React.createContext();
type Props = {
+children: React.Node,
};
function NUXTipsContextProvider(props: Props): React.Node {
const { children } = props;
const tipsProps = React.useRef<{ [tip: NUXTip]: ?TipProps }>({});
const registerTipButton = React.useCallback(
(type: NUXTip, tipProps: ?TipProps) => {
tipsProps.current[type] = tipProps;
},
[],
);
const getTipsProps = React.useCallback(() => {
const result: { [tip: NUXTip]: TipProps } = {};
for (const type of values(nuxTip)) {
if (!tipsProps.current[type]) {
return null;
}
result[type] = tipsProps.current[type];
}
return result;
}, []);
const value = React.useMemo(
() => ({
registerTipButton,
getTipsProps,
}),
[getTipsProps, registerTipButton],
);
return (
{children}
);
}
export { NUXTipsContext, NUXTipsContextProvider, nuxTip, getNUXTipParams };