post menu and file attachments (#2414)

* post menu and file attachments

* Fix post list selector test

* Use new Gesture handler to avoid gesture conflicts in post menu and reaction list

* Allow reaction list to scroll the header on Android

* Feedback review

* Add some comments

* Fix eslint
This commit is contained in:
Elias Nahum 2018-12-05 14:59:41 -03:00 committed by GitHub
parent 2a6ad4f9ae
commit 2184696e65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 710 additions and 616 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -164,6 +164,7 @@ export default class PostBody extends PureComponent {
backButtonTitle: '',
navigatorStyle: {
navBarHidden: true,
navBarTransparent: true,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},

View file

@ -95,6 +95,7 @@ export default class Reactions extends PureComponent {
backButtonTitle: '',
navigatorStyle: {
navBarHidden: true,
navBarTransparent: true,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},

View file

@ -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 (
<View style={styles.viewport}>
<TapGestureHandler
maxDurationMs={100000}
ref={this.masterRef}
maxDeltaY={lastSnap - this.snapPoints[0]}
>
<View
ref={this.setBackdropRef}
style={styles.backdrop}
pointerEvents='box-only'
onTouchEnd={this.handleTouchEnd}
{...this.secondaryPanGesture.panHandlers}
/>
<SlideUpPanelIndicator
containerPosition={containerPosition}
panHandlers={this.secondaryPanGesture.panHandlers}
/>
<Animated.View
ref={this.setViewRef}
style={[containerPosition, styles.container]}
{...this.mainPanGesture.panHandlers}
style={StyleSheet.absoluteFill}
pointerEvents='box-none'
>
<View style={{maxHeight: (this.props.containerHeight - this.props.headerHeight - CONTAINER_MARGIN)}}>
{children}
</View>
</Animated.View>
</View>
<TapGestureHandler
waitFor={this.backdropRef}
onHandlerStateChange={this.onSingleTap}
>
<Animated.View style={styles.viewport}>
<PanGestureHandler
simultaneousHandlers={[this.scrollRef, this.masterRef]}
waitFor={this.headerRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
ref={this.backdropRef}
>
<Animated.View
style={styles.backdrop}
pointerEvents='box-only'
/>
</PanGestureHandler>
</Animated.View>
</TapGestureHandler>
<Animated.View style={[StyleSheet.absoluteFill, translateStyle]}>
<PanGestureHandler
simultaneousHandlers={[this.scrollRef, this.masterRef]}
waitFor={this.headerRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHeaderHandlerStateChange}
>
<Animated.View>
<SlideUpPanelIndicator/>
{header(this.headerRef)}
</Animated.View>
</PanGestureHandler>
<PanGestureHandler
ref={this.panRef}
simultaneousHandlers={[this.scrollRef, this.masterRef]}
waitFor={this.headerRef}
shouldCancelWhenOutside={false}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
>
<Animated.View style={[styles.container, !header && styles.border]}>
<NativeViewGestureHandler
ref={this.scrollRef}
waitFor={this.masterRef}
simultaneousHandlers={this.panRef}
>
<Animated.ScrollView
ref={this.scrollViewRef}
bounces={false}
onScrollBeginDrag={this.onRegisterLastScroll}
scrollEventThrottle={1}
>
{children}
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</View>
</TapGestureHandler>
);
}
}
@ -241,6 +304,8 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
border: {
...Platform.select({
android: {
borderTopRightRadius: 2,

View file

@ -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 (
<Animated.View
style={[containerPosition, styles.dragIndicatorContainer]}
{...panHandlers}
>
<View style={styles.dragIndicator}/>
</Animated.View>
);
export default function slideUpPanelIndicator() {
if (Platform.OS === 'android') {
return null;
}
return (
<Animated.View
style={styles.dragIndicatorContainer}
>
<View style={styles.dragIndicator}/>
</Animated.View>
);
}
const styles = StyleSheet.create({

View file

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

View file

@ -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 (
<View style={style.container} >
<Touchable
onPressOut={onPress}
onPress={this.handleOnPress}
{...touchableProps}
style={style.row}
>
<View style={style.row}>
<View style={style.icon}>

View file

@ -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 (
<View style={style.flex}>
<View style={style.container}>
<SlideUpPanel
allowStayMiddle={false}
alwaysCaptureContainerMove={true}
ref={this.refSlideUpPanel}
marginFromTop={marginFromTop}
onRequestClose={this.close}
initialPosition={initialPosition}
initialPosition={325}
>
{options}
</SlideUpPanel>
@ -407,7 +405,7 @@ export default class PostOptions extends PureComponent {
}
const style = StyleSheet.create({
flex: {
container: {
flex: 1,
},
});

View file

@ -1,92 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactionHeader should match snapshot 1`] = `
<View
style={
Object {
"backgroundColor": "#FFFFFF",
"borderTopLeftRadius": 10,
"borderTopRightRadius": 10,
"height": 36.5,
"paddingHorizontal": 0,
<Handler>
<AnimatedComponent
style={
Object {
"backgroundColor": "#FFFFFF",
"borderTopLeftRadius": 10,
"borderTopRightRadius": 10,
"height": 36.5,
"paddingHorizontal": 0,
}
}
}
>
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode="never"
>
<ReactionHeaderItem
count={2}
emojiName="smile"
highlight={true}
onPress={[Function]}
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",
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode="never"
>
<ReactionHeaderItem
count={2}
emojiName="smile"
highlight={true}
onPress={[Function]}
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",
}
}
}
/>
<ReactionHeaderItem
count={1}
emojiName="+1"
highlight={false}
onPress={[Function]}
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",
/>
<ReactionHeaderItem
count={1}
emojiName="+1"
highlight={false}
onPress={[Function]}
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",
}
}
}
/>
</ScrollView>
</View>
/>
</ScrollView>
</AnimatedComponent>
</Handler>
`;
exports[`ReactionHeader should match snapshot, renderContent 1`] = `

View file

@ -9,231 +9,155 @@ exports[`ReactionList should match snapshot 1`] = `
}
>
<Connect(SlideUpPanel)
header={[Function]}
headerHeight={37.5}
initialPosition={0.55}
onRequestClose={[Function]}
>
<React.Fragment>
<View
style={
Object {
"height": 45,
"justifyContent": "center",
}
}
>
<ReactionRow
emojiName="+1"
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
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_2",
"username": "username_2",
}
}
/>
<View
style={
Object {
"borderBottomWidth": 1,
"borderColor": "rgba(61,60,64,0.2)",
"height": 37.5,
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
>
<ReactionHeader
onSelectReaction={[Function]}
reactions={
Array [
Object {
"count": 2,
"name": "all_emojis",
},
Object {
"count": 1,
"name": "+1",
"reactions": Array [
Object {
"emoji_name": "+1",
"user_id": "user_id_2",
},
/>
</View>
<View
style={
Object {
"height": 45,
"justifyContent": "center",
}
}
>
<ReactionRow
emojiName="smile"
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
},
Object {
"count": 1,
"name": "smile",
"reactions": Array [
Object {
"emoji_name": "smile",
"user_id": "user_id_1",
},
],
},
]
}
selected="all_emojis"
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",
}
}
/>
</View>
<ScrollView
bounce={false}
onScroll={[Function]}
>
<View
style={
Object {
"height": 45,
"justifyContent": "center",
}
}
>
<ReactionRow
emojiName="+1"
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
}
}
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_2",
"username": "username_2",
}
}
/>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</View>
<View
style={
Object {
"height": 45,
"justifyContent": "center",
}
],
},
}
>
<ReactionRow
emojiName="smile"
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
],
},
}
}
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",
}
}
/>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</View>
</ScrollView>
</React.Fragment>
}
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",
}
}
/>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</View>
</Connect(SlideUpPanel)>
</View>
`;

View file

@ -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 (
<View style={styles.container}>
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode='never'
>
{this.renderReactionHeaderItems()}
</ScrollView>
</View>
<NativeViewGestureHandler
ref={this.props.forwardedRef}
>
<Animated.View style={styles.container}>
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode='never'
>
{this.renderReactionHeaderItems()}
</ScrollView>
</Animated.View>
</NativeViewGestureHandler>
);
}
}

View file

@ -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 (
<ReactionHeader
selected={selected}
onSelectReaction={this.handleOnSelectReaction}
reactions={sortedReactionsForHeader}
theme={theme}
forwardedRef={forwardedRef}
/>
);
};
render() {
const {
theme,
} = this.props;
const {
selected,
sortedReactionsForHeader,
} = this.state;
const style = getStyleSheet(theme);
const style = getStyleSheet(this.props.theme);
return (
<View style={style.flex}>
@ -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}
>
<React.Fragment>
<View style={style.headerContainer}>
<ReactionHeader
selected={selected}
onSelectReaction={this.handleOnSelectReaction}
reactions={sortedReactionsForHeader}
theme={theme}
/>
</View>
<ScrollView
ref={this.refScrollView}
bounce={false}
onScroll={this.handleScroll}
>
{this.renderReactionRows()}
</ScrollView>
</React.Fragment>
{this.renderReactionRows()}
</SlideUpPanel>
</View>
);
@ -233,8 +205,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
headerContainer: {
height: 37.5,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 1,
},
rowContainer: {
justifyContent: 'center',

View file

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

View file

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

View file

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

View file

@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
4A22BC320BA04E18A7F2A4E6 /* libReactNativeExceptionHandler.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libReactNativeExceptionHandler.a; sourceTree = "<group>"; };
4B19E38FBCB94C57BB03B8E6 /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
9263CF9B16054263B13EA23B /* libRNSafeArea.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSafeArea.a; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>";
@ -1196,6 +1208,14 @@
name = Products;
sourceTree = "<group>";
};
7F46E07B21B5AF9E004060B8 /* Products */ = {
isa = PBXGroup;
children = (
7F46E07F21B5AF9E004060B8 /* libRNGestureHandler.a */,
);
name = Products;
sourceTree = "<group>";
};
7F5CA957208FE38F004F91CE /* Products */ = {
isa = PBXGroup;
children = (
@ -1393,6 +1413,7 @@
DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */,
41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */,
849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */,
920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */,
);
name = Libraries;
sourceTree = "<group>";
@ -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++",

12
package-lock.json generated
View file

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

View file

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

View file

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