diff --git a/.eslintrc.yaml b/.eslintrc.yaml index d425492c6a5..34aca5d10d2 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -106,7 +106,6 @@ rules: # React Native. This plugin isn't included in the airbnb config, and # nothing is enabled by default, so we enable its rules. - react-native/no-inline-styles: error # react-native/no-unused-styles: error # This is buggy on the `this.styles` pattern. # react-native/no-color-literals: error # TODO eliminate these and enable. diff --git a/src/account/AccountPickScreen.js b/src/account/AccountPickScreen.js index 4510767bcfb..1907de29c04 100644 --- a/src/account/AccountPickScreen.js +++ b/src/account/AccountPickScreen.js @@ -49,7 +49,13 @@ class AccountPickScreen extends PureComponent { const { accounts, dispatch } = this.props; return ( - + {accounts.length === 0 && } ; + +type Props = $ReadOnly<{| + spinnerColor?: 'black' | 'white' | 'default', + textColor?: string, + backgroundColor?: string, + + dispatch: Dispatch, + ...SelectorProps, +|}>; + +/** + * Display a notice that the app is connecting to the server, when appropriate. + */ +class LoadingBanner extends PureComponent { + static contextType = ThemeContext; + context: ThemeColors; + + render() { + if (!this.props.loading) { + return ; + } + const { + spinnerColor = 'default', + textColor = this.context.color, + backgroundColor = this.context.backgroundColor, + } = this.props; + const style = { + width: '100%', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor, + }; + return ( + + + + + + ); + } +} + +export default connect(state => ({ + loading: getLoading(state), +}))(LoadingBanner); diff --git a/src/common/LoadingIndicator.js b/src/common/LoadingIndicator.js index 519a706cc7e..ad36a51b6ec 100644 --- a/src/common/LoadingIndicator.js +++ b/src/common/LoadingIndicator.js @@ -21,13 +21,13 @@ const styles = StyleSheet.create({ }); type Props = $ReadOnly<{| - color: string, + color: 'default' | 'black' | 'white', showLogo: boolean, size: number, |}>; /** - * Renders a loading indicator - light circle and a darker + * Renders a loading indicator - a faint circle and a bold * quarter of a circle spinning around it. Optionally, * a Zulip logo in the center. * @@ -37,7 +37,7 @@ type Props = $ReadOnly<{| */ export default class LoadingIndicator extends PureComponent { static defaultProps = { - color: '82, 194, 175', + color: 'default', showLogo: false, size: 40, }; diff --git a/src/common/Screen.js b/src/common/Screen.js index 9c8203722db..69034eea1d0 100644 --- a/src/common/Screen.js +++ b/src/common/Screen.js @@ -9,6 +9,7 @@ import type { Context, Dimensions, LocalizableText, Dispatch } from '../types'; import { connect } from '../react-redux'; import KeyboardAvoider from './KeyboardAvoider'; import OfflineNotice from './OfflineNotice'; +import LoadingBanner from './LoadingBanner'; import ZulipStatusBar from './ZulipStatusBar'; import { getSession } from '../selectors'; import ModalNavBar from '../nav/ModalNavBar'; @@ -43,6 +44,7 @@ type Props = $ReadOnly<{| search: boolean, autoFocus: boolean, searchBarOnChange: (text: string) => void, + shouldShowLoadingBanner: boolean, canGoBack: boolean, +title: LocalizableText, @@ -85,6 +87,7 @@ class Screen extends PureComponent { search: false, autoFocus: false, searchBarOnChange: (text: string) => {}, + shouldShowLoadingBanner: true, canGoBack: true, title: '', @@ -104,6 +107,7 @@ class Screen extends PureComponent { searchBarOnChange, style, title, + shouldShowLoadingBanner, } = this.props; const { styles: contextStyles } = this.context; @@ -116,6 +120,7 @@ class Screen extends PureComponent { )} + {shouldShowLoadingBanner && } ; @@ -17,14 +18,23 @@ type Props = $ReadOnly<{| * * This is a temporary replacement of the ART-based SpinningProgress. * - * @prop color - The color of the circle. Works only for 'black' and 'default'. + * @prop color - The color of the circle. * @prop size - Diameter of the circle in pixels. */ export default class SpinningProgress extends React.PureComponent { render() { const { color, size } = this.props; const style = { width: size, height: size }; - const source = color === '0, 0, 0' ? spinningProgressBlackImg : spinningProgressImg; + const source = (() => { + switch (color) { + case 'white': + return spinningProgressWhiteImg; + case 'black': + return spinningProgressBlackImg; + default: + return spinningProgressImg; + } + })(); return ( diff --git a/src/common/index.js b/src/common/index.js index 7186a688ba6..1e7592322f8 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -15,6 +15,7 @@ export { default as KeyboardAvoider } from './KeyboardAvoider'; export { default as Label } from './Label'; export { default as LineSeparator } from './LineSeparator'; export { default as LoadingIndicator } from './LoadingIndicator'; +export { default as LoadingBanner } from './LoadingBanner'; export { default as Logo } from './Logo'; export { default as OfflineNotice } from './OfflineNotice'; export { default as OptionButton } from './OptionButton'; diff --git a/src/main/HomeTab.js b/src/main/HomeTab.js index e4e25b46990..62896e07f0b 100644 --- a/src/main/HomeTab.js +++ b/src/main/HomeTab.js @@ -12,6 +12,7 @@ import UnreadCards from '../unread/UnreadCards'; import { doNarrow, navigateToSearch } from '../actions'; import IconUnreadMentions from '../nav/IconUnreadMentions'; import { BRAND_COLOR } from '../styles'; +import { LoadingBanner } from '../common'; const styles = StyleSheet.create({ wrapper: { @@ -61,6 +62,7 @@ class HomeTab extends PureComponent { }} /> + ); diff --git a/src/nav/ChatNavBar.js b/src/nav/ChatNavBar.js index d6d0ad0c010..8a16ac8842c 100644 --- a/src/nav/ChatNavBar.js +++ b/src/nav/ChatNavBar.js @@ -4,8 +4,9 @@ import React, { PureComponent } from 'react'; import { View } from 'react-native'; import type { Dispatch, Narrow } from '../types'; +import { LoadingBanner } from '../common'; import { connect } from '../react-redux'; -import styles, { BRAND_COLOR } from '../styles'; +import { BRAND_COLOR, NAVBAR_SIZE } from '../styles'; import Title from '../title/Title'; import NavButton from './NavButton'; import { DEFAULT_TITLE_BACKGROUND_COLOR, getTitleBackgroundColor } from '../title/titleSelectors'; @@ -31,19 +32,44 @@ class ChatNavBar extends PureComponent { backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR ? BRAND_COLOR : foregroundColorFromBackground(backgroundColor); + const spinnerColor = + backgroundColor === DEFAULT_TITLE_BACKGROUND_COLOR + ? 'default' + : foregroundColorFromBackground(backgroundColor); return ( - - { - dispatch(navigateBack()); - }} + + + { + dispatch(navigateBack()); + }} + /> + + <ExtraButton color={color} narrow={narrow} /> + <InfoButton color={color} narrow={narrow} /> + </View> + <LoadingBanner + spinnerColor={spinnerColor} + backgroundColor={backgroundColor} + textColor={color} /> - <Title color={color} narrow={narrow} /> - <ExtraButton color={color} narrow={narrow} /> - <InfoButton color={color} narrow={narrow} /> </View> ); } diff --git a/src/nav/ModalNavBar.js b/src/nav/ModalNavBar.js index 96ac0f9b6e0..07ef74d22ba 100644 --- a/src/nav/ModalNavBar.js +++ b/src/nav/ModalNavBar.js @@ -3,9 +3,11 @@ import React, { PureComponent } from 'react'; import { View } from 'react-native'; -import type { Dispatch, Context, LocalizableText } from '../types'; +import type { Dispatch, LocalizableText } from '../types'; +import type { ThemeColors } from '../styles'; +import styles, { ThemeContext, NAVBAR_SIZE } from '../styles'; import { connect } from '../react-redux'; -import styles, { NAVBAR_SIZE } from '../styles'; + import Label from '../common/Label'; import NavButton from './NavButton'; import { navigateBack } from '../actions'; @@ -17,14 +19,10 @@ type Props = $ReadOnly<{| |}>; class ModalNavBar extends PureComponent<Props> { - context: Context; - - static contextTypes = { - styles: () => null, - }; + static contextType = ThemeContext; + context: ThemeColors; render() { - const { styles: contextStyles } = this.context; const { dispatch, canGoBack, title } = this.props; const textStyle = [ styles.navTitle, @@ -32,7 +30,18 @@ class ModalNavBar extends PureComponent<Props> { ]; return ( - <View style={[contextStyles.navBar]}> + <View + style={[ + { + borderColor: 'hsla(0, 0%, 50%, 0.25)', + flexDirection: 'row', + height: NAVBAR_SIZE, + alignItems: 'center', + borderBottomWidth: 1, + backgroundColor: this.context.backgroundColor, + }, + ]} + > {canGoBack && ( <NavButton name="arrow-left" diff --git a/src/nav/ModalSearchNavBar.js b/src/nav/ModalSearchNavBar.js index 603d9c6d804..976dd28115b 100644 --- a/src/nav/ModalSearchNavBar.js +++ b/src/nav/ModalSearchNavBar.js @@ -2,7 +2,9 @@ import React, { PureComponent } from 'react'; import { View } from 'react-native'; -import type { Dispatch, Context } from '../types'; +import type { Dispatch } from '../types'; +import type { ThemeColors } from '../styles'; +import { ThemeContext, NAVBAR_SIZE } from '../styles'; import { connect } from '../react-redux'; import SearchInput from '../common/SearchInput'; import NavButton from './NavButton'; @@ -15,17 +17,22 @@ type Props = $ReadOnly<{| |}>; class ModalSearchNavBar extends PureComponent<Props> { - context: Context; - - static contextTypes = { - styles: () => null, - }; + static contextType = ThemeContext; + context: ThemeColors; render() { - const { styles: contextStyles } = this.context; const { dispatch, autoFocus, searchBarOnChange } = this.props; return ( - <View style={contextStyles.navBar}> + <View + style={{ + borderColor: 'hsla(0, 0%, 50%, 0.25)', + flexDirection: 'row', + height: NAVBAR_SIZE, + alignItems: 'center', + borderBottomWidth: 1, + backgroundColor: this.context.backgroundColor, + }} + > <NavButton name="arrow-left" onPress={() => { diff --git a/src/pm-conversations/PmConversationsCard.js b/src/pm-conversations/PmConversationsCard.js index c2b6a652f47..3f62fe2efff 100644 --- a/src/pm-conversations/PmConversationsCard.js +++ b/src/pm-conversations/PmConversationsCard.js @@ -5,7 +5,7 @@ import { View, StyleSheet } from 'react-native'; import type { Context, Dispatch, PmConversationData, UserOrBot } from '../types'; import { connect } from '../react-redux'; -import { Label, ZulipButton } from '../common'; +import { Label, ZulipButton, LoadingBanner } from '../common'; import { IconPeople, IconSearch } from '../common/Icons'; import PmConversationList from './PmConversationList'; import { getRecentConversations, getAllUsersByEmail } from '../selectors'; @@ -72,6 +72,7 @@ class PmConversationsCard extends PureComponent<Props> { }} /> </View> + <LoadingBanner /> {conversations.length === 0 ? ( <Label style={styles.emptySlate} text="No recent conversations" /> ) : ( diff --git a/src/session/sessionReducer.js b/src/session/sessionReducer.js index 1f515159c69..a96a447caa2 100644 --- a/src/session/sessionReducer.js +++ b/src/session/sessionReducer.js @@ -37,7 +37,15 @@ export type SessionState = {| isActive: boolean, isHydrated: boolean, lastNarrow: ?Narrow, + + /** + * Whether the /register request is in progress. + * + * This happens on startup, or on re-init following a dead event + * queue after 10 minutes of inactivity. + */ loading: boolean, + needsInitialFetch: boolean, orientation: Orientation, outboxSending: boolean, diff --git a/src/start/AuthScreen.js b/src/start/AuthScreen.js index d1a732fadca..7f7122f183f 100644 --- a/src/start/AuthScreen.js +++ b/src/start/AuthScreen.js @@ -235,7 +235,7 @@ class AuthScreen extends PureComponent<Props> { const { serverSettings } = this.props.navigation.state.params; return ( - <Screen title="Log in" centerContent padding> + <Screen title="Log in" centerContent padding shouldShowLoadingBanner={false}> <Centerer> <RealmInfo name={serverSettings.realm_name} diff --git a/src/start/DevAuthScreen.js b/src/start/DevAuthScreen.js index 64533ae6b6f..e74ac01b239 100644 --- a/src/start/DevAuthScreen.js +++ b/src/start/DevAuthScreen.js @@ -84,7 +84,7 @@ class DevAuthScreen extends PureComponent<Props, State> { const { directAdmins, directUsers, error, progress } = this.state; return ( - <Screen title="Pick a dev account"> + <Screen title="Pick a dev account" shouldShowLoadingBanner={false}> <View style={componentStyles.container}> {progress && <ActivityIndicator />} {!!error && <ErrorMsg error={error} />} diff --git a/src/start/LoadingScreen.js b/src/start/LoadingScreen.js index 41be32a6c1c..f2178af1d04 100644 --- a/src/start/LoadingScreen.js +++ b/src/start/LoadingScreen.js @@ -19,7 +19,7 @@ export default class LoadingScreen extends PureComponent<{||}> { return ( <View style={styles.center}> <ZulipStatusBar backgroundColor={BRAND_COLOR} /> - <LoadingIndicator color="0, 0, 0" size={80} showLogo /> + <LoadingIndicator color="black" size={80} showLogo /> </View> ); } diff --git a/src/start/PasswordAuthScreen.js b/src/start/PasswordAuthScreen.js index 636cd255299..9b4dbc5128f 100644 --- a/src/start/PasswordAuthScreen.js +++ b/src/start/PasswordAuthScreen.js @@ -91,7 +91,13 @@ class PasswordAuthScreen extends PureComponent<Props, State> { || (requireEmailFormat && !isValidEmailFormat(email)); return ( - <Screen title="Log in" centerContent padding keyboardShouldPersistTaps="always"> + <Screen + title="Log in" + centerContent + padding + keyboardShouldPersistTaps="always" + shouldShowLoadingBanner={false} + > <Input autoFocus={email.length === 0} autoCapitalize="none" diff --git a/src/start/RealmScreen.js b/src/start/RealmScreen.js index 3217c6add22..d52bfdd664c 100644 --- a/src/start/RealmScreen.js +++ b/src/start/RealmScreen.js @@ -93,6 +93,7 @@ class RealmScreen extends PureComponent<Props, State> { padding centerContent keyboardShouldPersistTaps="always" + shouldShowLoadingBanner={false} > <Label text="Enter your Zulip server URL:" /> <SmartUrlInput diff --git a/src/streams/SubscriptionsCard.js b/src/streams/SubscriptionsCard.js index b21582d3d74..a64b05887fa 100644 --- a/src/streams/SubscriptionsCard.js +++ b/src/streams/SubscriptionsCard.js @@ -6,6 +6,7 @@ import { View, StyleSheet } from 'react-native'; import type { Dispatch, Subscription } from '../types'; import { connect } from '../react-redux'; import StreamList from './StreamList'; +import { LoadingBanner } from '../common'; import { streamNarrow } from '../utils/narrow'; import { getUnreadByStream } from '../selectors'; import { getSubscribedStreams } from '../subscriptions/subscriptionSelectors'; @@ -38,6 +39,7 @@ class SubscriptionsCard extends PureComponent<Props> { return ( <View style={styles.container}> + <LoadingBanner /> <StreamList streams={subscriptions} unreadByStream={unreadByStream} diff --git a/src/styles/navStyles.js b/src/styles/navStyles.js index 7aa6e024402..040a364260f 100644 --- a/src/styles/navStyles.js +++ b/src/styles/navStyles.js @@ -1,6 +1,5 @@ /* @flow strict-local */ -import type { ThemeColors } from './theme'; -import { BRAND_COLOR, NAVBAR_SIZE } from './constants'; +import { BRAND_COLOR } from './constants'; export const statics = { navWrapper: { @@ -17,18 +16,4 @@ export const statics = { textAlign: 'left', fontSize: 20, }, - navBar: { - borderColor: 'hsla(0, 0%, 50%, 0.25)', - flexDirection: 'row', - height: NAVBAR_SIZE, - alignItems: 'center', - borderBottomWidth: 1, - }, }; - -export default ({ backgroundColor }: ThemeColors) => ({ - navBar: { - ...statics.navBar, - backgroundColor, - }, -}); diff --git a/src/styles/theme.js b/src/styles/theme.js index b20ed4ecaa3..4aad30fab06 100644 --- a/src/styles/theme.js +++ b/src/styles/theme.js @@ -4,7 +4,6 @@ import type { Context } from 'react'; import { StyleSheet } from 'react-native'; import type { ThemeName } from '../types'; -import navStyles from './navStyles'; import miscStyles from './miscStyles'; export type ThemeColors = {| @@ -15,7 +14,6 @@ export type ThemeColors = {| |}; export type AppStyles = $ReadOnly<{| - ...$Call<typeof navStyles, ThemeColors>, ...$Call<typeof miscStyles, ThemeColors>, |}>; @@ -44,7 +42,6 @@ export const ThemeContext: Context<ThemeColors> = React.createContext(themeColor export const stylesFromTheme = (name: ThemeName) => { const colors = themeColors[name]; return StyleSheet.create({ - ...navStyles(colors), ...miscStyles(colors), }); }; diff --git a/src/subscriptions/StreamListCard.js b/src/subscriptions/StreamListCard.js index 123f7c7d132..81fbd5fbc14 100644 --- a/src/subscriptions/StreamListCard.js +++ b/src/subscriptions/StreamListCard.js @@ -5,7 +5,7 @@ import { StyleSheet, View } from 'react-native'; import type { Auth, Dispatch, Stream, Subscription } from '../types'; import { connect } from '../react-redux'; -import { ZulipButton } from '../common'; +import { ZulipButton, LoadingBanner } from '../common'; import * as api from '../api'; import { delay } from '../utils/async'; import { streamNarrow } from '../utils/narrow'; @@ -55,6 +55,7 @@ class StreamListCard extends PureComponent<Props> { return ( <View style={styles.wrapper}> + <LoadingBanner /> {canCreateStreams && ( <ZulipButton style={styles.button} diff --git a/src/unread/UnreadCards.js b/src/unread/UnreadCards.js index bd535c96250..352fd474ad1 100644 --- a/src/unread/UnreadCards.js +++ b/src/unread/UnreadCards.js @@ -5,13 +5,12 @@ import { SectionList } from 'react-native'; import type { Dispatch, PmConversationData, UnreadStreamItem, UserOrBot } from '../types'; import { connect } from '../react-redux'; -import { LoadingIndicator, SearchEmptyState } from '../common'; +import { SearchEmptyState } from '../common'; import PmConversationList from '../pm-conversations/PmConversationList'; import StreamItem from '../streams/StreamItem'; import TopicItem from '../streams/TopicItem'; import { streamNarrow, topicNarrow } from '../utils/narrow'; import { - getLoading, getUnreadConversations, getAllUsersByEmail, getUnreadStreamsAndTopicsSansMuted, @@ -21,7 +20,6 @@ import { doNarrow } from '../actions'; type Props = $ReadOnly<{| conversations: PmConversationData[], dispatch: Dispatch, - isLoading: boolean, usersByEmail: Map<string, UserOrBot>, unreadStreamsAndTopics: UnreadStreamItem[], |}>; @@ -36,7 +34,7 @@ class UnreadCards extends PureComponent<Props> { }; render() { - const { isLoading, conversations, unreadStreamsAndTopics, ...restProps } = this.props; + const { conversations, unreadStreamsAndTopics, ...restProps } = this.props; type Card = | UnreadStreamItem | { key: 'private', data: Array<$PropertyType<PmConversationList, 'props'>> }; @@ -49,11 +47,7 @@ class UnreadCards extends PureComponent<Props> { ]; if (unreadStreamsAndTopics.length === 0 && conversations.length === 0) { - return isLoading ? ( - <LoadingIndicator size={40} /> - ) : ( - <SearchEmptyState text="No unread messages" /> - ); + return <SearchEmptyState text="No unread messages" />; } return ( @@ -96,7 +90,6 @@ class UnreadCards extends PureComponent<Props> { } export default connect(state => ({ - isLoading: getLoading(state), conversations: getUnreadConversations(state), usersByEmail: getAllUsersByEmail(state), unreadStreamsAndTopics: getUnreadStreamsAndTopicsSansMuted(state), diff --git a/src/utils/color.js b/src/utils/color.js index b6122a7b88f..0bd4b376320 100644 --- a/src/utils/color.js +++ b/src/utils/color.js @@ -2,7 +2,7 @@ import Color from 'color'; import type { ColorValue } from 'react-native/Libraries/StyleSheet/StyleSheetTypes'; -export const foregroundColorFromBackground = (color: ColorValue): string => +export const foregroundColorFromBackground = (color: ColorValue): 'black' | 'white' => Color(color).luminosity() > 0.4 ? 'black' : 'white'; export const colorHashFromString = (name: string): string => { diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index 5cbd4e7a59c..9c874d450b6 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -321,7 +321,6 @@ class MessageList extends Component<Props> { source={{ baseUrl, html }} originWhitelist={['file://']} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - /* eslint-disable react-native/no-inline-styles */ style={{ backgroundColor: this.context.backgroundColor }} ref={webview => { this.webview = webview; diff --git a/static/img/spinning-progress-white.png b/static/img/spinning-progress-white.png new file mode 100644 index 00000000000..52c80b07be4 Binary files /dev/null and b/static/img/spinning-progress-white.png differ