Add Landscape support for both platforms (#909)

* Landscape support

* Fix image rotation on Android

* Fix landscape mode for login and login options

* Fix previewer will receive props

* Move device dimensions and others to redux

* Fix unit tests

* Include orientation and tablet in the store
This commit is contained in:
enahum 2017-09-20 16:54:24 -03:00 committed by Jarred Witt
parent 59762e4fac
commit 4995a76f2c
57 changed files with 673 additions and 323 deletions

View file

@ -20,8 +20,11 @@ POD := $(shell command -v pod 2> /dev/null)
.podinstall:
ifdef POD
@echo Getting CocoaPods dependencies;
@echo Getting Cocoapods dependencies;
@cd ios && pod install;
else
@echo "Cocoapods is not installed https://cocoapods.org/"
@exit 1
endif
@touch $@
@ -117,6 +120,7 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_SENSOR);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \

View file

@ -53,6 +53,9 @@
<service android:name=".NotificationReplyService"
android:enabled="true"
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
</application>
</manifest>

View file

@ -1,6 +1,7 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.RestrictionsManager;
@ -10,6 +11,7 @@ import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager.LayoutParams;
import android.content.res.Configuration;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
@ -143,6 +145,15 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
mListeners.remove(listener);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (mVisibleActivity != null) {
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
mVisibleActivity.sendBroadcast(intent);
}
}
private synchronized void switchToVisible(Activity activity) {
if (mVisibleActivity == null) {
mVisibleActivity = activity;

View file

@ -0,0 +1,55 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Dimensions} from 'react-native';
import {DeviceTypes} from 'app/constants';
export function calculateDeviceDimensions() {
const {height, width} = Dimensions.get('window');
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width
}
};
}
export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline
}, getState);
};
}
export function setStatusBarHeight(height = 20) {
return {
type: DeviceTypes.STATUSBAR_HEIGHT_CHANGED,
data: height
};
}
export function setDeviceOrientation(orientation) {
return {
type: DeviceTypes.DEVICE_ORIENTATION_CHANGED,
data: orientation
};
}
export function setDeviceAsTablet() {
return {
type: DeviceTypes.DEVICE_TYPE_CHANGED,
data: true
};
}
export default {
calculateDeviceDimensions,
connection,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight
};

View file

@ -1,13 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.CONNECTION_CHANGED,
data: isOnline
}, getState);
};
}

View file

@ -68,13 +68,6 @@ export function goToNotification(notification) {
};
}
export function setStatusBarHeight(height = 20) {
return {
type: ViewTypes.STATUSBAR_HEIGHT_CHANGED,
data: height
};
}
export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
@ -84,5 +77,5 @@ export default {
queueNotification,
clearNotification,
goToNotification,
setStatusBarHeight
purgeOfflineStore
};

View file

