From 9a5122c19c63701693a97b6e0add28d7fe06b1ef Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 2 Nov 2018 13:21:43 -0300 Subject: [PATCH] Slide up panel for reaction list (#2285) * Slide up panel for reaction list * Feedback review * UI Feedback review * Feedback review * Improve slide speed and fix end position * Scroll to top when changing reaction view * dismiss modal without animation so backdrop does not scroll down * Smooth animation to show/hide * Change animation speed --- app/components/autocomplete/autocomplete.js | 4 +- .../client_upgrade_listener.js | 11 +- app/components/emoji_picker/emoji_picker.js | 5 +- .../offline_indicator/offline_indicator.js | 8 +- app/components/reactions/reactions.js | 38 +- .../safe_area_view/safe_area_view.ios.js | 15 +- app/components/slide_up_panel/index.js | 17 + .../slide_up_panel/slide_up_panel.js | 246 ++++++++++ .../slide_up_panel_indicator.js | 45 ++ app/components/start/empty_toolbar.js | 11 +- app/constants/device.js | 4 +- app/constants/index.js | 3 +- app/screens/channel/channel.js | 9 +- .../channel_nav_bar/channel_nav_bar.js | 11 +- app/screens/client_upgrade/client_upgrade.js | 2 +- .../reaction_header.test.js.snap | 6 +- .../reaction_header_item.test.js.snap | 10 +- .../__snapshots__/reaction_list.test.js.snap | 436 +++++++++--------- .../__snapshots__/reaction_row.test.js.snap | 4 +- app/screens/reaction_list/reaction_header.js | 35 +- .../reaction_list/reaction_header_item.js | 12 +- app/screens/reaction_list/reaction_list.js | 91 ++-- .../reaction_list/reaction_list.test.js | 5 +- app/screens/reaction_list/reaction_row.js | 75 ++- app/screens/search/search.js | 7 +- app/screens/select_server/select_server.js | 2 +- app/store/middleware.test.js | 7 - app/utils/client_upgrade.js | 2 +- test/setup.js | 8 + 29 files changed, 716 insertions(+), 413 deletions(-) create mode 100644 app/components/slide_up_panel/index.js create mode 100644 app/components/slide_up_panel/slide_up_panel.js create mode 100644 app/components/slide_up_panel/slide_up_panel_indicator.js diff --git a/app/components/autocomplete/autocomplete.js b/app/components/autocomplete/autocomplete.js index d67b5b383..83e8f1a37 100644 --- a/app/components/autocomplete/autocomplete.js +++ b/app/components/autocomplete/autocomplete.js @@ -8,8 +8,8 @@ import { Platform, View, } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; +import {DeviceTypes} from 'app/constants'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import AtMention from './at_mention'; @@ -92,7 +92,7 @@ export default class Autocomplete extends PureComponent { } else { // List is expanding downwards, likely from the search box let offset = Platform.select({ios: 65, android: 75}); - if (DeviceInfo.getModel().includes('iPhone X')) { + if (DeviceTypes.IS_IPHONE_X) { offset = 90; } diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js index fe05b18b1..88dcf1d1f 100644 --- a/app/components/client_upgrade_listener/client_upgrade_listener.js +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -11,11 +11,10 @@ import { View, } from 'react-native'; import {intlShape} from 'react-intl'; -import DeviceInfo from 'react-native-device-info'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FormattedText from 'app/components/formatted_text'; -import {UpgradeTypes} from 'app/constants/view'; +import {DeviceTypes, UpgradeTypes} from 'app/constants'; import checkUpgradeType from 'app/utils/client_upgrade'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -47,8 +46,6 @@ export default class ClientUpgradeListener extends PureComponent { constructor(props) { super(props); - this.isX = DeviceInfo.getModel().includes('iPhone X'); - MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => { this.closeButton = source; }); @@ -73,7 +70,7 @@ export default class ClientUpgradeListener extends PureComponent { if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) { this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape); } else if (this.props.isLandscape !== nextProps.isLandscape && - this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && this.isX) { + this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && DeviceTypes.IS_IPHONE_X) { const newTop = nextProps.isLandscape ? 45 : 100; this.setState({top: new Animated.Value(newTop)}); } @@ -100,10 +97,10 @@ export default class ClientUpgradeListener extends PureComponent { toggleUpgradeMessage = (show = true, isLandscape) => { let toValue = -100; if (show) { - if (this.isX && isLandscape) { + if (DeviceTypes.IS_IPHONE_X && isLandscape) { toValue = 45; } else { - toValue = this.isX ? 100 : 75; + toValue = DeviceTypes.IS_IPHONE_X ? 100 : 75; } } Animated.timing(this.state.top, { diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js index 94aa3b407..c3565b04c 100644 --- a/app/components/emoji_picker/emoji_picker.js +++ b/app/components/emoji_picker/emoji_picker.js @@ -14,7 +14,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome'; import sectionListGetItemLayout from 'react-native-section-list-get-item-layout'; @@ -24,6 +23,7 @@ import Emoji from 'app/components/emoji'; import FormattedText from 'app/components/formatted_text'; import SafeAreaView from 'app/components/safe_area_view'; import SearchBar from 'app/components/search_bar'; +import {DeviceTypes} from 'app/constants'; import {emptyFunction} from 'app/utils/general'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; @@ -75,7 +75,6 @@ export default class EmojiPicker extends PureComponent { const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth); const emojiSectionIndexByOffset = this.measureEmojiSections(emojis); - this.isX = DeviceInfo.getModel().includes('iPhone X'); this.scrollToSectionTries = 0; this.state = { emojis, @@ -436,7 +435,7 @@ export default class EmojiPicker extends PureComponent { let keyboardOffset = 64; if (Platform.OS === 'android') { keyboardOffset = -200; - } else if (this.isX) { + } else if (DeviceTypes.IS_IPHONE_X) { keyboardOffset = isLandscape ? 35 : 107; } else if (isLandscape) { keyboardOffset = 52; diff --git a/app/components/offline_indicator/offline_indicator.js b/app/components/offline_indicator/offline_indicator.js index a7f42f137..27590438b 100644 --- a/app/components/offline_indicator/offline_indicator.js +++ b/app/components/offline_indicator/offline_indicator.js @@ -11,11 +11,10 @@ import { TouchableOpacity, View, } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import IonIcon from 'react-native-vector-icons/Ionicons'; import FormattedText from 'app/components/formatted_text'; -import {ViewTypes} from 'app/constants'; +import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostBucket from 'app/mattermost_bucket'; import checkNetwork from 'app/utils/network'; import {t} from 'app/utils/i18n'; @@ -55,7 +54,6 @@ export default class OfflineIndicator extends Component { constructor(props) { super(props); - this.isX = DeviceInfo.getModel().includes('iPhone X'); const navBar = this.getNavBarHeight(props.isLandscape); this.state = { @@ -154,9 +152,9 @@ export default class OfflineIndicator extends Component { return ANDROID_TOP_PORTRAIT; } - if (this.isX && isLandscape) { + if (DeviceTypes.IS_IPHONE_X && isLandscape) { return IOS_TOP_LANDSCAPE; - } else if (this.isX) { + } else if (DeviceTypes.IS_IPHONE_X) { return IOSX_TOP_PORTRAIT; } else if (isLandscape) { return IOS_TOP_LANDSCAPE + STATUS_BAR_HEIGHT; diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index e0fd31f5d..602767870 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -9,9 +9,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import {intlShape} from 'react-intl'; - -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import addReactionIcon from 'assets/images/icons/reaction.png'; @@ -34,24 +31,12 @@ export default class Reactions extends PureComponent { theme: PropTypes.object.isRequired, canAddReaction: PropTypes.bool, canRemoveReaction: PropTypes.bool.isRequired, - } + }; static defaultProps = { position: 'right', }; - static contextTypes = { - intl: intlShape, - }; - - constructor(props) { - super(props); - - MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => { - this.closeButton = source; - }); - } - componentDidMount() { const {actions, postId} = this.props; actions.getReactionsForPost(postId); @@ -67,25 +52,16 @@ export default class Reactions extends PureComponent { }; showReactionList = () => { - const {navigator, postId, theme} = this.props; - const {formatMessage} = this.context.intl; + const {navigator, postId} = this.props; + const options = { screen: 'ReactionList', - title: formatMessage({id: 'mobile.reaction_list.title', defaultMessage: 'Reactions'}), - animationType: 'slide-up', - animated: true, + animationType: 'none', backButtonTitle: '', navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - navigatorButtons: { - leftButtons: [{ - id: 'close-reaction-list', - icon: this.closeButton, - }], + navBarHidden: true, + screenBackgroundColor: 'transparent', + modalPresentationStyle: 'overCurrentContext', }, passProps: { postId, diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index 754be4d9f..05a4ccd1a 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -4,9 +4,10 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Dimensions, Keyboard, NativeModules, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import SafeArea from 'react-native-safe-area'; +import {DeviceTypes} from 'app/constants'; + const {StatusBarManager} = NativeModules; export default class SafeAreaIos extends PureComponent { @@ -31,8 +32,6 @@ export default class SafeAreaIos extends PureComponent { constructor(props) { super(props); - this.isX = DeviceInfo.getModel().includes('iPhone X'); - if (props.navigator) { props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); } @@ -40,9 +39,9 @@ export default class SafeAreaIos extends PureComponent { this.state = { keyboard: false, safeAreaInsets: { - top: this.isX ? 44 : 20, + top: DeviceTypes.IS_IPHONE_X ? 44 : 20, left: 0, - bottom: this.isX ? 34 : 15, + bottom: DeviceTypes.IS_IPHONE_X ? 34 : 15, right: 0, }, statusBarHeight: 20, @@ -87,7 +86,7 @@ export default class SafeAreaIos extends PureComponent { getSafeAreaInsets = () => { this.getStatusBarHeight(); - if (this.isX) { + if (DeviceTypes.IS_IPHONE_X) { SafeArea.getSafeAreaInsetsForRootView().then((result) => { const {safeAreaInsets} = result; @@ -130,7 +129,7 @@ export default class SafeAreaIos extends PureComponent { } let top = safeAreaInsets.top; - if (forceTop && this.isX && !hideTopBar) { + if (forceTop && DeviceTypes.IS_IPHONE_X && !hideTopBar) { top = forceTop; } @@ -174,7 +173,7 @@ export default class SafeAreaIos extends PureComponent { } let offset = 0; - if (keyboardOffset && this.isX) { + if (keyboardOffset && DeviceTypes.IS_IPHONE_X) { offset = keyboardOffset; } diff --git a/app/components/slide_up_panel/index.js b/app/components/slide_up_panel/index.js new file mode 100644 index 000000000..1bcce4cc7 --- /dev/null +++ b/app/components/slide_up_panel/index.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; + +import {getDimensions} from 'app/selectors/device'; + +import SlideUpPanel from './slide_up_panel'; + +function mapStateToProps(state, ownProps) { + const dimensions = getDimensions(state); + return { + containerHeight: ownProps.containerHeight || dimensions.deviceHeight, + }; +} + +export default connect(mapStateToProps, null, null, {withRef: true})(SlideUpPanel); diff --git a/app/components/slide_up_panel/slide_up_panel.js b/app/components/slide_up_panel/slide_up_panel.js new file mode 100644 index 000000000..efd9da28b --- /dev/null +++ b/app/components/slide_up_panel/slide_up_panel.js @@ -0,0 +1,246 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Animated, + PanResponder, + Platform, + StyleSheet, + View, +} from 'react-native'; + +import {DeviceTypes} from 'app/constants'; + +import SlideUpPanelIndicator from './slide_up_panel_indicator'; + +const TOP_IOS_MARGIN = DeviceTypes.IS_IPHONE_X ? 84 : 64; +const TOP_ANDROID_MARGIN = 44; +const TOP_MARGIN = Platform.OS === 'ios' ? TOP_IOS_MARGIN : TOP_ANDROID_MARGIN; +const BOTTOM_MARGIN = DeviceTypes.IS_IPHONE_X ? 24 : 0; +const CONTAINER_MARGIN = TOP_MARGIN - 10; + +export default class SlideUpPanel extends PureComponent { + static propTypes = { + containerHeight: PropTypes.number, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + headerHeight: PropTypes.number, + initialPosition: PropTypes.number, + marginFromTop: PropTypes.number, + onRequestClose: PropTypes.func, + }; + + static defaultProps = { + headerHeight: 0, + initialPosition: 0.5, + marginFromTop: TOP_MARGIN, + onRequestClose: () => true, + }; + + constructor(props) { + super(props); + + const initialUsedSpace = Math.abs(props.initialPosition); + const initialPosition = ((props.containerHeight - (props.headerHeight + BOTTOM_MARGIN)) * (1 - initialUsedSpace)); + + this.mainPanGesture = PanResponder.create({ + onMoveShouldSetPanResponder: (evt, gestureState) => { + const isGoingDown = gestureState.y0 < gestureState.dy; + return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown); + }, + onPanResponderMove: (evt, gestureState) => { + this.moveStart(gestureState); + }, + onPanResponderRelease: (evt, gestureState) => { + this.moveFinished(gestureState); + }, + }); + + this.secondaryPanGesture = PanResponder.create({ + onMoveShouldSetPanResponder: (evt, gestureState) => { + const isGoingDown = gestureState.y0 < gestureState.dy; + return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown, true); + }, + onPanResponderMove: (evt, gestureState) => { + this.moveStart(gestureState); + }, + onPanResponderRelease: (evt, gestureState) => { + this.moveFinished(gestureState); + }, + }); + + this.previousTop = initialPosition; + this.canDrag = true; + + this.state = { + position: new Animated.Value(props.containerHeight), + initialPosition, + finalPosition: props.marginFromTop, + endPosition: 0, + }; + } + + componentDidMount() { + this.startAnimation(this.props.containerHeight, this.state.initialPosition, false, true); + } + + handleTouchEnd = () => { + if (!this.isDragging) { + this.startAnimation(this.state.endPosition, this.props.containerHeight, false, true); + } + }; + + isAValidMovement = (distanceX, distanceY, isGoingDown, forceCheck = false) => { + const {endPosition, finalPosition} = this.state; + + if (finalPosition !== endPosition || forceCheck || (isGoingDown && this.canDrag)) { + const moveTravelledFarEnough = Math.abs(distanceY) > Math.abs(distanceX) && Math.abs(distanceY) > 2; + return moveTravelledFarEnough; + } + + return false; + }; + + moveStart = (gestureState) => { + if (this.viewRef && this.backdrop) { + const {endPosition} = this.state; + const position = endPosition - (gestureState.y0 - gestureState.moveY); + this.isDragging = true; + + this.backdrop.setNativeProps({pointerEvents: 'none'}); + this.updatePosition(position); + } + }; + + moveFinished = (gestureState) => { + if (this.viewRef) { + const isGoingDown = gestureState.y0 < gestureState.moveY; + let position = gestureState.moveY; + if (this.previousTop !== position) { + position = this.previousTop; + } + + this.startAnimation(gestureState.y0, position, isGoingDown); + } + }; + + setBackdropRef = (ref) => { + this.backdrop = ref; + }; + + setDrag = (val) => { + this.canDrag = val; + }; + + setViewRef = (ref) => { + this.viewRef = ref; + }; + + startAnimation = (initialY, positionY, isGoingDown, initial = false) => { + const {containerHeight, onRequestClose} = this.props; + const {finalPosition, initialPosition} = this.state; + const position = new Animated.Value(initial ? initialY : positionY); + let endPosition = (!isGoingDown && !initial ? finalPosition : positionY); + + position.removeAllListeners(); + if (isGoingDown) { + if (positionY <= this.state.initialPosition) { + endPosition = initialPosition; + } else { + endPosition = containerHeight; + } + } + + Animated.timing(position, { + toValue: endPosition, + duration: initial ? 200 : 100, + useNativeDriver: true, + }).start(() => { + if (this.viewRef && this.backdrop) { + this.setState({endPosition}); + this.backdrop.setNativeProps({pointerEvents: 'box-only'}); + this.isDragging = false; + + if (endPosition === containerHeight) { + onRequestClose(); + } + } + }); + + position.addListener((pos) => { + if (this.viewRef) { + this.updatePosition(pos.value); + } + }); + }; + + updatePosition = (newPosition) => { + const {position} = this.state; + this.previousTop = newPosition; + position.setValue(newPosition); + }; + + render() { + const {children} = this.props; + const containerPosition = { + top: this.state.position, + }; + + return ( + + + + + + {children} + + + + ); + } +} + +const styles = StyleSheet.create({ + viewport: { + flex: 1, + }, + container: { + flex: 1, + backgroundColor: 'white', + ...Platform.select({ + android: { + borderTopRightRadius: 2, + borderTopLeftRadius: 2, + }, + ios: { + borderTopRightRadius: 10, + borderTopLeftRadius: 10, + }, + }), + }, + backdrop: { + backgroundColor: 'rgba(0, 0, 0, 0.3)', + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, +}); diff --git a/app/components/slide_up_panel/slide_up_panel_indicator.js b/app/components/slide_up_panel/slide_up_panel_indicator.js new file mode 100644 index 000000000..53b3cb425 --- /dev/null +++ b/app/components/slide_up_panel/slide_up_panel_indicator.js @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Animated, Platform, StyleSheet, View} from 'react-native'; + +export default class SlideUpPanelIndicator extends PureComponent { + static propTypes = { + containerPosition: PropTypes.object.isRequired, + panHandlers: PropTypes.object.isRequired, + }; + + render() { + const {panHandlers, containerPosition} = this.props; + + if (Platform.OS === 'android') { + return null; + } + + return ( + + + + ); + } +} + +const styles = StyleSheet.create({ + dragIndicatorContainer: { + marginVertical: 10, + alignItems: 'center', + justifyContent: 'center', + }, + dragIndicator: { + backgroundColor: 'white', + height: 5, + width: 62.5, + opacity: 0.9, + borderRadius: 25, + }, +}); diff --git a/app/components/start/empty_toolbar.js b/app/components/start/empty_toolbar.js index a825ba980..8db278b75 100644 --- a/app/components/start/empty_toolbar.js +++ b/app/components/start/empty_toolbar.js @@ -4,9 +4,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Platform, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import {ViewTypes} from 'app/constants'; +import {DeviceTypes, ViewTypes} from 'app/constants'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -25,12 +24,6 @@ export default class EmptyToolbar extends PureComponent { theme: PropTypes.object.isRequired, }; - constructor(props) { - super(props); - - this.isX = DeviceInfo.getModel().includes('iPhone X'); - } - render() { const {isLandscape, theme} = this.props; @@ -51,7 +44,7 @@ export default class EmptyToolbar extends PureComponent { height = IOS_TOP_LANDSCAPE; } - if (this.isX && isLandscape) { + if (DeviceTypes.IS_IPHONE_X && isLandscape) { padding.paddingHorizontal = 10; } break; diff --git a/app/constants/device.js b/app/constants/device.js index 97223e3ce..43d1986b6 100644 --- a/app/constants/device.js +++ b/app/constants/device.js @@ -1,8 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import keyMirror from 'mattermost-redux/utils/key_mirror'; +import DeviceInfo from 'react-native-device-info'; import RNFetchBlobFS from 'rn-fetch-blob/fs'; +import keyMirror from 'mattermost-redux/utils/key_mirror'; const deviceTypes = keyMirror({ CONNECTION_CHANGED: null, @@ -16,5 +17,6 @@ export default { ...deviceTypes, DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`, IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`, + IS_IPHONE_X: DeviceInfo.getModel().includes('iPhone X'), VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`, }; diff --git a/app/constants/index.js b/app/constants/index.js index 28c0c18bd..575e5f161 100644 --- a/app/constants/index.js +++ b/app/constants/index.js @@ -5,12 +5,13 @@ import DeviceTypes from './device'; import ListTypes from './list'; import NavigationTypes from './navigation'; import PermissionTypes from './permissions'; -import ViewTypes from './view'; +import ViewTypes, {UpgradeTypes} from './view'; export { DeviceTypes, ListTypes, NavigationTypes, PermissionTypes, + UpgradeTypes, ViewTypes, }; diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index 32989b8a0..53ac32ce2 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -12,7 +12,6 @@ import { StyleSheet, View, } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -24,7 +23,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout'; import OfflineIndicator from 'app/components/offline_indicator'; import SafeAreaView from 'app/components/safe_area_view'; import StatusBar from 'app/components/status_bar'; -import {ViewTypes} from 'app/constants'; +import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostBucket from 'app/mattermost_bucket'; import {preventDoubleTap} from 'app/utils/tap'; import PostTextbox from 'app/components/post_textbox'; @@ -75,8 +74,6 @@ export default class Channel extends PureComponent { constructor(props) { super(props); - this.isX = DeviceInfo.getModel().includes('iPhone X'); - if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) { ClientUpgradeListener = require('app/components/client_upgrade_listener').default; } @@ -170,8 +167,8 @@ export default class Channel extends PureComponent { if (isLandscape) { top = IOS_TOP_LANDSCAPE; } else { - height = this.isX ? (height - IOSX_TOP_PORTRAIT) : (height - IOS_TOP_PORTRAIT); - top = this.isX ? IOSX_TOP_PORTRAIT : IOS_TOP_PORTRAIT; + height = DeviceTypes.IS_IPHONE_X ? (height - IOSX_TOP_PORTRAIT) : (height - IOS_TOP_PORTRAIT); + top = DeviceTypes.IS_IPHONE_X ? IOSX_TOP_PORTRAIT : IOS_TOP_PORTRAIT; } break; } diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 5202e5ce1..d3fb51ec0 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -4,9 +4,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Platform, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import {ViewTypes} from 'app/constants'; +import {DeviceTypes, ViewTypes} from 'app/constants'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDrawerButton from './channel_drawer_button'; @@ -32,12 +31,6 @@ export default class ChannelNavBar extends PureComponent { theme: PropTypes.object.isRequired, }; - constructor(props) { - super(props); - - this.isX = DeviceInfo.getModel().includes('iPhone X'); - } - render() { const {isLandscape, navigator, onPress, theme} = this.props; const {openChannelDrawer, openSettingsDrawer} = this.props; @@ -58,7 +51,7 @@ export default class ChannelNavBar extends PureComponent { height = IOS_TOP_LANDSCAPE; } - if (this.isX && isLandscape) { + if (DeviceTypes.IS_IPHONE_X && isLandscape) { padding.paddingHorizontal = 10; } break; diff --git a/app/screens/client_upgrade/client_upgrade.js b/app/screens/client_upgrade/client_upgrade.js index 3990caff1..3a8daea98 100644 --- a/app/screens/client_upgrade/client_upgrade.js +++ b/app/screens/client_upgrade/client_upgrade.js @@ -15,7 +15,7 @@ import {intlShape} from 'react-intl'; import FormattedText from 'app/components/formatted_text'; import StatusBar from 'app/components/status_bar'; -import {UpgradeTypes} from 'app/constants/view'; +import {UpgradeTypes} from 'app/constants'; import logo from 'assets/images/logo.png'; import checkUpgradeType from 'app/utils/client_upgrade'; import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme'; diff --git a/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap b/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap index 14689ca7b..ef8ce8798 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap @@ -4,8 +4,10 @@ exports[`ReactionHeader should match snapshot 1`] = ` - - - - - - + - - - + + + + + + + + - - - + > + + + + + + `; diff --git a/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap b/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap index 118578377..c3947b514 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_row.test.js.snap @@ -5,7 +5,6 @@ exports[`ReactionRow should match snapshot, renderContent 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#ffffff", "flexDirection": "row", "height": 44, "justifyContent": "flex-start", @@ -53,7 +52,6 @@ exports[`ReactionRow should match snapshot, renderContent 1`] = ` { this.props.onSelectReaction(emoji); @@ -37,12 +37,9 @@ export default class ReactionHeader extends PureComponent { theme={theme} /> )); - } + }; render() { - const {theme} = this.props; - const styles = getStyleSheet(theme); - return ( { - return { - container: { - backgroundColor: theme.centerChannelBg, - height: 37, - paddingHorizontal: 0, - }, - }; +const styles = StyleSheet.create({ + container: { + backgroundColor: '#FFFFFF', + height: 36.5, + paddingHorizontal: 0, + ...Platform.select({ + android: { + borderTopRightRadius: 2, + borderTopLeftRadius: 2, + }, + ios: { + borderTopRightRadius: 10, + borderTopLeftRadius: 10, + }, + }), + }, }); diff --git a/app/screens/reaction_list/reaction_header_item.js b/app/screens/reaction_list/reaction_header_item.js index 887d4a745..770bb855f 100644 --- a/app/screens/reaction_list/reaction_header_item.js +++ b/app/screens/reaction_list/reaction_header_item.js @@ -22,12 +22,12 @@ export default class ReactionHeaderItem extends PureComponent { highlight: PropTypes.bool.isRequired, onPress: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, - } + }; handleOnPress = () => { const {emojiName, highlight, onPress} = this.props; onPress(emojiName, highlight); - } + }; renderContent = () => { const {count, emojiName, theme} = this.props; @@ -56,7 +56,7 @@ export default class ReactionHeaderItem extends PureComponent { {count} ); - } + }; render() { const {emojiName, highlight, theme} = this.props; @@ -79,16 +79,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { marginLeft: 7, }, text: { - color: theme.linkColor, + color: theme.buttonBg, marginLeft: 4, fontSize: 16, }, highlight: { - borderColor: changeOpacity(theme.linkColor, 1), + borderColor: theme.buttonBg, borderBottomWidth: 2, }, regular: { - borderColor: theme.centerChannelBg, + borderColor: changeOpacity(theme.centerChannelColor, 0.2), borderBottomWidth: 2, }, reaction: { diff --git a/app/screens/reaction_list/reaction_list.js b/app/screens/reaction_list/reaction_list.js index 936d3bfdc..64ead7543 100644 --- a/app/screens/reaction_list/reaction_list.js +++ b/app/screens/reaction_list/reaction_list.js @@ -3,10 +3,10 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {View} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {intlShape} from 'react-intl'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; +import SlideUpPanel from 'app/components/slide_up_panel'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import { generateUserProfilesById, @@ -44,7 +44,9 @@ export default class ReactionList extends PureComponent { const reactionsByName = getReactionsByName(reactions); + this.contentOffsetY = -1; this.state = { + canDrag: true, allUserIds: getUniqueUserIds(reactions), reactions, reactionsByName, @@ -98,13 +100,17 @@ export default class ReactionList extends PureComponent { onNavigatorEvent = (event) => { if (event.type === 'NavBarButtonPress') { if (event.id === 'close-reaction-list') { - this.props.navigator.dismissModal({ - animationType: 'slide-down', - }); + this.close(); } } }; + close = () => { + this.props.navigator.dismissModal({ + animationType: 'none', + }); + }; + getMissingProfiles = () => { const {allUserIds, userProfiles, userProfilesById} = this.state; if (userProfiles.length !== allUserIds.length) { @@ -116,13 +122,37 @@ export default class ReactionList extends PureComponent { } } - scrollViewRef = (ref) => { - this.scrollView = ref; - }; - handleOnSelectReaction = (emoji) => { this.setState({selected: emoji}); - } + const slide = this.slideUpPanel?.getWrappedInstance(); + + if (slide) { + slide.setDrag(true); + } + + if (this.scrollView) { + this.scrollView.scrollTo({x: 0, y: 0, animated: false}); + } + }; + + handleScroll = (e) => { + const pageOffsetY = e.nativeEvent.contentOffset.y; + const canDrag = pageOffsetY <= 0; + const slide = this.slideUpPanel?.getWrappedInstance(); + + this.contentOffsetY = pageOffsetY; + if (slide) { + slide.setDrag(canDrag); + } + }; + + refSlideUpPanel = (r) => { + this.slideUpPanel = r; + }; + + refScrollView = (ref) => { + this.scrollView = ref; + }; renderReactionRows = () => { const { @@ -154,7 +184,7 @@ export default class ReactionList extends PureComponent { )); - } + }; render() { const { @@ -168,20 +198,30 @@ export default class ReactionList extends PureComponent { return ( - - - - - {this.renderReactionRows()} - + + + + + + {this.renderReactionRows()} + + + ); } @@ -190,11 +230,10 @@ export default class ReactionList extends PureComponent { const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { flex: { - backgroundColor: theme.centerChannelBg, flex: 1, }, headerContainer: { - height: 38, + height: 37.5, borderColor: changeOpacity(theme.centerChannelColor, 0.2), borderBottomWidth: 1, }, diff --git a/app/screens/reaction_list/reaction_list.test.js b/app/screens/reaction_list/reaction_list.test.js index a8db6e20b..695c713ab 100644 --- a/app/screens/reaction_list/reaction_list.test.js +++ b/app/screens/reaction_list/reaction_list.test.js @@ -2,10 +2,11 @@ // See LICENSE.txt for license information. import React from 'react'; import {shallow} from 'enzyme'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import Preferences from 'mattermost-redux/constants/preferences'; +import SlideUpPanel from 'app/components/slide_up_panel'; + import ReactionList from './reaction_list'; jest.mock('react-intl'); @@ -30,7 +31,7 @@ describe('ReactionList', () => { ); expect(wrapper.getElement()).toMatchSnapshot(); - expect(wrapper.find(KeyboardAwareScrollView).exists()).toEqual(true); + expect(wrapper.find(SlideUpPanel).exists()).toEqual(true); }); test('should match snapshot, renderReactionRows', () => { diff --git a/app/screens/reaction_list/reaction_row.js b/app/screens/reaction_list/reaction_row.js index f8c238947..da33b73ff 100644 --- a/app/screens/reaction_list/reaction_row.js +++ b/app/screens/reaction_list/reaction_row.js @@ -7,6 +7,7 @@ import {intlShape} from 'react-intl'; import { Text, TouchableOpacity, + StyleSheet, View, } from 'react-native'; @@ -14,7 +15,7 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils'; import ProfilePicture from 'app/components/profile_picture'; import {preventDoubleTap} from 'app/utils/tap'; -import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; +import {changeOpacity} from 'app/utils/theme'; import Emoji from 'app/components/emoji'; @@ -29,7 +30,7 @@ export default class ReactionRow extends React.PureComponent { static defaultProps = { user: {}, - } + }; static contextTypes = { intl: intlShape, @@ -62,7 +63,6 @@ export default class ReactionRow extends React.PureComponent { const { emojiName, teammateNameDisplay, - theme, user, } = this.props; @@ -71,7 +71,6 @@ export default class ReactionRow extends React.PureComponent { } const {id, username} = user; - const style = getStyleFromTheme(theme); const usernameDisplay = '@' + username; return ( @@ -114,40 +113,36 @@ export default class ReactionRow extends React.PureComponent { } } -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { - return { - container: { - backgroundColor: theme.centerChannelBg, - flexDirection: 'row', - justifyContent: 'flex-start', - height: 44, - width: '100%', - alignItems: 'center', - }, - profileContainer: { - alignItems: 'center', - width: '13%', - }, - profile: { - paddingTop: 3, - }, - textContainer: { - width: '74%', - flexDirection: 'row', - }, - username: { - fontSize: 14, - color: theme.centerChannelColor, - paddingRight: 5, - }, - displayName: { - fontSize: 14, - color: changeOpacity(theme.centerChannelColor, 0.5), - }, - emoji: { - alignItems: 'center', - width: '13%', - justifyContent: 'center', - }, - }; +const style = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'flex-start', + height: 44, + width: '100%', + alignItems: 'center', + }, + profileContainer: { + alignItems: 'center', + width: '13%', + }, + profile: { + paddingTop: 3, + }, + textContainer: { + width: '74%', + flexDirection: 'row', + }, + username: { + fontSize: 14, + paddingRight: 5, + }, + displayName: { + fontSize: 14, + color: changeOpacity('#000', 0.5), + }, + emoji: { + alignItems: 'center', + width: '13%', + justifyContent: 'center', + }, }); diff --git a/app/screens/search/search.js b/app/screens/search/search.js index e8e5b92bb..145252f33 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -13,7 +13,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import IonIcon from 'react-native-vector-icons/Ionicons'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; @@ -32,7 +31,7 @@ import PostSeparator from 'app/components/post_separator'; import SafeAreaView from 'app/components/safe_area_view'; import SearchBar from 'app/components/search_bar'; import StatusBar from 'app/components/status_bar'; -import {ListTypes} from 'app/constants'; +import {DeviceTypes, ListTypes} from 'app/constants'; import mattermostManaged from 'app/mattermost_managed'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -91,7 +90,7 @@ export default class Search extends PureComponent { super(props); props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); - this.isX = DeviceInfo.getModel().includes('iPhone X'); + this.contentOffsetY = 0; this.state = { channelName: '', @@ -736,7 +735,7 @@ export default class Search extends PureComponent { return ( diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index ebee7407a..3a29af689 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -25,7 +25,7 @@ import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; -import {UpgradeTypes} from 'app/constants/view'; +import {UpgradeTypes} from 'app/constants'; import fetchConfig from 'app/fetch_preconfig'; import mattermostBucket from 'app/mattermost_bucket'; import PushNotifications from 'app/push_notifications'; diff --git a/app/store/middleware.test.js b/app/store/middleware.test.js index b4d4de8cf..eec073285 100644 --- a/app/store/middleware.test.js +++ b/app/store/middleware.test.js @@ -8,13 +8,6 @@ import assert from 'assert'; import {ViewTypes} from 'app/constants'; import {messageRetention} from 'app/store/middleware'; -jest.mock('react-native-device-info', () => { - return { - getVersion: () => '0.0.0', - getBuildNumber: () => '0', - }; -}); - describe('store/middleware', () => { describe('messageRetention', () => { describe('should chain the same incoming action type', () => { diff --git a/app/utils/client_upgrade.js b/app/utils/client_upgrade.js index 31208591b..8a3ad0183 100644 --- a/app/utils/client_upgrade.js +++ b/app/utils/client_upgrade.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import semver from 'semver'; -import {UpgradeTypes} from 'app/constants/view'; +import {UpgradeTypes} from 'app/constants'; import LocalConfig from 'assets/config'; diff --git a/test/setup.js b/test/setup.js index c5543cfa2..b316378dd 100644 --- a/test/setup.js +++ b/test/setup.js @@ -17,6 +17,14 @@ jest.mock('NativeModules', () => { }); jest.mock('NativeEventEmitter'); +jest.mock('react-native-device-info', () => { + return { + getVersion: () => '0.0.0', + getBuildNumber: () => '0', + getModel: () => 'iPhone X', + }; +}); + let logs; let warns; let errors;