Make layout compatible with iPhone X (#1205)

* Make layout compatible with iPhone X

* feedback review
This commit is contained in:
enahum 2017-11-28 09:04:53 -03:00 committed by GitHub
parent 3760ba0b61
commit 87261e9a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 812 additions and 424 deletions

View file

@ -1511,3 +1511,36 @@ 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-safe-area
This product contains 'react-native-safe-area', React Native module to retrieve safe area insets for iOS 11 or later.
* HOMEPAGE:
* https://github.com/miyabi/react-native-safe-area
* LICENSE:
MIT License
Copyright (c) 2017 Masayuki Iwai
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.

View file

@ -11,6 +11,7 @@ import {
StyleSheet,
View
} from 'react-native';
import SafeAreaView from 'app/components/safe_area_view';
import Drawer from 'app/components/drawer';
import {alertErrorWithFallback} from 'app/utils/general';
@ -362,14 +363,20 @@ export default class ChannelDrawer extends Component {
);
return (
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
<SafeAreaView
backgroundColor={theme.sidebarHeaderBg}
navigator={navigator}
theme={theme}
>
{lists}
</DrawerSwiper>
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
>
{lists}
</DrawerSwiper>
</SafeAreaView>
);
};

View file

@ -34,7 +34,7 @@ class ChannelsList extends React.PureComponent {
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: ''
@ -192,12 +192,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
backgroundColor: theme.sidebarHeaderBg
},
headerContainer: {
alignItems: 'center',

View file

@ -148,12 +148,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
backgroundColor: theme.sidebarHeaderBg
},
headerContainer: {
alignItems: 'center',

View file

@ -10,7 +10,9 @@ import {
TouchableOpacity,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
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';
@ -21,7 +23,7 @@ const {View: AnimatedView} = Animated;
const UPDATE_TIMEOUT = 60000;
class ClientUpgradeListener extends PureComponent {
export default class ClientUpgradeListener extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
@ -30,7 +32,7 @@ class ClientUpgradeListener extends PureComponent {
currentVersion: PropTypes.string,
downloadLink: PropTypes.string,
forceUpgrade: PropTypes.bool,
intl: intlShape.isRequired,
isLandscape: PropTypes.bool,
lastUpgradeCheck: PropTypes.number,
latestVersion: PropTypes.string,
minVersion: PropTypes.string,
@ -38,14 +40,28 @@ class ClientUpgradeListener extends PureComponent {
theme: PropTypes.object.isRequired
};
state = {
top: new Animated.Value(-100)
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
this.state = {
top: new Animated.Value(-100)
};
}
componentDidMount() {
const {forceUpgrade, lastUpgradeCheck, latestVersion, minVersion} = this.props;
const {forceUpgrade, isLandscape, lastUpgradeCheck, latestVersion, minVersion} = this.props;
if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) {
this.checkUpgrade(minVersion, latestVersion);
this.checkUpgrade(minVersion, latestVersion, isLandscape);
}
}
@ -55,40 +71,54 @@ class ClientUpgradeListener extends PureComponent {
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion);
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
} else if (this.props.isLandscape !== nextProps.isLandscape &&
this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && this.isX) {
const newTop = nextProps.isLandscape ? 45 : 100;
this.setState({top: new Animated.Value(newTop)});
}
}
checkUpgrade = (minVersion, latestVersion) => {
checkUpgrade = (minVersion, latestVersion, isLandscape) => {
const {actions, currentVersion} = this.props;
const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError);
this.setState({upgradeType});
if (upgradeType === UpgradeTypes.NO_UPGRADE) {
return;
}
this.setState({upgradeType});
setTimeout(this.toggleUpgradeMessage, 500);
setTimeout(() => {
this.toggleUpgradeMessage(true, isLandscape);
}, 500);
actions.setLastUpgradeCheck();
}
};
toggleUpgradeMessage = (show = true) => {
const toValue = show ? 75 : -100;
toggleUpgradeMessage = (show = true, isLandscape) => {
let toValue = -100;
if (show) {
if (this.isX && isLandscape) {
toValue = 45;
} else {
toValue = this.isX ? 100 : 75;
}
}
Animated.timing(this.state.top, {
toValue,
duration: 300
}).start();
}
};
handleDismiss = () => {
this.toggleUpgradeMessage(false);
}
};
handleDownload = () => {
const {downloadLink, intl} = this.props;
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
@ -110,17 +140,25 @@ class ClientUpgradeListener extends PureComponent {
});
this.toggleUpgradeMessage(false);
}
};
handleLearnMore = () => {
this.props.navigator.dismissAllModals({animationType: 'none'});
const {intl} = this.context;
this.props.navigator.dismissModal({animationType: 'none'});
this.props.navigator.showModal({
screen: 'ClientUpgrade',
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}),
navigatorStyle: {
navBarHidden: true,
statusBarHidden: true,
statusBarHideWithNavBar: true
navBarHidden: false,
statusBarHidden: false,
statusBarHideWithNavBar: false
},
navigatorButtons: {
leftButtons: [{
id: 'close-upgrade',
icon: this.closeButton
}]
},
passProps: {
upgradeType: this.state.upgradeType
@ -128,9 +166,13 @@ class ClientUpgradeListener extends PureComponent {
});
this.toggleUpgradeMessage(false);
}
};
render() {
if (this.state.upgradeType === UpgradeTypes.NO_UPGRADE) {
return null;
}
const {forceUpgrade, theme} = this.props;
const styles = getStyleSheet(theme);
@ -227,5 +269,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
};
});
export default injectIntl(ClientUpgradeListener);

