diff --git a/src/chat/ChatScreen.js b/src/chat/ChatScreen.js index 3e54fcfb692..096fb63ba7e 100644 --- a/src/chat/ChatScreen.js +++ b/src/chat/ChatScreen.js @@ -21,6 +21,7 @@ import { canSendToNarrow } from '../utils/narrow'; import { getLoading, getSession } from '../directSelectors'; import { getFetchingForNarrow } from './fetchingSelectors'; import { getShownMessagesForNarrow, isNarrowValid as getIsNarrowValid } from './narrowsSelectors'; +import { getTitleBackgroundColor } from '../title/titleSelectors'; type Props = $ReadOnly<{| navigation: AppNavigationProp<'chat'>, @@ -112,11 +113,13 @@ export default function ChatScreen(props: Props) { const sayNoMessages = haveNoMessages && !isFetching; const showComposeBox = canSendToNarrow(narrow) && !showMessagePlaceholders; + const titleBackgroundColor = useSelector(state => getTitleBackgroundColor(state, narrow)); + return ( - + diff --git a/src/common/ZulipStatusBar.js b/src/common/ZulipStatusBar.js index dea62ffdc3f..5aa495b6b80 100644 --- a/src/common/ZulipStatusBar.js +++ b/src/common/ZulipStatusBar.js @@ -7,9 +7,9 @@ import { compose } from 'redux'; import { EdgeInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from '../react-native-safe-area-context'; -import type { Narrow, Orientation, ThemeName, Dispatch } from '../types'; +import type { Orientation, ThemeName, Dispatch } from '../types'; import { connect } from '../react-redux'; -import { DEFAULT_TITLE_BACKGROUND_COLOR, getTitleBackgroundColor } from '../title/titleSelectors'; +import { DEFAULT_TITLE_BACKGROUND_COLOR } from '../title/titleSelectors'; import { foregroundColorFromBackground } from '../utils/color'; import { getSession, getSettings } from '../selectors'; @@ -29,15 +29,13 @@ export const getStatusBarStyle = (statusBarColor: string): BarStyle => type SelectorProps = $ReadOnly<{| theme: ThemeName, - narrowTitleBackgroundColor: string, orientation: Orientation, |}>; type Props = $ReadOnly<{ insets: EdgeInsets, - backgroundColor?: string, - narrow?: Narrow, + backgroundColor: string, hidden: boolean, dispatch: Dispatch, @@ -45,21 +43,17 @@ type Props = $ReadOnly<{ }>; /** - * Controls the status bar settings depending on platform - * and current navigation position. - * If narrowed to a stream or topic the color of the status bar - * matches that of the stream. - * - * @prop [narrow] - Currently active narrow. + * Applies `hidden` and `backgroundColor` in platform-specific ways. */ class ZulipStatusBar extends PureComponent { static defaultProps = { hidden: false, + backgroundColor: DEFAULT_TITLE_BACKGROUND_COLOR, }; render() { const { theme, hidden, insets, orientation } = this.props; - const backgroundColor = this.props.backgroundColor ?? this.props.narrowTitleBackgroundColor; + const backgroundColor = this.props.backgroundColor; const style = { height: hidden ? 0 : insets.top, backgroundColor }; const statusBarColor = getStatusBarColor(backgroundColor, theme); return ( @@ -81,7 +75,6 @@ class ZulipStatusBar extends PureComponent { export default compose( connect((state, props) => ({ theme: getSettings(state).theme, - narrowTitleBackgroundColor: getTitleBackgroundColor(state, props.narrow), orientation: getSession(state).orientation, })), withSafeAreaInsets, diff --git a/src/lightbox/Lightbox.js b/src/lightbox/Lightbox.js index abc3b80a01e..ba88df16867 100644 --- a/src/lightbox/Lightbox.js +++ b/src/lightbox/Lightbox.js @@ -1,13 +1,13 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React, { useState, useCallback } from 'react'; import { View, Dimensions, Easing } from 'react-native'; import PhotoView from 'react-native-photo-view'; -import { connectActionSheet } from '@expo/react-native-action-sheet'; +import { useActionSheet } from '@expo/react-native-action-sheet'; import * as NavigationService from '../nav/NavigationService'; -import type { Auth, Dispatch, Message } from '../types'; -import { connect } from '../react-redux'; +import type { Message } from '../types'; +import { useSelector } from '../react-redux'; import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; import { getAuth } from '../selectors'; import { getResource } from '../utils/url'; @@ -38,106 +38,89 @@ const styles = createStyleSheet({ }); type Props = $ReadOnly<{| - auth: Auth, - dispatch: Dispatch, src: string, message: Message, - showActionSheetWithOptions: ShowActionSheetWithOptions, |}>; -type State = {| - movement: 'in' | 'out', -|}; +export default function Lightbox(props: Props) { + const [movement, setMovement] = useState<'in' | 'out'>('out'); + const showActionSheetWithOptions: ShowActionSheetWithOptions = useActionSheet() + .showActionSheetWithOptions; + const auth = useSelector(getAuth); -class Lightbox extends PureComponent { - state = { - movement: 'out', - }; - - handleImagePress = () => { - this.setState(({ movement }, props) => ({ - movement: movement === 'out' ? 'in' : 'out', - })); - }; - - handleOptionsPress = () => { - const options = constructActionSheetButtons(); - const cancelButtonIndex = options.length - 1; - const { showActionSheetWithOptions, src, auth } = this.props; - showActionSheetWithOptions( - { - options, - cancelButtonIndex, - }, - buttonIndex => { - executeActionSheetAction({ - title: options[buttonIndex], - src, - auth, - }); - }, - ); - }; + // Pulled out here just because this function is used twice. + const handleImagePress = useCallback(() => { + setMovement(m => (m === 'out' ? 'in' : 'out')); + }, [setMovement]); - handlePressBack = () => { - NavigationService.dispatch(navigateBack()); - }; + const { src, message } = props; + const footerMessage = + message.type === 'stream' + ? `Shared in #${streamNameOfStreamMessage(message)}` + : 'Shared with you'; + const resource = getResource(src, auth); + const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); - getAnimationProps = () => ({ + const animationProps = { easing: Easing.bezier(0.075, 0.82, 0.165, 1), duration: 300, - movement: this.state.movement, - }); - - render() { - const { src, message, auth } = this.props; - const footerMessage = - message.type === 'stream' - ? `Shared in #${streamNameOfStreamMessage(message)}` - : 'Shared with you'; - const resource = getResource(src, auth); - const { width, height } = Dimensions.get('window'); + movement, + }; - return ( - - + + + { + NavigationService.dispatch(navigateBack()); + }} + timestamp={message.timestamp} + avatarUrl={message.avatar_url} + senderName={message.sender_full_name} + senderEmail={message.sender_email} /> - - - - - - - - ); - } + + + { + const options = constructActionSheetButtons(); + const cancelButtonIndex = options.length - 1; + showActionSheetWithOptions( + { + options, + cancelButtonIndex, + }, + buttonIndex => { + executeActionSheetAction({ + title: options[buttonIndex], + src, + auth, + }); + }, + ); + }} + /> + + + ); } - -export default connectActionSheet( - connect(state => ({ - auth: getAuth(state), - }))(Lightbox), -); diff --git a/src/lightbox/LightboxScreen.js b/src/lightbox/LightboxScreen.js index b82e5951c2d..d12a9f13008 100644 --- a/src/lightbox/LightboxScreen.js +++ b/src/lightbox/LightboxScreen.js @@ -1,5 +1,5 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React from 'react'; import { View } from 'react-native'; import { ActionSheetProvider } from '@expo/react-native-action-sheet'; @@ -22,16 +22,14 @@ type Props = $ReadOnly<{| route: AppNavigationRouteProp<'lightbox'>, |}>; -export default class LightboxScreen extends PureComponent { - render() { - const { src, message } = this.props.route.params; - return ( - - - ); - } +export default function LightboxScreen(props: Props) { + const { src, message } = props.route.params; + return ( + + + ); } diff --git a/src/nav/ChatNavBar.js b/src/nav/ChatNavBar.js index 066427c9434..04d3a074abc 100644 --- a/src/nav/ChatNavBar.js +++ b/src/nav/ChatNavBar.js @@ -1,12 +1,12 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React from 'react'; import { View } from 'react-native'; import Color from 'color'; -import type { Dispatch, Narrow, EditMessage } from '../types'; +import type { Narrow, EditMessage } from '../types'; import { LoadingBanner } from '../common'; -import { connect } from '../react-redux'; +import { useSelector } from '../react-redux'; import { BRAND_COLOR, NAVBAR_SIZE } from '../styles'; import Title from '../title/Title'; import NavBarBackButton from './NavBarBackButton'; @@ -14,65 +14,53 @@ import { DEFAULT_TITLE_BACKGROUND_COLOR, getTitleBackgroundColor } from '../titl import { foregroundColorFromBackground } from '../utils/color'; import { ExtraButton, InfoButton } from '../title-buttons/titleButtonFromNarrow'; -type SelectorProps = {| - backgroundColor: string, -|}; - type Props = $ReadOnly<{| narrow: Narrow, editMessage: EditMessage | null, - - dispatch: Dispatch, - ...SelectorProps, |}>; -class ChatNavBar extends PureComponent { - render() { - const { backgroundColor, narrow, editMessage } = this.props; - const color = - backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR - ? BRAND_COLOR - : foregroundColorFromBackground(backgroundColor); - const spinnerColor = - backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR - ? 'default' - : foregroundColorFromBackground(backgroundColor); +export default function ChatNavBar(props: Props) { + const { narrow, editMessage } = props; + const backgroundColor = useSelector(state => getTitleBackgroundColor(state, narrow)); + const color = + backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR + ? BRAND_COLOR + : foregroundColorFromBackground(backgroundColor); + const spinnerColor = + backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR + ? 'default' + : foregroundColorFromBackground(backgroundColor); - return ( + return ( + - - - - <ExtraButton color={color} narrow={narrow} /> - <InfoButton color={color} narrow={narrow} /> - </View> - <LoadingBanner - spinnerColor={spinnerColor} - backgroundColor={backgroundColor} - textColor={color} - /> + <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={backgroundColor} + textColor={color} + /> + </View> + ); } - -export default connect<SelectorProps, _, _>((state, props) => ({ - backgroundColor: getTitleBackgroundColor(state, props.narrow), -}))(ChatNavBar); diff --git a/src/nav/ModalNavBar.js b/src/nav/ModalNavBar.js index 0a5a48cdab5..0d87bb96538 100644 --- a/src/nav/ModalNavBar.js +++ b/src/nav/ModalNavBar.js @@ -1,10 +1,9 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React, { useContext } from 'react'; import { View } from 'react-native'; import type { LocalizableText } from '../types'; -import type { ThemeData } from '../styles'; import styles, { ThemeContext, NAVBAR_SIZE } from '../styles'; import Label from '../common/Label'; import NavBarBackButton from './NavBarBackButton'; @@ -14,37 +13,31 @@ type Props = $ReadOnly<{| title: LocalizableText, |}>; -class ModalNavBar extends PureComponent<Props> { - static contextType = ThemeContext; - context: ThemeData; +export default function ModalNavBar(props: Props) { + const { canGoBack, title } = props; + const { backgroundColor } = useContext(ThemeContext); + const textStyle = [ + styles.navTitle, + canGoBack ? { marginRight: NAVBAR_SIZE } : { marginLeft: 16 }, + ]; - render() { - const { canGoBack, title } = this.props; - const textStyle = [ - styles.navTitle, - canGoBack ? { marginRight: NAVBAR_SIZE } : { marginLeft: 16 }, - ]; - - return ( - <View - style={[ - { - borderColor: 'hsla(0, 0%, 50%, 0.25)', - flexDirection: 'row', - height: NAVBAR_SIZE, - alignItems: 'center', - borderBottomWidth: 1, - backgroundColor: this.context.backgroundColor, - }, - ]} - > - {canGoBack && <NavBarBackButton />} - <View style={styles.flexedLeftAlign}> - <Label style={textStyle} text={title} numberOfLines={1} ellipsizeMode="tail" /> - </View> + return ( + <View + style={[ + { + borderColor: 'hsla(0, 0%, 50%, 0.25)', + flexDirection: 'row', + height: NAVBAR_SIZE, + alignItems: 'center', + borderBottomWidth: 1, + backgroundColor, + }, + ]} + > + {canGoBack && <NavBarBackButton />} + <View style={styles.flexedLeftAlign}> + <Label style={textStyle} text={title} numberOfLines={1} ellipsizeMode="tail" /> </View> - ); - } + </View> + ); } - -export default ModalNavBar; diff --git a/src/nav/ModalSearchNavBar.js b/src/nav/ModalSearchNavBar.js index 0f637ea6824..1fb0c635ea5 100644 --- a/src/nav/ModalSearchNavBar.js +++ b/src/nav/ModalSearchNavBar.js @@ -1,8 +1,7 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React, { useContext } from 'react'; import { View } from 'react-native'; -import type { ThemeData } from '../styles'; import { ThemeContext, NAVBAR_SIZE } from '../styles'; import SearchInput from '../common/SearchInput'; import NavBarBackButton from './NavBarBackButton'; @@ -10,35 +9,25 @@ import NavBarBackButton from './NavBarBackButton'; type Props = $ReadOnly<{| autoFocus: boolean, searchBarOnChange: (text: string) => void, - canGoBack: boolean, + canGoBack?: boolean, |}>; -class ModalSearchNavBar extends PureComponent<Props> { - static contextType = ThemeContext; - context: ThemeData; - - static defaultProps = { - canGoBack: true, - }; - - render() { - const { autoFocus, searchBarOnChange, canGoBack } = this.props; - return ( - <View - style={{ - borderColor: 'hsla(0, 0%, 50%, 0.25)', - flexDirection: 'row', - height: NAVBAR_SIZE, - alignItems: 'center', - borderBottomWidth: 1, - backgroundColor: this.context.backgroundColor, - }} - > - {canGoBack && <NavBarBackButton />} - <SearchInput autoFocus={autoFocus} onChangeText={searchBarOnChange} /> - </View> - ); - } +export default function ModalSearchNavBar(props: Props) { + const { autoFocus, searchBarOnChange, canGoBack = true } = props; + const { backgroundColor } = useContext(ThemeContext); + return ( + <View + style={{ + borderColor: 'hsla(0, 0%, 50%, 0.25)', + flexDirection: 'row', + height: NAVBAR_SIZE, + alignItems: 'center', + borderBottomWidth: 1, + backgroundColor, + }} + > + {canGoBack && <NavBarBackButton />} + <SearchInput autoFocus={autoFocus} onChangeText={searchBarOnChange} /> + </View> + ); } - -export default ModalSearchNavBar; diff --git a/src/start/LoadingScreen.js b/src/start/LoadingScreen.js index 86d3f5e1cd0..bf9bc0bec20 100644 --- a/src/start/LoadingScreen.js +++ b/src/start/LoadingScreen.js @@ -1,12 +1,12 @@ /* @flow strict-local */ -import React, { PureComponent } from 'react'; +import React from 'react'; import { View } from 'react-native'; import type { AppNavigationProp, AppNavigationRouteProp } from '../nav/AppNavigator'; import { BRAND_COLOR, createStyleSheet } from '../styles'; import { LoadingIndicator, ZulipStatusBar } from '../common'; -const styles = createStyleSheet({ +const componentStyles = createStyleSheet({ center: { flex: 1, justifyContent: 'center', @@ -25,13 +25,11 @@ type Props = $ReadOnly<{| route?: AppNavigationRouteProp<'loading'>, |}>; -export default class LoadingScreen extends PureComponent<Props> { - render() { - return ( - <View style={styles.center}> - <ZulipStatusBar backgroundColor={BRAND_COLOR} /> - <LoadingIndicator color="black" size={80} showLogo /> - </View> - ); - } +export default function LoadingScreen(props: Props) { + return ( + <View style={componentStyles.center}> + <ZulipStatusBar backgroundColor={BRAND_COLOR} /> + <LoadingIndicator color="black" size={80} showLogo /> + </View> + ); } diff --git a/src/title/__tests__/titleSelectors-test.js b/src/title/__tests__/titleSelectors-test.js index fba7f1c2e8e..0142e507559 100644 --- a/src/title/__tests__/titleSelectors-test.js +++ b/src/title/__tests__/titleSelectors-test.js @@ -10,10 +10,6 @@ describe('getTitleBackgroundColor', () => { subscriptions: [{ ...eg.makeSubscription({ stream: eg.stream }), color: exampleColor }], }); - test('return default for screens other than chat, i.e narrow is undefined', () => { - expect(getTitleBackgroundColor(state, undefined)).toEqual(DEFAULT_TITLE_BACKGROUND_COLOR); - }); - test('return stream color for stream and topic narrow', () => { expect(getTitleBackgroundColor(state, streamNarrow(eg.stream.name))).toEqual(exampleColor); }); diff --git a/src/title/titleSelectors.js b/src/title/titleSelectors.js index b0db78670c6..aeef4f10147 100644 --- a/src/title/titleSelectors.js +++ b/src/title/titleSelectors.js @@ -11,9 +11,9 @@ export const DEFAULT_TITLE_BACKGROUND_COLOR = 'transparent'; * If `narrow` is a stream or topic narrow, this is based on the stream color. * Otherwise, it takes a default value. */ -export const getTitleBackgroundColor = (state: GlobalState, narrow?: Narrow) => { +export const getTitleBackgroundColor = (state: GlobalState, narrow: Narrow) => { const subscriptionsByName = getSubscriptionsByName(state); - if (!narrow || !isStreamOrTopicNarrow(narrow)) { + if (!isStreamOrTopicNarrow(narrow)) { return DEFAULT_TITLE_BACKGROUND_COLOR; } const streamName = streamNameOfNarrow(narrow);