diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js index db4f6a526..2c7905712 100644 --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -1,437 +1,437 @@ // @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, type StackNavigationProp, type StackNavigationHelpers, type ParamListBase, } from '@react-navigation/native'; import { StackView, type StackHeaderProps } from '@react-navigation/stack'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, View, useWindowDimensions } from 'react-native'; import { useSelector } from 'react-redux'; import ThreadDraftUpdater from 'lib/components/thread-draft-updater.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { threadIsPending, threadMembersWithoutAddedAshoat, } from 'lib/shared/thread-utils.js'; import BackgroundChatThreadList from './background-chat-thread-list.react.js'; import ChatHeader from './chat-header.react.js'; import ChatRouter, { type ChatRouterNavigationHelpers } from './chat-router.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 MessageListContainer from './message-list-container.react.js'; import MessageListHeaderTitle from './message-list-header-title.react.js'; import MessageResultsScreen from './message-results-screen.react.js'; import MessageStorePruner from './message-store-pruner.react.js'; import DeleteThread from './settings/delete-thread.react.js'; import EmojiThreadAvatarCreation from './settings/emoji-thread-avatar-creation.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 SWMansionIcon from '../components/swmansion-icon.react.js'; import { InputStateContext } from '../input/input-state.js'; import CommunityDrawerButton from '../navigation/community-drawer-button.react.js'; import type { CommunityDrawerNavigationProp } from '../navigation/community-drawer-navigator.react.js'; import HeaderBackButton from '../navigation/header-back-button.react.js'; import { defaultStackScreenOptions } from '../navigation/options.js'; import { ComposeSubchannelRouteName, DeleteThreadRouteName, ThreadSettingsRouteName, EmojiThreadAvatarCreationRouteName, FullScreenThreadMediaGalleryRouteName, MessageResultsScreenRouteName, MessageListRouteName, ChatThreadListRouteName, HomeChatThreadListRouteName, BackgroundChatThreadListRouteName, type ScreenParamList, type ChatParamList, type ChatTopTabsParamList, MessageSearchRouteName, ChangeRolesScreenRouteName, } from '../navigation/route-names.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; 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 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(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: 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 headerRightStyle = { flexDirection: 'row' }; 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: props => ( ), headerRight: areSettingsEnabled ? // This is a render prop, not a component // eslint-disable-next-line react/display-name () => ( ) : 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 }) => ({ // eslint-disable-next-line react/display-name headerTitle: props => ( ), 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 = { // eslint-disable-next-line react/display-name headerTitle: () => , headerBackTitleVisible: false, headerTitleContainerStyle: { width: '100%', }, }; const messageResultsScreenOptions = { headerTitle: 'Pinned Messages', headerBackTitleVisible: false, }; -const changeRolesScreenOptions = { +const changeRolesScreenOptions = ({ route }) => ({ // eslint-disable-next-line react/display-name headerLeft: headerLeftProps => ( - + ), headerTitle: 'Change Role', presentation: 'modal', -}; +}); export type ChatNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...ChatRouterNavigationHelpers, }; const Chat = createChatNavigator< ScreenParamList, ChatParamList, ChatNavigationHelpers, >(); type Props = { +navigation: CommunityDrawerNavigationProp<'TabNavigator'>, ... }; 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 headerLeftButton = React.useCallback( headerProps => { if (headerProps.canGoBack) { return ; } return ; }, [props.navigation], ); const { width: screenWidth } = useWindowDimensions(); const screenOptions = React.useMemo( () => ({ ...defaultStackScreenOptions, header, headerLeft: headerLeftButton, headerStyle: { backgroundColor: colors.tabBarBackground, borderBottomWidth: 1, }, gestureEnabled: true, gestureResponseDistance: screenWidth, }), [colors.tabBarBackground, headerLeftButton, screenWidth], ); const chatThreadListOptions = React.useCallback( ({ navigation }) => ({ headerTitle: 'Inbox', headerRight: Platform.OS === 'ios' ? () => : undefined, headerBackTitleVisible: false, headerStyle: styles.threadListHeaderStyle, }), [styles.threadListHeaderStyle], ); return ( {draftUpdater} ); } diff --git a/native/roles/change-roles-header-left-button.react.js b/native/roles/change-roles-header-left-button.react.js index cceffb216..da1df2867 100644 --- a/native/roles/change-roles-header-left-button.react.js +++ b/native/roles/change-roles-header-left-button.react.js @@ -1,28 +1,58 @@ // @flow import { HeaderBackButton as BaseHeaderBackButton } from '@react-navigation/elements'; +import invariant from 'invariant'; import * as React from 'react'; -import { Text } from 'react-native'; +import { Alert, Text } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; +import type { NavigationRoute } from '../navigation/route-names'; import { useColors } from '../themes/colors.js'; -type Props = React.ElementConfig; -function ChangeRolesHeaderLeftButton(props: Props): React.Node { +type ChangeRolesHeaderLeftButtonProps = { + +route: NavigationRoute<'ChangeRolesScreen'>, + ...React.ElementConfig, +}; + +function ChangeRolesHeaderLeftButton( + props: ChangeRolesHeaderLeftButtonProps, +): React.Node { + const { memberInfo, role: selectedRole } = props.route.params; + const { role: memberRole } = memberInfo; + + const onCancel = React.useCallback(() => { + const { onPress } = props; + invariant(onPress, 'onPress must be defined'); + + if (selectedRole === memberRole) { + onPress(); + return; + } + + Alert.alert( + 'Discard changes?', + 'You have unsaved changes which will be discarded if you navigate away', + [ + { text: 'Leave', onPress }, + { text: 'Stay', style: 'cancel' }, + ], + ); + }, [memberRole, props, selectedRole]); + const { panelForegroundSecondaryLabel } = useColors(); const labelStyle = React.useMemo( () => ({ color: panelForegroundSecondaryLabel, marginLeft: 10, }), [panelForegroundSecondaryLabel], ); return ( - + Cancel ); } export default ChangeRolesHeaderLeftButton;