Sidebars improvement (#1787)

* Sidebars improvement
* Explicitly handle Android back button in code screen
* return null on getDerivedStateFromProps when no state changes needed
This commit is contained in:
Elias Nahum 2018-06-25 15:47:53 -04:00 committed by Sudheer
parent f273d26435
commit ba09fb9792
38 changed files with 336 additions and 438 deletions

View file

@ -6,7 +6,7 @@ import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
export function makeDirectChannel(otherUserId) {
export function makeDirectChannel(otherUserId, switchToChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
@ -27,7 +27,7 @@ export function makeDirectChannel(otherUserId) {
channel = result.data;
}
if (channel) {
if (channel && switchToChannel) {
dispatch(handleSelectChannel(channel.id));
}

View file

@ -9,15 +9,79 @@ import {
} from 'react-native';
import Placeholder from 'rn-placeholder';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelLoader extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
}).isRequired,
backgroundColor: PropTypes.string,
channelIsLoading: PropTypes.bool.isRequired,
maxRows: PropTypes.number,
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
};
static defaultProps = {
maxRows: 6,
};
state = {
switch: false,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (!nextProps.channelIsLoading && prevState.switch) {
return {
switch: false,
channel: null,
currentChannelId: null,
};
}
return null;
}
componentDidMount() {
EventEmitter.on('switch_channel', this.handleChannelSwitch);
}
componentWillUnmount() {
EventEmitter.off('switch_channel', this.handleChannelSwitch);
}
componentDidUpdate() {
if (this.state.switch) {
const {
handleSelectChannel,
markChannelAsRead,
markChannelAsViewed,
setChannelLoading,
} = this.props.actions;
const {channel, currentChannelId} = this.state;
setTimeout(() => {
handleSelectChannel(channel.id);
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
setChannelLoading(false);
}, 250);
}
}
buildSections({key, style, bg, color}) {
return (
<View
@ -38,8 +102,16 @@ export default class ChannelLoader extends PureComponent {
);
}
handleChannelSwitch = (channel, currentChannelId) => {
if (channel.id === currentChannelId) {
this.props.actions.setChannelLoading(false);
} else {
this.setState({switch: true, channel, currentChannelId});
}
};
render() {
const {channelIsLoading, theme} = this.props;
const {channelIsLoading, maxRows, style: styleProp, theme} = this.props;
if (!channelIsLoading) {
return null;
@ -49,8 +121,8 @@ export default class ChannelLoader extends PureComponent {
const bg = this.props.backgroundColor || theme.centerChannelBg;
return (
<View style={[style.container, {backgroundColor: bg}]}>
{Array(6).fill().map((item, index) => this.buildSections({
<View style={[style.container, styleProp, {backgroundColor: bg}]}>
{Array(maxRows).fill().map((item, index) => this.buildSections({
key: index,
style,
bg,
@ -70,7 +142,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
top: 0,
},
ios: {
top: 15,
paddingTop: 15,
},
}),
},

View file

@ -1,9 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel';
import ChannelLoader from './channel_loader';
function mapStateToProps(state, ownProps) {
@ -13,4 +18,15 @@ function mapStateToProps(state, ownProps) {
};
}
export default connect(mapStateToProps)(ChannelLoader);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
markChannelAsViewed,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelLoader);

View file

@ -1,99 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Keyboard, Dimensions} from 'react-native';
import PropTypes from 'prop-types';
import BaseDrawer from 'react-native-drawer';
// Extends react-native-drawer to allow better control over the open/closed state from the parent
export default class Drawer extends BaseDrawer {
static propTypes = {
...BaseDrawer.propTypes,
onRequestClose: PropTypes.func.isRequired,
bottomPanOffset: PropTypes.number,
topPanOffset: PropTypes.number,
};
constructor(props) {
super(props);
this.keyboardHeight = 0;
}
componentDidMount() {
Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
Keyboard.removeListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', this.keyboardDidHide);
}
getMainHeight = () => '100%';
keyboardDidShow = (e) => {
this.keyboardHeight = e.endCoordinates.height;
};
keyboardDidHide = () => {
this.keyboardHeight = 0;
};
isOpened = () => {
return this._open; //eslint-disable-line no-underscore-dangle
};
processTapGestures = () => {
// Note that we explicitly don't support tap to open or double tap because I didn't copy them over
if (this._activeTween) { //eslint-disable-line no-underscore-dangle
return false;
}
if (this.props.tapToClose && this._open) { //eslint-disable-line no-underscore-dangle
this.props.onRequestClose();
return true;
}
return false;
};
testPanResponderMask = (e) => {
if (this.props.disabled) {
return false;
}
// Disable if parent or child drawer exist and are open
if (this.context.drawer && this.context.drawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
if (this._childDrawer && this._childDrawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
const topPanOffset = this.props.topPanOffset || 0;
const bottomPanOffset = this.props.bottomPanOffset || 0;
const height = Dimensions.get('window').height;
if ((this.props.topPanOffset && e.nativeEvent.pageY < topPanOffset) ||
(this.props.bottomPanOffset && e.nativeEvent.pageY > (height - (bottomPanOffset + this.keyboardHeight)))) {
return false;
}
const pos0 = this.isLeftOrRightSide() ? e.nativeEvent.pageX : e.nativeEvent.pageY;
const deltaOpen = this.isLeftOrTopSide() ? this.getDeviceLength() - pos0 : pos0;
const deltaClose = this.isLeftOrTopSide() ? pos0 : this.getDeviceLength() - pos0;
if (this._open && deltaOpen > this.getOpenMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
if (!this._open && deltaClose > this.getClosedMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
return true;
};
}

View file

@ -102,6 +102,7 @@ export default class SearchBarAndroid extends PureComponent {
InteractionManager.runAfterInteractions(() => {
this.setState({
isFocused: false,
value: '',
}, () => {
this.props.onCancelButtonPress();
});

View file

@ -49,15 +49,15 @@ export default class ChannelsList extends PureComponent {
}
onSelectChannel = (channel, currentChannelId) => {
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
if (channel.fake) {
this.props.onJoinChannel(channel, currentChannelId);
} else {
this.props.onSelectChannel(channel, currentChannelId);
}
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
};
onSearch = (term) => {

View file

@ -19,7 +19,7 @@ import {General} from 'mattermost-redux/constants';
import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;

View file

@ -16,7 +16,7 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
@ -313,7 +313,7 @@ export default class List extends PureComponent {
emitUnreadIndicatorChange = debounce((showIndicator) => {
if (showIndicator && !UnreadIndicator) {
UnreadIndicator = require('app/components/channel_drawer/channels_list/unread_indicator').default;
UnreadIndicator = require('app/components/sidebars/main/channels_list/unread_indicator').default;
}
this.setState({showIndicator});
}, 100);

View file

@ -25,9 +25,11 @@ export default class DrawerSwiper extends Component {
};
shouldComponentUpdate(nextProps) {
const {deviceWidth, showTeams, theme} = this.props;
const {deviceWidth, openDrawerOffset, showTeams, theme} = this.props;
return nextProps.deviceWidth !== deviceWidth ||
nextProps.showTeams !== showTeams || nextProps.theme !== theme;
nextProps.showTeams !== showTeams ||
nextProps.openDrawerOffset !== openDrawerOffset ||
nextProps.theme !== theme;
}
runOnLayout = (shouldRun = true) => {

View file

@ -4,21 +4,22 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {joinChannel} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {isLandscape, isTablet, getDimensions} from 'app/selectors/device';
import ChannelDrawer from './channel_drawer.js';
import MainSidebar from './main_sidebar.js';
function mapStateToProps(state) {
const {currentUserId} = state.entities.users;
return {
...getDimensions(state),
currentTeamId: getCurrentTeamId(state),
currentUserId,
isLandscape: isLandscape(state),
@ -32,15 +33,12 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getTeams,
handleSelectChannel,
joinChannel,
markChannelAsViewed,
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,
setChannelLoading,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(MainSidebar);

View file

@ -5,42 +5,31 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
StyleSheet,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import DrawerLayout from 'react-native-drawer-layout';
import {General, WebsocketEvents} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import Drawer from 'app/components/drawer';
import SafeAreaView from 'app/components/safe_area_view';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swipper';
import TeamsList from './teams_list';
const {
ANDROID_TOP_LANDSCAPE,
ANDROID_TOP_PORTRAIT,
IOS_TOP_LANDSCAPE,
IOS_TOP_PORTRAIT,
} = ViewTypes;
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends Component {
export default class ChannelSidebar extends Component {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
makeDirectChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
}).isRequired,
@ -48,16 +37,18 @@ export default class ChannelDrawer extends Component {
children: PropTypes.node,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTablet: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
navigator: PropTypes.object,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
};
closeHandle = null;
openHandle = null;
static contextTypes = {
intl: intlShape.isRequired,
};
swiperIndex = 1;
constructor(props) {
@ -67,6 +58,9 @@ export default class ChannelDrawer extends Component {
if (props.isLandscape || props.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.drawerOpened = false;
this.state = {
show: false,
openDrawerOffset,
@ -118,8 +112,8 @@ export default class ChannelDrawer extends Component {
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.drawerOpened && this.refs.drawer) {
this.refs.drawer.closeDrawer();
return true;
}
@ -131,8 +125,8 @@ export default class ChannelDrawer extends Component {
};
closeChannelDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.drawerOpened && this.refs.drawer) {
this.refs.drawer.closeDrawer();
}
};
@ -141,53 +135,16 @@ export default class ChannelDrawer extends Component {
};
handleDrawerClose = () => {
this.drawerOpened = false;
this.resetDrawer();
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
}
};
handleDrawerCloseStart = () => {
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
}
};
handleDrawerOpen = () => {
this.drawerOpened = true;
if (this.state.openDrawerOffset !== 0) {
Keyboard.dismiss();
}
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
}
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: this.props.theme.centerChannelBg,
elevation: 3,
opacity,
},
drawerOverlay: {
backgroundColor: ratio ? '#000' : '#FFF',
opacity: ratio ? (1 - ratio) / 2 : 1,
},
};
};
handleUpdateTitle = (channel) => {
@ -198,11 +155,11 @@ export default class ChannelDrawer extends Component {
this.props.actions.setChannelDisplayName(channelName);
};
openChannelDrawer = () => {
openChannelSidebar = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
if (this.refs.drawer) {
this.refs.drawer.openDrawer();
}
};
@ -212,38 +169,25 @@ export default class ChannelDrawer extends Component {
} = this.props;
const {
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed,
} = actions;
tracker.channelSwitch = Date.now();
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
handleSelectChannel(channel.id);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
});
});
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
EventEmitter.emit('switch_channel', channel, currentChannelId);
};
joinChannel = async (channel, currentChannelId) => {
const {intl} = this.context;
const {
actions,
currentTeamId,
currentUserId,
intl,
} = this.props;
const {
@ -256,7 +200,7 @@ export default class ChannelDrawer extends Component {
let result;
if (channel.type === General.DM_CHANNEL) {
result = await makeDirectChannel(channel.id);
result = await makeDirectChannel(channel.id, false);
if (result.error) {
const dmFailedMessage = {
@ -292,10 +236,6 @@ export default class ChannelDrawer extends Component {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
@ -304,10 +244,6 @@ export default class ChannelDrawer extends Component {
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.setState({openDrawerOffset: 0});
};
@ -323,7 +259,7 @@ export default class ChannelDrawer extends Component {
}
};
renderContent = () => {
renderNavigationView = () => {
const {
navigator,
teamsCount,
@ -401,53 +337,19 @@ export default class ChannelDrawer extends Component {
};
render() {
const {children, isLandscape} = this.props;
const {children, deviceWidth} = this.props;
const {openDrawerOffset} = this.state;
const androidTop = isLandscape ? ANDROID_TOP_LANDSCAPE : ANDROID_TOP_PORTRAIT;
const iosTop = isLandscape ? IOS_TOP_LANDSCAPE : IOS_TOP_PORTRAIT;
return (
<Drawer
<DrawerLayout
ref='drawer'
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onCloseStart={this.handleDrawerCloseStart}
captureGestures='open'
type={Platform.OS === 'ios' ? 'static' : 'displace'}
acceptTap={true}
acceptPanOnDrawer={false}
disabled={false}
content={this.renderContent()}
tapToClose={true}
openDrawerOffset={openDrawerOffset}
onRequestClose={this.closeChannelDrawer}
panOpenMask={0.2}
panCloseMask={openDrawerOffset}
panThreshold={0.25}
acceptPan={true}
negotiatePan={true}
useInteractionManager={false}
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={-5}
bottomPanOffset={Platform.OS === 'ios' ? ANDROID_TOP_LANDSCAPE : IOS_TOP_PORTRAIT}
topPanOffset={Platform.OS === 'ios' ? iosTop : androidTop}
styles={{
main: {
shadowColor: '#000000',
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0,
},
},
}}
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}
drawerWidth={deviceWidth - openDrawerOffset}
>
{children}
</Drawer>
</DrawerLayout>
);
}
}

View file

@ -6,15 +6,18 @@ import {connect} from 'react-redux';
import {logout, setStatus} from 'mattermost-redux/actions/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import SettingsDrawer from './settings_drawer';
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import {getDimensions} from 'app/selectors/device';
import SettingsSidebar from './settings_sidebar';
function mapStateToProps(state) {
const currentUser = getCurrentUser(state);
const status = getStatusForUserId(state, currentUser.id);
return {
...getDimensions(state),
currentUser,
status,
theme: getTheme(state),
@ -30,4 +33,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(SettingsDrawer);
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(SettingsSidebar);

View file

@ -8,17 +8,16 @@ import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
ScrollView,
View,
} from 'react-native';
import DrawerLayout from 'react-native-drawer-layout';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import Drawer from 'app/components/drawer';
import UserStatus from 'app/components/user_status';
import {NavigationTypes} from 'app/constants';
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
@ -40,8 +39,9 @@ export default class SettingsDrawer extends PureComponent {
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentUser: PropTypes.object.isRequired,
status: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
navigator: PropTypes.object,
status: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
};
@ -56,10 +56,7 @@ export default class SettingsDrawer extends PureComponent {
constructor(props) {
super(props);
this.closeHandle = null;
this.openHandle = null;
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
@ -73,68 +70,30 @@ export default class SettingsDrawer extends PureComponent {
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.refs.drawer) {
this.refs.drawer.closeDrawer();
return true;
}
return false;
};
openSettingsDrawer = () => {
openSettingsSidebar = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
if (this.refs.drawer) {
this.refs.drawer.openDrawer();
}
};
closeSettingsDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
}
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: '#000',
elevation: 3,
opacity,
},
};
};
handleDrawerClose = () => {
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
}
};
handleDrawerCloseStart = () => {
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
closeSettingsSidebar = () => {
if (this.refs.drawer) {
this.refs.drawer.closeDrawer();
}
};
handleDrawerOpen = () => {
Keyboard.dismiss();
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
}
};
handleSetStatus = preventDoubleTap(() => {
@ -185,7 +144,6 @@ export default class SettingsDrawer extends PureComponent {
const {currentUser} = this.props;
const {formatMessage} = this.context.intl;
this.closeSettingsDrawer();
this.openModal(
'EditProfile',
formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
@ -196,7 +154,6 @@ export default class SettingsDrawer extends PureComponent {
goToFlagged = preventDoubleTap(() => {
const {formatMessage} = this.context.intl;
this.closeSettingsDrawer();
this.openModal(
'FlaggedPosts',
formatMessage({id: 'search_header.title3', defaultMessage: 'Flagged Posts'}),
@ -206,7 +163,6 @@ export default class SettingsDrawer extends PureComponent {
goToMentions = preventDoubleTap(() => {
const {intl} = this.context;
this.closeSettingsDrawer();
this.openModal(
'RecentMentions',
intl.formatMessage({id: 'search_header.title2', defaultMessage: 'Recent Mentions'}),
@ -216,7 +172,6 @@ export default class SettingsDrawer extends PureComponent {
goToSettings = preventDoubleTap(() => {
const {intl} = this.context;
this.closeSettingsDrawer();
this.openModal(
'Settings',
intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
@ -225,33 +180,36 @@ export default class SettingsDrawer extends PureComponent {
logout = preventDoubleTap(() => {
const {logout} = this.props.actions;
this.closeSettingsDrawer();
InteractionManager.runAfterInteractions(logout);
this.closeSettingsSidebar();
logout();
});
openModal = (screen, title, passProps) => {
const {navigator, theme} = this.props;
this.closeSettingsDrawer();
navigator.showModal({
screen,
title,
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton,
}],
},
passProps,
this.closeSettingsSidebar();
InteractionManager.runAfterInteractions(() => {
navigator.showModal({
screen,
title,
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton,
}],
},
passProps,
});
});
};
@ -270,7 +228,7 @@ export default class SettingsDrawer extends PureComponent {
);
};
renderContent = () => {
renderNavigationView = () => {
const {currentUser, navigator, theme} = this.props;
const style = getStyleSheet(theme);
@ -382,50 +340,18 @@ export default class SettingsDrawer extends PureComponent {
};
render() {
const {children} = this.props;
const {children, deviceWidth} = this.props;
return (
<Drawer
<DrawerLayout
ref='drawer'
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onCloseStart={this.handleDrawerCloseStart}
side='right'
captureGestures='open'
type='overlay'
acceptTap={true}
acceptPanOnDrawer={false}
disabled={false}
content={this.renderContent()}
tapToClose={true}
openDrawerOffset={DRAWER_INITIAL_OFFSET}
onRequestClose={this.closeSettingsDrawer}
panOpenMask={0.05}
panCloseMask={DRAWER_INITIAL_OFFSET}
panThreshold={0.25}
acceptPan={true}
negotiatePan={true}
useInteractionManager={false}
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={5}
bottomPanOffset={Platform.OS === 'ios' ? 46 : 64}
topPanOffset={Platform.OS === 'ios' ? 64 : 46}
styles={{
main: {
shadowColor: '#000000',
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0,
},
},
}}
renderNavigationView={this.renderNavigationView}
onDrawerOpen={this.handleDrawerOpen}
drawerPosition='right'
drawerWidth={deviceWidth - DRAWER_INITIAL_OFFSET}
>
{children}
</Drawer>
</DrawerLayout>
);
}
}

View file

@ -8,7 +8,9 @@ import {
ScrollView,
ViewPagerAndroid,
Platform,
StyleSheet, InteractionManager,
StyleSheet,
InteractionManager,
PanResponder,
} from 'react-native';
export default class Swiper extends PureComponent {
@ -52,6 +54,21 @@ export default class Swiper extends PureComponent {
this.state = this.initialState(props);
}
componentWillMount() {
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
});
}
componentWillReceiveProps(nextProps) {
if (this.props.width !== nextProps.width) {
this.scrollByWidth(nextProps.width);
}
}
componentDidUpdate(prevProps, prevState) {
// If the index has changed, we notify the parent via the onIndexChanged callback
if (this.state.index !== prevState.index) {
@ -61,7 +78,6 @@ export default class Swiper extends PureComponent {
initialState = (props) => {
const index = props.initialPage;
return {
index,
total: React.Children.count(props.children),
@ -69,20 +85,7 @@ export default class Swiper extends PureComponent {
};
onLayout = (e) => {
this.offset = e.nativeEvent.layout.width * this.state.index;
if (this.runOnLayout) {
if (Platform.OS === 'ios') {
setTimeout(() => {
if (this.scrollView) {
this.scrollView.scrollTo({x: this.props.width * this.state.index, animated: false});
}
}, 100);
} else if (this.scrollView) {
this.scrollView.setPageWithoutAnimation(this.state.index);
}
this.runOnLayout = false;
}
this.scrollByWidth(e.nativeEvent.layout.width);
};
onScrollBegin = () => {
@ -114,6 +117,20 @@ export default class Swiper extends PureComponent {
}
};
scrollByWidth = (width) => {
this.offset = width * this.state.index;
if (Platform.OS === 'ios') {
setTimeout(() => {
if (this.scrollView) {
this.scrollView.scrollTo({x: width * this.state.index, animated: false});
}
}, 0);
} else if (this.scrollView) {
this.scrollView.setPageWithoutAnimation(this.state.index);
}
};
scrollToStart = () => {
if (Platform.OS === 'ios') {
InteractionManager.runAfterInteractions(() => {
@ -135,6 +152,7 @@ export default class Swiper extends PureComponent {
if (Platform.OS === 'ios') {
return (
<ScrollView
{...this.panResponder.panHandlers}
ref={this.refScrollView}
bounces={false}
horizontal={true}
@ -147,7 +165,7 @@ export default class Swiper extends PureComponent {
onMomentumScrollEnd={this.onScrollEnd}
pagingEnabled={true}
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
scrollEnabled={scrollEnabled}
scrollEnabled={true}
>
{pages}
</ScrollView>
@ -287,7 +305,8 @@ const styles = StyleSheet.create({
flex: 1,
},
slide: {
backgroundColor: 'transparent',
flex: 1,
width: '100%',
},
pagination: {
position: 'absolute',

View file

@ -6,15 +6,18 @@ import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
AppState,
Dimensions,
Platform,
View,
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ChannelDrawer from 'app/components/channel_drawer';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import SettingsDrawer from 'app/components/settings_drawer';
import ChannelLoader from 'app/components/channel_loader';
import MainSidebar from 'app/components/sidebars/main';
import SettingsSidebar from 'app/components/sidebars/settings';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import OfflineIndicator from 'app/components/offline_indicator';
import SafeAreaView from 'app/components/safe_area_view';
@ -30,6 +33,15 @@ import LocalConfig from 'assets/config';
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
import {ViewTypes} from 'app/constants';
const {
ANDROID_TOP_LANDSCAPE,
ANDROID_TOP_PORTRAIT,
IOS_TOP_LANDSCAPE,
IOS_TOP_PORTRAIT,
IOSX_TOP_PORTRAIT,
} = ViewTypes;
let ClientUpgradeListener;
export default class Channel extends PureComponent {
@ -61,6 +73,8 @@ export default class Channel extends PureComponent {
constructor(props) {
super(props);
this.isX = DeviceInfo.getModel() === 'iPhone X';
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
}
@ -137,15 +151,41 @@ export default class Channel extends PureComponent {
}
};
channelDrawerRef = (ref) => {
channelLoaderDimensions = () => {
const {isLandscape} = this.props;
let top = 0;
let {height} = Dimensions.get('window');
switch (Platform.OS) {
case 'android':
if (isLandscape) {
top = ANDROID_TOP_LANDSCAPE;
} else {
top = ANDROID_TOP_PORTRAIT;
height = (height - 84);
}
break;
case 'ios':
if (isLandscape) {
top = IOS_TOP_LANDSCAPE;
} else {
height = this.isX ? (height - IOSX_TOP_PORTRAIT) : (height - IOS_TOP_PORTRAIT);
top = this.isX ? IOSX_TOP_PORTRAIT : IOS_TOP_PORTRAIT;
}
break;
}
return {height, top};
};
channelSidebarRef = (ref) => {
if (ref) {
this.channelDrawer = ref.getWrappedInstance();
this.channelSidebar = ref.getWrappedInstance();
}
};
settingsDrawerRef = (ref) => {
settingsSidebarRef = (ref) => {
if (ref) {
this.settingsDrawer = ref.getWrappedInstance();
this.settingsSidebar = ref.getWrappedInstance();
}
};
@ -231,15 +271,15 @@ export default class Channel extends PureComponent {
});
};
openChannelDrawer = () => {
if (this.channelDrawer) {
this.channelDrawer.openChannelDrawer();
openChannelSidebar = () => {
if (this.channelSidebar) {
this.channelSidebar.openChannelSidebar();
}
};
openSettingsDrawer = () => {
if (this.settingsDrawer) {
this.settingsDrawer.openSettingsDrawer();
openSettingsSidebar = () => {
if (this.settingsSidebar) {
this.settingsSidebar.openSettingsSidebar();
}
};
@ -248,10 +288,10 @@ export default class Channel extends PureComponent {
};
render() {
const {intl} = this.context;
const {
channelsRequestFailed,
currentChannelId,
isLandscape,
navigator,
theme,
} = this.props;
@ -283,15 +323,17 @@ export default class Channel extends PureComponent {
);
}
const loaderDimensions = this.channelLoaderDimensions();
// console.warn('height', height, Date.now())
return (
<ChannelDrawer
ref={this.channelDrawerRef}
<MainSidebar
ref={this.channelSidebarRef}
blurPostTextBox={this.blurPostTextBox}
intl={intl}
navigator={navigator}
>
<SettingsDrawer
ref={this.settingsDrawerRef}
<SettingsSidebar
ref={this.settingsSidebarRef}
blurPostTextBox={this.blurPostTextBox}
navigator={navigator}
>
@ -300,8 +342,8 @@ export default class Channel extends PureComponent {
<OfflineIndicator/>
<ChannelNavBar
navigator={navigator}
openChannelDrawer={this.openChannelDrawer}
openSettingsDrawer={this.openSettingsDrawer}
openChannelDrawer={this.openChannelSidebar}
openSettingsDrawer={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<KeyboardLayout>
@ -313,10 +355,14 @@ export default class Channel extends PureComponent {
navigator={navigator}
/>
</KeyboardLayout>
<ChannelLoader
style={[style.channelLoader, loaderDimensions]}
maxRows={isLandscape ? 4 : 6}
/>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
</SettingsDrawer>
</ChannelDrawer>
</SettingsSidebar>
</MainSidebar>
);
}
}
@ -330,5 +376,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
channelLoader: {
position: 'absolute',
width: '100%',
flex: 1,
},
};
});

View file

@ -4,6 +4,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
BackHandler,
Platform,
ScrollView,
StyleSheet,
@ -22,12 +23,25 @@ export default class Code extends React.PureComponent {
content: PropTypes.string.isRequired,
};
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
handleAndroidBack = () => {
this.props.navigator.pop();
return true;
};
countLines = (content) => {
return content.split('\n').length;
};

33
package-lock.json generated
View file

@ -14666,22 +14666,10 @@
"resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-2.1.0.tgz",
"integrity": "sha512-BFCBXwz8xuLvHLVFVeQM+RhaY8yZ38PEWt9WSbq5VIoZ/VssP6uu51XxOfdwaMALOrAHIojK0SiYnd155upZAg=="
},
"react-native-drawer": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/react-native-drawer/-/react-native-drawer-2.5.0.tgz",
"integrity": "sha512-I8rGv0EM6PxRWeq8g463OBt4DYoTri7v9rh98Qpg9q/JToZYTIjavjY0BwchDwyV7J5LdAg7IPbfZUYBkZJsZQ==",
"requires": {
"prop-types": "^15.5.8",
"tween-functions": "^1.0.1"
}
},
"react-native-drawer-layout": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-1.3.2.tgz",
"integrity": "sha512-fjO0scqbJUfNu2wuEpvywL7DYLXuCXJ2W/zYhWz986rdLytidbys1QGVvkaszHrb4Y7OqO96mTkgpOcP8KWevw==",
"requires": {
"react-native-dismiss-keyboard": "1.0.0"
}
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-2.0.0.tgz",
"integrity": "sha512-zYmzv+4QKDwxmAj7pF1yr8VUb+Hp7oBlwMjRZsvv56vPwO89I+kPTI8G78uwSNIf5b4e/iOChE4vpB5J2XsJFA=="
},
"react-native-drawer-layout-polyfill": {
"version": "1.3.2",
@ -14689,6 +14677,16 @@
"integrity": "sha512-XzPhfLDJrYHru+e8+dFwhf0FtTeAp7JXPpFYezYV6P1nTeA1Tia/kDpFT+O2DWTrBKBEI8FGhZnThrroZmHIxg==",
"requires": {
"react-native-drawer-layout": "1.3.2"
},
"dependencies": {
"react-native-drawer-layout": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-1.3.2.tgz",
"integrity": "sha512-fjO0scqbJUfNu2wuEpvywL7DYLXuCXJ2W/zYhWz986rdLytidbys1QGVvkaszHrb4Y7OqO96mTkgpOcP8KWevw==",
"requires": {
"react-native-dismiss-keyboard": "1.0.0"
}
}
}
},
"react-native-exception-handler": {
@ -17795,11 +17793,6 @@
"safe-buffer": "^5.0.1"
}
},
"tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha1-GuOlDnxguz3vd06scHrLynO7w/8="
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",

View file

@ -29,7 +29,7 @@
"react-native-device-info": "enahum/react-native-device-info.git#a7bb3cff1086780b2c791a3e43e5b826fdf3ab11",
"react-native-doc-viewer": "2.7.8",
"react-native-document-picker": "2.1.0",
"react-native-drawer": "2.5.0",
"react-native-drawer-layout": "2.0.0",
"react-native-exception-handler": "2.7.5",
"react-native-fast-image": "4.0.8",
"react-native-fetch-blob": "enahum/react-native-fetch-blob.git#27983c83d7fad1cb4821dd6ba8e405da4af64f3a",