Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/actionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const PRESENCE_RESPONSE: 'PRESENCE_RESPONSE' = 'PRESENCE_RESPONSE';
export const GET_USER_RESPONSE: 'GET_USER_RESPONSE' = 'GET_USER_RESPONSE';
export const SETTINGS_CHANGE: 'SETTINGS_CHANGE' = 'SETTINGS_CHANGE';
export const DEBUG_FLAG_TOGGLE: 'DEBUG_FLAG_TOGGLE' = 'DEBUG_FLAG_TOGGLE';
export const DISMISS_SERVER_COMPAT_NOTICE: 'DISMISS_SERVER_COMPAT_NOTICE' =
'DISMISS_SERVER_COMPAT_NOTICE';

export const GOT_PUSH_TOKEN: 'GOT_PUSH_TOKEN' = 'GOT_PUSH_TOKEN';
export const ACK_PUSH_TOKEN: 'ACK_PUSH_TOKEN' = 'ACK_PUSH_TOKEN';
Expand Down
6 changes: 6 additions & 0 deletions src/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
EVENT_SUBMESSAGE,
EVENT_SUBSCRIPTION,
EVENT,
DISMISS_SERVER_COMPAT_NOTICE,
} from './actionConstants';

import type {
Expand Down Expand Up @@ -132,6 +133,10 @@ type DebugFlagToggleAction = {|
value: boolean,
|};

type DismissServerCompatNoticeAction = {|
type: typeof DISMISS_SERVER_COMPAT_NOTICE,
|};

type AccountSwitchAction = {|
type: typeof ACCOUNT_SWITCH,
index: number,
Expand Down Expand Up @@ -610,6 +615,7 @@ type SessionAction =
| AppOrientationAction
| GotPushTokenAction
| DebugFlagToggleAction
| DismissServerCompatNoticeAction
| ToggleOutboxSendingAction;

/** Covers all actions we ever `dispatch`. */
Expand Down
34 changes: 10 additions & 24 deletions src/common/Label.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,32 @@
/* @flow strict-local */
import React, { PureComponent } from 'react';
import { Text } from 'react-native';
import TranslatedText from './TranslatedText';

import type { ThemeData } from '../styles';
import { ThemeContext } from '../styles';
import type { BoundedDiff } from '../generics';
import RawLabel from './RawLabel';
import type { LocalizableText } from '../types';

type Props = $ReadOnly<{|
...$Exact<React$ElementConfig<typeof Text>>,
...BoundedDiff<$Exact<React$ElementConfig<typeof RawLabel>>, {| children: ?React$Node |}>,
text: LocalizableText,
|}>;

/**
* A component that on top of a standard Text component
* provides seamless translation and ensures consistent
* styling for the default and night themes.
* A wrapper for `RawLabel` that also translates the text.
*
* Use `RawLabel` if you don't want the text translated.
* Use `RawLabel` instead if you don't want the text translated.
*
* @prop text - Translated before putting inside Text.
* @prop [style] - Can override our default style for this component.
* @prop ...all other Text props - Passed through verbatim to Text.
* See upstream: https://reactnative.dev/docs/text
* Unlike `RawLabel`, only accepts a `LocalizableText`, as the `text`
* prop, and doesn't support `children`.
*/
export default class Label extends PureComponent<Props> {
static contextType = ThemeContext;
context: ThemeData;

styles = {
label: {
fontSize: 15,
},
};

render() {
const { text, style, ...restProps } = this.props;
const { text, ...restProps } = this.props;

return (
<Text style={[this.styles.label, { color: this.context.color }, style]} {...restProps}>
<RawLabel {...restProps}>
<TranslatedText text={text} />
</Text>
</RawLabel>
);
}
}
31 changes: 19 additions & 12 deletions src/common/RawLabel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* @flow strict-local */
import invariant from 'invariant';
import React, { PureComponent } from 'react';
import { Text } from 'react-native';

Expand All @@ -7,14 +8,15 @@ import { ThemeContext } from '../styles';

type Props = $ReadOnly<{|
...$Exact<React$ElementConfig<typeof Text>>,
text: string,
text?: string,
|}>;

/**
* A component that on top of a standard Text component
* ensures consistent styling for the default and night themes.
* A thin wrapper for `Text` that ensures a consistent, themed style.
*
* Unlike `Label` it does not translate its contents.
* Unlike `Label`, it does not translate its contents.
*
* Pass either `text` or `children`, but not both.
*
* @prop text - Contents for Text.
* @prop [style] - Can override our default style for this component.
Expand All @@ -25,18 +27,23 @@ export default class RawLabel extends PureComponent<Props> {
static contextType = ThemeContext;
context: ThemeData;

styles = {
label: {
fontSize: 15,
},
};

render() {
const { text, style, ...restProps } = this.props;
const { text, children, style, ...restProps } = this.props;

invariant(!!text !== !!children, 'pass either `text` or `children`');

// These attributes will be applied unless specifically overridden
// with the `style` prop -- even if this `<RawLabel />` is nested
// and would otherwise inherit the attributes from its ancestors.
const aggressiveDefaultStyle = {
fontSize: 15,
color: this.context.color,
};

return (
<Text style={[this.styles.label, { color: this.context.color }, style]} {...restProps}>
<Text style={[aggressiveDefaultStyle, style]} {...restProps}>
{text}
{children}
</Text>
);
}
Expand Down
108 changes: 108 additions & 0 deletions src/common/ServerCompatBanner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* @flow strict-local */

import React from 'react';
import { View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

import store from '../boot/store';
import { createStyleSheet, HALF_COLOR } from '../styles';
import { useSelector, useDispatch } from '../react-redux';
import Label from './Label';
import { getActiveAccount } from '../account/accountsSelectors';
import { getIsAdmin, getSession } from '../directSelectors';
import { dismissCompatNotice } from '../session/sessionActions';
import ZulipTextButton from './ZulipTextButton';
import { openLinkWithUserPreference } from '../utils/openLink';

// In fact the oldest version we currently support is 2.1.0, per our docs:
// https://zulip.readthedocs.io/en/4.2/overview/release-lifecycle.html
// For now we only show this banner for servers older than 2.0.0, though,
// in order to phase that in gradually.
const minSupportedVersion = '2.0.0';

const styles = createStyleSheet({
wrapper: {
backgroundColor: HALF_COLOR,
paddingLeft: 16,
paddingRight: 8,
paddingBottom: 8,
},
textRow: {
flexDirection: 'row',
},
text: {
marginTop: 16,
lineHeight: 20,
},
buttonsRow: {
marginTop: 12,
flexDirection: 'row',
justifyContent: 'flex-end',
},
});

type Props = $ReadOnly<{||}>;

/**
* A "nag banner" saying the server version is unsupported, if so.
*/
// Made with somewhat careful attention to
// https://material.io/components/banners. Please consult that before making
// layout changes, and try to make them in a direction that brings us closer
// to those guidelines.
export default function ServerCompatBanner(props: Props) {
const dispatch = useDispatch();
const hasDismissedServerCompatNotice = useSelector(
state => getSession(state).hasDismissedServerCompatNotice,
);
const zulipVersion = useSelector(state => getActiveAccount(state).zulipVersion);
const realm = useSelector(state => getActiveAccount(state).realm);
const isAdmin = useSelector(getIsAdmin);

if (!zulipVersion || zulipVersion.isAtLeast(minSupportedVersion)) {
return null;
} else if (hasDismissedServerCompatNotice) {
return null;
}

return (
<SafeAreaView mode="padding" edges={['right', 'left']} style={styles.wrapper}>
<View style={styles.textRow}>
<Label
style={styles.text}
text={
isAdmin
? {
text:
'{realm} is running Zulip Server {serverVersion}, which is unsupported. Please upgrade your server as soon as possible.',
values: { realm: realm.toString(), serverVersion: zulipVersion.raw() },
}
: {
text:
'{realm} is running Zulip Server {serverVersion}, which is unsupported. Please contact your administrator about upgrading.',
values: { realm: realm.toString(), serverVersion: zulipVersion.raw() },
}
}
/>
</View>
<View style={styles.buttonsRow}>
<ZulipTextButton
label="Dismiss"
onPress={() => {
dispatch(dismissCompatNotice());
}}
/>
<ZulipTextButton
leftMargin
label={isAdmin ? 'Fix now' : 'Learn more'}
onPress={() => {
openLinkWithUserPreference(
'https://zulip.readthedocs.io/en/stable/overview/release-lifecycle.html#compatibility-and-upgrading',
store.getState,
);
}}
/>
</View>
</SafeAreaView>
);
}
3 changes: 0 additions & 3 deletions src/common/WebLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ type Props = $ReadOnly<{|

const componentStyles = createStyleSheet({
link: {
marginTop: 10,
fontSize: 15,
color: BRAND_COLOR,
textAlign: 'right',
},
});

Expand Down
118 changes: 118 additions & 0 deletions src/common/ZulipTextButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* @flow strict-local */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we can't pull in a button like this from a library of material components? I guess this is BRAND_COLOR by default, is there anything else?

Copy link
Contributor Author

@chrisbobbe chrisbobbe May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good idea—maybe https://callstack.github.io/react-native-paper/button.html; I'll give it a try.

We may like to make our own thin wrapper around that component, for potentially a few reasons:

  • setting project-specific things like BRAND_COLOR, as you mention
  • That component says it accepts a style prop. Depending on how much micromanaging that style prop allows, we might want our wrapper to not expose it directly, and instead expose a few coherent options. See this implementation comment in ZulipButton:
      // The use of specific style properties for callers to micromanage the
      // layout of the button is pretty chaotic and fragile, introducing
      // implicit dependencies on details of the ZulipButton implementation.
      // TODO: Assimilate those usages into a more coherent set of options
      //   provided by the ZulipButton interface explicitly.
      //
      // Until then, by listing here the properties we do use, we at least
      // make it possible when working on the ZulipButton implementation to
      // know the scope of different ways that callers can mess with the
      // styles.  If you need one not listed here and it's in the same
      // spirit as others that are, feel free to add it.

import React, { useMemo } from 'react';
import { View } from 'react-native';

import type { LocalizableText } from '../types';
import { BRAND_COLOR, createStyleSheet } from '../styles';
import { Label } from '.';
import Touchable from './Touchable';

// When adding a variant, take care that it's legitimate, it addresses a
// common use case consistently, and its styles are defined coherently. We
// don't want this component to grow brittle with lots of little flags to
// micromanage its styles.
type Variant = 'standard';

const styleSheetForVariant = (variant: Variant) =>
createStyleSheet({
// See https://material.io/components/buttons#specs.
touchable: {
height: 36,
paddingHorizontal: 8,
minWidth: 64,
},

// Chosen because of the value for this distance at
// https://material.io/components/banners#specs. A banner is one context
// where we've wanted to use text buttons.
leftMargin: {
marginLeft: 8,
},
rightMargin: {
marginRight: 8,
},

// `Touchable` only accepts one child, so make sure it fills the
// `Touchable`'s full area and centers everything in it.
childOfTouchable: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},

text: {
// From the spec:
// > Text labels need to be distinct from other elements. If the text
// > label isn’t fully capitalized, it should use a different color,
// > style, or layout from other text.
textTransform: 'uppercase',
color: BRAND_COLOR,

textAlign: 'center',
textAlignVertical: 'center',
},
});

type Props = $ReadOnly<{|
/** See note on the `Variant` type. */
variant?: Variant,

/**
* Give a left margin of the correct in-between space for a set of buttons
*/
leftMargin?: true,

/**
* Give a right margin of the correct in-between space for a set of
* buttons
*/
rightMargin?: true,

/**
* The text label: https://material.io/components/buttons#text-button
*
* Should be short.
*/
label: LocalizableText,

onPress: () => void | Promise<void>,
|}>;

/**
* A button modeled on Material Design's "text button" concept.
*
* See https://material.io/components/buttons#text-button :
*
* > Text buttons are typically used for less-pronounced actions, including
* > those located
* >
* > - In dialogs
* > - In cards
* >
* > In cards, text buttons help maintain an emphasis on card content.
*/
// TODO: Consider making this a thin wrapper around something like
// react-native-paper's `Button`
// (https://callstack.github.io/react-native-paper/button.html), encoding
// things like project-specific styles and making any sensible adjustments
// to the interface.
export default function ZulipTextButton(props: Props) {
const { variant = 'standard', leftMargin, rightMargin, label, onPress } = props;

const variantStyles = useMemo(() => styleSheetForVariant(variant), [variant]);

return (
<Touchable
style={[
variantStyles.touchable,
leftMargin && variantStyles.leftMargin,
rightMargin && variantStyles.rightMargin,
]}
onPress={onPress}
>
<View style={variantStyles.childOfTouchable}>
<Label style={variantStyles.text} text={label} />
</View>
</Touchable>
);
}
Loading