diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js index d54b4472b..40f4ce371 100644 --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -1,306 +1,320 @@ // @flow import { createMaterialTopTabNavigator, type MaterialTopTabNavigationProp, } from '@react-navigation/material-top-tabs'; import { createNavigatorFactory, useNavigationBuilder, type StackNavigationState, type StackOptions, type StackNavigationEventMap, type StackNavigatorProps, type ExtraStackNavigatorProps, type StackHeaderProps as CoreStackHeaderProps, } from '@react-navigation/native'; import { StackView, type StackHeaderProps } from '@react-navigation/stack'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, View } from 'react-native'; import { useSelector } from 'react-redux'; import { isLoggedIn } from 'lib/selectors/user-selectors'; -import { threadIsPending } from 'lib/shared/thread-utils'; +import { + threadIsPending, + threadMembersWithoutAddedAshoat, +} from 'lib/shared/thread-utils'; import { firstLine } from 'lib/utils/string-utils'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react'; import SWMansionIcon from '../components/swmansion-icon.react'; import { InputStateContext } from '../input/input-state'; import HeaderBackButton from '../navigation/header-back-button.react'; import { defaultStackScreenOptions } from '../navigation/options'; import { ComposeSubchannelRouteName, DeleteThreadRouteName, ThreadSettingsRouteName, MessageListRouteName, ChatThreadListRouteName, HomeChatThreadListRouteName, BackgroundChatThreadListRouteName, type ScreenParamList, type ChatParamList, type ChatTopTabsParamList, } from '../navigation/route-names'; import { useColors, useStyles } from '../themes/colors'; import BackgroundChatThreadList from './background-chat-thread-list.react'; import ChatHeader from './chat-header.react'; import ChatRouter, { type ChatRouterNavigationProp } from './chat-router'; import ComposeSubchannel from './compose-subchannel.react'; import ComposeThreadButton from './compose-thread-button.react'; import HomeChatThreadList from './home-chat-thread-list.react'; import MessageListContainer from './message-list-container.react'; import MessageListHeaderTitle from './message-list-header-title.react'; import MessageStorePruner from './message-store-pruner.react'; import DeleteThread from './settings/delete-thread.react'; import ThreadSettings from './settings/thread-settings.react'; import ThreadDraftUpdater from './thread-draft-updater.react'; import ThreadScreenPruner from './thread-screen-pruner.react'; import ThreadSettingsButton from './thread-settings-button.react'; const unboundStyles = { keyboardAvoidingView: { flex: 1, }, view: { flex: 1, backgroundColor: 'listBackground', }, threadListHeaderStyle: { elevation: 0, shadowOffset: { width: 0, height: 0 }, borderBottomWidth: 0, backgroundColor: '#0A0A0A', }, }; export type ChatTopTabsNavigationProp< RouteName: $Keys = $Keys, > = MaterialTopTabNavigationProp; const homeChatThreadListOptions = { title: 'Focused', // eslint-disable-next-line react/display-name tabBarIcon: ({ color }) => ( ), }; const backgroundChatThreadListOptions = { title: 'Background', // eslint-disable-next-line react/display-name tabBarIcon: ({ color }) => ( ), }; const ChatThreadsTopTab = createMaterialTopTabNavigator(); function ChatThreadsComponent(): React.Node { const colors = useColors(); const { tabBarBackground, tabBarAccent } = colors; const tabBarOptions = React.useMemo( () => ({ showIcon: true, style: { backgroundColor: tabBarBackground, }, tabStyle: { flexDirection: 'row', }, indicatorStyle: { borderColor: tabBarAccent, borderBottomWidth: 2, }, }), [tabBarAccent, tabBarBackground], ); return ( ); } type ChatNavigatorProps = StackNavigatorProps>; function ChatNavigator({ initialRouteName, children, screenOptions, ...rest }: ChatNavigatorProps) { const { state, descriptors, navigation } = useNavigationBuilder(ChatRouter, { initialRouteName, children, screenOptions, }); // 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, ChatRouterNavigationProp<>, ExtraStackNavigatorProps, >(ChatNavigator); const header = (props: CoreStackHeaderProps) => { // 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 headerBackButton = props => ; const chatThreadListOptions = ({ navigation }) => ({ headerTitle: 'Inbox', headerRight: Platform.OS === 'ios' ? () => : undefined, headerBackTitleVisible: false, headerStyle: unboundStyles.threadListHeaderStyle, }); -const messageListOptions = ({ navigation, route }) => ({ - // This is a render prop, not a component - // eslint-disable-next-line react/display-name - headerTitle: () => ( - - ), - headerTitleContainerStyle: { - marginHorizontal: Platform.select({ ios: 80, default: 0 }), - flex: 1, - }, - headerRight: - Platform.OS === 'android' && !threadIsPending(route.params.threadInfo.id) - ? // This is a render prop, not a component - // eslint-disable-next-line react/display-name - () => ( - - ) - : undefined, - headerBackTitleVisible: false, -}); + +const messageListOptions = ({ navigation, route }) => { + const isSearchEmpty = + !!route.params.searching && + threadMembersWithoutAddedAshoat(route.params.threadInfo).length === 1; + + const areSettingsEnabled = + !threadIsPending(route.params.threadInfo.id) && !isSearchEmpty; + + return { + // This is a render prop, not a component + // eslint-disable-next-line react/display-name + headerTitle: () => ( + + ), + headerTitleContainerStyle: { + marginHorizontal: Platform.select({ ios: 80, default: 0 }), + flex: 1, + }, + headerRight: + Platform.OS === 'android' && areSettingsEnabled + ? // This is a render prop, not a component + // eslint-disable-next-line react/display-name + () => ( + + ) + : undefined, + headerBackTitleVisible: false, + }; +}; const composeThreadOptions = { headerTitle: 'Compose chat', headerBackTitleVisible: false, }; const threadSettingsOptions = ({ route }) => ({ headerTitle: firstLine(route.params.threadInfo.uiName), headerBackTitleVisible: false, }); const deleteThreadOptions = { headerTitle: 'Delete chat', headerBackTitleVisible: false, }; export type ChatNavigationProp< RouteName: $Keys = $Keys, > = ChatRouterNavigationProp; const Chat = createChatNavigator< ScreenParamList, ChatParamList, ChatNavigationProp<>, >(); // eslint-disable-next-line no-unused-vars export default function ChatComponent(props: { ... }): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const loggedIn = useSelector(isLoggedIn); let draftUpdater = null; if (loggedIn) { draftUpdater = ; } const screenOptions = React.useMemo( () => ({ ...defaultStackScreenOptions, header, headerLeft: headerBackButton, headerStyle: { backgroundColor: colors.tabBarBackground, borderBottomWidth: 1, }, }), [colors.tabBarBackground], ); return ( {draftUpdater} ); } diff --git a/native/chat/message-list-header-title.react.js b/native/chat/message-list-header-title.react.js index 7f9701449..978cc39ab 100644 --- a/native/chat/message-list-header-title.react.js +++ b/native/chat/message-list-header-title.react.js @@ -1,114 +1,110 @@ // @flow import { HeaderTitle } from '@react-navigation/stack'; import * as React from 'react'; import { View, Platform } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; -import { - threadIsPending, - threadMembersWithoutAddedAshoat, -} from 'lib/shared/thread-utils'; import type { ThreadInfo } from 'lib/types/thread-types'; import { firstLine } from 'lib/utils/string-utils'; import Button from '../components/button.react'; import { ThreadSettingsRouteName } from '../navigation/route-names'; import { useStyles } from '../themes/colors'; import type { ChatNavigationProp } from './chat.react'; type BaseProps = { +threadInfo: ThreadInfo, - +searching: boolean | void, +navigate: $PropertyType, 'navigate'>, + +isSearchEmpty: boolean, + +areSettingsEnabled: boolean, }; type Props = { ...BaseProps, +styles: typeof unboundStyles, }; class MessageListHeaderTitle extends React.PureComponent { render() { - const isSearchEmpty = - this.props.searching && - threadMembersWithoutAddedAshoat(this.props.threadInfo).length === 1; let icon, fakeIcon; - const areSettingsDisabled = - threadIsPending(this.props.threadInfo.id) || isSearchEmpty; - if (Platform.OS === 'ios' && !areSettingsDisabled) { + if (Platform.OS === 'ios' && this.props.areSettingsEnabled) { icon = ( ); fakeIcon = ( ); } - const title = isSearchEmpty ? 'New Message' : this.props.threadInfo.uiName; + + const title = this.props.isSearchEmpty + ? 'New Message' + : this.props.threadInfo.uiName; + return ( ); } onPress = () => { const threadInfo = this.props.threadInfo; this.props.navigate({ name: ThreadSettingsRouteName, params: { threadInfo }, key: `${ThreadSettingsRouteName}${threadInfo.id}`, }); }; } const unboundStyles = { button: { flex: 1, }, container: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: Platform.OS === 'android' ? 'flex-start' : 'center', }, fakeIcon: { paddingRight: 7, paddingTop: 3, flex: 1, minWidth: 25, opacity: 0, }, forwardIcon: { paddingLeft: 7, paddingTop: 3, color: 'headerChevron', flex: 1, minWidth: 25, }, }; const ConnectedMessageListHeaderTitle: React.ComponentType = React.memo( function ConnectedMessageListHeaderTitle(props: BaseProps) { const styles = useStyles(unboundStyles); return ; }, ); export default ConnectedMessageListHeaderTitle;