[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
This commit is contained in:
Diego Mello 2020-07-06 17:56:28 -03:00 committed by GitHub
parent 2ec2a52f45
commit 5834ab5e22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 234 additions and 322 deletions

View File

@ -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',

View File

@ -17,7 +17,7 @@ const styles = StyleSheet.create({
export const DisclosureImage = React.memo(({ theme }) => (
<CustomIcon
name='chevron-right'
color={themes[theme].auxiliaryTintColor}
color={themes[theme].auxiliaryText}
size={20}
/>
));

View File

@ -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,

View File

@ -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 }) => (
<CustomHeaderButtons left>
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
<Item title='drawer' iconName='menu_hamburguer' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
</CustomHeaderButtons>
));

View File

@ -47,7 +47,7 @@ const styles = StyleSheet.create({
const CancelButton = (onCancelPress, theme) => (
<Touchable onPress={onCancelPress} style={styles.cancel}>
<Text style={[styles.cancelText, { color: themes[theme].tintColor }]}>{I18n.t('Cancel')}</Text>
<Text style={[styles.cancelText, { color: themes[theme].headerTintColor }]}>{I18n.t('Cancel')}</Text>
</Touchable>
);

View File

@ -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';
}
}

View File

@ -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?',

View File

@ -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?',

View File

@ -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 (
<Text
@ -122,7 +109,6 @@ HeaderTitle.propTypes = {
tmid: PropTypes.string,
prid: PropTypes.string,
scale: PropTypes.number,
connecting: PropTypes.bool,
theme: PropTypes.string
};
@ -147,7 +133,7 @@ const Header = React.memo(({
style={styles.container}
disabled={tmid}
>
<View style={[styles.titleContainer, tmid && styles.threadContainer]}>
<View style={styles.titleContainer}>
<Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />
<HeaderTitle
title={title}

View File

@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import { STATUS_COLORS, themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import Status from '../../../containers/Status/Status';
import { isAndroid } from '../../../utils/deviceInfo';
const ICON_SIZE = 15;
@ -32,7 +31,7 @@ const Icon = React.memo(({
if (type === 'l') {
colorStyle = { color: STATUS_COLORS[status] };
} else {
colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText };
colorStyle = { color: themes[theme].auxiliaryText };
}
let icon;

View File

@ -13,16 +13,21 @@ const styles = StyleSheet.create({
}
});
const RoomHeaderLeft = React.memo(({
const LeftButtons = React.memo(({
tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, isMasterDetail
}) => {
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 (
<HeaderBackButton
label={unreadsCount > 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;

View File

@ -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}
<Item
title='search'
iconName='magnifier'
onPress={this.goSearchView}
testID='room-view-search'
/>
</CustomHeaderButtons>
);
}

View File

@ -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 (
<Header
prid={prid}
@ -108,7 +122,8 @@ const mapStateToProps = (state, ownProps) => {
}
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 };

View File

@ -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: () => (
<LeftButtons
tmid={tmid}
unreadsCount={unreadsCount}
navigation={navigation}
baseUrl={baseUrl}
userId={userId}
token={token}
title={avatar}
theme={theme}
t={t}
goRoomActionsView={this.goRoomActionsView}
isMasterDetail={isMasterDetail}
/>
),
headerTitle: () => (
<RoomHeaderView
rid={rid}
@ -323,21 +352,6 @@ class RoomView extends React.Component {
navigation={navigation}
toggleFollowThread={this.toggleFollowThread}
/>
),
headerLeft: () => (
<RoomHeaderLeft
tmid={tmid}
unreadsCount={unreadsCount}
navigation={navigation}
baseUrl={baseUrl}
userId={userId}
token={token}
title={avatar}
theme={theme}
t={t}
goRoomActionsView={this.goRoomActionsView}
isMasterDetail={isMasterDetail}
/>
)
});
}
@ -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))));

View File

@ -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 <Text style={[styles.title, { color: themes[theme].headerTitleColor }]}>{title}</Text>;
});
const Header = React.memo(({
connecting, isFetching, serverName, showServerDropdown, onPress, theme
}) => (
<View style={styles.container}>
<TouchableOpacity
onPress={onPress}
testID='rooms-list-header-server-dropdown-button'
style={styles.container}
// disabled={connecting || isFetching}
>
<HeaderTitle connecting={connecting} isFetching={isFetching} theme={theme} />
<View style={styles.button}>
<Text style={[styles.server, { color: themes[theme].headerTintColor }]} numberOfLines={1}>{serverName}</Text>
<CustomIcon
name='chevron-down'
color={themes[theme].headerTintColor}
style={[showServerDropdown && styles.upsideDown]}
size={18}
/>
</View>
</TouchableOpacity>
</View>
));
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;

View File

@ -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 (
<View style={styles.container}>
<TextInput
autoFocus
style={[styles.server, isLight && titleColorStyle]}
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]}
placeholder='Search'
onChangeText={onSearchChangeText}
theme={theme}
testID='rooms-list-view-search-input'
/>
</View>
);
}
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 (
<View style={styles.container}>
<TouchableOpacity
onPress={onPress}
testID='rooms-list-header-server-dropdown-button'
disabled={connecting || isFetching}
>
{connecting ? <Text style={[styles.updating, titleColorStyle]}>{I18n.t('Connecting')}</Text> : null}
{isFetching ? <Text style={[styles.updating, titleColorStyle]}>{I18n.t('Updating')}</Text> : null}
<View style={styles.button}>
<Text style={[styles.server, isFetching && styles.serverSmall, titleColorStyle]} numberOfLines={1}>{serverName}</Text>
<Text style={[styles.title, isFetching && styles.serverSmall, titleColorStyle, { fontSize: titleFontSize }]} numberOfLines={1}>{serverName}</Text>
<CustomIcon
name='chevron-down'
color={themes[theme].headerTintColor}
style={[showServerDropdown && styles.upsideDown]}
style={[showServerDropdown && styles.upsideDown, { fontSize: subTitleFontSize }]}
size={18}
/>
</View>
{subtitle ? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{subtitle}</Text> : null}
</TouchableOpacity>
</View>
);
@ -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
};

