Slide up panel for reaction list (#2285)

* Slide up panel for reaction list

* Feedback review

* UI Feedback review

* Feedback review

* Improve slide speed and fix end position

* Scroll to top when changing reaction view

* dismiss modal without animation so backdrop does not scroll down

* Smooth animation to show/hide

* Change animation speed
This commit is contained in:
Elias Nahum 2018-11-02 13:21:43 -03:00 committed by GitHub
parent 7fa9326ec4
commit 9a5122c19c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 716 additions and 413 deletions

View file

@ -8,8 +8,8 @@ import {
Platform,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import AtMention from './at_mention';
@ -92,7 +92,7 @@ export default class Autocomplete extends PureComponent {
} else {
// List is expanding downwards, likely from the search box
let offset = Platform.select({ios: 65, android: 75});
if (DeviceInfo.getModel().includes('iPhone X')) {
if (DeviceTypes.IS_IPHONE_X) {
offset = 90;
}

View file

@ -11,11 +11,10 @@ import {
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import DeviceInfo from 'react-native-device-info';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {UpgradeTypes} from 'app/constants/view';
import {DeviceTypes, UpgradeTypes} from 'app/constants';
import checkUpgradeType from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -47,8 +46,6 @@ export default class ClientUpgradeListener extends PureComponent {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
@ -73,7 +70,7 @@ export default class ClientUpgradeListener extends PureComponent {
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
} else if (this.props.isLandscape !== nextProps.isLandscape &&
this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && this.isX) {
this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && DeviceTypes.IS_IPHONE_X) {
const newTop = nextProps.isLandscape ? 45 : 100;
this.setState({top: new Animated.Value(newTop)});
}
@ -100,10 +97,10 @@ export default class ClientUpgradeListener extends PureComponent {
toggleUpgradeMessage = (show = true, isLandscape) => {
let toValue = -100;
if (show) {
if (this.isX && isLandscape) {
if (DeviceTypes.IS_IPHONE_X && isLandscape) {
toValue = 45;
} else {
toValue = this.isX ? 100 : 75;
toValue = DeviceTypes.IS_IPHONE_X ? 100 : 75;
}
}
Animated.timing(this.state.top, {

View file

@ -14,7 +14,6 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
@ -24,6 +23,7 @@ import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@ -75,7 +75,6 @@ export default class EmojiPicker extends PureComponent {
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
this.isX = DeviceInfo.getModel().includes('iPhone X');
this.scrollToSectionTries = 0;
this.state = {
emojis,
@ -436,7 +435,7 @@ export default class EmojiPicker extends PureComponent {
let keyboardOffset = 64;
if (Platform.OS === 'android') {
keyboardOffset = -200;
} else if (this.isX) {
} else if (DeviceTypes.IS_IPHONE_X) {
keyboardOffset = isLandscape ? 35 : 107;
} else if (isLandscape) {
keyboardOffset = 52;

View file

@ -11,11 +11,10 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import IonIcon from 'react-native-vector-icons/Ionicons';
import FormattedText from 'app/components/formatted_text';
import {ViewTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostBucket from 'app/mattermost_bucket';
import checkNetwork from 'app/utils/network';
import {t} from 'app/utils/i18n';
@ -55,7 +54,6 @@ export default class OfflineIndicator extends Component {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
const navBar = this.getNavBarHeight(props.isLandscape);
this.state = {
@ -154,9 +152,9 @@ export default class OfflineIndicator extends Component {
return ANDROID_TOP_PORTRAIT;
}
if (this.isX && isLandscape) {
if (DeviceTypes.IS_IPHONE_X && isLandscape) {
return IOS_TOP_LANDSCAPE;
} else if (this.isX) {
} else if (DeviceTypes.IS_IPHONE_X) {
return IOSX_TOP_PORTRAIT;
} else if (isLandscape) {
return IOS_TOP_LANDSCAPE + STATUS_BAR_HEIGHT;

View file

@ -9,9 +9,6 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import addReactionIcon from 'assets/images/icons/reaction.png';
@ -34,24 +31,12 @@ export default class Reactions extends PureComponent {
theme: PropTypes.object.isRequired,
canAddReaction: PropTypes.bool,
canRemoveReaction: PropTypes.bool.isRequired,
}
};
static defaultProps = {
position: 'right',
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
componentDidMount() {
const {actions, postId} = this.props;
actions.getReactionsForPost(postId);
@ -67,25 +52,16 @@ export default class Reactions extends PureComponent {
};
showReactionList = () => {
const {navigator, postId, theme} = this.props;
const {formatMessage} = this.context.intl;
const {navigator, postId} = this.props;
const options = {
screen: 'ReactionList',
title: formatMessage({id: 'mobile.reaction_list.title', defaultMessage: 'Reactions'}),
animationType: 'slide-up',
animated: true,
animationType: 'none',
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
navigatorButtons: {
leftButtons: [{
id: 'close-reaction-list',
icon: this.closeButton,
}],
navBarHidden: true,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},
passProps: {
postId,

View file

@ -4,9 +4,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Dimensions, Keyboard, NativeModules, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import SafeArea from 'react-native-safe-area';
import {DeviceTypes} from 'app/constants';
const {StatusBarManager} = NativeModules;
export default class SafeAreaIos extends PureComponent {
@ -31,8 +32,6 @@ export default class SafeAreaIos extends PureComponent {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
if (props.navigator) {
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
@ -40,9 +39,9 @@ export default class SafeAreaIos extends PureComponent {
this.state = {
keyboard: false,
safeAreaInsets: {
top: this.isX ? 44 : 20,
top: DeviceTypes.IS_IPHONE_X ? 44 : 20,
left: 0,
bottom: this.isX ? 34 : 15,
bottom: DeviceTypes.IS_IPHONE_X ? 34 : 15,
right: 0,
},
statusBarHeight: 20,
@ -87,7 +86,7 @@ export default class SafeAreaIos extends PureComponent {
getSafeAreaInsets = () => {
this.getStatusBarHeight();
if (this.isX) {
if (DeviceTypes.IS_IPHONE_X) {
SafeArea.getSafeAreaInsetsForRootView().then((result) => {
const {safeAreaInsets} = result;
@ -130,7 +129,7 @@ export default class SafeAreaIos extends PureComponent {
}
let top = safeAreaInsets.top;
if (forceTop && this.isX && !hideTopBar) {
if (forceTop && DeviceTypes.IS_IPHONE_X && !hideTopBar) {
top = forceTop;
}
@ -174,7 +173,7 @@ export default class SafeAreaIos extends PureComponent {
}
let offset = 0;
if (keyboardOffset && this.isX) {
if (keyboardOffset && DeviceTypes.IS_IPHONE_X) {
offset = keyboardOffset;
}

View file

@ -0,0 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getDimensions} from 'app/selectors/device';
import SlideUpPanel from './slide_up_panel';
function mapStateToProps(state, ownProps) {
const dimensions = getDimensions(state);
return {
containerHeight: ownProps.containerHeight || dimensions.deviceHeight,
};
}
export default connect(mapStateToProps, null, null, {withRef: true})(SlideUpPanel);

View file

@ -0,0 +1,246 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
PanResponder,
Platform,
StyleSheet,
View,
} from 'react-native';
import {DeviceTypes} from 'app/constants';
import SlideUpPanelIndicator from './slide_up_panel_indicator';
const TOP_IOS_MARGIN = DeviceTypes.IS_IPHONE_X ? 84 : 64;
const TOP_ANDROID_MARGIN = 44;
const TOP_MARGIN = Platform.OS === 'ios' ? TOP_IOS_MARGIN : TOP_ANDROID_MARGIN;
const BOTTOM_MARGIN = DeviceTypes.IS_IPHONE_X ? 24 : 0;
const CONTAINER_MARGIN = TOP_MARGIN - 10;
export default class SlideUpPanel extends PureComponent {
static propTypes = {
containerHeight: PropTypes.number,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
headerHeight: PropTypes.number,
initialPosition: PropTypes.number,
marginFromTop: PropTypes.number,
onRequestClose: PropTypes.func,
};
static defaultProps = {
headerHeight: 0,
initialPosition: 0.5,
marginFromTop: TOP_MARGIN,
onRequestClose: () => true,
};
constructor(props) {
super(props);
const initialUsedSpace = Math.abs(props.initialPosition);
const initialPosition = ((props.containerHeight - (props.headerHeight + BOTTOM_MARGIN)) * (1 - initialUsedSpace));
this.mainPanGesture = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isGoingDown = gestureState.y0 < gestureState.dy;
return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown);
},
onPanResponderMove: (evt, gestureState) => {
this.moveStart(gestureState);
},
onPanResponderRelease: (evt, gestureState) => {
this.moveFinished(gestureState);
},
});
this.secondaryPanGesture = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isGoingDown = gestureState.y0 < gestureState.dy;
return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown, true);
},
onPanResponderMove: (evt, gestureState) => {
this.moveStart(gestureState);
},
onPanResponderRelease: (evt, gestureState) => {
this.moveFinished(gestureState);
},
});
this.previousTop = initialPosition;
this.canDrag = true;
this.state = {
position: new Animated.Value(props.containerHeight),
initialPosition,
finalPosition: props.marginFromTop,
endPosition: 0,
};
}
componentDidMount() {
this.startAnimation(this.props.containerHeight, this.state.initialPosition, false, true);
}
handleTouchEnd = () => {
if (!this.isDragging) {
this.startAnimation(this.state.endPosition, this.props.containerHeight, false, true);
}
};
isAValidMovement = (distanceX, distanceY, isGoingDown, forceCheck = false) => {
const {endPosition, finalPosition} = this.state;
if (finalPosition !== endPosition || forceCheck || (isGoingDown && this.canDrag)) {
const moveTravelledFarEnough = Math.abs(distanceY) > Math.abs(distanceX) && Math.abs(distanceY) > 2;
return moveTravelledFarEnough;
}
return false;
};
moveStart = (gestureState) => {
if (this.viewRef && this.backdrop) {
const {endPosition} = this.state;
const position = endPosition - (gestureState.y0 - gestureState.moveY);
this.isDragging = true;
this.backdrop.setNativeProps({pointerEvents: 'none'});
this.updatePosition(position);
}
};
moveFinished = (gestureState) => {
if (this.viewRef) {
const isGoingDown = gestureState.y0 < gestureState.moveY;
let position = gestureState.moveY;
if (this.previousTop !== position) {
position = this.previousTop;
}
this.startAnimation(gestureState.y0, position, isGoingDown);
}
};
setBackdropRef = (ref) => {
this.backdrop = ref;
};
setDrag = (val) => {
this.canDrag = val;
};
setViewRef = (ref) => {
this.viewRef = ref;
};
startAnimation = (initialY, positionY, isGoingDown, initial = false) => {
const {containerHeight, onRequestClose} = this.props;
const {finalPosition, initialPosition} = this.state;
const position = new Animated.Value(initial ? initialY : positionY);
let endPosition = (!isGoingDown && !initial ? finalPosition : positionY);
position.removeAllListeners();
if (isGoingDown) {
if (positionY <= this.state.initialPosition) {
endPosition = initialPosition;
} else {
endPosition = containerHeight;
}
}
Animated.timing(position, {
toValue: endPosition,
duration: initial ? 200 : 100,
useNativeDriver: true,
}).start(() => {
if (this.viewRef && this.backdrop) {
this.setState({endPosition});
this.backdrop.setNativeProps({pointerEvents: 'box-only'});
this.isDragging = false;
if (endPosition === containerHeight) {
onRequestClose();
}
}
});
position.addListener((pos) => {
if (this.viewRef) {
this.updatePosition(pos.value);
}
});
};
updatePosition = (newPosition) => {
const {position} = this.state;
this.previousTop = newPosition;
position.setValue(newPosition);
};
render() {
const {children} = this.props;
const containerPosition = {
top: this.state.position,
};
return (
<View style={styles.viewport}>
<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}
>
<View style={{maxHeight: (this.props.containerHeight - this.props.headerHeight - CONTAINER_MARGIN)}}>
{children}
</View>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
viewport: {
flex: 1,
},
container: {
flex: 1,
backgroundColor: 'white',
...Platform.select({
android: {
borderTopRightRadius: 2,
borderTopLeftRadius: 2,
},
ios: {
borderTopRightRadius: 10,
borderTopLeftRadius: 10,
},
}),
},
backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});

View file

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated, Platform, StyleSheet, View} from 'react-native';
export default class SlideUpPanelIndicator extends PureComponent {
static propTypes = {
containerPosition: PropTypes.object.isRequired,
panHandlers: PropTypes.object.isRequired,
};
render() {
const {panHandlers, containerPosition} = this.props;
if (Platform.OS === 'android') {
return null;
}
return (
<Animated.View
style={[containerPosition, styles.dragIndicatorContainer]}
{...panHandlers}
>
<View style={styles.dragIndicator}/>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
dragIndicatorContainer: {
marginVertical: 10,
alignItems: 'center',
justifyContent: 'center',
},
dragIndicator: {
backgroundColor: 'white',
height: 5,
width: 62.5,
opacity: 0.9,
borderRadius: 25,
},
});

View file

@ -4,9 +4,8 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {ViewTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import Icon from 'react-native-vector-icons/Ionicons';
@ -25,12 +24,6 @@ export default class EmptyToolbar extends PureComponent {
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
}
render() {
const {isLandscape, theme} = this.props;
@ -51,7 +44,7 @@ export default class EmptyToolbar extends PureComponent {
height = IOS_TOP_LANDSCAPE;
}
if (this.isX && isLandscape) {
if (DeviceTypes.IS_IPHONE_X && isLandscape) {
padding.paddingHorizontal = 10;
}
break;

View file

@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import keyMirror from 'mattermost-redux/utils/key_mirror';
import DeviceInfo from 'react-native-device-info';
import RNFetchBlobFS from 'rn-fetch-blob/fs';
import keyMirror from 'mattermost-redux/utils/key_mirror';
const deviceTypes = keyMirror({
CONNECTION_CHANGED: null,
@ -16,5 +17,6 @@ export default {
...deviceTypes,
DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`,
IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`,
IS_IPHONE_X: DeviceInfo.getModel().includes('iPhone X'),
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
};

View file

@ -5,12 +5,13 @@ import DeviceTypes from './device';
import ListTypes from './list';
import NavigationTypes from './navigation';
import PermissionTypes from './permissions';
import ViewTypes from './view';
import ViewTypes, {UpgradeTypes} from './view';
export {
DeviceTypes,
ListTypes,
NavigationTypes,
PermissionTypes,
UpgradeTypes,
ViewTypes,
};

View file

@ -12,7 +12,6 @@ import {
StyleSheet,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -24,7 +23,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout';
import OfflineIndicator from 'app/components/offline_indicator';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {ViewTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostBucket from 'app/mattermost_bucket';
import {preventDoubleTap} from 'app/utils/tap';
import PostTextbox from 'app/components/post_textbox';
@ -75,8 +74,6 @@ export default class Channel extends PureComponent {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
}
@ -170,8 +167,8 @@ export default class Channel extends PureComponent {
if (isLandscape) {
top = IOS_TOP_LANDSCAPE;
} else {
height = this.isX ? (height - IOSX_TOP_PORTRAIT) : (height - IOS_TOP_PORTRAIT);
top = this.isX ? IOSX_TOP_PORTRAIT : IOS_TOP_PORTRAIT;
height = DeviceTypes.IS_IPHONE_X ? (height - IOSX_TOP_PORTRAIT) : (height - IOS_TOP_PORTRAIT);
top = DeviceTypes.IS_IPHONE_X ? IOSX_TOP_PORTRAIT : IOS_TOP_PORTRAIT;
}
break;
}

View file

@ -4,9 +4,8 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {ViewTypes} from 'app/constants';
import {DeviceTypes, ViewTypes} from 'app/constants';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDrawerButton from './channel_drawer_button';
@ -32,12 +31,6 @@ export default class ChannelNavBar extends PureComponent {
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel().includes('iPhone X');
}
render() {
const {isLandscape, navigator, onPress, theme} = this.props;
const {openChannelDrawer, openSettingsDrawer} = this.props;
@ -58,7 +51,7 @@ export default class ChannelNavBar extends PureComponent {
height = IOS_TOP_LANDSCAPE;
}
if (this.isX && isLandscape) {
if (DeviceTypes.IS_IPHONE_X && isLandscape) {
padding.paddingHorizontal = 10;
}
break;

View file

@ -15,7 +15,7 @@ import {intlShape} from 'react-intl';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import {UpgradeTypes} from 'app/constants/view';
import {UpgradeTypes} from 'app/constants';
import logo from 'assets/images/logo.png';
import checkUpgradeType from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';

View file

@ -4,8 +4,10 @@ exports[`ReactionHeader should match snapshot 1`] = `
<Component
style={
Object {
"backgroundColor": "#ffffff",
"height": 37,
"backgroundColor": "#FFFFFF",
"borderTopLeftRadius": 10,
"borderTopRightRadius": 10,
"height": 36.5,
"paddingHorizontal": 0,
}
}

View file

@ -18,7 +18,7 @@ exports[`ReactionHeaderItem should match snapshot 1`] = `
},
Object {
"borderBottomWidth": 2,
"borderColor": "#ffffff",
"borderColor": "rgba(61,60,64,0.2)",
},
false,
]
@ -33,7 +33,7 @@ exports[`ReactionHeaderItem should match snapshot 1`] = `
<Component
style={
Object {
"color": "#2389d7",
"color": "#166de0",
"fontSize": 16,
"marginLeft": 4,
}
@ -55,7 +55,7 @@ exports[`ReactionHeaderItem should match snapshot, renderContent 1`] = `
<Component
style={
Object {
"color": "#2389d7",
"color": "#166de0",
"fontSize": 16,
"marginLeft": 4,
}
@ -73,7 +73,7 @@ exports[`ReactionHeaderItem should match snapshot, renderContent 2`] = `
id="mobile.reaction_header.all_emojis"
style={
Object {
"color": "#2389d7",
"color": "#166de0",
"fontSize": 16,
"marginLeft": 4,
}
@ -82,7 +82,7 @@ exports[`ReactionHeaderItem should match snapshot, renderContent 2`] = `
<Component
style={
Object {
"color": "#2389d7",
"color": "#166de0",
"fontSize": 16,
"marginLeft": 4,
}

View file

@ -4,237 +4,237 @@ exports[`ReactionList should match snapshot 1`] = `
<Component
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Component
style={
Object {
"borderBottomWidth": 1,
"borderColor": "rgba(61,60,64,0.2)",
"height": 38,
}
}
<Connect(SlideUpPanel)
headerHeight={37.5}
initialPosition={0.55}
onRequestClose={[Function]}
>
<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",
},
],
},
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",
}
}
/>
</Component>
<KeyboardAwareScrollView
bounces={true}
enableAutomaticScroll={true}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraHeight={75}
extraScrollHeight={0}
innerRef={[Function]}
keyboardOpeningTime={250}
viewIsInsideTabBar={false}
>
<Component
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",
}
}
/>
<React.Fragment>
<Component
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
"borderBottomWidth": 1,
"borderColor": "rgba(61,60,64,0.2)",
"height": 37.5,
}
}
/>
</Component>
<Component
style={
Object {
"height": 45,
"justifyContent": "center",
}
}
>
<ReactionRow
emojiName="smile"
navigator={
Object {
"setOnNavigatorEvent": [MockFunction] {
"calls": Array [
Array [
[Function],
>
<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",
},
],
],
"results": Array [
Object {
"isThrow": false,
"value": undefined,
},
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",
}
}
/>
</Component>
<ScrollView
bounce={false}
onScroll={[Function]}
>
<Component
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",
}
}
/>
<Component
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</Component>
<Component
style={
Object {
"height": 45,
"justifyContent": "center",
}
}
}
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",
}
}
/>
<Component
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</Component>
</KeyboardAwareScrollView>
>
<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",
}
}
/>
<Component
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"height": 1,
}
}
/>
</Component>
</ScrollView>
</React.Fragment>
</Connect(SlideUpPanel)>
</Component>
`;

View file

@ -5,7 +5,6 @@ exports[`ReactionRow should match snapshot, renderContent 1`] = `
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"flexDirection": "row",
"height": 44,
"justifyContent": "flex-start",
@ -53,7 +52,6 @@ exports[`ReactionRow should match snapshot, renderContent 1`] = `
<Component
style={
Object {
"color": "#3d3c40",
"fontSize": 14,
"paddingRight": 5,
}
@ -67,7 +65,7 @@ exports[`ReactionRow should match snapshot, renderContent 1`] = `
<Component
style={
Object {
"color": "rgba(61,60,64,0.5)",
"color": "rgba(0,0,0,0.5)",
"fontSize": 14,
}
}

View file

@ -4,12 +4,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
ScrollView,
StyleSheet,
View,
} from 'react-native';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ReactionHeaderItem from './reaction_header_item';
export default class ReactionHeader extends PureComponent {
@ -18,7 +18,7 @@ export default class ReactionHeader extends PureComponent {
onSelectReaction: PropTypes.func.isRequired,
reactions: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
}
};
handleOnPress = (emoji) => {
this.props.onSelectReaction(emoji);
@ -37,12 +37,9 @@ export default class ReactionHeader extends PureComponent {
theme={theme}
/>
));
}
};
render() {
const {theme} = this.props;
const styles = getStyleSheet(theme);
return (
<View style={styles.container}>
<ScrollView
@ -57,12 +54,20 @@ export default class ReactionHeader extends PureComponent {
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
height: 37,
paddingHorizontal: 0,
},
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
height: 36.5,
paddingHorizontal: 0,
...Platform.select({
android: {
borderTopRightRadius: 2,
borderTopLeftRadius: 2,
},
ios: {
borderTopRightRadius: 10,
borderTopLeftRadius: 10,
},
}),
},
});

View file

@ -22,12 +22,12 @@ export default class ReactionHeaderItem extends PureComponent {
highlight: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
}
};
handleOnPress = () => {
const {emojiName, highlight, onPress} = this.props;
onPress(emojiName, highlight);
}
};
renderContent = () => {
const {count, emojiName, theme} = this.props;
@ -56,7 +56,7 @@ export default class ReactionHeaderItem extends PureComponent {
<Text style={styles.text}>{count}</Text>
</React.Fragment>
);
}
};
render() {
const {emojiName, highlight, theme} = this.props;
@ -79,16 +79,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginLeft: 7,
},
text: {
color: theme.linkColor,
color: theme.buttonBg,
marginLeft: 4,
fontSize: 16,
},
highlight: {
borderColor: changeOpacity(theme.linkColor, 1),
borderColor: theme.buttonBg,
borderBottomWidth: 2,
},
regular: {
borderColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 2,
},
reaction: {

View file

@ -3,10 +3,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {ScrollView, View} from 'react-native';
import {intlShape} from 'react-intl';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import SlideUpPanel from 'app/components/slide_up_panel';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {
generateUserProfilesById,
@ -44,7 +44,9 @@ export default class ReactionList extends PureComponent {
const reactionsByName = getReactionsByName(reactions);
this.contentOffsetY = -1;
this.state = {
canDrag: true,
allUserIds: getUniqueUserIds(reactions),
reactions,
reactionsByName,
@ -98,13 +100,17 @@ export default class ReactionList extends PureComponent {
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close-reaction-list') {
this.props.navigator.dismissModal({
animationType: 'slide-down',
});
this.close();
}
}
};
close = () => {
this.props.navigator.dismissModal({
animationType: 'none',
});
};
getMissingProfiles = () => {
const {allUserIds, userProfiles, userProfilesById} = this.state;
if (userProfiles.length !== allUserIds.length) {
@ -116,13 +122,37 @@ export default class ReactionList extends PureComponent {
}
}
scrollViewRef = (ref) => {
this.scrollView = ref;
};
handleOnSelectReaction = (emoji) => {
this.setState({selected: emoji});
}
const slide = this.slideUpPanel?.getWrappedInstance();
if (slide) {
slide.setDrag(true);
}
if (this.scrollView) {
this.scrollView.scrollTo({x: 0, y: 0, animated: false});
}
};
handleScroll = (e) => {
const pageOffsetY = e.nativeEvent.contentOffset.y;
const canDrag = pageOffsetY <= 0;
const slide = this.slideUpPanel?.getWrappedInstance();
this.contentOffsetY = pageOffsetY;
if (slide) {
slide.setDrag(canDrag);
}
};
refSlideUpPanel = (r) => {
this.slideUpPanel = r;
};
refScrollView = (ref) => {
this.scrollView = ref;
};
renderReactionRows = () => {
const {
@ -154,7 +184,7 @@ export default class ReactionList extends PureComponent {
<View style={style.separator}/>
</View>
));
}
};
render() {
const {
@ -168,20 +198,30 @@ export default class ReactionList extends PureComponent {
return (
<View style={style.flex}>
<View style={style.headerContainer}>
<ReactionHeader
selected={selected}
onSelectReaction={this.handleOnSelectReaction}
reactions={sortedReactionsForHeader}
theme={theme}
/>
</View>
<KeyboardAwareScrollView
bounces={true}
innerRef={this.scrollViewRef}
<SlideUpPanel
ref={this.refSlideUpPanel}
onRequestClose={this.close}
initialPosition={0.55}
headerHeight={37.5}
>
{this.renderReactionRows()}
</KeyboardAwareScrollView>
<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>
</SlideUpPanel>
</View>
);
}
@ -190,11 +230,10 @@ export default class ReactionList extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
flex: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
headerContainer: {
height: 38,
height: 37.5,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 1,
},

View file

@ -2,10 +2,11 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import Preferences from 'mattermost-redux/constants/preferences';
import SlideUpPanel from 'app/components/slide_up_panel';
import ReactionList from './reaction_list';
jest.mock('react-intl');
@ -30,7 +31,7 @@ describe('ReactionList', () => {
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find(KeyboardAwareScrollView).exists()).toEqual(true);
expect(wrapper.find(SlideUpPanel).exists()).toEqual(true);
});
test('should match snapshot, renderReactionRows', () => {

View file

@ -7,6 +7,7 @@ import {intlShape} from 'react-intl';
import {
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
@ -14,7 +15,7 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ProfilePicture from 'app/components/profile_picture';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {changeOpacity} from 'app/utils/theme';
import Emoji from 'app/components/emoji';
@ -29,7 +30,7 @@ export default class ReactionRow extends React.PureComponent {
static defaultProps = {
user: {},
}
};
static contextTypes = {
intl: intlShape,
@ -62,7 +63,6 @@ export default class ReactionRow extends React.PureComponent {
const {
emojiName,
teammateNameDisplay,
theme,
user,
} = this.props;
@ -71,7 +71,6 @@ export default class ReactionRow extends React.PureComponent {
}
const {id, username} = user;
const style = getStyleFromTheme(theme);
const usernameDisplay = '@' + username;
return (
@ -114,40 +113,36 @@ export default class ReactionRow extends React.PureComponent {
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
justifyContent: 'flex-start',
height: 44,
width: '100%',
alignItems: 'center',
},
profileContainer: {
alignItems: 'center',
width: '13%',
},
profile: {
paddingTop: 3,
},
textContainer: {
width: '74%',
flexDirection: 'row',
},
username: {
fontSize: 14,
color: theme.centerChannelColor,
paddingRight: 5,
},
displayName: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
emoji: {
alignItems: 'center',
width: '13%',
justifyContent: 'center',
},
};
const style = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'flex-start',
height: 44,
width: '100%',
alignItems: 'center',
},
profileContainer: {
alignItems: 'center',
width: '13%',
},
profile: {
paddingTop: 3,
},
textContainer: {
width: '74%',
flexDirection: 'row',
},
username: {
fontSize: 14,
paddingRight: 5,
},
displayName: {
fontSize: 14,
color: changeOpacity('#000', 0.5),
},
emoji: {
alignItems: 'center',
width: '13%',
justifyContent: 'center',
},
});

View file

@ -13,7 +13,6 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import IonIcon from 'react-native-vector-icons/Ionicons';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
@ -32,7 +31,7 @@ import PostSeparator from 'app/components/post_separator';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {ListTypes} from 'app/constants';
import {DeviceTypes, ListTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -91,7 +90,7 @@ export default class Search extends PureComponent {
super(props);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.isX = DeviceInfo.getModel().includes('iPhone X');
this.contentOffsetY = 0;
this.state = {
channelName: '',
@ -736,7 +735,7 @@ export default class Search extends PureComponent {
return (
<SafeAreaView
excludeHeader={isLandscape && this.isX}
excludeHeader={isLandscape && DeviceTypes.IS_IPHONE_X}
forceTop={44}
>
<KeyboardLayout>

View file

@ -25,7 +25,7 @@ import {Client4} from 'mattermost-redux/client';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import {UpgradeTypes} from 'app/constants/view';
import {UpgradeTypes} from 'app/constants';
import fetchConfig from 'app/fetch_preconfig';
import mattermostBucket from 'app/mattermost_bucket';
import PushNotifications from 'app/push_notifications';

View file

@ -8,13 +8,6 @@ import assert from 'assert';
import {ViewTypes} from 'app/constants';
import {messageRetention} from 'app/store/middleware';
jest.mock('react-native-device-info', () => {
return {
getVersion: () => '0.0.0',
getBuildNumber: () => '0',
};
});
describe('store/middleware', () => {
describe('messageRetention', () => {
describe('should chain the same incoming action type', () => {

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import semver from 'semver';
import {UpgradeTypes} from 'app/constants/view';
import {UpgradeTypes} from 'app/constants';
import LocalConfig from 'assets/config';

View file

@ -17,6 +17,14 @@ jest.mock('NativeModules', () => {
});
jest.mock('NativeEventEmitter');
jest.mock('react-native-device-info', () => {
return {
getVersion: () => '0.0.0',
getBuildNumber: () => '0',
getModel: () => 'iPhone X',
};
});
let logs;
let warns;
let errors;