diff --git a/src/boot/ThemeProvider.js b/src/boot/ThemeProvider.js index dbfa14cb444..de02eb78d7d 100644 --- a/src/boot/ThemeProvider.js +++ b/src/boot/ThemeProvider.js @@ -7,6 +7,7 @@ import type { ThemeName, Dispatch } from '../types'; import { connect } from '../react-redux'; import { getSettings } from '../directSelectors'; import { themeData, ThemeContext } from '../styles/theme'; +import { ZulipStatusBar } from '../common'; type Props = $ReadOnly<{| dispatch: Dispatch, @@ -21,7 +22,12 @@ class ThemeProvider extends PureComponent { render() { const { children, theme } = this.props; - return {children}; + return ( + + + {children} + + ); } } diff --git a/src/chat/ChatScreen.js b/src/chat/ChatScreen.js index 364dedda157..23c37ec9076 100644 --- a/src/chat/ChatScreen.js +++ b/src/chat/ChatScreen.js @@ -1,14 +1,13 @@ /* @flow strict-local */ import React from 'react'; -import { View } from 'react-native'; import { useIsFocused } from '@react-navigation/native'; import { useSelector, useDispatch } from '../react-redux'; import type { RouteProp } from '../react-navigation'; import type { AppNavigationProp } from '../nav/AppNavigator'; -import styles, { ThemeContext, createStyleSheet } from '../styles'; +import { ThemeContext, createStyleSheet } from '../styles'; import type { Narrow, EditMessage } from '../types'; -import { KeyboardAvoider, OfflineNotice, ZulipStatusBar } from '../common'; +import { KeyboardAvoider, OfflineNotice } from '../common'; import ChatNavBar from '../nav/ChatNavBar'; import MessageList from '../webview/MessageList'; import NoMessages from '../message/NoMessages'; @@ -21,11 +20,10 @@ import { canSendToNarrow } from '../utils/narrow'; import { getLoading, getSession } from '../directSelectors'; import { getFetchingForNarrow } from './fetchingSelectors'; import { getShownMessagesForNarrow, isNarrowValid as getIsNarrowValid } from './narrowsSelectors'; -import { getStreamColorForNarrow } from '../subscriptions/subscriptionSelectors'; type Props = $ReadOnly<{| navigation: AppNavigationProp<'chat'>, - route: RouteProp<'chat', {| narrow: Narrow |}>, + route: RouteProp<'chat', {| narrow: Narrow, editMessage: EditMessage | null |}>, |}>; const componentStyles = createStyleSheet({ @@ -99,11 +97,12 @@ const useFetchMessages = args => { }; export default function ChatScreen(props: Props) { + const { route, navigation } = props; const { backgroundColor } = React.useContext(ThemeContext); - const [editMessage, setEditMessage] = React.useState(null); - - const { narrow } = props.route.params; + const { narrow, editMessage } = route.params; + const setEditMessage = (value: EditMessage | null) => + navigation.setParams({ editMessage: value }); const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow)); @@ -113,40 +112,35 @@ export default function ChatScreen(props: Props) { const sayNoMessages = haveNoMessages && !isFetching; const showComposeBox = canSendToNarrow(narrow) && !showMessagePlaceholders; - const streamColor = useSelector(state => getStreamColorForNarrow(state, narrow)); - return ( - - - - - - - {(() => { - if (!isNarrowValid) { - return ; - } else if (fetchError !== null) { - return ; - } else if (sayNoMessages) { - return ; - } else { - return ( - - ); - } - })()} - {showComposeBox && ( - setEditMessage(null)} - /> - )} - - + + + + + {(() => { + if (!isNarrowValid) { + return ; + } else if (fetchError !== null) { + return ; + } else if (sayNoMessages) { + return ; + } else { + return ( + + ); + } + })()} + {showComposeBox && ( + setEditMessage(null)} + /> + )} + ); } diff --git a/src/common/FullScreenLoading.js b/src/common/FullScreenLoading.js index 4ae63bc83bd..fc717182906 100644 --- a/src/common/FullScreenLoading.js +++ b/src/common/FullScreenLoading.js @@ -24,15 +24,17 @@ export default function FullScreenLoading(props: Props) { const insets = useSafeAreaInsets(); return ( - - + <> - - + + + + + ); } diff --git a/src/common/KeyboardAvoider.js b/src/common/KeyboardAvoider.js index d12692d969c..e63ec44a362 100644 --- a/src/common/KeyboardAvoider.js +++ b/src/common/KeyboardAvoider.js @@ -9,11 +9,37 @@ type Props = $ReadOnly<{| children: React$Node, style?: ViewStyleProp, contentContainerStyle?: ViewStyleProp, + + /** How much the top of `KeyboardAvoider`'s layout *parent* is + * displaced downward from the top of the screen. + * + * If this isn't set correctly, the keyboard will hide some of + * the bottom of the screen (an area whose height is what this + * value should have been set to). + * + * I think `KeyboardAvoidingView`'s implementation mistakes `x` + * and `y` from `View#onLayout` to be a `View`'s position + * relative to the top left of the screen. In reality, I'm + * pretty sure they represent a `View`'s position relative to + * its parent: + * https://github.com/facebook/react-native-website/issues/2056#issuecomment-773618381 + * + * But at least `KeyboardAvoidingView` exposes this prop, which + * we can use to balance the equation if we need to. + */ + keyboardVerticalOffset?: number, |}>; +/** + * Renders RN's `KeyboardAvoidingView` on iOS, `View` on Android. + * + * This component's props that are named after + * `KeyboardAvoidingView`'s special props get passed straight through + * to that component. + */ export default class KeyboardAvoider extends PureComponent { render() { - const { behavior, children, style, contentContainerStyle } = this.props; + const { behavior, children, style, contentContainerStyle, keyboardVerticalOffset } = this.props; if (Platform.OS === 'android') { return {children}; @@ -23,6 +49,8 @@ export default class KeyboardAvoider extends PureComponent { {children} diff --git a/src/common/Screen.js b/src/common/Screen.js index 14a54dcc911..7f302b33e8a 100644 --- a/src/common/Screen.js +++ b/src/common/Screen.js @@ -1,19 +1,16 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React, { useContext } from 'react'; import type { Node as React$Node } from 'react'; -import { View, ScrollView } from 'react-native'; +import { ScrollView } from 'react-native'; import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'; -import { type EdgeInsets } from 'react-native-safe-area-context'; +import { SafeAreaView } from 'react-native-safe-area-context'; -import { withSafeAreaInsets } from '../react-native-safe-area-context'; -import type { ThemeData } from '../styles'; import styles, { createStyleSheet, ThemeContext } from '../styles'; import type { LocalizableText } from '../types'; import KeyboardAvoider from './KeyboardAvoider'; import OfflineNotice from './OfflineNotice'; import LoadingBanner from './LoadingBanner'; -import ZulipStatusBar from './ZulipStatusBar'; import ModalNavBar from '../nav/ModalNavBar'; import ModalSearchNavBar from '../nav/ModalSearchNavBar'; @@ -37,21 +34,20 @@ const componentStyles = createStyleSheet({ }); type Props = $ReadOnly<{| - centerContent: boolean, + centerContent?: boolean, +children: React$Node, - insets: EdgeInsets, - keyboardShouldPersistTaps: 'never' | 'always' | 'handled', - padding: boolean, - scrollEnabled: boolean, + keyboardShouldPersistTaps?: 'never' | 'always' | 'handled', + padding?: boolean, + scrollEnabled?: boolean, style?: ViewStyleProp, - search: boolean, - autoFocus: boolean, - searchBarOnChange: (text: string) => void, - shouldShowLoadingBanner: boolean, + search?: boolean, + autoFocus?: boolean, + searchBarOnChange?: (text: string) => void, + shouldShowLoadingBanner?: boolean, - canGoBack: boolean, - +title: LocalizableText, + canGoBack?: boolean, + +title?: LocalizableText, |}>; /** @@ -75,89 +71,64 @@ type Props = $ReadOnly<{| * @prop [title] - Text shown as the title of the screen. * Required unless `search` is true. */ -class Screen extends PureComponent { - static contextType = ThemeContext; - context: ThemeData; +export default function Screen(props: Props) { + const { backgroundColor } = useContext(ThemeContext); + const { + autoFocus = false, + canGoBack = true, + centerContent = false, + children, + keyboardShouldPersistTaps = 'handled', + padding = false, + scrollEnabled = true, + search = false, + searchBarOnChange = (text: string) => {}, + style, + title = '', + shouldShowLoadingBanner = true, + } = props; - static defaultProps = { - centerContent: false, - keyboardShouldPersistTaps: 'handled', - padding: false, - scrollEnabled: true, - - search: false, - autoFocus: false, - searchBarOnChange: (text: string) => {}, - shouldShowLoadingBanner: true, - - canGoBack: true, - title: '', - }; - - render() { - const { - autoFocus, - canGoBack, - centerContent, - children, - keyboardShouldPersistTaps, - padding, - insets, - scrollEnabled, - search, - searchBarOnChange, - style, - title, - shouldShowLoadingBanner, - } = this.props; - - return ( - + {search ? ( + + ) : ( + + )} + + {shouldShowLoadingBanner && } + - - {search ? ( - - ) : ( - - )} - - {shouldShowLoadingBanner && } - - - {children} - - - - ); - } + {children} + + + + ); } - -export default withSafeAreaInsets(Screen); diff --git a/src/common/ZulipStatusBar.js b/src/common/ZulipStatusBar.js index e7c90a32a8e..61ea8f71bf9 100644 --- a/src/common/ZulipStatusBar.js +++ b/src/common/ZulipStatusBar.js @@ -33,10 +33,21 @@ type Props = $ReadOnly<{| |}>; /** - * Applies `hidden` and `backgroundColor` in platform-specific ways. + * Renders an RN `StatusBar` with appropriate props, and nothing else. * - * Like `StatusBar`, which is the only thing it ever renders, this - * doesn't have any effect on the spatial layout of the UI. + * Specifically, it controls the status bar's hidden/visible state and + * its background color in platform-specific ways. Omitting `hidden` + * will make the status bar visible, and omitting `backgroundColor` + * will give a theme-appropriate default. + * + * `StatusBar` renders `null` every time. Therefore, don't look to + * `ZulipStatusBar`'s position in the hierarchy of `View`s to affect + * the layout in any way. + * + * That being said, hiding and un-hiding the status bar can change the + * size of the top inset. E.g., on an iPhone without the "notch", the + * top inset grows to accommodate a visible status bar, and shrinks to + * give more room to the app's content when the status bar is hidden. */ class ZulipStatusBar extends PureComponent { static defaultProps = { diff --git a/src/lightbox/Lightbox.js b/src/lightbox/Lightbox.js index 5d0637f4d4f..5211ae74d1a 100644 --- a/src/lightbox/Lightbox.js +++ b/src/lightbox/Lightbox.js @@ -17,6 +17,7 @@ import { constructActionSheetButtons, executeActionSheetAction } from './Lightbo import { createStyleSheet } from '../styles'; import { navigateBack } from '../actions'; import { streamNameOfStreamMessage } from '../utils/recipient'; +import { ZulipStatusBar } from '../common'; const styles = createStyleSheet({ img: { @@ -70,68 +71,71 @@ export default function Lightbox(props: Props) { const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); return ( - - - - { - NavigationService.dispatch(navigateBack()); - }} - timestamp={message.timestamp} - avatarUrl={message.avatar_url} - senderName={message.sender_full_name} - senderEmail={message.sender_email} + <> + + ); } diff --git a/src/lightbox/LightboxScreen.js b/src/lightbox/LightboxScreen.js index cc63632ef0f..38eddb3d5db 100644 --- a/src/lightbox/LightboxScreen.js +++ b/src/lightbox/LightboxScreen.js @@ -5,7 +5,6 @@ import { View } from 'react-native'; import type { Message } from '../types'; import type { RouteProp } from '../react-navigation'; import type { AppNavigationProp } from '../nav/AppNavigator'; -import { ZulipStatusBar } from '../common'; import { createStyleSheet } from '../styles'; import Lightbox from './Lightbox'; @@ -28,7 +27,6 @@ export default function LightboxScreen(props: Props) { return ( - ); diff --git a/src/main/MainTabsScreen.js b/src/main/MainTabsScreen.js index 1257acf28e9..10378d657dd 100644 --- a/src/main/MainTabsScreen.js +++ b/src/main/MainTabsScreen.js @@ -1,11 +1,11 @@ /* @flow strict-local */ import React, { useContext } from 'react'; -import { Platform, View } from 'react-native'; +import { Platform } from 'react-native'; import { createBottomTabNavigator, type BottomTabNavigationProp, } from '@react-navigation/bottom-tabs'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { SafeAreaView } from 'react-native-safe-area-context'; import type { RouteProp, RouteParamsOf } from '../react-navigation'; import type { AppNavigationProp } from '../nav/AppNavigator'; @@ -16,7 +16,7 @@ import StreamTabsScreen from './StreamTabsScreen'; import PmConversationsScreen from '../pm-conversations/PmConversationsScreen'; import SettingsScreen from '../settings/SettingsScreen'; import { IconInbox, IconSettings, IconStream } from '../common/Icons'; -import { OwnAvatar, OfflineNotice, ZulipStatusBar } from '../common'; +import { OwnAvatar, OfflineNotice } from '../common'; import IconUnreadConversations from '../nav/IconUnreadConversations'; import ProfileScreen from '../account-info/ProfileScreen'; import styles, { ThemeContext } from '../styles'; @@ -47,12 +47,8 @@ type Props = $ReadOnly<{| export default function MainTabsScreen(props: Props) { const { backgroundColor } = useContext(ThemeContext); - const insets = useSafeAreaInsets(); - return ( - - - + - + ); } diff --git a/src/nav/ChatNavBar.js b/src/nav/ChatNavBar.js index a97f0911f03..b3bb11e3929 100644 --- a/src/nav/ChatNavBar.js +++ b/src/nav/ChatNavBar.js @@ -6,7 +6,7 @@ import Color from 'color'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { Narrow, EditMessage } from '../types'; -import { LoadingBanner } from '../common'; +import { LoadingBanner, ZulipStatusBar } from '../common'; import { useSelector } from '../react-redux'; import { BRAND_COLOR, NAVBAR_SIZE } from '../styles'; import Title from '../title/Title'; @@ -29,34 +29,41 @@ export default function ChatNavBar(props: Props) { streamColor === undefined ? 'default' : foregroundColorFromBackground(streamColor); return ( - - + + - - - <ExtraButton color={color} narrow={narrow} /> - <InfoButton color={color} narrow={narrow} /> - </View> - <LoadingBanner spinnerColor={spinnerColor} backgroundColor={streamColor} textColor={color} /> - </SafeAreaView> + <View + style={{ + flexDirection: 'row', + height: NAVBAR_SIZE, + alignItems: 'center', + }} + > + <NavBarBackButton color={color} /> + <Title color={color} narrow={narrow} editMessage={editMessage} /> + <ExtraButton color={color} narrow={narrow} /> + <InfoButton color={color} narrow={narrow} /> + </View> + <LoadingBanner + spinnerColor={spinnerColor} + backgroundColor={streamColor} + textColor={color} + /> + </SafeAreaView> + </> ); } diff --git a/src/nav/navActions.js b/src/nav/navActions.js index 27b8bd784f9..266cb589215 100644 --- a/src/nav/navActions.js +++ b/src/nav/navActions.js @@ -27,7 +27,7 @@ export const resetToMainTabs = (): GenericNavigationAction => /** Only call this via `doNarrow`. See there for details. */ export const navigateToChat = (narrow: Narrow): GenericNavigationAction => - StackActions.push('chat', { narrow }); + StackActions.push('chat', { narrow, editMessage: null }); export const navigateToUsersScreen = (): GenericNavigationAction => StackActions.push('users'); diff --git a/src/types.js b/src/types.js index 86f2898d766..ee301f7c487 100644 --- a/src/types.js +++ b/src/types.js @@ -124,6 +124,10 @@ export type AggregatedReaction = {| users: $ReadOnlyArray<UserId>, |}; +/** + * ID and original topic/content of an already-sent message that the + * user is currently editing. + */ export type EditMessage = {| id: number, content: string,