From 5834ab5e22e539c3ca8562869908e179a524e561 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 6 Jul 2020 17:56:28 -0300 Subject: [PATCH] [IMPROVEMENT] Unified header UX (#2234) * Change drawer icon * Removed iOS variation * Patch to react-navigation-header-buttons... easier to patch then to overwrite its behaviour :( * Correctly position title * Header subtitle * Layout * Alignment * RoomView header * Renamed RoomHeaderLeft to LeftButtons * RoomView back button * Search icon on RoomView * Refactor * Fix header on tablet * Fix search messages close button on tablet * Search key command * Network status on RoomView header subtitle * Update tests * Scale content * SearchBox cancel color --- app/constants/colors.js | 24 +++-- app/containers/DisclosureIndicator.js | 2 +- app/containers/Header/index.js | 5 + app/containers/HeaderButton.js | 10 +- app/containers/SearchBox.js | 2 +- app/containers/StatusBar.js | 3 +- app/i18n/locales/en.js | 1 + app/i18n/locales/pt-BR.js | 1 + app/views/RoomView/Header/Header.js | 18 +--- app/views/RoomView/Header/Icon.js | 3 +- .../{RoomHeaderLeft.js => LeftButtons.js} | 13 ++- app/views/RoomView/Header/RightButtons.js | 17 ++++ app/views/RoomView/Header/index.js | 25 ++++- app/views/RoomView/index.js | 56 +++++++---- app/views/RoomsListView/Header/Header.ios.js | 95 ------------------- .../Header/{Header.android.js => Header.js} | 49 ++++++---- app/views/RoomsListView/Header/index.js | 10 +- .../RoomsListView/ListHeader/SearchBar.js | 36 ------- app/views/RoomsListView/ListHeader/index.js | 20 +--- app/views/RoomsListView/index.js | 86 ++++++++--------- app/views/RoomsListView/styles.js | 16 ++-- e2e/helpers/app.js | 11 ++- e2e/tests/assorted/02-broadcast.spec.js | 6 +- e2e/tests/assorted/05-joinpublicroom.spec.js | 6 +- e2e/tests/onboarding/06-roomslist.spec.js | 8 +- e2e/tests/room/02-room.spec.js | 6 +- e2e/tests/room/03-roomactions.spec.js | 7 +- e2e/tests/room/04-roominfo.spec.js | 8 +- ...eact-navigation-header-buttons+3.0.5.patch | 12 +++ 29 files changed, 234 insertions(+), 322 deletions(-) rename app/views/RoomView/Header/{RoomHeaderLeft.js => LeftButtons.js} (78%) delete mode 100644 app/views/RoomsListView/Header/Header.ios.js rename app/views/RoomsListView/Header/{Header.android.js => Header.js} (54%) delete mode 100644 app/views/RoomsListView/ListHeader/SearchBar.js create mode 100644 patches/react-navigation-header-buttons+3.0.5.patch diff --git a/app/constants/colors.js b/app/constants/colors.js index 7b9d5676..0b0a8ae7 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -1,5 +1,3 @@ -import { isIOS, isAndroid } from '../utils/deviceInfo'; - export const STATUS_COLORS = { online: '#2de0a5', busy: '#f5455c', @@ -8,7 +6,7 @@ export const STATUS_COLORS = { }; export const SWITCH_TRACK_COLOR = { - false: isAndroid ? '#f5455c' : null, + false: '#f5455c', true: '#2de0a5' }; @@ -34,11 +32,11 @@ export const themes = { separatorColor: '#cbcbcc', navbarBackground: '#ffffff', headerBorder: '#B2B2B2', - headerBackground: isIOS ? '#f8f8f8' : '#2f343d', + headerBackground: '#EEEFF1', headerSecondaryBackground: '#ffffff', - headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', - headerTitleColor: isAndroid ? '#ffffff' : '#0d0e12', - headerSecondaryText: isAndroid ? '#9ca2a8' : '#1d74f5', + headerTintColor: '#6C727A', + headerTitleColor: '#0C0D0F', + headerSecondaryText: '#1d74f5', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', @@ -63,7 +61,7 @@ export const themes = { chatComponentBackground: '#192132', auxiliaryBackground: '#07101e', bannerBackground: '#0e1f38', - titleText: '#FFFFFF', + titleText: '#f9f9f9', bodyText: '#e8ebed', backdropColor: '#000000', dangerColor: '#f5455c', @@ -80,9 +78,9 @@ export const themes = { headerBorder: '#2F3A4B', headerBackground: '#0b182c', headerSecondaryBackground: '#0b182c', - headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', - headerTitleColor: '#FFFFFF', - headerSecondaryText: isAndroid ? '#9297a2' : '#1d74f5', + headerTintColor: '#f9f9f9', + headerTitleColor: '#f9f9f9', + headerSecondaryText: '#9297a2', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', @@ -124,9 +122,9 @@ export const themes = { headerBorder: '#323232', headerBackground: '#0d0d0d', headerSecondaryBackground: '#0d0d0d', - headerTintColor: isAndroid ? '#ffffff' : '#1e9bfe', + headerTintColor: '#f9f9f9', headerTitleColor: '#f9f9f9', - headerSecondaryText: isAndroid ? '#b2b8c6' : '#1e9bfe', + headerSecondaryText: '#b2b8c6', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', diff --git a/app/containers/DisclosureIndicator.js b/app/containers/DisclosureIndicator.js index 9d574de6..e33dbe81 100644 --- a/app/containers/DisclosureIndicator.js +++ b/app/containers/DisclosureIndicator.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ export const DisclosureImage = React.memo(({ theme }) => ( )); diff --git a/app/containers/Header/index.js b/app/containers/Header/index.js index 249b832e..2137a71a 100644 --- a/app/containers/Header/index.js +++ b/app/containers/Header/index.js @@ -20,6 +20,11 @@ export const getHeaderHeight = (isLandscape) => { return 56; }; +export const getHeaderTitlePosition = insets => ({ + left: 60 + insets.left, + right: 80 + insets.right +}); + const styles = StyleSheet.create({ container: { height: headerHeight, diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js index 3ac44d45..e712a7e1 100644 --- a/app/containers/HeaderButton.js +++ b/app/containers/HeaderButton.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { HeaderButtons, HeaderButton, Item } from 'react-navigation-header-buttons'; import { CustomIcon } from '../lib/Icons'; -import { isIOS, isAndroid } from '../utils/deviceInfo'; +import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -15,11 +15,7 @@ const CustomHeaderButton = React.memo(withTheme(({ theme, ...props }) => ( {...props} IconComponent={CustomIcon} iconSize={headerIconSize} - color={ - isAndroid - ? themes[theme].headerTitleColor - : themes[theme].headerTintColor - } + color={themes[theme].headerTintColor} /> ))); @@ -32,7 +28,7 @@ export const CustomHeaderButtons = React.memo(props => ( export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => ( - + )); diff --git a/app/containers/SearchBox.js b/app/containers/SearchBox.js index 7c3e337e..89c51def 100644 --- a/app/containers/SearchBox.js +++ b/app/containers/SearchBox.js @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ const CancelButton = (onCancelPress, theme) => ( - {I18n.t('Cancel')} + {I18n.t('Cancel')} ); diff --git a/app/containers/StatusBar.js b/app/containers/StatusBar.js index cc9bd73c..8add422f 100644 --- a/app/containers/StatusBar.js +++ b/app/containers/StatusBar.js @@ -2,13 +2,12 @@ import React from 'react'; import { StatusBar as StatusBarRN } from 'react-native'; import PropTypes from 'prop-types'; -import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => { if (!barStyle) { barStyle = 'light-content'; - if (theme === 'light' && isIOS) { + if (theme === 'light') { barStyle = 'dark-content'; } } diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index c973245b..7237b3b2 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -543,6 +543,7 @@ export default { Video_call: 'Video call', View_Original: 'View Original', Voice_call: 'Voice call', + Waiting_for_network: 'Waiting for network...', Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}', Welcome: 'Welcome', What_are_you_doing_right_now: 'What are you doing right now?', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 8055e7ea..14c76d81 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -479,6 +479,7 @@ export default { Verify_your_email_for_the_code_we_sent: 'Verifique em seu e-mail o código que enviamos', Video_call: 'Chamada de vídeo', Voice_call: 'Chamada de voz', + Waiting_for_network: 'Aguardando rede...', Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}', Welcome: 'Bem vindo', Whats_your_2fa: 'Qual seu código de autenticação?', diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js index 58792de2..9022514a 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/views/RoomView/Header/Header.js @@ -6,28 +6,20 @@ import { import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; -import { isAndroid, isTablet } from '../../../utils/deviceInfo'; import Icon from './Icon'; import { themes } from '../../../constants/colors'; import Markdown from '../../../containers/markdown'; -const androidMarginLeft = isTablet ? 0 : 4; - const TITLE_SIZE = 16; const styles = StyleSheet.create({ container: { flex: 1, - marginRight: isAndroid ? 15 : 5, - marginLeft: isAndroid ? androidMarginLeft : -10, justifyContent: 'center' }, titleContainer: { alignItems: 'center', flexDirection: 'row' }, - threadContainer: { - marginRight: isAndroid ? 20 : undefined - }, title: { ...sharedStyles.textSemibold, fontSize: TITLE_SIZE @@ -36,7 +28,6 @@ const styles = StyleSheet.create({ alignItems: 'center' }, subtitle: { - marginRight: -16, ...sharedStyles.textRegular, fontSize: 12 }, @@ -87,12 +78,8 @@ SubTitle.propTypes = { }; const HeaderTitle = React.memo(({ - title, tmid, prid, scale, connecting, theme + title, tmid, prid, scale, theme }) => { - if (connecting) { - title = I18n.t('Connecting'); - } - if (!tmid && !prid) { return ( - + { if (!isMasterDetail || tmid) { const onPress = useCallback(() => navigation.goBack()); + const label = unreadsCount > 99 ? '+99' : unreadsCount || ' '; + const labelLength = label.length ? label.length : 1; + const marginLeft = -2 * labelLength; + const fontSize = labelLength > 1 ? 14 : 17; return ( 999 ? '+999' : unreadsCount || ' '} + label={label} onPress={onPress} tintColor={themes[theme].headerTintColor} + labelStyle={{ fontSize, marginLeft }} /> ); } @@ -44,7 +49,7 @@ const RoomHeaderLeft = React.memo(({ return null; }); -RoomHeaderLeft.propTypes = { +LeftButtons.propTypes = { tmid: PropTypes.string, unreadsCount: PropTypes.number, navigation: PropTypes.object, @@ -58,4 +63,4 @@ RoomHeaderLeft.propTypes = { isMasterDetail: PropTypes.bool }; -export default RoomHeaderLeft; +export default LeftButtons; diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/Header/RightButtons.js index caec40d2..201866fe 100644 --- a/app/views/RoomView/Header/RightButtons.js +++ b/app/views/RoomView/Header/RightButtons.js @@ -68,6 +68,17 @@ class RightButtonsContainer extends React.PureComponent { } } + goSearchView = () => { + const { + rid, navigation, isMasterDetail + } = this.props; + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } }); + } else { + navigation.navigate('SearchMessagesView', { rid }); + } + } + toggleFollowThread = () => { const { isFollowingThread } = this.state; const { toggleFollowThread } = this.props; @@ -104,6 +115,12 @@ class RightButtonsContainer extends React.PureComponent { testID='room-view-header-threads' /> ) : null} + ); } diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index e94b58ca..61b4eb1c 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -4,10 +4,11 @@ import { connect } from 'react-redux'; import equal from 'deep-equal'; import Header from './Header'; +import LeftButtons from './LeftButtons'; import RightButtons from './RightButtons'; import { withTheme } from '../../../theme'; -import RoomHeaderLeft from './RoomHeaderLeft'; import { withDimensions } from '../../../dimensions'; +import I18n from '../../../i18n'; class RoomHeaderView extends Component { static propTypes = { @@ -20,6 +21,7 @@ class RoomHeaderView extends Component { status: PropTypes.string, statusText: PropTypes.string, connecting: PropTypes.bool, + connected: PropTypes.bool, theme: PropTypes.string, roomUserId: PropTypes.string, widthOffset: PropTypes.number, @@ -30,7 +32,7 @@ class RoomHeaderView extends Component { shouldComponentUpdate(nextProps) { const { - type, title, subtitle, status, statusText, connecting, goRoomActionsView, usersTyping, theme, width, height + type, title, subtitle, status, statusText, connecting, connected, goRoomActionsView, usersTyping, theme, width, height } = this.props; if (nextProps.theme !== theme) { return true; @@ -53,6 +55,9 @@ class RoomHeaderView extends Component { if (nextProps.connecting !== connecting) { return true; } + if (nextProps.connected !== connected) { + return true; + } if (nextProps.width !== width) { return true; } @@ -70,9 +75,18 @@ class RoomHeaderView extends Component { render() { const { - title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, roomUserId, theme, width, height + title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height } = this.props; + let subtitle; + if (connecting) { + subtitle = I18n.t('Connecting'); + } else if (!connected) { + subtitle = I18n.t('Waiting_for_network'); + } else { + subtitle = subtitleProp; + } + return (
{ } return { - connecting: state.meteor.connecting, + connecting: state.meteor.connecting || state.server.loading, + connected: state.meteor.connected, usersTyping: state.usersTyping, status, statusText @@ -117,4 +132,4 @@ const mapStateToProps = (state, ownProps) => { export default connect(mapStateToProps)(withDimensions(withTheme(RoomHeaderView))); -export { RightButtons, RoomHeaderLeft }; +export { RightButtons, LeftButtons }; diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 62553d36..a4a7549f 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -8,6 +8,7 @@ import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; import isEqual from 'lodash/isEqual'; +import { withSafeAreaInsets } from 'react-native-safe-area-context'; import Touch from '../../utils/touch'; import { @@ -26,7 +27,7 @@ import styles from './styles'; import log from '../../utils/log'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; -import RoomHeaderView, { RightButtons, RoomHeaderLeft } from './Header'; +import RoomHeaderView, { RightButtons, LeftButtons } from './Header'; import StatusBar from '../../containers/StatusBar'; import Separator from './Separator'; import { themes } from '../../constants/colors'; @@ -53,6 +54,7 @@ import Banner from './Banner'; import Navigation from '../../lib/Navigation'; import SafeAreaView from '../../containers/SafeAreaView'; import { withDimensions } from '../../dimensions'; +import { getHeaderTitlePosition } from '../../containers/Header'; const stateAttrsUpdate = [ 'joined', @@ -91,7 +93,8 @@ class RoomView extends React.Component { theme: PropTypes.string, replyBroadcast: PropTypes.func, width: PropTypes.number, - height: PropTypes.number + height: PropTypes.number, + insets: PropTypes.object }; constructor(props) { @@ -178,7 +181,7 @@ class RoomView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { state } = this; const { roomUpdate, member } = state; - const { appState, theme } = this.props; + const { appState, theme, insets } = this.props; if (theme !== nextProps.theme) { return true; } @@ -192,12 +195,15 @@ class RoomView extends React.Component { if (stateUpdated) { return true; } + if (!isEqual(nextProps.insets, insets)) { + return true; + } return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key])); } componentDidUpdate(prevProps, prevState) { const { roomUpdate } = this.state; - const { appState } = this.props; + const { appState, insets } = this.props; if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { @@ -222,6 +228,9 @@ class RoomView extends React.Component { if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { this.setHeader(); } + if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { + this.setHeader(); + } this.setReadOnly(); } @@ -281,7 +290,7 @@ class RoomView extends React.Component { setHeader = () => { const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state; const { - navigation, route, isMasterDetail, theme, baseUrl, user + navigation, route, isMasterDetail, theme, baseUrl, user, insets } = this.props; const rid = route.params?.rid; const prid = route.params?.prid; @@ -299,9 +308,29 @@ class RoomView extends React.Component { if (!rid) { return; } + const headerTitlePosition = getHeaderTitlePosition(insets); navigation.setOptions({ headerShown: true, headerTitleAlign: 'left', + headerTitleContainerStyle: { + left: headerTitlePosition.left, + right: headerTitlePosition.right + }, + headerLeft: () => ( + + ), headerTitle: () => ( - ), - headerLeft: () => ( - ) }); } @@ -1040,4 +1054,4 @@ const mapDispatchToProps = dispatch => ({ replyBroadcast: message => dispatch(replyBroadcastAction(message)) }); -export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(RoomView))); +export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView)))); diff --git a/app/views/RoomsListView/Header/Header.ios.js b/app/views/RoomsListView/Header/Header.ios.js deleted file mode 100644 index e45d8bae..00000000 --- a/app/views/RoomsListView/Header/Header.ios.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { - Text, View, TouchableOpacity, StyleSheet -} from 'react-native'; -import PropTypes from 'prop-types'; - -import I18n from '../../../i18n'; -import sharedStyles from '../../Styles'; -import { themes } from '../../../constants/colors'; -import { CustomIcon } from '../../../lib/Icons'; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center' - }, - button: { - flexDirection: 'row', - alignItems: 'center' - }, - title: { - fontSize: 14, - ...sharedStyles.textRegular - }, - server: { - fontSize: 12, - ...sharedStyles.textRegular - }, - disclosure: { - marginLeft: 3, - marginTop: 1, - width: 12, - height: 9 - }, - upsideDown: { - transform: [{ scaleY: -1 }] - } -}); - -const HeaderTitle = React.memo(({ connecting, isFetching, theme }) => { - let title = I18n.t('Messages'); - if (connecting) { - title = I18n.t('Connecting'); - } - if (isFetching) { - title = I18n.t('Updating'); - } - return {title}; -}); - -const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, onPress, theme -}) => ( - - - - - {serverName} - - - - -)); - -Header.propTypes = { - connecting: PropTypes.bool, - isFetching: PropTypes.bool, - serverName: PropTypes.string, - theme: PropTypes.string, - showServerDropdown: PropTypes.bool.isRequired, - onPress: PropTypes.func.isRequired -}; - -Header.defaultProps = { - serverName: 'Rocket.Chat' -}; - -HeaderTitle.propTypes = { - connecting: PropTypes.bool, - isFetching: PropTypes.bool, - theme: PropTypes.string -}; - -export default Header; diff --git a/app/views/RoomsListView/Header/Header.android.js b/app/views/RoomsListView/Header/Header.js similarity index 54% rename from app/views/RoomsListView/Header/Header.android.js rename to app/views/RoomsListView/Header/Header.js index d1d4fb74..839048bc 100644 --- a/app/views/RoomsListView/Header/Header.android.js +++ b/app/views/RoomsListView/Header/Header.js @@ -9,26 +9,23 @@ import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; import { themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; +import { isTablet, isIOS } from '../../../utils/deviceInfo'; +import { useOrientation } from '../../../dimensions'; const styles = StyleSheet.create({ container: { flex: 1, - justifyContent: 'center' + justifyContent: 'center', + marginLeft: isTablet ? 10 : 0 }, button: { flexDirection: 'row', - alignItems: 'center', - marginRight: 64 + alignItems: 'center' }, - server: { - fontSize: 20, - ...sharedStyles.textRegular + title: { + ...sharedStyles.textSemibold }, - serverSmall: { - fontSize: 16 - }, - updating: { - fontSize: 14, + subtitle: { ...sharedStyles.textRegular }, upsideDown: { @@ -37,41 +34,55 @@ const styles = StyleSheet.create({ }); const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, showSearchHeader, theme, onSearchChangeText, onPress + connecting, connected, isFetching, serverName, server, showServerDropdown, showSearchHeader, theme, onSearchChangeText, onPress }) => { const titleColorStyle = { color: themes[theme].headerTitleColor }; const isLight = theme === 'light'; + const { isLandscape } = useOrientation(); + const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; + const titleFontSize = 16 * scale; + const subTitleFontSize = 12 * scale; + if (showSearchHeader) { return ( ); } + let subtitle; + if (connecting) { + subtitle = I18n.t('Connecting'); + } else if (isFetching) { + subtitle = I18n.t('Updating'); + } else if (!connected) { + subtitle = I18n.t('Waiting_for_network'); + } else { + subtitle = server?.replace(/(^\w+:|^)\/\//, ''); + } return ( - {connecting ? {I18n.t('Connecting')} : null} - {isFetching ? {I18n.t('Updating')} : null} - {serverName} + {serverName} + {subtitle ? {subtitle} : null} ); @@ -83,8 +94,10 @@ Header.propTypes = { onPress: PropTypes.func.isRequired, onSearchChangeText: PropTypes.func.isRequired, connecting: PropTypes.bool, + connected: PropTypes.bool, isFetching: PropTypes.bool, serverName: PropTypes.string, + server: PropTypes.string, theme: PropTypes.string }; diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index badcb59a..12a57dc0 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -18,8 +18,10 @@ class RoomsListHeaderView extends PureComponent { showSearchHeader: PropTypes.bool, serverName: PropTypes.string, connecting: PropTypes.bool, + connected: PropTypes.bool, isFetching: PropTypes.bool, theme: PropTypes.string, + server: PropTypes.string, open: PropTypes.func, close: PropTypes.func, closeSort: PropTypes.func, @@ -68,16 +70,18 @@ class RoomsListHeaderView extends PureComponent { render() { const { - serverName, showServerDropdown, showSearchHeader, connecting, isFetching, theme + serverName, showServerDropdown, showSearchHeader, connecting, connected, isFetching, theme, server } = this.props; return (
({ showSortDropdown: state.rooms.showSortDropdown, showSearchHeader: state.rooms.showSearchHeader, connecting: state.meteor.connecting || state.server.loading, + connected: state.meteor.connected, isFetching: state.rooms.isFetching, - serverName: state.settings.Site_Name + serverName: state.settings.Site_Name, + server: state.server.server }); const mapDispatchtoProps = dispatch => ({ diff --git a/app/views/RoomsListView/ListHeader/SearchBar.js b/app/views/RoomsListView/ListHeader/SearchBar.js deleted file mode 100644 index 6446d4cf..00000000 --- a/app/views/RoomsListView/ListHeader/SearchBar.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import SearchBox from '../../../containers/SearchBox'; -import { isIOS } from '../../../utils/deviceInfo'; -import { withTheme } from '../../../theme'; - -const SearchBar = React.memo(({ - theme, onChangeSearchText, inputRef, searching, onCancelSearchPress, onSearchFocus -}) => { - if (isIOS) { - return ( - - ); - } - return null; -}); - -SearchBar.propTypes = { - theme: PropTypes.string, - searching: PropTypes.bool, - inputRef: PropTypes.func, - onChangeSearchText: PropTypes.func, - onCancelSearchPress: PropTypes.func, - onSearchFocus: PropTypes.func -}; - -export default withTheme(SearchBar); diff --git a/app/views/RoomsListView/ListHeader/index.js b/app/views/RoomsListView/ListHeader/index.js index 63d20ca9..dec38b50 100644 --- a/app/views/RoomsListView/ListHeader/index.js +++ b/app/views/RoomsListView/ListHeader/index.js @@ -1,28 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import SearchBar from './SearchBar'; import Directory from './Directory'; import Sort from './Sort'; const ListHeader = React.memo(({ searching, sortBy, - onChangeSearchText, toggleSort, - goDirectory, - inputRef, - onCancelSearchPress, - onSearchFocus + goDirectory }) => ( <> - @@ -31,12 +19,8 @@ const ListHeader = React.memo(({ ListHeader.propTypes = { searching: PropTypes.bool, sortBy: PropTypes.string, - onChangeSearchText: PropTypes.func, toggleSort: PropTypes.func, - goDirectory: PropTypes.func, - inputRef: PropTypes.func, - onCancelSearchPress: PropTypes.func, - onSearchFocus: PropTypes.func + goDirectory: PropTypes.func }; export default ListHeader; diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 39b035e6..aa5038a4 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { isEqual, orderBy } from 'lodash'; import Orientation from 'react-native-orientation-locker'; import { Q } from '@nozbe/watermelondb'; +import { withSafeAreaInsets } from 'react-native-safe-area-context'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; @@ -30,7 +31,7 @@ import { } from '../../actions/rooms'; import { appStart as appStartAction, ROOT_BACKGROUND } from '../../actions/app'; import debounce from '../../utils/debounce'; -import { isIOS, isAndroid, isTablet } from '../../utils/deviceInfo'; +import { isIOS, isTablet } from '../../utils/deviceInfo'; import RoomsListHeaderView from './Header'; import { DrawerButton, @@ -59,10 +60,9 @@ import { MAX_SIDEBAR_WIDTH } from '../../constants/tablet'; import { getUserSelector } from '../../selectors/login'; import { goRoom } from '../../utils/goRoom'; import SafeAreaView from '../../containers/SafeAreaView'; -import Header from '../../containers/Header'; +import Header, { getHeaderTitlePosition } from '../../containers/Header'; import { withDimensions } from '../../dimensions'; -const SCROLL_OFFSET = 56; const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12; const CHATS_HEADER = 'Chats'; const UNREAD_HEADER = 'Unread'; @@ -129,7 +129,8 @@ class RoomsListView extends React.Component { connected: PropTypes.bool, isMasterDetail: PropTypes.bool, rooms: PropTypes.array, - width: PropTypes.number + width: PropTypes.number, + insets: PropTypes.object }; constructor(props) { @@ -242,7 +243,7 @@ class RoomsListView extends React.Component { loading, search } = this.state; - const { rooms, width } = this.props; + const { rooms, width, insets } = this.props; if (nextState.loading !== loading) { return true; } @@ -255,6 +256,9 @@ class RoomsListView extends React.Component { if (!isEqual(nextProps.rooms, rooms)) { return true; } + if (!isEqual(nextProps.insets, insets)) { + return true; + } // If it's focused and there are changes, update if (chatsNotEqual) { this.shouldUpdate = false; @@ -273,7 +277,8 @@ class RoomsListView extends React.Component { connected, roomsRequest, rooms, - isMasterDetail + isMasterDetail, + insets } = this.props; const { item } = this.state; @@ -298,6 +303,9 @@ class RoomsListView extends React.Component { // eslint-disable-next-line react/no-did-update-set-state this.setState({ item: { rid: rooms[0] } }); } + if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { + this.setHeader(); + } } componentWillUnmount() { @@ -318,9 +326,11 @@ class RoomsListView extends React.Component { getHeader = () => { const { searching } = this.state; - const { navigation, isMasterDetail } = this.props; + const { navigation, isMasterDetail, insets } = this.props; + const headerTitlePosition = getHeaderTitlePosition(insets); return { - headerLeft: () => (searching && isAndroid ? ( + headerTitleAlign: 'left', + headerLeft: () => (searching ? ( navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' }) : () => navigation.toggleDrawer()} + onPress={isMasterDetail + ? () => navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' }) + : () => navigation.toggleDrawer()} /> )), headerTitle: () => , - headerRight: () => (searching && isAndroid ? null : ( + headerTitleContainerStyle: { + left: headerTitlePosition.left, + right: headerTitlePosition.right + }, + headerRight: () => (searching ? null : ( - {isAndroid ? ( - - ) : null} navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' }) : () => navigation.navigate('NewMessageStackNavigator')} + onPress={isMasterDetail + ? () => navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' }) + : () => navigation.navigate('NewMessageStackNavigator')} testID='rooms-list-view-create-channel' /> + )) }; @@ -476,10 +493,8 @@ class RoomsListView extends React.Component { initSearching = () => { const { openSearchHeader } = this.props; this.internalSetState({ searching: true }, () => { - if (isAndroid) { - openSearchHeader(); - this.setHeader(); - } + openSearchHeader(); + this.setHeader(); }); }; @@ -493,18 +508,11 @@ class RoomsListView extends React.Component { Keyboard.dismiss(); - if (isIOS && this.inputRef) { - this.inputRef.blur(); - this.inputRef.clear(); - } - this.setState({ searching: false, search: [] }, () => { - if (isAndroid) { - this.setHeader(); - closeSearchHeader(); - } + this.setHeader(); + closeSearchHeader(); setTimeout(() => { - const offset = isAndroid ? 0 : SCROLL_OFFSET; + const offset = 0; if (this.scroll.scrollTo) { this.scroll.scrollTo({ x: 0, y: offset, animated: true }); } else if (this.scroll.scrollToOffset) { @@ -564,7 +572,7 @@ class RoomsListView extends React.Component { toggleSort = () => { const { toggleSortDropdown } = this.props; - const offset = isAndroid ? 0 : SCROLL_OFFSET; + const offset = 0; if (this.scroll.scrollTo) { this.scroll.scrollTo({ x: 0, y: offset, animated: true }); } else if (this.scroll.scrollToOffset) { @@ -714,8 +722,7 @@ class RoomsListView extends React.Component { if (handleCommandShowPreferences(event)) { navigation.navigate('SettingsView'); } else if (handleCommandSearching(event)) { - this.scroll.scrollToOffset({ animated: true, offset: 0 }); - this.inputRef.focus(); + this.initSearching(); } else if (handleCommandSelectRoom(event)) { this.goRoomByIndex(input); } else if (handleCommandPreviousRoom(event)) { @@ -744,19 +751,13 @@ class RoomsListView extends React.Component { getScrollRef = ref => (this.scroll = ref); - getInputRef = ref => (this.inputRef = ref); - renderListHeader = () => { const { searching } = this.state; const { sortBy } = this.props; return ( @@ -869,7 +870,6 @@ class RoomsListView extends React.Component { ref={this.getScrollRef} data={searching ? search : chats} extraData={searching ? search : chats} - contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} keyExtractor={keyExtractor} style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} renderItem={this.renderItem} @@ -953,4 +953,4 @@ const mapDispatchToProps = dispatch => ({ closeServerDropdown: () => dispatch(closeServerDropdownAction()) }); -export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(RoomsListView))); +export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomsListView)))); diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 9deb03dc..70f2bb6d 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -23,7 +23,7 @@ export default StyleSheet.create({ sortToggleText: { fontSize: 16, flex: 1, - marginLeft: 15, + marginLeft: 12, ...sharedStyles.textRegular }, dropdownContainer: { @@ -50,16 +50,16 @@ export default StyleSheet.create({ }, sortSeparator: { height: StyleSheet.hairlineWidth, - marginHorizontal: 15, + marginHorizontal: 12, flex: 1 }, sortIcon: { width: 22, height: 22, - marginHorizontal: 15 + marginHorizontal: 12 }, groupTitleContainer: { - paddingHorizontal: 15, + paddingHorizontal: 12, paddingTop: 17, paddingBottom: 10 }, @@ -75,12 +75,12 @@ export default StyleSheet.create({ }, serverHeaderText: { fontSize: 16, - marginLeft: 15, + marginLeft: 12, ...sharedStyles.textRegular }, serverHeaderAdd: { fontSize: 16, - marginRight: 15, + marginRight: 12, paddingVertical: 10, ...sharedStyles.textRegular }, @@ -95,7 +95,7 @@ export default StyleSheet.create({ serverIcon: { width: 42, height: 42, - marginHorizontal: 15, + marginHorizontal: 12, marginVertical: 13, borderRadius: 4, resizeMode: 'contain' @@ -120,7 +120,7 @@ export default StyleSheet.create({ directoryIcon: { width: 22, height: 22, - marginHorizontal: 15 + marginHorizontal: 12 }, directoryText: { fontSize: 16, diff --git a/e2e/helpers/app.js b/e2e/helpers/app.js index 5657d4b1..9841c412 100644 --- a/e2e/helpers/app.js +++ b/e2e/helpers/app.js @@ -72,6 +72,14 @@ async function sleep(ms) { return new Promise(res => setTimeout(res, ms)); } +async function searchRoom(room) { + await element(by.id('rooms-list-view-search')).tap(); + await expect(element(by.id('rooms-list-view-search-input'))).toExist(); + await waitFor(element(by.id('rooms-list-view-search-input'))).toExist().withTimeout(5000); + await element(by.id('rooms-list-view-search-input')).typeText(room); + await sleep(2000); +} + module.exports = { navigateToWorkspace, navigateToLogin, @@ -80,5 +88,6 @@ module.exports = { logout, createUser, tapBack, - sleep + sleep, + searchRoom }; \ No newline at end of file diff --git a/e2e/tests/assorted/02-broadcast.spec.js b/e2e/tests/assorted/02-broadcast.spec.js index 04e3964d..ada0641c 100644 --- a/e2e/tests/assorted/02-broadcast.spec.js +++ b/e2e/tests/assorted/02-broadcast.spec.js @@ -3,7 +3,7 @@ const { } = require('detox'); const OTP = require('otp.js'); const GA = OTP.googleAuthenticator; -const { navigateToLogin, login, tapBack, sleep, createUser } = require('../../helpers/app'); +const { navigateToLogin, login, tapBack, sleep, searchRoom } = require('../../helpers/app'); const data = require('../../data'); describe('Broadcast room', () => { @@ -73,9 +73,7 @@ describe('Broadcast room', () => { await sleep(1000); await element(by.id('two-factor-send')).tap(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); - await element(by.type('UIScrollView')).atIndex(1).scrollTo('top'); - await element(by.id('rooms-list-view-search')).typeText(`broadcast${ data.random }`); - await sleep(2000); + await searchRoom(`broadcast${ data.random }`); await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000); await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist(); await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap(); diff --git a/e2e/tests/assorted/05-joinpublicroom.spec.js b/e2e/tests/assorted/05-joinpublicroom.spec.js index 1e284db5..8ebd7146 100644 --- a/e2e/tests/assorted/05-joinpublicroom.spec.js +++ b/e2e/tests/assorted/05-joinpublicroom.spec.js @@ -2,7 +2,7 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { tapBack, sleep } = require('../../helpers/app'); +const { tapBack, sleep, searchRoom } = require('../../helpers/app'); const room = 'detox-public'; @@ -16,9 +16,7 @@ async function mockMessage(message) { async function navigateToRoom() { await sleep(2000); - await element(by.type('UIScrollView')).atIndex(1).scrollTo('top'); - await element(by.id('rooms-list-view-search')).typeText(room); - await sleep(2000); + await searchRoom(room); await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toBeVisible().withTimeout(60000); await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); diff --git a/e2e/tests/onboarding/06-roomslist.spec.js b/e2e/tests/onboarding/06-roomslist.spec.js index 990fe94c..5e7e1e8a 100644 --- a/e2e/tests/onboarding/06-roomslist.spec.js +++ b/e2e/tests/onboarding/06-roomslist.spec.js @@ -1,7 +1,7 @@ const { device, expect, element, by, waitFor } = require('detox'); -const { logout, tapBack, sleep } = require('../../helpers/app'); +const { logout, tapBack, sleep, searchRoom } = require('../../helpers/app'); describe('Rooms list screen', () => { describe('Render', () => { @@ -27,10 +27,7 @@ describe('Rooms list screen', () => { describe('Usage', () => { it('should search room and navigate', async() => { - await element(by.type('UIScrollView')).atIndex(1).scrollTo('top'); - await waitFor(element(by.id('rooms-list-view-search'))).toExist().withTimeout(2000); - await element(by.id('rooms-list-view-search')).typeText('rocket.cat'); - await sleep(2000); + await searchRoom('rocket.cat'); await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(60000); await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible(); await element(by.id('rooms-list-view-item-rocket.cat')).tap(); @@ -41,7 +38,6 @@ describe('Rooms list screen', () => { await tapBack(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('rooms-list-view'))).toBeVisible(); - // await element(by.id('rooms-list-view-search')).typeText(''); await sleep(2000); await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toExist().withTimeout(60000); await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toExist(); diff --git a/e2e/tests/room/02-room.spec.js b/e2e/tests/room/02-room.spec.js index f307e436..95906ae1 100644 --- a/e2e/tests/room/02-room.spec.js +++ b/e2e/tests/room/02-room.spec.js @@ -2,7 +2,7 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { tapBack, sleep } = require('../../helpers/app'); +const { tapBack, sleep, searchRoom } = require('../../helpers/app'); async function mockMessage(message) { await element(by.id('messagebox-input')).tap(); @@ -13,9 +13,7 @@ async function mockMessage(message) { }; async function navigateToRoom() { - await element(by.type('UIScrollView')).atIndex(1).scrollTo('top'); - await element(by.id('rooms-list-view-search')).typeText(`private${ data.random }`); - await sleep(2000); + await searchRoom(`private${ data.random }`); await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toExist().withTimeout(60000); await element(by.id(`rooms-list-view-item-private${ data.random }`)).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); diff --git a/e2e/tests/room/03-roomactions.spec.js b/e2e/tests/room/03-roomactions.spec.js index 83f989b0..5294389a 100644 --- a/e2e/tests/room/03-roomactions.spec.js +++ b/e2e/tests/room/03-roomactions.spec.js @@ -2,7 +2,7 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { tapBack, sleep } = require('../../helpers/app'); +const { tapBack, sleep, searchRoom } = require('../../helpers/app'); const scrollDown = 200; @@ -13,10 +13,7 @@ async function navigateToRoomActions(type) { } else { room = `private${ data.random }`; } - await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); - await element(by.type('UIScrollView')).atIndex(1).scrollTo('top'); - await element(by.id('rooms-list-view-search')).typeText(room); - await sleep(2000); + await searchRoom(room); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); diff --git a/e2e/tests/room/04-roominfo.spec.js b/e2e/tests/room/04-roominfo.spec.js index 5fc10eb9..35a45441 100644 --- a/e2e/tests/room/04-roominfo.spec.js +++ b/e2e/tests/room/04-roominfo.spec.js @@ -2,7 +2,7 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { tapBack, sleep } = require('../../helpers/app'); +const { tapBack, sleep, searchRoom } = require('../../helpers/app'); async function navigateToRoomInfo(type) { let room; @@ -11,10 +11,7 @@ async function navigateToRoomInfo(type) { } else { room = `private${ data.random }`; } - await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); - await element(by.type('UIScrollView')).atIndex(1).swipe('down'); - await element(by.id('rooms-list-view-search')).typeText(room); - await sleep(2000); + await searchRoom(room); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); @@ -311,7 +308,6 @@ describe('Room info screen', () => { await expect(element(by.text('Yes, delete it!'))).toExist(); await element(by.text('Yes, delete it!')).tap(); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); - // await element(by.id('rooms-list-view-search')).typeText(''); await sleep(2000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000); await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible(); diff --git a/patches/react-navigation-header-buttons+3.0.5.patch b/patches/react-navigation-header-buttons+3.0.5.patch new file mode 100644 index 00000000..3b47953e --- /dev/null +++ b/patches/react-navigation-header-buttons+3.0.5.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-navigation-header-buttons/src/HeaderButtons.js b/node_modules/react-navigation-header-buttons/src/HeaderButtons.js +index 70ff376..01fba5e 100644 +--- a/node_modules/react-navigation-header-buttons/src/HeaderButtons.js ++++ b/node_modules/react-navigation-header-buttons/src/HeaderButtons.js +@@ -144,6 +144,6 @@ const styles = StyleSheet.create({ + }), + }, + button: { +- marginHorizontal: 11, ++ marginHorizontal: 6 + }, + });