View File

@ -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 (
<Header
theme={theme}
serverName={serverName}
server={server}
showServerDropdown={showServerDropdown}
showSearchHeader={showSearchHeader}
connecting={connecting}
connected={connected}
isFetching={isFetching}
onPress={this.onPress}
onSearchChangeText={this.onSearchChangeText}
@ -91,8 +95,10 @@ const mapStateToProps = state => ({
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 => ({

View File

@ -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 (
<SearchBox
onChangeText={onChangeSearchText}
testID='rooms-list-view-search'
inputRef={inputRef}
theme={theme}
hasCancel={searching}
onCancelPress={onCancelSearchPress}
onFocus={onSearchFocus}
/>
);
}
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);

View File

@ -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
}) => (
<>
<SearchBar
inputRef={inputRef}
searching={searching}
onChangeSearchText={onChangeSearchText}
onCancelSearchPress={onCancelSearchPress}
onSearchFocus={onSearchFocus}
/>
<Directory searching={searching} goDirectory={goDirectory} />
<Sort searching={searching} sortBy={sortBy} toggleSort={toggleSort} />
</>
@ -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;

View File

@ -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 ? (
<CustomHeaderButtons left>
<Item
title='cancel'
@ -332,24 +342,31 @@ class RoomsListView extends React.Component {
<DrawerButton
navigation={navigation}
testID='rooms-list-view-sidebar'
onPress={isMasterDetail ? () => navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' }) : () => navigation.toggleDrawer()}
onPress={isMasterDetail
? () => navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' })
: () => navigation.toggleDrawer()}
/>
)),
headerTitle: () => <RoomsListHeaderView />,
headerRight: () => (searching && isAndroid ? null : (
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerRight: () => (searching ? null : (
<CustomHeaderButtons>
{isAndroid ? (
<Item
title='new'
iconName='new-chat'
onPress={isMasterDetail
? () => navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' })
: () => navigation.navigate('NewMessageStackNavigator')}
testID='rooms-list-view-create-channel'
/>
<Item
title='search'
iconName='magnifier'
onPress={this.initSearching}
/>
) : null}
<Item
title='new'
iconName='new-chat'
onPress={isMasterDetail ? () => navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' }) : () => navigation.navigate('NewMessageStackNavigator')}
testID='rooms-list-view-create-channel'
testID='rooms-list-view-search'
/>
</CustomHeaderButtons>
))
@ -476,10 +493,8 @@ class RoomsListView extends React.Component {
initSearching = () => {
const { openSearchHeader } = this.props;
this.internalSetState({ searching: true }, () => {
if (isAndroid) {
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();
}
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 (
<ListHeader
inputRef={this.getInputRef}
searching={searching}
sortBy={sortBy}
onChangeSearchText={this.search}
onCancelSearchPress={this.cancelSearch}
onSearchFocus={this.initSearching}
toggleSort={this.toggleSort}
goDirectory={this.goDirectory}
/>
@ -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))));

View File

@ -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,

View File

@ -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
};

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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
},
});