@ -22,6 +22,7 @@ import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends PureComponent {
static propTypes = {
@ -39,19 +40,29 @@ export default class ChannelDrawer extends PureComponent {
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTablet: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
navigator: PropTypes.object,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
state = {
openDrawer: false,
openDrawerOffset: DRAWER_INITIAL_OFFSET
};
swiperIndex = 1;
constructor(props) {
super(props);
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (props.isLandscape || props.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawer: false,
openDrawerOffset
};
}
componentWillMount() {
this.props.actions.getTeams();
}
@ -62,6 +73,17 @@ export default class ChannelDrawer extends PureComponent {
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
componentWillReceiveProps(nextProps) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || nextProps.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
}
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
@ -81,6 +103,10 @@ export default class ChannelDrawer extends PureComponent {
this.setState({openDrawer: false});
};
drawerSwiperRef = (ref) => {
this.drawerSwiper = ref;
};
handleDrawerClose = () => {
this.resetDrawer();
@ -93,7 +119,7 @@ export default class ChannelDrawer extends PureComponent {
};
handleDrawerOpen = () => {
if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) {
if (this.state.openDrawerOffset !== 0) {
Keyboard.dismiss();
}
};
@ -210,8 +236,13 @@ export default class ChannelDrawer extends PureComponent {
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
this.setState({openDrawerOffset: DRAWER_INITIAL_OFFSET});
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
};
onSearchStart = () => {
@ -221,13 +252,13 @@ export default class ChannelDrawer extends PureComponent {
showTeams = () => {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.refs.swiper.showTeamsPage();
this.drawerSwiper.getWrappedInstance().showTeamsPage();
}
};
resetDrawer = () => {
if (this.swiperIndex !== 1) {
this.refs.swiper.resetPage();
this.drawerSwiper.getWrappedInstance().resetPage();
}
};
@ -242,7 +273,7 @@ export default class ChannelDrawer extends PureComponent {
openDrawerOffset
} = this.state;
const showTeams = openDrawerOffset === DRAWER_INITIAL_OFFSET && teamsCount > 1;
const showTeams = openDrawerOffset !== 0 && teamsCount > 1;
const teams = (
<View style={style.swiperContent}>
@ -268,7 +299,7 @@ export default class ChannelDrawer extends PureComponent {
return (
<DrawerSwiper
ref='swiper'
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}

View file

@ -0,0 +1,93 @@
// 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 Swiper from 'react-native-swiper';
import {changeOpacity} from 'app/utils/theme';
export default class DrawerSwiper extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
isLandscape: PropTypes.bool.isRequired,
onPageSelected: PropTypes.func,
openDrawerOffset: PropTypes.number,
showTeams: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPageSelected: () => true,
openDrawerOffset: 0
};
state = {
index: 1
};
componentWillReceiveProps(nextProps) {
if (this.refs.swiper) {
if (nextProps.openDrawerOffset !== this.props.openDrawerOffset || nextProps.isLandscape !== this.props.isLandscape) {
this.refs.swiper.initialRender = true;
if (this.state.index === 1) {
this.resetPage();
}
}
}
}
swiperPageSelected = (e, state, context) => {
this.props.onPageSelected(context.state.index);
this.setState({index: context.state.index});
};
showTeamsPage = () => {
this.refs.swiper.scrollBy(-1, true);
};
resetPage = () => {
this.refs.swiper.scrollBy(1, false);
};
render() {
const {
children,
deviceHeight,
deviceWidth,
openDrawerOffset,
showTeams,
theme
} = this.props;
const pagination = {bottom: 0};
if (showTeams) {
pagination.bottom = 0;
}
return (
<Swiper
ref='swiper'
horizontal={true}
loop={false}
index={this.state.index}
onMomentumScrollEnd={this.swiperPageSelected}
paginationStyle={[{position: 'absolute'}, pagination]}
width={deviceWidth - openDrawerOffset}
height={deviceHeight}
style={{backgroundColor: theme.sidebarBg}}
activeDotColor={theme.sidebarText}
dotColor={changeOpacity(theme.sidebarText, 0.5)}
removeClippedSubviews={true}
automaticallyAdjustContentInsets={true}
scrollEnabled={showTeams}
showsPagination={showTeams}
keyboardShouldPersistTaps={'always'}
>
{children}
</Swiper>
);
}
}

View file

@ -1,82 +1,18 @@
// 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 {Dimensions} from 'react-native';
import Swiper from 'react-native-swiper';
import {connect} from 'react-redux';
import {changeOpacity} from 'app/utils/theme';
import {getDimensions, isLandscape} from 'app/selectors/device';
export default class DrawerSwiper extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
onPageSelected: PropTypes.func,
openDrawerOffset: PropTypes.number,
showTeams: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired
import DraweSwiper from './drawer_swiper';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
isLandscape: isLandscape(state)
};
static defaultProps = {
onPageSelected: () => true,
openDrawerOffset: 0
};
componentWillReceiveProps(nextProps) {
if (nextProps.openDrawerOffset !== this.props.openDrawerOffset && this.refs.swiper) {
this.refs.swiper.initialRender = true;
}
}
swiperPageSelected = (e, state, context) => {
this.props.onPageSelected(context.state.index);
};
showTeamsPage = () => {
this.refs.swiper.scrollBy(-1, true);
};
resetPage = () => {
this.refs.swiper.scrollBy(1, false);
};
render() {
const {
children,
openDrawerOffset,
showTeams,
theme
} = this.props;
const pagination = {bottom: 0};
if (showTeams) {
pagination.bottom = 0;
}
// Get the dimensions here so when the orientation changes we get the right dimensions
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
return (
<Swiper
ref='swiper'
horizontal={true}
loop={false}
index={1}
onMomentumScrollEnd={this.swiperPageSelected}
paginationStyle={[{position: 'absolute'}, pagination]}
width={deviceWidth - openDrawerOffset}
height={deviceHeight}
style={{backgroundColor: theme.sidebarBg}}
activeDotColor={theme.sidebarText}
dotColor={changeOpacity(theme.sidebarText, 0.5)}
removeClippedSubviews={true}
automaticallyAdjustContentInsets={true}
scrollEnabled={showTeams}
showsPagination={showTeams}
keyboardShouldPersistTaps={'always'}
>
{children}
</Swiper>
);
}
}
export default connect(mapStateToProps, null, null, {withRef: true})(DraweSwiper);

View file

@ -11,6 +11,7 @@ import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/e
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import ChannelDrawer from './channel_drawer.js';
@ -23,6 +24,8 @@ function mapStateToProps(state, ownProps) {
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
theme: getTheme(state)
};

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
SectionList,
TouchableOpacity,
View
@ -17,7 +16,6 @@ import SearchBar from 'app/components/search_bar';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const {width: deviceWidth} = Dimensions.get('window');
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
@ -25,6 +23,7 @@ const SECTION_MARGIN = 15;
class EmojiPicker extends PureComponent {
static propTypes = {
emojis: PropTypes.array.isRequired,
deviceWidth: PropTypes.number.isRequired,
intl: intlShape.isRequired,
onEmojiPress: PropTypes.func,
theme: PropTypes.object.isRequired
@ -43,10 +42,19 @@ class EmojiPicker extends PureComponent {
this.state = {
emojis: props.emojis,
searchTerm: ''
searchTerm: '',
width: props.deviceWidth - (SECTION_MARGIN * 2)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.deviceWidth !== this.props.deviceWidth) {
this.setState({
width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
});
}
}
changeSearchTerm = (text) => {
this.setState({
searchTerm: text
@ -67,11 +75,11 @@ class EmojiPicker extends PureComponent {
emojis: this.props.emojis,
searchTerm: ''
});
}
};
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
}
};
searchEmojis = (searchTerm) => {
const {emojis} = this.props;
@ -106,7 +114,7 @@ class EmojiPicker extends PureComponent {
});
return nextEmojis;
}
};
renderSectionHeader = ({section}) => {
const {theme} = this.props;
@ -121,7 +129,7 @@ class EmojiPicker extends PureComponent {
/>
</View>
);
}
};
renderEmojis = (emojis, index) => {
const {theme} = this.props;
@ -157,13 +165,13 @@ class EmojiPicker extends PureComponent {
})}
</View>
);
}
};
renderItem = ({item}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
const numColumns = Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
const numColumns = Number((this.state.width / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
@ -180,7 +188,7 @@ class EmojiPicker extends PureComponent {
{slices.map(this.renderEmojis)}
</View>
);
}
};
render() {
const {intl, theme} = this.props;
@ -253,8 +261,7 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
marginRight: 0
},
listView: {
backgroundColor: theme.centerChannelBg,
width: deviceWidth - (SECTION_MARGIN * 2)
backgroundColor: theme.centerChannelBg
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),

View file

@ -6,6 +6,7 @@ import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
@ -98,9 +99,11 @@ const getEmojisBySection = createSelector(
function mapStateToProps(state) {
const emojis = getEmojisBySection(state);
const {deviceWidth} = getDimensions(state);
return {
emojis,
deviceWidth,
theme: getTheme(state)
};
}

View file

@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Platform,
ScrollView,
StyleSheet,
@ -17,8 +16,6 @@ import FileAttachmentImage from 'app/components/file_attachment_list/file_attach
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
const {height: deviceHeight} = Dimensions.get('window');
export default class FileUploadPreview extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@ -29,6 +26,7 @@ export default class FileUploadPreview extends PureComponent {
channelId: PropTypes.string.isRequired,
channelIsLoading: PropTypes.bool,
createPostRequestStatus: PropTypes.string.isRequired,
deviceHeight: PropTypes.number.isRequired,
fetchCache: PropTypes.object.isRequired,
files: PropTypes.array.isRequired,
inputHeight: PropTypes.number.isRequired,
@ -98,7 +96,7 @@ export default class FileUploadPreview extends PureComponent {
</View>
);
});
}
};
render() {
if (this.props.channelIsLoading || (!this.props.files.length && !this.props.filesUploadingForCurrentChannel)) {
@ -107,7 +105,7 @@ export default class FileUploadPreview extends PureComponent {
return (
<KeyboardLayout>
<View style={[style.container]}>
<View style={[style.container, {height: this.props.deviceHeight}]}>
<ScrollView
horizontal={true}
style={style.scrollView}
@ -124,7 +122,6 @@ export default class FileUploadPreview extends PureComponent {
const style = StyleSheet.create({
container: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
height: deviceHeight,
left: 0,
bottom: 0,
position: 'absolute',

View file

@ -7,6 +7,7 @@ import {createSelector} from 'reselect';
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import FileUploadPreview from './file_upload_preview';
@ -25,10 +26,13 @@ const checkForFileUploadingInChannel = createSelector(
);
function mapStateToProps(state, ownProps) {
const {deviceHeight} = getDimensions(state);
return {
...ownProps,
channelIsLoading: state.views.channel.loading,
createPostRequestStatus: state.requests.posts.createPost.status,
deviceHeight,
fetchCache: state.views.fetchCache,
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
theme: getTheme(state)

View file

@ -6,20 +6,20 @@ import {connect} from 'react-redux';
import {close as closeWebSocket, init as initWebSocket} from 'mattermost-redux/actions/websocket';
import {getConnection} from 'app/selectors/device';
import OfflineIndicator from './offline_indicator';
function mapStateToProps(state, ownProps) {
const {websocket} = state.requests.general;
const {appState} = state.entities.general;
const {connection} = state.views;
const webSocketStatus = websocket.status;
const isConnecting = websocket.error >= 2;
return {
appState,
isConnecting,
isOnline: connection,
isOnline: getConnection(state),
webSocketStatus,
...ownProps
};

View file

@ -12,6 +12,7 @@ import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/po
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {ViewTypes} from 'app/constants';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {extractFirstLink} from 'app/utils/url';
@ -24,6 +25,7 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
config,
link,
openGraphData: getOpenGraphMetadataForUrl(state, link),

View file

@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Image,
Linking,
Platform,
@ -12,7 +11,7 @@ import {
TouchableWithoutFeedback,
View
} from 'react-native';
import Orientation from 'react-native-orientation';
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
import youTubeVideoId from 'youtube-video-id';
import youtubePlayIcon from 'assets/images/icons/youtube-play-icon.png';
@ -29,6 +28,8 @@ export default class PostBodyAdditionalContent extends PureComponent {
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
config: PropTypes.object,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
link: PropTypes.string,
message: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
@ -71,9 +72,10 @@ export default class PostBodyAdditionalContent extends PureComponent {
}
calculateDimensions = (width, height) => {
const {width: deviceWidth} = Dimensions.get('window');
const {deviceHeight, deviceWidth} = this.props;
let maxHeight = MAX_IMAGE_HEIGHT;
let maxWidth = deviceWidth - 68;
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
let maxWidth = deviceSize - 78;
if (height <= MAX_IMAGE_HEIGHT) {
maxHeight = height;
@ -218,11 +220,8 @@ export default class PostBodyAdditionalContent extends PureComponent {
const {link} = this.props;
const videoId = youTubeVideoId(link);
Orientation.unlockAllOrientations();
if (Platform.OS === 'ios') {
YouTubeStandaloneIOS.playVideo(videoId).then(() => {
Orientation.lockToPortrait();
});
YouTubeStandaloneIOS.playVideo(videoId);
} else {
const {config} = this.props;
@ -231,12 +230,9 @@ export default class PostBodyAdditionalContent extends PureComponent {
apiKey: config.GoogleDeveloperKey,
videoId,
autoplay: true
}).then(() => {
Orientation.lockToPortrait();
});
} else {
Linking.openURL(link);
Orientation.lockToPortrait();
}
}
};

View file

@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Platform,
Text,
TouchableOpacity,
@ -90,7 +89,6 @@ export default class SearchPreview extends PureComponent {
render() {
const {channelName, currentUserId, posts, theme} = this.props;
const {height, width} = Dimensions.get('window');
const style = getStyleSheet(theme);
let postList;
@ -113,7 +111,7 @@ export default class SearchPreview extends PureComponent {
return (
<View
style={[style.container, {width, height}]}
style={style.container}
>
<Animatable.View
ref='view'
@ -170,9 +168,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
container: {
position: 'absolute',
backgroundColor: changeOpacity('#000', 0.3),
height: '100%',
top: 0,
left: 0,
zIndex: 10
zIndex: 10,
width: '100%'
},
wrapper: {
flex: 1,

12
app/constants/device.js Normal file
View file

@ -0,0 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import keyMirror from 'mattermost-redux/utils/key_mirror';
export default keyMirror({
CONNECTION_CHANGED: null,
DEVICE_DIMENSIONS_CHANGED: null,
DEVICE_TYPE_CHANGED: null,
DEVICE_ORIENTATION_CHANGED: null,
STATUSBAR_HEIGHT_CHANGED: null
});

View file

@ -1,10 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import DeviceTypes from './device';
import NavigationTypes from './navigation';
import ViewTypes from './view';
export {
DeviceTypes,
NavigationTypes,
ViewTypes
};

View file

@ -20,8 +20,6 @@ const ViewTypes = keyMirror({
NOTIFICATION_IN_APP: null,
NOTIFICATION_TAPPED: null,
CONNECTION_CHANGED: null,
SET_POST_DRAFT: null,
SET_COMMENT_DRAFT: null,
@ -47,9 +45,7 @@ const ViewTypes = keyMirror({
INCREASE_POST_VISIBILITY: null,
RECEIVED_FOCUSED_POST: null,
LOADING_POSTS: null,
STATUSBAR_HEIGHT_CHANGED: null
LOADING_POSTS: null
});
export default {

View file

@ -246,6 +246,9 @@ const state = {
}
}
},
device: {
connection: true
},
navigation: '',
views: {
channel: {
@ -253,7 +256,6 @@ const state = {
loading: false,
tooltipVisible: false
},
connection: true,
fetchCache: {},
i18n: {
locale: ''

View file

@ -29,11 +29,16 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {Client, Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {
calculateDeviceDimensions,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight
} from 'app/actions/device';
import {
goToNotification,
loadConfigAndLicense,
queueNotification,
setStatusBarHeight,
purgeOfflineStore
} from 'app/actions/views/root';
import {setChannelDisplayName} from 'app/actions/views/channel';
@ -69,7 +74,7 @@ export default class Mattermost {
this.isConfigured = false;
this.allowOtherServers = true;
Orientation.lockToPortrait();
Orientation.unlockAllOrientations();
initializeSentry();
@ -80,7 +85,7 @@ export default class Mattermost {
EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset);
EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetDisplayName);
EventEmitter.on(NavigationTypes.RESTART_APP, this.restartApp);
Orientation.addOrientationListener(this.orientationDidChange);
mattermostManaged.addEventListener('managedConfigDidChange', this.handleManagedConfig);
this.handleAppStateChange(AppState.currentState);
@ -373,6 +378,10 @@ export default class Mattermost {
// We need to wait for hydration to occur before load the router.
listenForHydration = () => {
const state = this.store.getState();
Orientation.getOrientation((orientation) => {
this.orientationDidChange(orientation);
});
if (state.views.root.hydrationComplete) {
this.unsubscribeFromStore();
@ -498,6 +507,17 @@ export default class Mattermost {
}
};
orientationDidChange = (orientation) => {
const {dispatch} = this.store;
dispatch(setDeviceOrientation(orientation));
if (DeviceInfo.isTablet()) {
dispatch(setDeviceAsTablet());
}
setTimeout(() => {
dispatch(calculateDeviceDimensions());
}, 100);
};
resetBadgeAndVersion = () => {
const {dispatch, getState} = this.store;
Client4.serverVersion = '';
@ -558,6 +578,9 @@ export default class Mattermost {
passProps: {
allowOtherServers: this.allowOtherServers
},
appStyle: {
orientation: 'auto'
},
animationType
});
};

View file

@ -2,11 +2,11 @@
// See License.txt for license information.
import {UserTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
import {DeviceTypes} from 'app/constants';
export default function connection(state = true, action) {
switch (action.type) {
case ViewTypes.CONNECTION_CHANGED:
case DeviceTypes.CONNECTION_CHANGED:
return action.data;
case UserTypes.LOGOUT_SUCCESS:

View file

@ -0,0 +1,30 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Dimensions} from 'react-native';
import {UserTypes} from 'mattermost-redux/action_types';
import {DeviceTypes} from 'app/constants';
const {height, width} = Dimensions.get('window');
const initialState = {
deviceHeight: height,
deviceWidth: width
};
export default function dimension(state = initialState, action) {
switch (action.type) {
case DeviceTypes.DEVICE_DIMENSIONS_CHANGED: {
const {data} = action;
if (state.deviceWidth !== data.deviceWidth || state.deviceHeight !== data.deviceHeight) {
return {...data};
}
break;
}
case UserTypes.LOGOUT_SUCCESS:
return initialState;
}
return state;
}

View file

@ -0,0 +1,18 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {combineReducers} from 'redux';
import connection from './connection';
import dimension from './dimension';
import isTablet from './is_tablet';
import orientation from './orientation';
import statusBarHeight from './status_bar';
export default combineReducers({
connection,
dimension,
isTablet,
orientation,
statusBarHeight
});

View file

@ -0,0 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {DeviceTypes} from 'app/constants';
export default function isTablet(state = false, action) {
switch (action.type) {
case DeviceTypes.DEVICE_TYPE_CHANGED:
return action.data;
}
return state;
}

View file

@ -0,0 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {DeviceTypes} from 'app/constants';
export default function orientation(state = 'PORTRAIT', action) {
switch (action.type) {
case DeviceTypes.DEVICE_ORIENTATION_CHANGED:
return action.data;
}
return state;
}

View file

@ -0,0 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {DeviceTypes} from 'app/constants';
export default function height(state = 0, action) {
switch (action.type) {
case DeviceTypes.STATUSBAR_HEIGHT_CHANGED:
return action.data;
default:
return state;
}
}

View file

@ -1,10 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import device from './device';
import navigation from './navigation';
import views from './views';
export default {
device,
navigation,
views
};

View file

@ -4,7 +4,6 @@
import {combineReducers} from 'redux';
import channel from './channel';
import connection from './connection';
import fetchCache from './fetch_cache';
import i18n from './i18n';
import login from './login';
@ -16,7 +15,6 @@ import team from './team';
import thread from './thread';
export default combineReducers({
connection,
channel,
fetchCache,
i18n,

View file

@ -37,18 +37,8 @@ function purge(state = false, action) {
}
}
function statusBarHeight(state = 20, action) {
switch (action.type) {
case ViewTypes.STATUSBAR_HEIGHT_CHANGED:
return action.data;
default:
return state;
}
}
export default combineReducers({
appInitializing,
hydrationComplete,
purge,
statusBarHeight
purge
});

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
NetInfo,
Platform,
View
@ -199,6 +198,16 @@ class Channel extends PureComponent {
navigator={navigator}
>
<StatusBar/>
<View>
<OfflineIndicator/>
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle
onPress={this.goToChannelInfo}
/>
<ChannelSearchButton navigator={navigator}/>
</View>
</View>
<KeyboardLayout
behavior='padding'
style={style.keyboardLayout}
@ -216,16 +225,6 @@ class Channel extends PureComponent {
navigator={navigator}
/>
</KeyboardLayout>
<View style={style.headerContainer}>
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle
onPress={this.goToChannelInfo}
/>
<ChannelSearchButton navigator={navigator}/>
</View>
<OfflineIndicator/>
</View>
</ChannelDrawer>
);
}
@ -233,17 +232,12 @@ class Channel extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
headerContainer: {
flex: 1,
position: 'absolute'
},
header: {
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
justifyContent: 'flex-start',
width: Dimensions.get('window').width,
width: '100%',
zIndex: 10,
elevation: 2,
...Platform.select({
android: {
height: 46
@ -255,15 +249,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
})
},
postList: {
flex: 1,
...Platform.select({
android: {
marginTop: 46
},
ios: {
marginTop: 64
}
})
flex: 1
},
loading: {
backgroundColor: theme.centerChannelBg,
@ -272,6 +258,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
keyboardLayout: {
backgroundColor: theme.centerChannelBg,
flex: 1,
zIndex: -1,
paddingBottom: 0
}
};

View file

@ -25,7 +25,6 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
class ChannelDrawerButton extends PureComponent {
static propTypes = {
applicationInitializing: PropTypes.bool.isRequired,
currentTeamId: PropTypes.string.isRequired,
myTeamMembers: PropTypes.object,
theme: PropTypes.object,
@ -80,7 +79,6 @@ class ChannelDrawerButton extends PureComponent {
render() {
const {
applicationInitializing,
currentTeamId,
mentionCount,
messageCount,
@ -89,10 +87,6 @@ class ChannelDrawerButton extends PureComponent {
} = this.props;
const style = getStyleFromTheme(theme);
if (applicationInitializing) {
return null;
}
let mentions = mentionCount;
let messages = messageCount;
@ -184,7 +178,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
function mapStateToProps(state) {
return {
applicationInitializing: state.views.root.appInitializing,
currentTeamId: getCurrentTeamId(state),
myTeamMembers: getTeamMemberships(state),
theme: getTheme(state),

View file

@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Animated,
Dimensions,
Platform,
StyleSheet,
View
@ -207,7 +206,6 @@ class ChannelPostList extends PureComponent {
}
const refreshIndicatorDimensions = {
width: Dimensions.get('window').width,
height: retryMessageHeight
};
@ -234,7 +232,8 @@ const style = StyleSheet.create({
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden'
overflow: 'hidden',
width: '100%'
}
});

View file

@ -9,6 +9,7 @@ import {RequestStatus} from 'mattermost-redux/constants';
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
import {getConnection} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import ChannelPostList from './channel_post_list';
@ -22,7 +23,7 @@ function makeMapStateToProps() {
const {getPosts, getPostsRetryAttempts, getPostsSince, getPostsSinceRetryAttempts} = state.requests.posts;
const posts = getPostsInChannel(state, channelId) || [];
const {websocket: websocketRequest} = state.requests.general;
const {connection: networkOnline} = state.views;
const networkOnline = getConnection(state);
const webSocketOnline = websocketRequest.status === RequestStatus.SUCCESS;
let getPostsStatus;

View file

@ -29,7 +29,6 @@ class ChannelSearchButton extends PureComponent {
clearSearch: PropTypes.func.isRequired,
handlePostDraftChanged: PropTypes.func.isRequired
}).isRequired,
applicationInitializing: PropTypes.bool.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object
};
@ -57,14 +56,9 @@ class ChannelSearchButton extends PureComponent {
render() {
const {
applicationInitializing,
theme
} = this.props;
if (applicationInitializing) {
return null;
}
return (
<TouchableOpacity
onPress={() => preventDoubleTap(this.handlePress, this)}
@ -98,7 +92,6 @@ const style = StyleSheet.create({
function mapStateToProps(state, ownProps) {
return {
applicationInitializing: state.views.root.appInitializing,
theme: getTheme(state),
...ownProps
};

View file

@ -16,10 +16,6 @@ import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
function ChannelTitle(props) {
if (props.applicationInitializing) {
return null;
}
const channelName = props.displayName || props.currentChannel.display_name;
let icon;
if (channelName) {
@ -56,7 +52,6 @@ function ChannelTitle(props) {
}
ChannelTitle.propTypes = {
applicationInitializing: PropTypes.bool.isRequired,
currentChannel: PropTypes.object,
displayName: PropTypes.string,
onPress: PropTypes.func,
@ -71,7 +66,6 @@ ChannelTitle.defaultProps = {
function mapStateToProps(state) {
return {
applicationInitializing: state.views.root.appInitializing,
currentChannel: getCurrentChannel(state) || {},
displayName: state.views.channel.displayName,
theme: getTheme(state)

View file

@ -9,8 +9,9 @@ import {
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel
} from 'app/actions/views/channel';
import {connection} from 'app/actions/views/connection';
import {connection} from 'app/actions/device';
import {selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {getStatusBarHeight} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
@ -27,7 +28,6 @@ import Channel from './channel';
function mapStateToProps(state, ownProps) {
const {websocket} = state.requests.general;
const {myChannels: channelsRequest} = state.requests.channels;
const {statusBarHeight} = state.views.root;
return {
...ownProps,
@ -35,7 +35,7 @@ function mapStateToProps(state, ownProps) {
currentChannelId: getCurrentChannelId(state),
theme: getTheme(state),
webSocketRequest: websocket,
statusBarHeight,
statusBarHeight: getStatusBarHeight(state),
channelsRequestStatus: channelsRequest.status
};
}

View file

@ -4,7 +4,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Dimensions,
ScrollView,
StyleSheet,
Text,
@ -14,10 +13,6 @@ import {
import {getCodeFont} from 'app/utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {
width: deviceWidth
} = Dimensions.get('window');
export default class Code extends React.PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
@ -26,7 +21,7 @@ export default class Code extends React.PureComponent {
countLines = (content) => {
return content.split('\n').length;
}
};
render() {
const style = getStyleSheet(this.props.theme);
@ -100,7 +95,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
codeContainer: {
flexGrow: 0,
flexShrink: 1,
width: deviceWidth
width: '100%'
},
code: {
paddingHorizontal: 6,

View file

@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Animated,
CameraRoll,
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
@ -22,10 +21,11 @@ import FormattedText from 'app/components/formatted_text';
import {emptyFunction} from 'app/utils/general';
const {View: AnimatedView} = Animated;
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
export default class Downloader extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
file: PropTypes.object.isRequired,
onDownloadCancel: PropTypes.func,
onDownloadSuccess: PropTypes.func,
@ -38,9 +38,13 @@ export default class Downloader extends PureComponent {
show: false
};
state = {
downloaderTop: new Animated.Value(deviceHeight),
progress: 0
constructor(props) {
super(props);
this.state = {
downloaderTop: new Animated.Value(props.deviceHeight),
progress: 0
};
}
componentWillReceiveProps(nextProps) {
@ -52,10 +56,27 @@ export default class Downloader extends PureComponent {
});
} else if (!nextProps.show && this.props.show) {
this.toggleDownloader(false);
} else if (this.props.deviceHeight !== nextProps.deviceHeight) {
this.recenterDownloader(nextProps);
}
}
recenterDownloader = (props) => {
const {deviceHeight, show} = props;
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
Animated.sequence([
Animated.delay(200),
Animated.spring(this.state.downloaderTop, {
toValue: top,
tension: 8,
friction: 5
})
]).start();
};
toggleDownloader = (show = true) => {
const {deviceHeight} = this.props;
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
Animated.spring(this.state.downloaderTop, {
@ -67,7 +88,7 @@ export default class Downloader extends PureComponent {
this.startDownload();
}
});
}
};
startDownload = async () => {
try {
@ -181,15 +202,19 @@ export default class Downloader extends PureComponent {
</View>
</View>
);
}
};
render() {
const {show} = this.props;
if (!show) {
return null;
}
const {didCancel, progress} = this.state;
const trueProgress = didCancel ? 0 : progress;
const containerHeight = show ? deviceHeight : 0;
const containerHeight = show ? '100%' : 0;
return (
<View style={[styles.container, {height: containerHeight}]}>
<AnimatedView style={[styles.downloader, {top: this.state.downloaderTop}]}>
@ -241,11 +266,11 @@ const styles = StyleSheet.create({
position: 'absolute',
top: 0,
left: 0,
width: deviceWidth
width: '100%'
},
downloader: {
alignItems: 'center',
left: (deviceWidth / 2) - 118,
alignSelf: 'center',
height: 220,
width: 236,
borderRadius: 8,

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Dimensions,
InteractionManager,
PanResponder,
Platform,
@ -18,7 +17,6 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import LinearGradient from 'react-native-linear-gradient';
import Orientation from 'react-native-orientation';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -30,7 +28,6 @@ import Downloader from './downloader';
import Previewer from './previewer';
const {View: AnimatedView} = Animated;
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
const DRAG_VERTICAL_THRESHOLD_START = 25; // When do we want to start capturing the drag
const DRAG_VERTICAL_THRESHOLD_END = 100; // When do we want to navigate back
const DRAG_HORIZONTAL_THRESHOLD = 50; // Make sure that it's not a sloppy horizontal swipe
@ -46,6 +43,8 @@ export default class ImagePreview extends PureComponent {
addFileToFetchCache: PropTypes.func.isRequired
}),
canDownloadFiles: PropTypes.bool.isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
fetchCache: PropTypes.object.isRequired,
fileId: PropTypes.string.isRequired,
files: PropTypes.array.isRequired,
@ -60,10 +59,9 @@ export default class ImagePreview extends PureComponent {
this.zoomableImages = {};
const currentFile = props.files.findIndex((file) => file.id === props.fileId);
this.state = {
currentFile,
deviceHeight: deviceHeight - STATUSBAR_HEIGHT,
deviceWidth,
drag: new Animated.ValueXY(),
footerOpacity: new Animated.Value(1),
pagingEnabled: true,
@ -85,7 +83,6 @@ export default class ImagePreview extends PureComponent {
}
componentDidMount() {
Orientation.unlockAllOrientations();
InteractionManager.runAfterInteractions(() => {
this.scrollView.scrollTo({x: (this.state.currentFile) * this.state.deviceWidth, animated: false});
Animated.timing(this.state.wrapperViewOpacity, {
@ -95,8 +92,15 @@ export default class ImagePreview extends PureComponent {
});
}
componentWillReceiveProps(nextProps) {
if (this.props.deviceWidth !== nextProps.deviceWidth && Platform.OS === 'android') {
InteractionManager.runAfterInteractions(() => {
this.scrollView.scrollTo({x: (this.state.currentFile * nextProps.deviceWidth), animated: false});
});
}
}
componentWillUnmount() {
Orientation.lockToPortrait();
if (Platform.OS === 'ios') {
StatusBar.setHidden(false, 'fade');
}
@ -145,7 +149,7 @@ export default class ImagePreview extends PureComponent {
handleImageDoubleTap = (x, y) => {
this.zoomableImages[this.state.currentFile].toggleZoom(x, y);
}
};
setHeaderAndFileInfoVisible = (show) => {
this.setState({
@ -165,7 +169,7 @@ export default class ImagePreview extends PureComponent {
};
handleScroll = (event) => {
const offset = event.nativeEvent.contentOffset.x / this.state.deviceWidth;
const offset = event.nativeEvent.contentOffset.x / this.props.deviceWidth;
const wholeNumber = Number((offset).toFixed(0));
if (Math.abs(offset - wholeNumber) < 0.01) {
this.setState({
@ -184,15 +188,6 @@ export default class ImagePreview extends PureComponent {
this.scrollView = c;
};
onLayout = (event) => {
if (event.nativeEvent.layout.width !== this.state.deviceWidth) {
this.setState({
deviceHeight: event.nativeEvent.layout.height,
deviceWidth: event.nativeEvent.layout.width
});
}
};
imageIsZooming = (zooming) => {
if (zooming !== this.state.isZooming) {
this.setHeaderAndFileInfoVisible(!zooming);
@ -212,7 +207,7 @@ export default class ImagePreview extends PureComponent {
} else {
this.showIOSDownloadOptions();
}
}
};
showIOSDownloadOptions = () => {
this.setHeaderAndFileInfoVisible(false);
@ -244,7 +239,7 @@ export default class ImagePreview extends PureComponent {
modalPresentationStyle: 'overCurrentContext'
}
});
}
};
showDownloader = () => {
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
@ -252,14 +247,14 @@ export default class ImagePreview extends PureComponent {
this.setState({
showDownloader: true
});
}
};
hideDownloader = (hideFileInfo = true) => {
this.setState({showDownloader: false});
if (hideFileInfo) {
this.setHeaderAndFileInfoVisible(true);
}
}
};
renderDownloadButton = () => {
const {canDownloadFiles, files} = this.props;
@ -297,10 +292,10 @@ export default class ImagePreview extends PureComponent {
{icon}
</TouchableOpacity>
);
}
};
render() {
const maxImageHeight = this.state.deviceHeight - STATUSBAR_HEIGHT;
const maxImageHeight = this.props.deviceHeight - STATUSBAR_HEIGHT;
const marginStyle = {
...Platform.select({
@ -314,10 +309,7 @@ export default class ImagePreview extends PureComponent {
};
return (
<View
style={[style.wrapper, {height: this.state.deviceHeight, width: this.state.deviceWidth}]}
onLayout={this.onLayout}
>
<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}
@ -332,7 +324,6 @@ export default class ImagePreview extends PureComponent {
bounces={false}
onScroll={this.handleScroll}
scrollEventThrottle={2}
>
{this.props.files.map((file, index) => {
let component;
@ -347,10 +338,10 @@ export default class ImagePreview extends PureComponent {
file={file}
theme={this.props.theme}
imageHeight={Math.min(maxImageHeight, file.height)}
imageWidth={Math.min(this.state.deviceWidth, file.width)}
imageWidth={Math.min(this.props.deviceWidth, file.width)}
shrink={this.state.shouldShrinkImages}
wrapperHeight={this.state.deviceHeight}
wrapperWidth={this.state.deviceWidth}
wrapperHeight={this.props.deviceHeight}
wrapperWidth={this.props.deviceWidth}
onImageTap={this.handleImageTap}
onImageDoubleTap={this.handleImageDoubleTap}
onZoom={this.imageIsZooming}
@ -372,7 +363,7 @@ export default class ImagePreview extends PureComponent {
return (
<View
key={file.id}
style={[style.pageWrapper, {height: this.state.deviceHeight, width: this.state.deviceWidth}]}
style={[style.pageWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth}]}
>
{component}
</View>
@ -380,7 +371,7 @@ export default class ImagePreview extends PureComponent {
})}
</ScrollView>
<AnimatedView
style={[style.footerHeaderWrapper, {height: this.state.deviceHeight, width: this.state.deviceWidth, opacity: this.state.footerOpacity}]}
style={[style.footerHeaderWrapper, {height: this.props.deviceHeight, width: this.props.deviceWidth, opacity: this.state.footerOpacity}]}
pointerEvents='box-none'
>
<View style={style.header}>
@ -417,6 +408,8 @@ export default class ImagePreview extends PureComponent {
<Downloader
show={this.state.showDownloader}
file={this.props.files[this.state.currentFile]}
deviceHeight={this.props.deviceHeight}
deviceWidth={this.props.deviceWidth}
onDownloadCancel={this.hideDownloader}
onDownloadStart={this.hideDownloader}
onDownloadSuccess={this.hideDownloader}

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {Platform} from 'react-native';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getDimensions, getStatusBarHeight} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
@ -19,11 +20,12 @@ function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
canDownloadFiles: canDownloadFilesOnMobile(state),
fetchCache: state.views.fetchCache,
files: getFilesForPost(state, ownProps.postId),
theme: getTheme(state),
statusBarHeight: Platform.OS === 'ios' ? state.views.root.statusBarHeight : STATUSBAR_HEIGHT
statusBarHeight: Platform.OS === 'ios' ? getStatusBarHeight(state) : STATUSBAR_HEIGHT
};
};
}

View file

@ -90,6 +90,14 @@ export default class Previewer extends Component {
this.setShrink();
} else if (!this.props.shrink && nextProps.shrink) {
this.setShrink(true);
} else if (
nextProps.imageHeight !== this.props.imageHeight ||
nextProps.imageWidth !== this.props.imageWidth
) {
this.setState({
imageHeight: new Animated.Value(nextProps.imageHeight),
imageWidth: new Animated.Value(nextProps.imageWidth)
});
}
}
@ -248,7 +256,7 @@ export default class Previewer extends Component {
<View
{...this.panResponder.panHandlers}
onResponderRelease={this.handleResponderRelease}
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth}]}
style={[style.fileImageWrapper, {height: '100%', width: '100%'}]}
>
<AnimatedView style={{height: imageHeight, width: this.state.imageWidth, backgroundColor: '#000', opacity}}>
<ImageView

View file

@ -40,7 +40,7 @@ import UserProfile from 'app/screens/user_profile';
import IntlWrapper from 'app/components/root';
function wrapWithContextProvider(Comp, excludeEvents = false) {
function wrapWithContextProvider(Comp, excludeEvents = true) {
return (props) => { //eslint-disable-line react/display-name
const {navigator} = props; //eslint-disable-line react/prop-types
return (
@ -58,7 +58,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider);
Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(AddReaction), store, Provider);
Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(AdvancedSettings), store, Provider);
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider);
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel, false), store, Provider);
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
@ -73,7 +73,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider);
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider);
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification, true), store, Provider);
Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification), store, Provider);
Navigation.registerComponent('NotificationSettings', () => wrapWithContextProvider(NotificationSettings), store, Provider);
Navigation.registerComponent('NotificationSettingsEmail', () => wrapWithContextProvider(NotificationSettingsEmail), store, Provider);
Navigation.registerComponent('NotificationSettingsMentions', () => wrapWithContextProvider(NotificationSettingsMentions), store, Provider);

View file

@ -9,14 +9,15 @@ import {
Image,
InteractionManager,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableWithoutFeedback,
View
} from 'react-native';
import Button from 'react-native-button';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import Orientation from 'react-native-orientation';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
@ -57,10 +58,8 @@ class Login extends PureComponent {
};
}
componentDidMount() {
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
}
componentWillMount() {
Orientation.addOrientationListener(this.orientationDidChange);
}
componentWillReceiveProps(nextProps) {
@ -70,9 +69,7 @@ class Login extends PureComponent {
}
componentWillUnmount() {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
Orientation.removeOrientationListener(this.orientationDidChange);
}
goToLoadTeam = (expiresAt) => {
@ -123,10 +120,6 @@ class Login extends PureComponent {
});
};
handleAndroidKeyboard = () => {
this.blur();
};
blur = () => {
this.loginId.blur();
this.passwd.blur();
@ -286,6 +279,14 @@ class Login extends PureComponent {
this.passwd.focus();
};
orientationDidChange = () => {
this.scroll.scrollToPosition(0, 0, true);
};
scrollRef = (ref) => {
this.scroll = ref;
};
render() {
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED;
@ -313,14 +314,14 @@ class Login extends PureComponent {
}
return (
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={Platform.OS === 'ios' ? 65 : 0}
>
<View style={{flex: 1}}>
<StatusBar/>
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
contentContainerStyle={style.innerContainer}
>
<Image
source={logo}
/>
@ -347,6 +348,7 @@ class Login extends PureComponent {
returnKeyType='next'
underlineColorAndroid='transparent'
onSubmitEditing={this.passwordFocus}
blurOnSubmit={false}
/>
<TextInput
ref={this.passwordRef}
@ -362,11 +364,25 @@ class Login extends PureComponent {
onSubmitEditing={this.preSignIn}
/>
{proceed}
</View>
</KeyboardAwareScrollView>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
</View>
);
}
}
const style = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
flex: 1
},
innerContainer: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 50
}
});
export default injectIntl(Login);

View file

@ -6,10 +6,12 @@ import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Image,
Text,
View
ScrollView,
StyleSheet,
Text
} from 'react-native';
import Button from 'react-native-button';
import Orientation from 'react-native-orientation';
import semver from 'semver';
import {ViewTypes} from 'app/constants';
@ -31,6 +33,14 @@ class LoginOptions extends PureComponent {
theme: PropTypes.object
};
componentWillMount() {
Orientation.addOrientationListener(this.orientationDidChange);
}
componentWillUnmount() {
Orientation.removeOrientationListener(this.orientationDidChange);
}
goToLogin = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
@ -66,6 +76,10 @@ class LoginOptions extends PureComponent {
});
};
orientationDidChange = () => {
this.scroll.scrollTo({x: 0, y: 0, animated: true});
};
renderEmailOption = () => {
const {config} = this.props;
if (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') {
@ -172,9 +186,17 @@ class LoginOptions extends PureComponent {
return null;
};
scrollRef = (ref) => {
this.scroll = ref;
};
render() {
return (
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>
<ScrollView
style={style.container}
contentContainerStyle={style.innerContainer}
ref={this.scrollRef}
>
<StatusBar/>
<Image
source={logo}
@ -196,9 +218,23 @@ class LoginOptions extends PureComponent {
{this.renderLdapOption()}
{this.renderGitlabOption()}
{this.renderSamlOption()}
</View>
</ScrollView>
);
}
}
const style = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
flex: 1
},
innerContainer: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 50
}
});
export default injectIntl(LoginOptions);

View file

@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToNotification} from 'app/actions/views/root';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
@ -15,6 +16,7 @@ import Notification from './notification';
function mapStateToProps(state, ownProps) {
const {data} = ownProps.notification;
const {deviceWidth} = getDimensions(state);
let user;
if (data.sender_id) {
@ -30,6 +32,7 @@ function mapStateToProps(state, ownProps) {
...ownProps,
config: state.entities.general.config,
channel,
deviceWidth,
user,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state)

View file

@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Image,
Platform,
StyleSheet,
@ -32,6 +31,7 @@ export default class Notification extends PureComponent {
}).isRequired,
channel: PropTypes.object,
config: PropTypes.object,
deviceWidth: PropTypes.number.isRequired,
notification: PropTypes.object.isRequired,
teammateNameDisplay: PropTypes.string,
navigator: PropTypes.object,
@ -161,7 +161,8 @@ export default class Notification extends PureComponent {
};
render() {
const {message} = this.props.notification;
const {deviceWidth, notification} = this.props;
const {message} = notification;
if (message) {
const msg = message.split(':');
@ -172,7 +173,7 @@ export default class Notification extends PureComponent {
const icon = this.getNotificationIcon();
return (
<View style={style.container}>
<View style={[style.container, {width: deviceWidth}]}>
<TouchableOpacity
style={{flex: 1, flexDirection: 'row'}}
onPress={this.notificationTapped}
@ -208,7 +209,6 @@ const style = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'flex-start',
paddingHorizontal: 10,
width: Dimensions.get('window').width,
...Platform.select({
android: {
height: 68

View file

@ -3,11 +3,14 @@
import {connect} from 'react-redux';
import {getDimensions} from 'app/selectors/device';
import OptionsModal from './options_modal';
function mapStateToProps(state, ownProps) {
return {
...ownProps
...ownProps,
...getDimensions(state)
};
}

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Dimensions,
StyleSheet,
TouchableWithoutFeedback,
View
@ -19,12 +18,13 @@ import {emptyFunction} from 'app/utils/general';
import OptionsModalList from './options_modal_list';
const {View: AnimatedView} = Animated;
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
const DURATION = 200;
export default class OptionsModal extends PureComponent {
static propTypes = {
items: PropTypes.array.isRequired,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
navigator: PropTypes.object,
onCancelPress: PropTypes.func,
title: PropTypes.oneOfType([
@ -35,12 +35,16 @@ export default class OptionsModal extends PureComponent {
static defaultProps = {
onCancelPress: emptyFunction
}
state = {
top: new Animated.Value(deviceHeight)
};
constructor(props) {
super(props);
this.state = {
top: new Animated.Value(props.deviceHeight)
};
}
componentDidMount() {
EventEmitter.on(NavigationTypes.NAVIGATION_CLOSE_MODAL, this.close);
Animated.timing(this.state.top, {
@ -56,11 +60,11 @@ export default class OptionsModal extends PureComponent {
handleCancel = () => {
this.props.onCancelPress();
this.close();
}
};
close = () => {
Animated.timing(this.state.top, {
toValue: deviceHeight,
toValue: this.props.deviceHeight,
duration: DURATION
}).start(() => {
this.props.navigator.dismissModal({
@ -78,7 +82,7 @@ export default class OptionsModal extends PureComponent {
return (
<TouchableWithoutFeedback onPress={this.close}>
<View style={style.wrapper}>
<AnimatedView style={{height: deviceHeight, left: 0, top: this.state.top, width: deviceWidth}}>
<AnimatedView style={{height: this.props.deviceHeight, left: 0, top: this.state.top, width: this.props.deviceWidth}}>
<OptionsModalList
items={items}
onCancelPress={this.handleCancel}

View file

@ -5,7 +5,6 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
Keyboard,
Platform,
SectionList,
@ -609,7 +608,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
header: {
backgroundColor: theme.sidebarHeaderBg,
width: Dimensions.get('window').width,
width: '100%',
...Platform.select({
android: {
height: 46,

View file

@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {handleCommentDraftChanged} from 'app/actions/views/thread';
import {getStatusBarHeight} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {selectPost} from 'mattermost-redux/actions/posts';
@ -21,7 +22,6 @@ function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
const posts = getPostsForThread(state, ownProps);
const threadDraft = state.views.thread.drafts[ownProps.rootId];
const {statusBarHeight} = state.views.root;
return {
...ownProps,
@ -31,7 +31,7 @@ function makeMapStateToProps() {
draft: threadDraft.draft,
files: threadDraft.files,
posts,
statusBarHeight,
statusBarHeight: getStatusBarHeight(state),
theme: getTheme(state)
};
};

22
app/selectors/device.js Normal file
View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export function getDimensions(state) {
return state.device.dimension;
}
export function getConnection(state) {
return state.device.connection;
}
export function getStatusBarHeight(state) {
return state.device.statusBarHeight;
}
export function isLandscape(state) {
return state.device.orientation === 'LANDSCAPE';
}
export function isTablet(state) {
return state.device.isTablet;
}

View file

@ -160,7 +160,7 @@ export default function configureAppStore(initialState) {
autoRehydrate: {
log: false
},
blacklist: ['navigation', 'offline', 'requests'],
blacklist: ['device', 'navigation', 'offline', 'requests'],
debounce: 500,
transforms: [
setTransformer,

View file

@ -24,7 +24,6 @@
140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
1BCA51319AC6442991C6A208 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0A091BF1A3D04650AD306A0D /* Zocial.ttf */; };
2A167658767AAB5161E3271B /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 65FD5EA57EBAE06106094B2F /* libPods-Mattermost.a */; };
2B4C9B708010475DA575B81D /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */; };
2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; };
374634801E8480C2005E1244 /* libRCTOrientation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 374634671E848085005E1244 /* libRCTOrientation.a */; };
@ -60,6 +59,7 @@
7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6877B01E7835E50094B63F /* libToolTipMenu.a */; };
7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; };
7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */; };
7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */; };
7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; };
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; };
7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */; };
@ -67,6 +67,7 @@
895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; };
9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; };
A08D512E7ADC40CCAD055A9E /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */; };
A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 65FD5EA57EBAE06106094B2F /* libPods-Mattermost.a */; };
C035DB50ED2045F09923FFAE /* MaterialCommunityIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 04AA5E8EF3B54735A11E3B95 /* MaterialCommunityIcons.ttf */; };
C337E8708D2845C6A8DBB47E /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */; };
D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */; };
@ -441,6 +442,13 @@
remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D;
remoteInfo = ReactNativeNavigation;
};
7FDB92A61F706F45006CDFD1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 014A3B5C1C6CF33500B6D375;
remoteInfo = RNImagePicker;
};
7FDF28E01E1F4B1F00DBBE56 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */;
@ -543,6 +551,7 @@
7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = "<group>"; };
7F6877AA1E7835E50094B63F /* ToolTipMenu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ToolTipMenu.xcodeproj; path = "../node_modules/react-native-tooltip/ToolTipMenu.xcodeproj"; sourceTree = "<group>"; };
7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = "<group>"; };
7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = "<group>"; };
7FEB10961F6101710039A015 /* BlurAppScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BlurAppScreen.h; path = Mattermost/BlurAppScreen.h; sourceTree = "<group>"; };
7FEB10971F6101710039A015 /* BlurAppScreen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BlurAppScreen.m; path = Mattermost/BlurAppScreen.m; sourceTree = "<group>"; };
7FEB10991F61019C0039A015 /* MattermostManaged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MattermostManaged.h; path = Mattermost/MattermostManaged.h; sourceTree = "<group>"; };
@ -615,9 +624,10 @@
DEBCF44F1F46413BB407D316 /* libz.tbd in Frameworks */,
7F43D5DD1F6BF95F001FC614 /* libRNCookieManagerIOS.a in Frameworks */,
7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */,
7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */,
7F43D5DE1F6BF96A001FC614 /* libRCTYouTube.a in Frameworks */,
7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */,
2A167658767AAB5161E3271B /* libPods-Mattermost.a in Frameworks */,
A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -975,6 +985,14 @@
name = Products;
sourceTree = "<group>";
};
7FDB92761F706F45006CDFD1 /* Products */ = {
isa = PBXGroup;
children = (
7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */,
);
name = Products;
sourceTree = "<group>";
};
7FDF28C41E1F4B1F00DBBE56 /* Products */ = {
isa = PBXGroup;
children = (
@ -995,6 +1013,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */,
37ABD3971F4CE13B001FDE6B /* ART.xcodeproj */,
3752184A1F4B9E980035444B /* RCTCameraRoll.xcodeproj */,
7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */,
@ -1244,6 +1263,10 @@
ProductGroup = 3752181C1F4B9E320035444B /* Products */;
ProjectRef = 546CB0A5BB8F453890CBA559 /* RNFetchBlob.xcodeproj */;
},
{
ProductGroup = 7FDB92761F706F45006CDFD1 /* Products */;
ProjectRef = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */;
},
{
ProductGroup = 7F8C49621F3DFC30003A22BA /* Products */;
ProjectRef = D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */;
@ -1639,6 +1662,13 @@
remoteRef = 7FC200DE1EBB65100099331B /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNImagePicker.a;
remoteRef = 7FDB92A61F706F45006CDFD1 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -1957,6 +1987,7 @@
PRODUCT_NAME = Mattermost;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@ -2002,6 +2033,7 @@
PRODUCT_NAME = Mattermost;
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

View file

@ -16,6 +16,11 @@ mockery.enable({
warnOnUnregistered: false
});
mockery.registerMock('react-native', {
Dimensions: {
get: () => {
return {width: 0, height: 0}
}
},
NativeModules: {}
});
mockery.registerMock('react-native-device-info', {