View file

@ -5,6 +5,7 @@ import {logError} from 'mattermost-redux/actions/errors';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ClientUpgradeListener from './client_upgrade_listener';
@ -16,6 +17,7 @@ function mapStateToProps(state) {
currentVersion,
downloadLink,
forceUpgrade,
isLandscape: isLandscape(state),
lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck,
latestVersion,
minVersion,

View file

@ -13,11 +13,13 @@ 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';
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 {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@ -45,7 +47,7 @@ export default class EmojiPicker extends PureComponent {
static contextTypes = {
intl: intlShape.isRequired
}
};
constructor(props) {
super(props);
@ -60,6 +62,7 @@ export default class EmojiPicker extends PureComponent {
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
this.isX = DeviceInfo.getModel() === 'iPhone X';
this.state = {
emojis,
emojiSectionIndexByOffset,
@ -74,6 +77,10 @@ export default class EmojiPicker extends PureComponent {
this.setState({
emojis: this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth)
});
if (this.refs.search_bar) {
this.refs.search_bar.blur();
}
}
}
@ -116,7 +123,7 @@ export default class EmojiPicker extends PureComponent {
});
return nextEmojis;
}
};
measureEmojiSections = (emojiSections) => {
let lastOffset = 0;
@ -127,7 +134,7 @@ export default class EmojiPicker extends PureComponent {
return start;
});
}
};
changeSearchTerm = (text) => {
this.setState({
@ -149,11 +156,11 @@ export default class EmojiPicker extends PureComponent {
filteredEmojis: [],
searchTerm: ''
});
}
};
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
}
};
searchEmojis = (searchTerm) => {
const {emojisByName} = this.props;
@ -174,11 +181,11 @@ export default class EmojiPicker extends PureComponent {
});
return [...startsWith.sort(), ...includes.sort()];
}
};
getNumberOfColumns = (deviceWidth) => {
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2)))));
}
};
renderItem = ({item}) => {
return (
@ -190,7 +197,7 @@ export default class EmojiPicker extends PureComponent {
onEmojiPress={this.props.onEmojiPress}
/>
);
}
};
flatListKeyExtractor = (item) => item;
@ -234,7 +241,7 @@ export default class EmojiPicker extends PureComponent {
currentSectionIndex: nextIndex
});
}
}
};
onMomentumScrollEnd = () => {
if (this.state.jumpToSection) {
@ -242,7 +249,7 @@ export default class EmojiPicker extends PureComponent {
jumpToSection: false
});
}
}
};
scrollToSection = (index) => {
this.setState({
@ -255,7 +262,7 @@ export default class EmojiPicker extends PureComponent {
viewOffset: 25
});
});
}
};
renderSectionHeader = ({section}) => {
const {theme} = this.props;
@ -273,7 +280,7 @@ export default class EmojiPicker extends PureComponent {
/>
</View>
);
}
};
renderSectionIcons = () => {
const {theme} = this.props;
@ -296,13 +303,11 @@ export default class EmojiPicker extends PureComponent {
</TouchableOpacity>
);
});
}
sectionListKeyExtractor = (item) => item.key
};
attachSectionList = (c) => {
this.sectionList = c;
}
};
render() {
const {deviceWidth, isLandscape, theme} = this.props;
@ -346,48 +351,55 @@ export default class EmojiPicker extends PureComponent {
let keyboardOffset = 64;
if (Platform.OS === 'android') {
keyboardOffset = -200;
} else if (this.isX) {
keyboardOffset = isLandscape ? 35 : 107;
} else if (isLandscape) {
keyboardOffset = 51;
keyboardOffset = 52;
}
return (
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={keyboardOffset}
<SafeAreaView
excludeHeader={true}
theme={theme}
>
<View style={styles.searchBar}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
value={searchTerm}
/>
</View>
<View style={styles.container}>
{listComponent}
{!searchTerm &&
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={keyboardOffset}
>
<View style={styles.searchBar}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
value={searchTerm}
/>
</View>
<View style={styles.container}>
{listComponent}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
}
</View>
</KeyboardAvoidingView>
}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
@ -407,6 +419,7 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
left: 0,
right: 0,
height: 35,
width: '100%',
backgroundColor: 'white'
},
columnStyle: {

View file

@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {init as initWebSocket} from 'mattermost-redux/actions/websocket';
import {connection} from 'app/actions/device';
import {getConnection} from 'app/selectors/device';
import {getConnection, isLandscape} from 'app/selectors/device';
import OfflineIndicator from './offline_indicator';
@ -18,6 +18,7 @@ function mapStateToProps(state) {
return {
isConnecting,
isLandscape: isLandscape(state),
isOnline: getConnection(state),
webSocketStatus
};

View file

@ -11,6 +11,7 @@ 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';
@ -19,8 +20,6 @@ import checkNetwork from 'app/utils/network';
import {RequestStatus} from 'mattermost-redux/constants';
const HEIGHT = 38;
const NAVBAR = Platform.OS === 'ios' ? 64 : 46;
const INITIAL_TOP = NAVBAR - HEIGHT;
const OFFLINE = 'offline';
const CONNECTING = 'connecting';
const CONNECTED = 'connected';
@ -32,6 +31,7 @@ export default class OfflineIndicator extends Component {
initWebSocket: PropTypes.func.isRequired
}).isRequired,
isConnecting: PropTypes.bool,
isLandscape: PropTypes.bool,
isOnline: PropTypes.bool,
webSocketStatus: PropTypes.string
};
@ -43,16 +43,27 @@ export default class OfflineIndicator extends Component {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
const navBar = this.getNavBarHeight(props.isLandscape);
this.state = {
network: null,
top: new Animated.Value(INITIAL_TOP)
navBar,
top: new Animated.Value(navBar - HEIGHT)
};
this.backgroundColor = new Animated.Value(0);
}
componentWillReceiveProps(nextProps) {
const {webSocketStatus} = this.props;
const {isLandscape, webSocketStatus} = this.props;
if (nextProps.isLandscape !== isLandscape && this.state.network) {
const navBar = this.getNavBarHeight(nextProps.isLandscape);
const top = new Animated.Value(navBar - HEIGHT);
this.setState({navBar, top});
}
if (nextProps.isOnline) {
if (this.state.network && webSocketStatus === RequestStatus.STARTED && nextProps.webSocketStatus === RequestStatus.SUCCESS) {
// Show the connected animation only if we had a previous network status
@ -67,7 +78,7 @@ export default class OfflineIndicator extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.network !== this.state.network && nextState.network;
return (nextState.network !== this.state.network || nextProps.isLandscape !== this.props.isLandscape);
}
connect = () => {
@ -94,7 +105,7 @@ export default class OfflineIndicator extends Component {
),
Animated.timing(
this.state.top, {
toValue: INITIAL_TOP,
toValue: (this.state.navBar - HEIGHT),
duration: 300,
delay: 500
}
@ -114,6 +125,23 @@ export default class OfflineIndicator extends Component {
});
};
getNavBarHeight = (isLandscape) => {
let navBar = 46;
if (Platform.OS === 'ios') {
if (this.isX && isLandscape) {
navBar = 32;
} else if (this.isX) {
navBar = 88;
} else if (isLandscape) {
navBar = 52;
} else {
navBar = 64;
}
}
return navBar;
};
offline = () => {
this.setState({network: OFFLINE}, () => {
this.show();
@ -123,7 +151,7 @@ export default class OfflineIndicator extends Component {
show = () => {
Animated.timing(
this.state.top, {
toValue: NAVBAR,
toValue: this.state.navBar,
duration: 300
}
).start();

View file

@ -0,0 +1,5 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import SafeAreaView from './safe_area_view';
export default SafeAreaView;

View file

@ -0,0 +1,26 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, View} from 'react-native';
export default class SafeAreaAndroid extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired
};
render() {
return (
<View style={style.container}>
{this.props.children}
</View>
);
}
}
const style = StyleSheet.create({
container: {
flex: 1
}
});

View file

@ -0,0 +1,131 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Keyboard, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import SafeArea from 'react-native-safe-area';
import Orientation from 'react-native-orientation';
export default class SafeAreaIos extends PureComponent {
static propTypes = {
backgroundColor: PropTypes.string,
children: PropTypes.node.isRequired,
excludeHeader: PropTypes.bool,
forceTop: PropTypes.number,
keyboardOffset: PropTypes.number.isRequired,
navBarBackgroundColor: PropTypes.string,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
static defaultProps = {
keyboardOffset: 0
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
if (props.navigator) {
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
this.state = {
keyboard: false,
safeAreaInsets: {
top: 20, left: 0, bottom: 15, right: 0
}
};
}
componentWillMount() {
this.getSafeAreaInsets();
}
componentDidMount() {
Orientation.addOrientationListener(this.getSafeAreaInsets);
this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
Orientation.removeOrientationListener(this.getSafeAreaInsets);
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
getSafeAreaInsets = () => {
if (this.isX) {
SafeArea.getSafeAreaInsetsForRootView().then((result) => {
const {safeAreaInsets} = result;
this.setState({safeAreaInsets});
});
}
};
keyboardWillHide = () => {
this.setState({keyboard: false});
};
keyboardWillShow = () => {
this.setState({keyboard: true});
};
onNavigatorEvent = (event) => {
switch (event.id) {
case 'willAppear':
case 'didDisappear':
this.getSafeAreaInsets();
break;
}
};
render() {
const {backgroundColor, children, excludeHeader, forceTop, keyboardOffset, navBarBackgroundColor, theme} = this.props;
const {keyboard, safeAreaInsets} = this.state;
let bgColor = theme.centerChannelBg;
if (backgroundColor) {
bgColor = backgroundColor;
}
let topColor = theme.sidebarHeaderBg;
if (navBarBackgroundColor) {
topColor = navBarBackgroundColor;
}
let offset = 0;
if (keyboardOffset && this.isX) {
offset = keyboardOffset;
}
let top = safeAreaInsets.top;
if (forceTop && this.isX && !excludeHeader) {
top = forceTop;
}
return (
<View
style={{
flex: 1,
paddingBottom: keyboard ? offset : safeAreaInsets.bottom - 15,
backgroundColor: bgColor
}}
>
{!excludeHeader &&
<View
style={{
backgroundColor: topColor,
paddingTop: top,
zIndex: 10
}}
/>
}
{children}
</View>
);
}
}

View file

@ -134,6 +134,8 @@ export default class Search extends Component {
blur = () => {
this.refs.input_keyword.getNode().blur();
this.setState({expanded: false});
this.collapseAnimation();
};
focus = () => {

View file

@ -410,13 +410,14 @@ export default class Mattermost {
this.configurePushNotifications();
}
Orientation.getOrientation((orientation) => {
this.orientationDidChange(orientation);
});
if (state.views.root.hydrationComplete) {
this.unsubscribeFromStore();
const orientation = Orientation.getInitialOrientation();
if (orientation) {
this.orientationDidChange(orientation);
}
const isNotActive = AppState.currentState !== 'active';
const notification = PushNotifications.getNotification();
if (notification) {

View file

@ -8,7 +8,6 @@ import {
Platform,
View
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ClientUpgradeListener from 'app/components/client_upgrade_listener';
@ -18,6 +17,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import OfflineIndicator from 'app/components/offline_indicator';
import PostListRetry from 'app/components/post_list_retry';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@ -26,10 +26,8 @@ import networkConnectionListener from 'app/utils/network';
import LocalConfig from 'assets/config';
import ChannelDrawerButton from './channel_drawer_button';
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
import ChannelSearchButton from './channel_search_button';
import ChannelTitle from './channel_title';
class Channel extends PureComponent {
static propTypes = {
@ -184,33 +182,32 @@ class Channel extends PureComponent {
intl={intl}
navigator={navigator}
>
<StatusBar/>
<View>
<OfflineIndicator/>
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle onPress={this.goToChannelInfo}/>
<ChannelSearchButton
navigator={navigator}
theme={theme}
/>
</View>
</View>
<KeyboardLayout
behavior='padding'
style={style.keyboardLayout}
<SafeAreaView
navigator={navigator}
theme={theme}
>
<View style={style.postList}>
<ChannelPostList navigator={navigator}/>
</View>
<ChannelLoader theme={theme}/>
<PostTextbox
ref={this.attachPostTextbox}
<StatusBar/>
<OfflineIndicator/>
<ChannelNavBar
navigator={navigator}
onPress={this.goToChannelInfo}
/>
<ChannelLoader theme={theme}/>
</KeyboardLayout>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
<KeyboardLayout
behavior='padding'
style={style.keyboardLayout}
>
<View style={style.postList}>
<ChannelPostList navigator={navigator}/>
</View>
<ChannelLoader theme={theme}/>
<PostTextbox
ref={this.attachPostTextbox}
navigator={navigator}
/>
<ChannelLoader theme={theme}/>
</KeyboardLayout>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
</ChannelDrawer>
);
}
@ -218,22 +215,6 @@ class Channel extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
header: {
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
justifyContent: 'flex-start',
width: '100%',
zIndex: 10,
...Platform.select({
android: {
height: 46
},
ios: {
height: 64,
paddingTop: 20
}
})
},
postList: {
flex: 1
},
@ -244,7 +225,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
keyboardLayout: {
backgroundColor: theme.centerChannelBg,
flex: 1,
zIndex: -1,
paddingBottom: 0
}
};

View file

@ -0,0 +1,69 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDrawerButton from './channel_drawer_button';
import ChannelSearchButton from './channel_search_button';
import ChannelTitle from './channel_title';
export default class ChannelNavBar extends PureComponent {
static propTypes = {
isLandscape: PropTypes.bool.isRequired,
navigator: PropTypes.object.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
}
render() {
const {isLandscape, navigator, theme, onPress} = this.props;
const style = getStyleFromTheme(theme);
const padding = {paddingHorizontal: 0};
let height = 46;
if (Platform.OS === 'ios') {
height = 44;
if (isLandscape) {
height = 32;
}
if (this.isX && isLandscape) {
padding.paddingHorizontal = 10;
}
}
return (
<View style={[style.header, padding, {height}]}>
<ChannelDrawerButton/>
<ChannelTitle onPress={onPress}/>
<ChannelSearchButton
navigator={navigator}
theme={theme}
/>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
header: {
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
justifyContent: 'flex-start',
width: '100%',
zIndex: 10
}
};
});

View file

@ -0,0 +1,18 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelNavBar from './channel_nav_bar';
function mapStateToProps(state) {
return {
isLandscape: isLandscape(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelNavBar);

View file

@ -11,7 +11,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape} from 'react-intl';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
@ -20,7 +20,7 @@ import logo from 'assets/images/logo.png';
import checkUpgradeType from 'app/utils/client_upgrade';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
class ClientUpgrade extends PureComponent {
export default class ClientUpgrade extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
@ -31,15 +31,23 @@ class ClientUpgrade extends PureComponent {
userCheckedForUpgrade: PropTypes.bool,
downloadLink: PropTypes.string.isRequired,
forceUpgrade: PropTypes.bool,
intl: intlShape.isRequired,
latestVersion: PropTypes.string,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
upgradeType: PropTypes.string
};
state = {
upgradeType: UpgradeTypes.NO_UPGRADE
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.state = {
upgradeType: UpgradeTypes.NO_UPGRADE
};
}
componentDidMount() {
@ -77,10 +85,11 @@ class ClientUpgrade extends PureComponent {
} else {
this.props.navigator.dismissModal();
}
}
};
handleDownload = () => {
const {downloadLink, intl} = this.props;
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
@ -100,17 +109,17 @@ class ClientUpgrade extends PureComponent {
return false;
});
}
};
handleUpgrade = () => {
const {actions, downloadLink} = this.props;
try {
Linking.openURL(downloadLink);
} catch (error) {
actions.logError(error);
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close-upgrade') {
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
}
}
}
};
renderMustUpgrade() {
const {theme} = this.props;
@ -318,5 +327,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
}
};
});
export default injectIntl(ClientUpgrade);

View file

@ -26,6 +26,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {DeviceTypes} from 'app/constants/';
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from 'app/components/file_attachment_list/file_attachment_document';
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import SafeAreaView from 'app/components/safe_area_view';
import {NavigationTypes} from 'app/constants';
import {emptyFunction} from 'app/utils/general';
@ -476,116 +477,156 @@ export default class ImagePreview extends PureComponent {
};
render() {
const marginStyle = {
...Platform.select({
ios: {
marginTop: this.props.statusBarHeight
},
android: {
marginTop: 10
}
})
};
return (
<View style={[style.wrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth}]}>
<AnimatedView
style={[this.state.drag.getLayout(), {opacity: this.state.wrapperViewOpacity}]}
{...this.mainViewPanResponder.panHandlers}
>
<ScrollView
ref={this.attachScrollView}
style={[style.ScrollView]}
contentContainerStyle={style.scrollViewContent}
scrollEnabled={!this.state.isZooming}
horizontal={true}
pagingEnabled={!this.state.isZooming}
bounces={false}
onScroll={this.handleScroll}
onMomentumScrollEnd={this.handleScrollStopped}
scrollEventThrottle={2}
>
{this.state.files.map((file, index) => {
let mime = file.mime_type;
if (mime && mime.includes(';')) {
mime = mime.split(';')[0];
}
let component;
if (file.has_preview_image || file.mime_type === 'image/gif') {
component = this.renderPreviewer(file, index);
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
component = this.renderAttachmentDocument(file);
} else if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
component = this.renderVideoPreview(file);
} else {
component = this.renderAttachmentIcon(file);
}
return (
<View
key={file.id}
style={[style.pageWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth}]}
>
{component}
</View>
);
})}
</ScrollView>
<SafeAreaView
backgroundColor='#000'
theme={this.props.theme}
navBarBackgroundColor='#000'
>
<View style={[style.wrapper]}>
<AnimatedView
style={[style.footerHeaderWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}
pointerEvents='box-none'
style={[this.state.drag.getLayout(), {opacity: this.state.wrapperViewOpacity}]}
{...this.mainViewPanResponder.panHandlers}
>
<View style={style.header}>
<View style={[style.headerControls, marginStyle]}>
<TouchableOpacity
onPress={this.handleClose}
style={style.headerIcon}
>
<Icon
name='md-close'
size={26}
color='#fff'
/>
</TouchableOpacity>
<Text style={style.title}>
{`${this.state.currentFile + 1}/${this.state.files.length}`}
</Text>
{this.renderDownloadButton()}
</View>
</View>
<LinearGradient
style={style.footer}
start={{x: 0.0, y: 0.0}}
end={{x: 0.0, y: 0.9}}
colors={['transparent', '#000000']}
pointerEvents='none'
<ScrollView
ref={this.attachScrollView}
style={[style.ScrollView]}
contentContainerStyle={style.scrollViewContent}
scrollEnabled={!this.state.isZooming}
horizontal={true}
pagingEnabled={!this.state.isZooming}
bounces={false}
onScroll={this.handleScroll}
onMomentumScrollEnd={this.handleScrollStopped}
scrollEventThrottle={2}
>
<Text style={style.filename}>
{this.state.files[this.state.currentFile].name}
</Text>
</LinearGradient>
{this.state.files.map((file, index) => {
let mime = file.mime_type;
if (mime && mime.includes(';')) {
mime = mime.split(';')[0];
}
let component;
if (file.has_preview_image || file.mime_type === 'image/gif') {
component = this.renderPreviewer(file, index);
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
component = this.renderAttachmentDocument(file);
} else if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
component = this.renderVideoPreview(file);
} else {
component = this.renderAttachmentIcon(file);
}
return (
<View
key={file.id}
style={[style.pageWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth}]}
>
{component}
</View>
);
})}
</ScrollView>
<AnimatedView style={[style.headerContainer, {width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}>
<View style={style.header}>
<View style={style.headerControls}>
<TouchableOpacity
onPress={this.handleClose}
style={style.headerIcon}
>
<Icon
name='md-close'
size={26}
color='#fff'
/>
</TouchableOpacity>
<Text style={style.title}>
{`${this.state.currentFile + 1}/${this.state.files.length}`}
</Text>
{this.renderDownloadButton()}
</View>
</View>
</AnimatedView>
<AnimatedView style={[style.footerContainer, {width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}>
<LinearGradient
style={style.footer}
start={{x: 0.0, y: 0.0}}
end={{x: 0.0, y: 0.9}}
colors={['transparent', '#000000']}
pointerEvents='none'
>
<Text style={style.filename}>
{this.state.files[this.state.currentFile].name}
</Text>
</LinearGradient>
</AnimatedView>
</AnimatedView>
</AnimatedView>
<Downloader
ref='downloader'
show={this.state.showDownloader}
file={this.state.files[this.state.currentFile]}
deviceHeight={this.props.deviceHeight}
deviceWidth={this.props.deviceWidth}
onDownloadCancel={this.hideDownloader}
onDownloadStart={this.hideDownloader}
onDownloadSuccess={this.hideDownloader}
/>
</View>
<Downloader
ref='downloader'
show={this.state.showDownloader}
file={this.state.files[this.state.currentFile]}
deviceHeight={this.props.deviceHeight}
deviceWidth={this.props.deviceWidth}
onDownloadCancel={this.hideDownloader}
onDownloadStart={this.hideDownloader}
onDownloadSuccess={this.hideDownloader}
/>
</View>
</SafeAreaView>
);
}
}
const style = StyleSheet.create({
filename: {
wrapper: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.8)'
},
scrollView: {
flex: 1,
backgroundColor: '#000'
},
scrollViewContent: {
backgroundColor: '#000'
},
pageWrapper: {
alignItems: 'center',
justifyContent: 'center',
flex: 1
},
headerContainer: {
position: 'absolute',
top: 0
},
header: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
height: HEADER_HEIGHT,
position: 'absolute',
top: 0,
left: 0,
width: '100%'
},
headerControls: {
alignItems: 'center',
justifyContent: 'space-around',
flexDirection: 'row'
},
headerIcon: {
height: 44,
width: 48,
alignItems: 'center',
justifyContent: 'center'
},
title: {
flex: 1,
marginHorizontal: 10,
color: 'white',
fontSize: 15
fontSize: 15,
textAlign: 'center'
},
footerContainer: {
position: 'absolute',
bottom: 0
},
footer: {
position: 'absolute',
@ -599,66 +640,11 @@ const style = StyleSheet.create({
...Platform.select({
android: {
marginBottom: 13
},
ios: {
marginBottom: 0
}
})
},
footerHeaderWrapper: {
position: 'absolute',
bottom: 0,
left: 0
},
header: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
height: HEADER_HEIGHT,
position: 'absolute',
top: 0,
left: 0,
width: '100%'
},
headerControls: {
alignItems: 'center',
justifyContent: 'space-around',
flexDirection: 'row',
...Platform.select({
android: {
marginTop: 0
},
ios: {
marginTop: 5
}
})
},
headerIcon: {
height: 44,
width: 48,
alignItems: 'center',
justifyContent: 'center'
},
pageWrapper: {
alignItems: 'center',
justifyContent: 'center'
},
scrollView: {
flex: 1,
backgroundColor: '#000'
},
scrollViewContent: {
backgroundColor: '#000'
},
title: {
flex: 1,
marginHorizontal: 10,
filename: {
color: 'white',
fontSize: 15,
textAlign: 'center'
},
wrapper: {
position: 'absolute',
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)'
fontSize: 15
}
});

View file

@ -16,6 +16,7 @@ import {
setChannelDisplayName,
setChannelLoading
} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
import {handleSearchDraftChanged} from 'app/actions/views/search';
import Search from './search';
@ -29,6 +30,7 @@ function mapStateToProps(state) {
return {
currentTeamId,
currentChannelId,
isLandscape: isLandscape(state),
postIds: state.entities.search.results,
recent: recent[currentTeamId],
searchingStatus: searchRequest.status

View file

@ -14,6 +14,7 @@ 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';
@ -23,6 +24,7 @@ import Autocomplete from 'app/components/autocomplete';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import PostListRetry from 'app/components/post_list_retry';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import SearchPreview from 'app/components/search_preview';
import StatusBar from 'app/components/status_bar';
@ -55,6 +57,7 @@ class Search extends PureComponent {
currentTeamId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
isLandscape: PropTypes.bool.isRequired,
navigator: PropTypes.object,
postIds: PropTypes.array,
recent: PropTypes.array.isRequired,
@ -71,6 +74,7 @@ class Search extends PureComponent {
super(props);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.isX = DeviceInfo.getModel() === 'iPhone X';
this.state = {
channelName: '',
postId: null,
@ -87,7 +91,9 @@ class Search extends PureComponent {
componentDidMount() {
this.setManagedConfig();
if (this.refs.searchBar) {
this.refs.searchBar.focus();
setTimeout(() => {
this.refs.searchBar.focus();
}, 150);
}
}
@ -97,6 +103,10 @@ class Search extends PureComponent {
const recentLength = recent.length;
const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED);
if (this.props.isLandscape !== prevProps.isLandscape) {
this.refs.searchBar.blur();
}
if (shouldScroll) {
requestAnimationFrame(() => {
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
@ -468,6 +478,7 @@ class Search extends PureComponent {
render() {
const {
intl,
isLandscape,
navigator,
postIds,
recent,
@ -580,48 +591,54 @@ class Search extends PureComponent {
}
return (
<View style={{flex: 1}}>
<StatusBar/>
<View style={style.header}>
<SearchBar
ref='searchBar'
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={Platform.OS === 'ios' ? 33 : 46}
inputStyle={style.searchBarInput}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
<SafeAreaView
excludeHeader={isLandscape && this.isX}
forceTop={44}
theme={theme}
>
<View style={{flex: 1}}>
<StatusBar/>
<View style={style.header}>
<SearchBar
ref='searchBar'
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={Platform.OS === 'ios' ? 33 : 46}
inputStyle={style.searchBarInput}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
onChangeText={this.handleTextChanged}
onSearchButtonPress={this.handleSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
onSelectionChange={this.handleSelectionChange}
autoCapitalize='none'
value={value}
containerStyle={style.searchBarContainer}
backArrowSize={28}
/>
</View>
<Autocomplete
ref={this.attachAutocomplete}
onChangeText={this.handleTextChanged}
onSearchButtonPress={this.handleSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
onSelectionChange={this.handleSelectionChange}
autoCapitalize='none'
isSearch={true}
value={value}
containerStyle={style.searchBarContainer}
backArrowSize={28}
/>
<SectionList
ref='list'
style={style.sectionList}
renderSectionHeader={this.renderSectionHeader}
sections={sections}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
stickySectionHeadersEnabled={Platform.OS === 'ios'}
/>
{previewComponent}
</View>
<Autocomplete
ref={this.attachAutocomplete}
onChangeText={this.handleTextChanged}
isSearch={true}
value={value}
/>
<SectionList
ref='list'
style={style.sectionList}
renderSectionHeader={this.renderSectionHeader}
sections={sections}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
stickySectionHeadersEnabled={Platform.OS === 'ios'}
/>
{previewComponent}
</View>
</SafeAreaView>
);
}
}
@ -637,8 +654,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center'
},
ios: {
height: 64,
paddingTop: 20
height: 44
}
})
},

View file

@ -14,7 +14,7 @@ import DeviceInfo from 'react-native-device-info';
import SettingsItem from 'app/screens/settings/settings_item';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {isValidUrl} from 'app/utils/url';
@ -68,7 +68,7 @@ class Settings extends PureComponent {
return contents.join('\n');
};
goToAbout = () => {
goToAbout = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'About',
@ -81,9 +81,9 @@ class Settings extends PureComponent {
navBarButtonColor: theme.sidebarHeaderTextColor
}
});
};
});
goToNotifications = () => {
goToNotifications = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'NotificationSettings',
@ -97,9 +97,9 @@ class Settings extends PureComponent {
screenBackgroundColor: theme.centerChannelBg
}
});
};
});
goToAdvancedSettings = () => {
goToAdvancedSettings = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'AdvancedSettings',
@ -113,9 +113,9 @@ class Settings extends PureComponent {
screenBackgroundColor: theme.centerChannelBg
}
});
};
});
goToSelectTeam = () => {
goToSelectTeam = wrapWithPreventDoubleTap(() => {
const {currentUrl, intl, navigator, theme} = this.props;
navigator.push({
@ -134,14 +134,16 @@ class Settings extends PureComponent {
theme
}
});
};
});
goToClientUpgrade = () => {
goToClientUpgrade = wrapWithPreventDoubleTap(() => {
const {intl, theme} = this.props;
this.props.navigator.push({
screen: 'ClientUpgrade',
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}),
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarHidden: false,
statusBarHidden: true,
@ -154,16 +156,12 @@ class Settings extends PureComponent {
userCheckedForUpgrade: true
}
});
}
});
handlePress = (action) => {
preventDoubleTap(action, this);
};
logout = () => {
logout = wrapWithPreventDoubleTap(() => {
const {logout} = this.props.actions;
InteractionManager.runAfterInteractions(logout);
};
});
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
@ -175,20 +173,30 @@ class Settings extends PureComponent {
}
};
openErrorEmail = () => {
openErrorEmail = wrapWithPreventDoubleTap(() => {
const {config} = this.props;
const recipient = config.SupportEmail;
const subject = `Problem with ${config.SiteName} React Native app`;
Linking.openURL(
`mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`
);
this.props.actions.clearErrors();
};
const mailTo = `mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`;
openHelp = () => {
Linking.canOpenURL(mailTo).then((supported) => {
if (supported) {
Linking.openURL(mailTo);
this.props.actions.clearErrors();
}
});
});
openHelp = wrapWithPreventDoubleTap(() => {
const {config} = this.props;
Linking.openURL(config.HelpLink.toLowerCase());
};
const link = config.HelpLink ? config.HelpLink.toLowerCase() : '';
Linking.canOpenURL(link).then((supported) => {
if (supported) {
Linking.openURL(link);
}
});
});
render() {
const {config, joinableTeams, theme} = this.props;
@ -207,7 +215,7 @@ class Settings extends PureComponent {
i18nId='user.settings.modal.notifications'
iconName='ios-notifications'
iconType='ion'
onPress={() => this.handlePress(this.goToNotifications)}
onPress={this.goToNotifications}
showArrow={showArrow}
theme={theme}
/>
@ -217,7 +225,7 @@ class Settings extends PureComponent {
i18nId='mobile.select_team.join_open'
iconName='list'
iconType='foundation'
onPress={() => this.handlePress(this.goToSelectTeam)}
onPress={this.goToSelectTeam}
showArrow={showArrow}
theme={theme}
/>
@ -228,7 +236,7 @@ class Settings extends PureComponent {
i18nId='mobile.help.title'
iconName='md-help'
iconType='ion'
onPress={() => this.handlePress(this.openHelp)}
onPress={this.openHelp}
showArrow={showArrow}
theme={theme}
/>
@ -238,7 +246,7 @@ class Settings extends PureComponent {
i18nId='sidebar_right_menu.report'
iconName='exclamation'
iconType='fontawesome'
onPress={() => this.handlePress(this.openErrorEmail)}
onPress={this.openErrorEmail}
showArrow={showArrow}
theme={theme}
/>
@ -247,7 +255,7 @@ class Settings extends PureComponent {
i18nId='mobile.advanced_settings.title'
iconName='ios-hammer'
iconType='ion'
onPress={() => this.handlePress(this.goToAdvancedSettings)}
onPress={this.goToAdvancedSettings}
showArrow={showArrow}
theme={theme}
/>
@ -257,7 +265,7 @@ class Settings extends PureComponent {
i18nId='mobile.settings.modal.check_for_upgrade'
iconName='update'
iconType='material'
onPress={() => this.handlePress(this.goToClientUpgrade)}
onPress={this.goToClientUpgrade}
showArrow={showArrow}
theme={theme}
/>
@ -267,7 +275,7 @@ class Settings extends PureComponent {
i18nId='about.title'
iconName='ios-information-circle'
iconType='ion'
onPress={() => this.handlePress(this.goToAbout)}
onPress={this.goToAbout}
separator={false}
showArrow={showArrow}
theme={theme}
@ -280,7 +288,7 @@ class Settings extends PureComponent {
defaultMessage='Logout'
i18nId='sidebar_right_menu.logout'
isDestructor={true}
onPress={() => this.handlePress(this.logout)}
onPress={this.logout}
separator={false}
showArrow={false}
theme={theme}

View file

@ -11,6 +11,7 @@ import {General} from 'mattermost-redux/constants';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@ -88,25 +89,31 @@ class Thread extends PureComponent {
const style = getStyle(theme);
return (
<KeyboardLayout
behavior='padding'
style={style.container}
keyboardVerticalOffset={65}
<SafeAreaView
excludeHeader={true}
keyboardOffset={20}
theme={theme}
>
<StatusBar/>
<PostList
indicateNewMessages={true}
postIds={postIds}
currentUserId={myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
navigator={navigator}
/>
<PostTextbox
rootId={rootId}
channelId={channelId}
navigator={navigator}
/>
</KeyboardLayout>
<KeyboardLayout
behavior='padding'
style={style.container}
keyboardVerticalOffset={65}
>
<PostList
indicateNewMessages={true}
postIds={postIds}
currentUserId={myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
navigator={navigator}
/>
<PostTextbox
rootId={rootId}
channelId={channelId}
navigator={navigator}
/>
</KeyboardLayout>
</SafeAreaView>
);
}
}

View file

@ -33,9 +33,9 @@
"AutoSelectServerUrl": false,
"EnableMobileClientUpgrade": false,
"EnableMobileClientUpgradeUserSetting": false,
"EnableForceMobileClientUpgrade": false,
"MobileClientUpgradeAndroidApkLink": "",
"MobileClientUpgradeIosIpaLink": ""
"EnableMobileClientUpgrade": true,
"EnableMobileClientUpgradeUserSetting": true,
"EnableForceMobileClientUpgrade": true,
"MobileClientUpgradeAndroidApkLink": "https://about.mattermost.com/mattermost-android-app/",
"MobileClientUpgradeIosIpaLink": "https://about.mattermost.com/mattermost-ios-app/"
}

View file

@ -76,6 +76,7 @@
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
F23C99AA5FA10E457A76803A /* libPods-MattermostTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4246DC09024BB33A3E491E25 /* libPods-MattermostTests.a */; };
3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */; };
8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9263CF9B16054263B13EA23B /* libRNSafeArea.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -638,6 +639,8 @@
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = "<group>"; };
41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */ = {isa = PBXFileReference; name = "RNReactNativeDocViewer.xcodeproj"; path = "../node_modules/react-native-doc-viewer/ios/RNReactNativeDocViewer.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */ = {isa = PBXFileReference; name = "libRNReactNativeDocViewer.a"; path = "libRNReactNativeDocViewer.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; name = "RNSafeArea.xcodeproj"; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
9263CF9B16054263B13EA23B /* libRNSafeArea.a */ = {isa = PBXFileReference; name = "libRNSafeArea.a"; path = "libRNSafeArea.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -691,6 +694,7 @@
A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */,
5A0920184BD344979BCFCD5C /* libRCTVideo.a in Frameworks */,
3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */,
8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1123,6 +1127,7 @@
A2968536BEC74A4ABBB73BD9 /* RCTYouTube.xcodeproj */,
DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */,
41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */,
849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */,
);
name = Libraries;
sourceTree = "<group>";
@ -2044,6 +2049,7 @@
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-video/ios",
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
"$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea",
);
INFOPLIST_FILE = MattermostTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@ -2053,6 +2059,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"$(inherited)",
@ -2075,6 +2082,7 @@
"$(inherited)",
"$(SRCROOT)/../node_modules/react-native-video/ios",
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
"$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea",
);
INFOPLIST_FILE = MattermostTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@ -2084,6 +2092,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"$(inherited)",
@ -2127,6 +2136,7 @@
"$(SRCROOT)/../node_modules/react-native-youtube/**",
"$(SRCROOT)/../node_modules/react-native-video/ios",
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
"$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea",
);
INFOPLIST_FILE = Mattermost/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
@ -2176,6 +2186,7 @@
"$(SRCROOT)/../node_modules/react-native-youtube/**",
"$(SRCROOT)/../node_modules/react-native-video/ios",
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
"$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea",
);
INFOPLIST_FILE = Mattermost/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;

View file

@ -38,6 +38,7 @@
"react-native-notifications": "enahum/react-native-notifications.git",
"react-native-orientation": "enahum/react-native-orientation.git",
"react-native-passcode-status": "1.1.0",
"react-native-safe-area": "0.2.1",
"react-native-section-list-get-item-layout": "2.1.0",
"react-native-sentry": "0.15.1",
"react-native-slider": "0.11.0",

View file

@ -3961,7 +3961,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "1.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/4f36c293b136cdfb9f44b9536a5006084d0dc758"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/fb083f67b5560ff64fe251415460a1af5d690e5a"
dependencies:
deep-equal "1.0.1"
form-data "2.3.1"
@ -5140,6 +5140,10 @@ react-native-passcode-status@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-native-passcode-status/-/react-native-passcode-status-1.1.0.tgz#e5922d5f4d79bc09849438d620406e5ccd31168a"
react-native-safe-area@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/react-native-safe-area/-/react-native-safe-area-0.2.1.tgz#9bf8eb8dd052b76fc4e830addcd63235dd29cf3c"
react-native-section-list-get-item-layout@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.1.0.tgz#ec5336fa769cfbe0c49ace50086a3cfdb2be2812"