diff --git a/NOTICE.txt b/NOTICE.txt index dec80e254..4fbbd80c4 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1466,6 +1466,41 @@ SOFTWARE. --- +## react-native-gesture-handler + +This product contains 'react-native-gesture-handler' by kmagiera + +Declarative API exposing platform native touch and gesture system to React Native. + +* HOMEPAGE: + * https://github.com/kmagiera/react-native-gesture-handler + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2016 Krzysztof Magiera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + ## react-native-image-gallery This product contains a modified version of 'react-native-image-gallery' by Archriss. diff --git a/android/app/build.gradle b/android/app/build.gradle index 26ff70a8c..6a0484b8c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -216,6 +216,7 @@ dependencies { implementation project(':rn-fetch-blob') implementation project(':react-native-recyclerview-list') implementation project(':react-native-webview') + implementation project(':react-native-gesture-handler') // For animated GIF support implementation 'com.facebook.fresco:fresco:1.10.0' diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index d1cb92490..665001ccc 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -24,6 +24,7 @@ import com.gantix.JailMonkey.JailMonkeyPackage; import io.tradle.react.LocalAuthPackage; import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage; import com.reactnativecommunity.webview.RNCWebViewPackage; +import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; import com.facebook.react.ReactPackage; import com.facebook.soloader.SoLoader; @@ -84,7 +85,8 @@ public class MainApplication extends NavigationApplication implements INotificat new KeychainPackage(), new InitializationPackage(this), new RNRecyclerviewListPackage(), - new RNCWebViewPackage() + new RNCWebViewPackage(), + new RNGestureHandlerPackage() ); } diff --git a/android/settings.gradle b/android/settings.gradle index 54aeafb59..b3afe985e 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Mattermost' +include ':react-native-gesture-handler' +project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':react-native-document-picker' project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android') include ':react-native-keychain' diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 12949ef87..86c5f14a6 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -228,8 +228,8 @@ export function loadFilesForPostIfNecessary(postId) { const {files} = getState().entities; const fileIdsForPost = files.fileIdsByPostId[postId]; - if (!fileIdsForPost) { - await getFilesForPost(postId)(dispatch, getState); + if (!fileIdsForPost?.length) { + await dispatch(getFilesForPost(postId)); } }; } diff --git a/app/components/post/post.js b/app/components/post/post.js index 018783fdc..e63028afc 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -268,7 +268,7 @@ export default class Post extends PureComponent { const highlightFlagged = isFlagged && !skipFlaggedHeader; const hightlightPinned = post.is_pinned && !skipPinnedHeader; - let highlighted = highlight ? style.highlight : null; + let highlighted; if (highlight) { highlighted = style.highlight; } else if ((highlightFlagged || hightlightPinned) && highlightPinnedOrFlagged) { diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index d16f529c3..9eae3e12f 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -164,6 +164,7 @@ export default class PostBody extends PureComponent { backButtonTitle: '', navigatorStyle: { navBarHidden: true, + navBarTransparent: true, screenBackgroundColor: 'transparent', modalPresentationStyle: 'overCurrentContext', }, diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index e29f83908..aaaa87bae 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -95,6 +95,7 @@ export default class Reactions extends PureComponent { backButtonTitle: '', navigatorStyle: { navBarHidden: true, + navBarTransparent: true, screenBackgroundColor: 'transparent', modalPresentationStyle: 'overCurrentContext', }, diff --git a/app/components/slide_up_panel/slide_up_panel.js b/app/components/slide_up_panel/slide_up_panel.js index 270a028ac..90e65ad61 100644 --- a/app/components/slide_up_panel/slide_up_panel.js +++ b/app/components/slide_up_panel/slide_up_panel.js @@ -3,13 +3,13 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {Animated, Platform, StyleSheet, View} from 'react-native'; import { - Animated, - PanResponder, - Platform, - StyleSheet, - View, -} from 'react-native'; + PanGestureHandler, + NativeViewGestureHandler, + State as GestureState, + TapGestureHandler, +} from 'react-native-gesture-handler'; import {DeviceTypes} from 'app/constants'; @@ -19,25 +19,33 @@ export const BOTTOM_MARGIN = DeviceTypes.IS_IPHONE_X ? 24 : 0; 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 CONTAINER_MARGIN = TOP_MARGIN - 10; export default class SlideUpPanel extends PureComponent { static propTypes = { + + // Whether or not to allow the panel to snap to the initial position after it has been opened allowStayMiddle: PropTypes.bool, - alwaysCaptureContainerMove: PropTypes.bool, + containerHeight: PropTypes.number, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node, ]).isRequired, + header: PropTypes.func, headerHeight: PropTypes.number, + + // The initial position of the SlideUpPanel when it's first opened. If this value is between 0 and 1, + // it is treated as a percentage of the containerHeight. initialPosition: PropTypes.number, + + // The space between the top of the panel and the top of the container when the SlideUpPanel is fully open. marginFromTop: PropTypes.number, onRequestClose: PropTypes.func, }; static defaultProps = { allowStayMiddle: true, + header: () => null, headerHeight: 0, initialPosition: 0.5, marginFromTop: TOP_MARGIN, @@ -47,189 +55,244 @@ export default class SlideUpPanel extends PureComponent { constructor(props) { super(props); + const {containerHeight, headerHeight, marginFromTop} = props; + + this.masterRef = React.createRef(); + this.panRef = React.createRef(); + this.scrollRef = React.createRef(); + this.scrollViewRef = React.createRef(); + this.headerRef = React.createRef(); + this.backdropRef = React.createRef(); + const initialUsedSpace = Math.abs(props.initialPosition); let initialPosition; if (initialUsedSpace <= 1) { - initialPosition = ((props.containerHeight - (props.headerHeight + BOTTOM_MARGIN)) * (1 - initialUsedSpace)); + initialPosition = ((containerHeight - (headerHeight + BOTTOM_MARGIN)) * (1 - initialUsedSpace)); } else { - initialPosition = ((props.containerHeight - (props.headerHeight + BOTTOM_MARGIN)) - initialUsedSpace); + initialPosition = ((containerHeight - (headerHeight + BOTTOM_MARGIN)) - initialUsedSpace); } - this.mainPanGesture = PanResponder.create({ - onMoveShouldSetPanResponderCapture: (evt, gestureState) => { - if (this.props.alwaysCaptureContainerMove) { - return gestureState.dy !== 0; - } - const isGoingDown = gestureState.y0 < gestureState.dy; - return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown); - }, - onPanResponderMove: (evt, gestureState) => { - const isGoingDown = gestureState.dy > 0; - if (this.props.alwaysCaptureContainerMove && - !this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown)) { - return; - } - - 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; + // These values correspond to when the panel is fully open, when it is initially opened, and when it is closed + this.snapPoints = [marginFromTop, initialPosition, containerHeight]; this.state = { - position: new Animated.Value(props.containerHeight), - initialPosition, - finalPosition: props.marginFromTop, - endPosition: 0, + lastSnap: initialPosition, }; + + this.lastScrollYValue = 0; + this.lastScrollY = new Animated.Value(0); + this.onRegisterLastScroll = Animated.event( + [{ + nativeEvent: { + contentOffset: { + y: this.lastScrollY, + }, + }, + }], + {useNativeDriver: true}, + ); + this.lastScrollY.addListener(({value}) => { + this.lastScrollYValue = value; + }); + + this.dragY = new Animated.Value(0); + this.onGestureEvent = Animated.event( + [{ + nativeEvent: { + translationY: this.dragY, + }, + }], + {useNativeDriver: true}, + ); + + this.reverseLastScrollY = Animated.multiply( + new Animated.Value(-1), + this.lastScrollY + ); + + this.translateYOffset = new Animated.Value(containerHeight); + this.translateY = Animated.add( + this.translateYOffset, + Animated.add(this.dragY, this.reverseLastScrollY) + ).interpolate({ + inputRange: [marginFromTop, containerHeight], + outputRange: [marginFromTop, containerHeight], + extrapolate: 'clamp', + }); } componentDidMount() { - this.startAnimation(this.props.containerHeight, this.state.initialPosition, false, true); + Animated.timing(this.translateYOffset, { + duration: 200, + toValue: this.snapPoints[1], + useNativeDriver: true, + }).start(); } - 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 {allowStayMiddle, 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 && allowStayMiddle) { - endPosition = initialPosition; - } else { - endPosition = containerHeight; - } - } - - Animated.timing(position, { - toValue: endPosition, - duration: initial ? 200 : 100, + closeWithAnimation = () => { + Animated.timing(this.translateYOffset, { + duration: 200, + toValue: this.snapPoints[2], 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); - } - }); + }).start(() => this.props.onRequestClose()); }; - updatePosition = (newPosition) => { - const {position} = this.state; - this.previousTop = newPosition; - position.setValue(newPosition); + onHeaderHandlerStateChange = ({nativeEvent}) => { + if (nativeEvent.oldState === GestureState.BEGAN) { + this.lastScrollY.setValue(0); + } + this.onHandlerStateChange({nativeEvent}); + }; + + onHandlerStateChange = ({nativeEvent}) => { + if (nativeEvent.oldState === GestureState.ACTIVE) { + const {translationY, velocityY} = nativeEvent; + const {allowStayMiddle} = this.props; + const {lastSnap} = this.state; + const isGoingDown = translationY > 0; + const translation = translationY - this.lastScrollYValue; + + const endOffsetY = lastSnap + translation; + let destSnapPoint = this.snapPoints[0]; + + if (Math.abs(translationY) < 50) { + // Only drag the panel after moving 50 or more points + destSnapPoint = lastSnap; + } else if (isGoingDown && !allowStayMiddle) { + // Just close the panel if the user pans down and we can't snap to the middle + destSnapPoint = this.snapPoints[2]; + } else if (isGoingDown) { + destSnapPoint = this.snapPoints.find((s) => s >= endOffsetY); + } else { + destSnapPoint = this.snapPoints.find((s) => s <= endOffsetY); + } + + if (destSnapPoint) { + this.translateYOffset.extractOffset(); + this.translateYOffset.setValue(translationY); + this.translateYOffset.flattenOffset(); + this.dragY.setValue(0); + + if (destSnapPoint === this.snapPoints[2]) { + this.closeWithAnimation(); + } else { + Animated.spring(this.translateYOffset, { + velocity: velocityY, + tension: 68, + friction: 12, + toValue: destSnapPoint, + useNativeDriver: true, + }).start(() => { + this.setState({lastSnap: destSnapPoint}); + }); + } + } else { + Animated.spring(this.translateYOffset, { + velocity: velocityY, + tension: 68, + friction: 12, + toValue: lastSnap, + useNativeDriver: true, + }).start(); + } + } + }; + + onSingleTap = ({nativeEvent}) => { + if (nativeEvent.state === GestureState.ACTIVE) { + this.closeWithAnimation(); + } + }; + + scrollToTop = () => { + if (this.scrollViewRef?.current) { + this.scrollViewRef.current._component.scrollTo({ //eslint-disable-line no-underscore-dangle + x: 0, + y: 0, + animated: false, + }); + } }; render() { - const {children} = this.props; - const containerPosition = { - top: this.state.position, + const {children, header} = this.props; + const {lastSnap} = this.state; + const translateStyle = { + transform: [{translateY: this.translateY}], }; return ( - + - - - - {children} - - - + + + + + + + + + + + + + {header(this.headerRef)} + + + + + + + {children} + + + + + + + ); } } @@ -241,6 +304,8 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'white', + }, + border: { ...Platform.select({ android: { borderTopRightRadius: 2, diff --git a/app/components/slide_up_panel/slide_up_panel_indicator.js b/app/components/slide_up_panel/slide_up_panel_indicator.js index 53b3cb425..e50d96162 100644 --- a/app/components/slide_up_panel/slide_up_panel_indicator.js +++ b/app/components/slide_up_panel/slide_up_panel_indicator.js @@ -1,32 +1,21 @@ // 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 React from 'react'; 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 ( - - - - ); +export default function slideUpPanelIndicator() { + if (Platform.OS === 'android') { + return null; } + + return ( + + + + ); } const styles = StyleSheet.create({ diff --git a/app/screens/index.js b/app/screens/index.js index 92b2ef1e0..b2cfa0072 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {Navigation} from 'react-native-navigation'; +import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import Channel from 'app/screens/channel'; import Entry from 'app/screens/entry'; @@ -47,8 +48,8 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(require('app/screens/options_modal').default), store, Provider); Navigation.registerComponent('Permalink', () => wrapWithContextProvider(require('app/screens/permalink').default), store, Provider); Navigation.registerComponent('PinnedPosts', () => wrapWithContextProvider(require('app/screens/pinned_posts').default), store, Provider); - Navigation.registerComponent('PostOptions', () => wrapWithContextProvider(require('app/screens/post_options').default), store, Provider); - Navigation.registerComponent('ReactionList', () => wrapWithContextProvider(require('app/screens/reaction_list').default), store, Provider); + Navigation.registerComponent('PostOptions', () => gestureHandlerRootHOC(wrapWithContextProvider(require('app/screens/post_options').default)), store, Provider); + Navigation.registerComponent('ReactionList', () => gestureHandlerRootHOC(wrapWithContextProvider(require('app/screens/reaction_list').default)), store, Provider); Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider); Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider); Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider); diff --git a/app/screens/post_options/post_option.js b/app/screens/post_options/post_option.js index 30fc70163..43c937efc 100644 --- a/app/screens/post_options/post_option.js +++ b/app/screens/post_options/post_option.js @@ -39,8 +39,15 @@ export default class PostOption extends PureComponent { text: PropTypes.string.isRequired, }; + handleOnPress = () => { + // Wait for the tap animation so that the user has some feedback + setTimeout(() => { + this.props.onPress(); + }, 250); + }; + render() { - const {destructive, icon, onPress, text} = this.props; + const {destructive, icon, text} = this.props; const image = icons[icon]; const Touchable = Platform.select({ @@ -50,12 +57,12 @@ export default class PostOption extends PureComponent { const touchableProps = Platform.select({ ios: { - underlayColor: 'rgba(0, 0, 0, 0.05)', + underlayColor: 'rgba(0, 0, 0, 0.1)', }, android: { background: TouchableNativeFeedback.Ripple( //eslint-disable-line new-cap - 'rgba(0, 0, 0, 0.05)', - true, + 'rgba(0, 0, 0, 0.1)', + false, ), }, }); @@ -63,8 +70,9 @@ export default class PostOption extends PureComponent { return ( diff --git a/app/screens/post_options/post_options.js b/app/screens/post_options/post_options.js index ff091e4bd..31f576e69 100644 --- a/app/screens/post_options/post_options.js +++ b/app/screens/post_options/post_options.js @@ -55,7 +55,7 @@ export default class PostOptions extends PureComponent { closeWithAnimation = () => { if (this.slideUpPanel) { - this.slideUpPanel.getWrappedInstance().handleTouchEnd(); + this.slideUpPanel.getWrappedInstance().closeWithAnimation(); } else { this.close(); } @@ -386,18 +386,16 @@ export default class PostOptions extends PureComponent { render() { const {deviceHeight} = this.props; const options = this.getPostOptions(); - const initialPosition = (options.length + 1) * OPTION_HEIGHT; const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT); return ( - + {options} @@ -407,7 +405,7 @@ export default class PostOptions extends PureComponent { } const style = StyleSheet.create({ - flex: { + container: { flex: 1, }, }); 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 a78c499fa..c99530a23 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_header.test.js.snap @@ -1,92 +1,94 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReactionHeader should match snapshot 1`] = ` - + - - + - + - - + /> + + + `; exports[`ReactionHeader should match snapshot, renderContent 1`] = ` diff --git a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap index a54ade334..b24e7b8e7 100644 --- a/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap +++ b/app/screens/reaction_list/__snapshots__/reaction_list.test.js.snap @@ -9,231 +9,155 @@ exports[`ReactionList should match snapshot 1`] = ` } > - + + - + + + - - - - - - - - - - - - + } + teammateNameDisplay="username" + theme={ + Object { + "awayIndicator": "#ffbc42", + "buttonBg": "#166de0", + "buttonColor": "#ffffff", + "centerChannelBg": "#ffffff", + "centerChannelColor": "#3d3c40", + "codeTheme": "github", + "dndIndicator": "#f74343", + "errorTextColor": "#fd5960", + "linkColor": "#2389d7", + "mentionBj": "#ffffff", + "mentionColor": "#145dbf", + "mentionHighlightBg": "#ffe577", + "mentionHighlightLink": "#166de0", + "newMessageSeparator": "#ff8800", + "onlineIndicator": "#06d6a0", + "sidebarBg": "#145dbf", + "sidebarHeaderBg": "#1153ab", + "sidebarHeaderTextColor": "#ffffff", + "sidebarText": "#ffffff", + "sidebarTextActiveBorder": "#579eff", + "sidebarTextActiveColor": "#ffffff", + "sidebarTextHoverBg": "#4578bf", + "sidebarUnreadText": "#ffffff", + "type": "Mattermost", + } + } + user={ + Object { + "id": "user_id_1", + "username": "username_1", + } + } + /> + + `; diff --git a/app/screens/reaction_list/reaction_header.js b/app/screens/reaction_list/reaction_header.js index 988cc9f31..af25802a4 100644 --- a/app/screens/reaction_list/reaction_header.js +++ b/app/screens/reaction_list/reaction_header.js @@ -4,16 +4,18 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { + Animated, Platform, ScrollView, StyleSheet, - View, } from 'react-native'; +import {NativeViewGestureHandler} from 'react-native-gesture-handler'; import ReactionHeaderItem from './reaction_header_item'; export default class ReactionHeader extends PureComponent { static propTypes = { + forwardedRef: PropTypes.object, selected: PropTypes.string.isRequired, onSelectReaction: PropTypes.func.isRequired, reactions: PropTypes.array.isRequired, @@ -41,15 +43,19 @@ export default class ReactionHeader extends PureComponent { render() { return ( - - - {this.renderReactionHeaderItems()} - - + + + + {this.renderReactionHeaderItems()} + + + ); } } diff --git a/app/screens/reaction_list/reaction_list.js b/app/screens/reaction_list/reaction_list.js index af226b5e1..259f78bb4 100644 --- a/app/screens/reaction_list/reaction_list.js +++ b/app/screens/reaction_list/reaction_list.js @@ -3,7 +3,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; + import {intlShape} from 'react-intl'; import SlideUpPanel from 'app/components/slide_up_panel'; @@ -41,10 +42,8 @@ export default class ReactionList extends PureComponent { constructor(props) { super(props); const {reactions, userProfiles} = props; - const reactionsByName = getReactionsByName(reactions); - this.contentOffsetY = -1; this.state = { allUserIds: getUniqueUserIds(reactions), reactions, @@ -123,25 +122,9 @@ export default class ReactionList extends PureComponent { 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); + if (this.slideUpPanel) { + this.slideUpPanel.getWrappedInstance().scrollToTop(); } }; @@ -149,10 +132,6 @@ export default class ReactionList extends PureComponent { this.slideUpPanel = r; }; - refScrollView = (ref) => { - this.scrollView = ref; - }; - renderReactionRows = () => { const { navigator, @@ -185,15 +164,23 @@ export default class ReactionList extends PureComponent { )); }; + renderHeader = (forwardedRef) => { + const {theme} = this.props; + const {selected, sortedReactionsForHeader} = this.state; + + return ( + + ); + }; + render() { - const { - theme, - } = this.props; - const { - selected, - sortedReactionsForHeader, - } = this.state; - const style = getStyleSheet(theme); + const style = getStyleSheet(this.props.theme); return ( @@ -201,25 +188,10 @@ export default class ReactionList extends PureComponent { ref={this.refSlideUpPanel} onRequestClose={this.close} initialPosition={0.55} + header={this.renderHeader} headerHeight={37.5} > - - - - - - {this.renderReactionRows()} - - + {this.renderReactionRows()} ); @@ -233,8 +205,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }, headerContainer: { height: 37.5, - borderColor: changeOpacity(theme.centerChannelColor, 0.2), - borderBottomWidth: 1, }, rowContainer: { justifyContent: 'center', diff --git a/app/selectors/post_list.js b/app/selectors/post_list.js index 1cb608043..38c996cf9 100644 --- a/app/selectors/post_list.js +++ b/app/selectors/post_list.js @@ -58,9 +58,10 @@ export function makePreparePostIdsForPostList() { // Push on a date header if the last post was on a different day than the current one const postDate = new Date(post.create_at); + postDate.setHours(0, 0, 0, 0); if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) { - out.push(DATE_LINE + post.create_at); + out.push(DATE_LINE + postDate.getTime()); lastDate = postDate; } diff --git a/app/selectors/post_list.test.js b/app/selectors/post_list.test.js index d69965e00..bad10549b 100644 --- a/app/selectors/post_list.test.js +++ b/app/selectors/post_list.test.js @@ -18,13 +18,16 @@ describe('Selectors.PostList', () => { describe('makePreparePostIdsForPostList', () => { it('filter join/leave posts', () => { const preparePostIdsForPostList = makePreparePostIdsForPostList(); + const time = Date.now(); + const today = new Date(); + today.setHours(0, 0, 0, 0); let state = { entities: { posts: { posts: { - 1001: {id: '1001', create_at: 0, type: ''}, - 1002: {id: '1002', create_at: 1, type: Posts.POST_TYPES.JOIN_CHANNEL}, + 1001: {id: '1001', create_at: time, type: ''}, + 1002: {id: '1002', create_at: time + 1, type: Posts.POST_TYPES.JOIN_CHANNEL}, }, }, preferences: { @@ -47,7 +50,7 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1002', '1001', - 'date-0', + 'date-' + today.getTime(), ]); // Show join/leave posts @@ -73,7 +76,7 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1002', '1001', - 'date-0', + 'date-' + today.getTime(), ]); // Hide join/leave posts @@ -98,7 +101,7 @@ describe('Selectors.PostList', () => { now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages}); assert.deepEqual(now, [ '1001', - 'date-0', + 'date-' + today.getTime(), ]); // always show join/leave posts for the current user @@ -110,7 +113,7 @@ describe('Selectors.PostList', () => { ...state.entities.posts, posts: { ...state.entities.posts.posts, - 1002: {id: '1002', create_at: 1, type: Posts.POST_TYPES.JOIN_CHANNEL, props: {username: 'user'}}, + 1002: {id: '1002', create_at: time + 1, type: Posts.POST_TYPES.JOIN_CHANNEL, props: {username: 'user'}}, }, }, }, @@ -121,20 +124,23 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1002', '1001', - 'date-0', + 'date-' + today.getTime(), ]); }); it('new messages indicator', () => { const preparePostIdsForPostList = makePreparePostIdsForPostList(); + const time = Date.now(); + const today = new Date(); + today.setHours(0, 0, 0, 0); const state = { entities: { posts: { posts: { - 1000: {id: '1000', create_at: 1000, type: ''}, - 1005: {id: '1005', create_at: 1005, type: ''}, - 1010: {id: '1010', create_at: 1010, type: ''}, + 1000: {id: '1000', create_at: time + 1000, type: ''}, + 1005: {id: '1005', create_at: time + 1005, type: ''}, + 1010: {id: '1010', create_at: time + 1010, type: ''}, }, }, preferences: { @@ -157,7 +163,7 @@ describe('Selectors.PostList', () => { '1010', '1005', '1000', - 'date-1000', + 'date-' + today.getTime(), ]); now = preparePostIdsForPostList(state, {postIds, indicateNewMessages: true}); @@ -165,67 +171,72 @@ describe('Selectors.PostList', () => { '1010', '1005', '1000', - 'date-1000', + 'date-' + today.getTime(), ]); - now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 999, indicateNewMessages: false}); + now = preparePostIdsForPostList(state, {postIds, lastViewedAt: time + 999, indicateNewMessages: false}); assert.deepEqual(now, [ '1010', '1005', '1000', - 'date-1000', + 'date-' + today.getTime(), ]); // Show new messages indicator before all posts - now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 999, indicateNewMessages: true}); + now = preparePostIdsForPostList(state, {postIds, lastViewedAt: time + 999, indicateNewMessages: true}); assert.deepEqual(now, [ '1010', '1005', '1000', START_OF_NEW_MESSAGES, - 'date-1000', + 'date-' + today.getTime(), ]); // Show indicator between posts - now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1003, indicateNewMessages: true}); + now = preparePostIdsForPostList(state, {postIds, lastViewedAt: time + 1003, indicateNewMessages: true}); assert.deepEqual(now, [ '1010', '1005', START_OF_NEW_MESSAGES, '1000', - 'date-1000', + 'date-' + today.getTime(), ]); - now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1006, indicateNewMessages: true}); + now = preparePostIdsForPostList(state, {postIds, lastViewedAt: time + 1006, indicateNewMessages: true}); assert.deepEqual(now, [ '1010', START_OF_NEW_MESSAGES, '1005', '1000', - 'date-1000', + 'date-' + today.getTime(), ]); // Don't show indicator when all posts are read - now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1020}); + now = preparePostIdsForPostList(state, {postIds, lastViewedAt: time + 1020}); assert.deepEqual(now, [ '1010', '1005', '1000', - 'date-1000', + 'date-' + today.getTime(), ]); }); it('memoization', () => { const preparePostIdsForPostList = makePreparePostIdsForPostList(); + const time = Date.now(); + const today = new Date(); + const tomorrow = new Date((24 * 60 * 60 * 1000) + today.getTime()); + today.setHours(0, 0, 0, 0); + tomorrow.setHours(0, 0, 0, 0); // Posts 7 hours apart so they should appear on multiple days const initialPosts = { - 1001: {id: '1001', create_at: 1 * 60 * 60 * 1000, type: ''}, - 1002: {id: '1002', create_at: (1 * 60 * 60 * 1000) + 5, type: ''}, - 1003: {id: '1003', create_at: (1 * 60 * 60 * 1000) + 10, type: ''}, - 1004: {id: '1004', create_at: 25 * 60 * 60 * 1000, type: ''}, - 1005: {id: '1005', create_at: (25 * 60 * 60 * 1000) + 5, type: ''}, - 1006: {id: '1006', create_at: (25 * 60 * 60 * 1000) + 10, type: Posts.POST_TYPES.JOIN_CHANNEL}, + 1001: {id: '1001', create_at: time, type: ''}, + 1002: {id: '1002', create_at: time + 5, type: ''}, + 1003: {id: '1003', create_at: time + 10, type: ''}, + 1004: {id: '1004', create_at: tomorrow, type: ''}, + 1005: {id: '1005', create_at: tomorrow + 5, type: ''}, + 1006: {id: '1006', create_at: tomorrow + 10, type: Posts.POST_TYPES.JOIN_CHANNEL}, }; let state = { entities: { @@ -262,11 +273,11 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1006', '1004', - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', START_OF_NEW_MESSAGES, '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // No changes @@ -276,11 +287,11 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1006', '1004', - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', START_OF_NEW_MESSAGES, '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // lastViewedAt changed slightly @@ -292,15 +303,15 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1006', '1004', - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', START_OF_NEW_MESSAGES, '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // lastViewedAt changed a lot - lastViewedAt += initialPosts['1003'].create_at + 1; + lastViewedAt = initialPosts['1003'].create_at + 1; prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); @@ -309,10 +320,10 @@ describe('Selectors.PostList', () => { '1006', '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); prev = now; @@ -322,10 +333,10 @@ describe('Selectors.PostList', () => { '1006', '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // postIds changed, but still shallowly equal @@ -338,10 +349,10 @@ describe('Selectors.PostList', () => { '1006', '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // Post changed, not in postIds @@ -366,10 +377,10 @@ describe('Selectors.PostList', () => { '1006', '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // Post changed, in postIds @@ -394,10 +405,10 @@ describe('Selectors.PostList', () => { '1006', '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); // Filter changed @@ -425,10 +436,10 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); prev = now; @@ -437,10 +448,10 @@ describe('Selectors.PostList', () => { assert.deepEqual(now, [ '1004', START_OF_NEW_MESSAGES, - 'date-90000000', + 'date-' + tomorrow.getTime(), '1003', '1001', - 'date-3600000', + 'date-' + today.getTime(), ]); }); }); diff --git a/index.js b/index.js index 5746e3135..1b5b1e97e 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import 'react-native/Libraries/Core/InitializeCore'; import {AppRegistry, Platform} from 'react-native'; +import 'react-native-gesture-handler'; import 'app/mattermost'; import ShareExtension from 'share_extension/android'; diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 800f5c690..8727d9825 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 3C0B333879CE4730843D8584 /* Roboto-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D08F54E10DF14AE4BBF4E2B4 /* Roboto-MediumItalic.ttf */; }; 3D38ABA732A34A9BB3294F90 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; }; 3D7B4E6EE6B948AAA6A1E4E6 /* Roboto-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C46342B0E5FD4D878AF3A129 /* Roboto-BoldItalic.ttf */; }; + 3F55075810254369AB39F032 /* libRNGestureHandler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 476BA76E31644398A6012911 /* libRNGestureHandler.a */; }; 3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */; }; 420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EDC04CBCF81642219D199CBB /* Octicons.ttf */; }; 460A48EA5C6C4D8B8D9A2C75 /* Roboto-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 031EF04FB2D14EEFAACBAA1A /* Roboto-Italic.ttf */; }; @@ -438,6 +439,13 @@ remoteGlobalIDString = 7F43D6041F6BF9CA001FC614; remoteInfo = "RNSVG-tvOS"; }; + 7F46E07E21B5AF9E004060B8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNGestureHandler; + }; 7F4C1F831FBE572B0029D1DF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; @@ -696,6 +704,7 @@ 41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNReactNativeDocViewer.xcodeproj; path = "../node_modules/react-native-doc-viewer/ios/RNReactNativeDocViewer.xcodeproj"; sourceTree = ""; }; 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Italic.ttf"; path = "../assets/fonts/OpenSans-Italic.ttf"; sourceTree = ""; }; 4246DC09024BB33A3E491E25 /* libPods-MattermostTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MattermostTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 476BA76E31644398A6012911 /* libRNGestureHandler.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNGestureHandler.a; sourceTree = ""; }; 4A22BC320BA04E18A7F2A4E6 /* libReactNativeExceptionHandler.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libReactNativeExceptionHandler.a; sourceTree = ""; }; 4B19E38FBCB94C57BB03B8E6 /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; 51F5A483EA9F4A9A87ACFB59 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; }; @@ -769,6 +778,7 @@ 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSafeArea.xcodeproj; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = ""; }; 8F0B22D2C9924FAFA7FB681C /* Roboto-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-LightItalic.ttf"; path = "../assets/fonts/Roboto-LightItalic.ttf"; sourceTree = ""; }; + 920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNGestureHandler.xcodeproj; path = "../node_modules/react-native-gesture-handler/ios/RNGestureHandler.xcodeproj"; sourceTree = ""; }; 9263CF9B16054263B13EA23B /* libRNSafeArea.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSafeArea.a; sourceTree = ""; }; A2968536BEC74A4ABBB73BD9 /* RCTYouTube.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTYouTube.xcodeproj; path = "../node_modules/react-native-youtube/RCTYouTube.xcodeproj"; sourceTree = ""; }; A734E00E7E184582A877F2B3 /* Roboto-Thin.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Thin.ttf"; path = "../assets/fonts/Roboto-Thin.ttf"; sourceTree = ""; }; @@ -849,6 +859,7 @@ 3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */, 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */, C3BD62ED5B5940AB8C0AC678 /* libz.tbd in Frameworks */, + 3F55075810254369AB39F032 /* libRNGestureHandler.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1098,6 +1109,7 @@ B89192186C764B9FA473403A /* libRCTVideo.a */, 6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */, 9263CF9B16054263B13EA23B /* libRNSafeArea.a */, + 476BA76E31644398A6012911 /* libRNGestureHandler.a */, ); name = "Recovered References"; sourceTree = ""; @@ -1196,6 +1208,14 @@ name = Products; sourceTree = ""; }; + 7F46E07B21B5AF9E004060B8 /* Products */ = { + isa = PBXGroup; + children = ( + 7F46E07F21B5AF9E004060B8 /* libRNGestureHandler.a */, + ); + name = Products; + sourceTree = ""; + }; 7F5CA957208FE38F004F91CE /* Products */ = { isa = PBXGroup; children = ( @@ -1393,6 +1413,7 @@ DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */, 41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */, 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */, + 920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -1666,6 +1687,10 @@ ProductGroup = 7FF31C0F21330B4200680B75 /* Products */; ProjectRef = 7FF31C0E21330B4200680B75 /* RNFetchBlob.xcodeproj */; }, + { + ProductGroup = 7F46E07B21B5AF9E004060B8 /* Products */; + ProjectRef = 920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */; + }, { ProductGroup = 7FDB92761F706F45006CDFD1 /* Products */; ProjectRef = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */; @@ -2022,6 +2047,13 @@ remoteRef = 7F43D63A1F6BF9EB001FC614 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F46E07F21B5AF9E004060B8 /* libRNGestureHandler.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNGestureHandler.a; + remoteRef = 7F46E07E21B5AF9E004060B8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7F4C1F841FBE572B0029D1DF /* libfishhook.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2436,11 +2468,15 @@ "$(SRCROOT)/../node_modules/react-native-video/ios", "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = MattermostTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2463,11 +2499,15 @@ "$(SRCROOT)/../node_modules/react-native-video/ios", "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = MattermostTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2512,6 +2552,7 @@ "$(SRCROOT)../node_modules/react-native-webview/ios", "$(SRCROOT)../node_modules/react-native-svg/ios/**", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.3; @@ -2563,6 +2604,7 @@ "$(SRCROOT)../node_modules/react-native-webview/ios", "$(SRCROOT)../node_modules/react-native-svg/ios/**", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.3; @@ -2619,10 +2661,15 @@ "$(SRCROOT)/../node_modules/react-native-video/ios/**", "$(SRCROOT)/../node_modules/react-native-tableview/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = MattermostShare/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", @@ -2676,10 +2723,15 @@ "$(SRCROOT)/../node_modules/react-native-video/ios/**", "$(SRCROOT)/../node_modules/react-native-tableview/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", + "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", ); INFOPLIST_FILE = MattermostShare/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", diff --git a/package-lock.json b/package-lock.json index dbae32a17..6f246f96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3800,7 +3800,7 @@ }, "event-target-shim": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz", "integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE=" }, "eventemitter3": { @@ -11228,6 +11228,16 @@ "resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.10.2.tgz", "integrity": "sha512-hcPXfjSTSEbJYhUUbgarKZNwJFG2rehbJVW5ZbQc2rVkFor6tS4diyqufkWayS0mKg+N/Q3sP1w0j3QWv+u4mg==" }, + "react-native-gesture-handler": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.0.10.tgz", + "integrity": "sha512-GNVZ+rbwfi147qOxIvWyfHcTswr+CRT/bSy/Gr5Dz17/tWmomf9D6tGr1yJhyW8IhEZ307KLBWRCXLBM9Q2YQg==", + "requires": { + "hoist-non-react-statics": "^2.3.1", + "invariant": "^2.2.2", + "prop-types": "^15.5.10" + } + }, "react-native-image-gallery": { "version": "github:enahum/react-native-image-gallery#a98b1051e94f5a394541ca1ff9b15e2c9ffed84f", "from": "github:enahum/react-native-image-gallery#a98b1051e94f5a394541ca1ff9b15e2c9ffed84f", diff --git a/package.json b/package.json index cd802bf99..adfacacda 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-native-doc-viewer": "2.7.8", "react-native-document-picker": "2.1.0", "react-native-exception-handler": "2.10.2", + "react-native-gesture-handler": "1.0.10", "react-native-image-gallery": "github:enahum/react-native-image-gallery#a98b1051e94f5a394541ca1ff9b15e2c9ffed84f", "react-native-image-picker": "0.27.1", "react-native-keyboard-aware-scroll-view": "0.7.4", diff --git a/test/setup.js b/test/setup.js index b316378dd..6ac9e1113 100644 --- a/test/setup.js +++ b/test/setup.js @@ -9,10 +9,23 @@ configure({adapter: new Adapter()}); jest.mock('NativeModules', () => { return { + UIManager: { + RCTView: { + directEventTypes: {}, + }, + }, BlurAppScreen: () => true, MattermostManaged: { getConfig: jest.fn(), }, + RNGestureHandlerModule: { + State: { + BEGAN: 'BEGAN', + FAILED: 'FAILED', + ACTIVE: 'ACTIVE', + END: 'END', + }, + }, }; }); jest.mock('NativeEventEmitter');