diff --git a/app/actions/views/more_dms.js b/app/actions/views/more_dms.js index 88a506236..4b6e55d8b 100644 --- a/app/actions/views/more_dms.js +++ b/app/actions/views/more_dms.js @@ -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)); } diff --git a/app/components/channel_loader/channel_loader.js b/app/components/channel_loader/channel_loader.js index 4e2b8e66e..aed9b2d6a 100644 --- a/app/components/channel_loader/channel_loader.js +++ b/app/components/channel_loader/channel_loader.js @@ -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 ( { + 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 ( - - {Array(6).fill().map((item, index) => this.buildSections({ + + {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, }, }), }, diff --git a/app/components/channel_loader/index.js b/app/components/channel_loader/index.js index ffa398f6d..11f80c3e2 100644 --- a/app/components/channel_loader/index.js +++ b/app/components/channel_loader/index.js @@ -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); diff --git a/app/components/drawer.js b/app/components/drawer.js deleted file mode 100644 index 73d535f6c..000000000 --- a/app/components/drawer.js +++ /dev/null @@ -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; - }; -} diff --git a/app/components/search_bar/search_bar.android.js b/app/components/search_bar/search_bar.android.js index de42fde33..784e05847 100644 --- a/app/components/search_bar/search_bar.android.js +++ b/app/components/search_bar/search_bar.android.js @@ -102,6 +102,7 @@ export default class SearchBarAndroid extends PureComponent { InteractionManager.runAfterInteractions(() => { this.setState({ isFocused: false, + value: '', }, () => { this.props.onCancelButtonPress(); }); diff --git a/app/components/channel_drawer/channels_list/channel_item/__snapshots__/channel_item.test.js.snap b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap similarity index 100% rename from app/components/channel_drawer/channels_list/channel_item/__snapshots__/channel_item.test.js.snap rename to app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap diff --git a/app/components/channel_drawer/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js similarity index 100% rename from app/components/channel_drawer/channels_list/channel_item/channel_item.js rename to app/components/sidebars/main/channels_list/channel_item/channel_item.js diff --git a/app/components/channel_drawer/channels_list/channel_item/channel_item.test.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.test.js similarity index 100% rename from app/components/channel_drawer/channels_list/channel_item/channel_item.test.js rename to app/components/sidebars/main/channels_list/channel_item/channel_item.test.js diff --git a/app/components/channel_drawer/channels_list/channel_item/index.js b/app/components/sidebars/main/channels_list/channel_item/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/channel_item/index.js rename to app/components/sidebars/main/channels_list/channel_item/index.js diff --git a/app/components/channel_drawer/channels_list/channels_list.js b/app/components/sidebars/main/channels_list/channels_list.js similarity index 100% rename from app/components/channel_drawer/channels_list/channels_list.js rename to app/components/sidebars/main/channels_list/channels_list.js index bd8e1e4ad..6e97a8bdf 100644 --- a/app/components/channel_drawer/channels_list/channels_list.js +++ b/app/components/sidebars/main/channels_list/channels_list.js @@ -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) => { diff --git a/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js similarity index 99% rename from app/components/channel_drawer/channels_list/filtered_list/filtered_list.js rename to app/components/sidebars/main/channels_list/filtered_list/filtered_list.js index beea9333e..9632a6983 100644 --- a/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js +++ b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js @@ -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; diff --git a/app/components/channel_drawer/channels_list/filtered_list/index.js b/app/components/sidebars/main/channels_list/filtered_list/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/filtered_list/index.js rename to app/components/sidebars/main/channels_list/filtered_list/index.js diff --git a/app/components/channel_drawer/channels_list/index.js b/app/components/sidebars/main/channels_list/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/index.js rename to app/components/sidebars/main/channels_list/index.js diff --git a/app/components/channel_drawer/channels_list/list/index.js b/app/components/sidebars/main/channels_list/list/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/list/index.js rename to app/components/sidebars/main/channels_list/list/index.js diff --git a/app/components/channel_drawer/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js similarity index 98% rename from app/components/channel_drawer/channels_list/list/list.js rename to app/components/sidebars/main/channels_list/list/list.js index be07d259a..e5b3a48d6 100644 --- a/app/components/channel_drawer/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -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); diff --git a/app/components/channel_drawer/channels_list/switch_teams_button/index.js b/app/components/sidebars/main/channels_list/switch_teams_button/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/switch_teams_button/index.js rename to app/components/sidebars/main/channels_list/switch_teams_button/index.js diff --git a/app/components/channel_drawer/channels_list/switch_teams_button/switch_teams_button.js b/app/components/sidebars/main/channels_list/switch_teams_button/switch_teams_button.js similarity index 100% rename from app/components/channel_drawer/channels_list/switch_teams_button/switch_teams_button.js rename to app/components/sidebars/main/channels_list/switch_teams_button/switch_teams_button.js diff --git a/app/components/channel_drawer/channels_list/unread_indicator/index.js b/app/components/sidebars/main/channels_list/unread_indicator/index.js similarity index 100% rename from app/components/channel_drawer/channels_list/unread_indicator/index.js rename to app/components/sidebars/main/channels_list/unread_indicator/index.js diff --git a/app/components/channel_drawer/drawer_swipper/drawer_swiper.js b/app/components/sidebars/main/drawer_swipper/drawer_swiper.js similarity index 91% rename from app/components/channel_drawer/drawer_swipper/drawer_swiper.js rename to app/components/sidebars/main/drawer_swipper/drawer_swiper.js index c1fd4d07c..715d5c8f0 100644 --- a/app/components/channel_drawer/drawer_swipper/drawer_swiper.js +++ b/app/components/sidebars/main/drawer_swipper/drawer_swiper.js @@ -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) => { diff --git a/app/components/channel_drawer/drawer_swipper/index.js b/app/components/sidebars/main/drawer_swipper/index.js similarity index 100% rename from app/components/channel_drawer/drawer_swipper/index.js rename to app/components/sidebars/main/drawer_swipper/index.js diff --git a/app/components/channel_drawer/index.js b/app/components/sidebars/main/index.js similarity index 71% rename from app/components/channel_drawer/index.js rename to app/components/sidebars/main/index.js index 65719d943..8d96e5fd5 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/sidebars/main/index.js @@ -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); diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/sidebars/main/main_sidebar.js similarity index 66% rename from app/components/channel_drawer/channel_drawer.js rename to app/components/sidebars/main/main_sidebar.js index 0aae0f1c2..8abb74914 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/sidebars/main/main_sidebar.js @@ -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 ( - {children} - + ); } } diff --git a/app/components/channel_drawer/teams_list/index.js b/app/components/sidebars/main/teams_list/index.js similarity index 100% rename from app/components/channel_drawer/teams_list/index.js rename to app/components/sidebars/main/teams_list/index.js diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/sidebars/main/teams_list/teams_list.js similarity index 100% rename from app/components/channel_drawer/teams_list/teams_list.js rename to app/components/sidebars/main/teams_list/teams_list.js diff --git a/app/components/channel_drawer/teams_list/teams_list_item/index.js b/app/components/sidebars/main/teams_list/teams_list_item/index.js similarity index 100% rename from app/components/channel_drawer/teams_list/teams_list_item/index.js rename to app/components/sidebars/main/teams_list/teams_list_item/index.js diff --git a/app/components/channel_drawer/teams_list/teams_list_item/teams_list_item.js b/app/components/sidebars/main/teams_list/teams_list_item/teams_list_item.js similarity index 100% rename from app/components/channel_drawer/teams_list/teams_list_item/teams_list_item.js rename to app/components/sidebars/main/teams_list/teams_list_item/teams_list_item.js diff --git a/app/components/settings_drawer/drawer_item.js b/app/components/sidebars/settings/drawer_item.js similarity index 100% rename from app/components/settings_drawer/drawer_item.js rename to app/components/sidebars/settings/drawer_item.js diff --git a/app/components/settings_drawer/index.js b/app/components/sidebars/settings/index.js similarity index 83% rename from app/components/settings_drawer/index.js rename to app/components/sidebars/settings/index.js index 0628000ae..4e4b8f263 100644 --- a/app/components/settings_drawer/index.js +++ b/app/components/sidebars/settings/index.js @@ -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); diff --git a/app/components/settings_drawer/settings_drawer.js b/app/components/sidebars/settings/settings_sidebar.js similarity index 72% rename from app/components/settings_drawer/settings_drawer.js rename to app/components/sidebars/settings/settings_sidebar.js index d6317053b..ceee93d13 100644 --- a/app/components/settings_drawer/settings_drawer.js +++ b/app/components/sidebars/settings/settings_sidebar.js @@ -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 ( - {children} - + ); } } diff --git a/app/components/settings_drawer/status_label/index.js b/app/components/sidebars/settings/status_label/index.js similarity index 100% rename from app/components/settings_drawer/status_label/index.js rename to app/components/sidebars/settings/status_label/index.js diff --git a/app/components/settings_drawer/status_label/status_label.js b/app/components/sidebars/settings/status_label/status_label.js similarity index 100% rename from app/components/settings_drawer/status_label/status_label.js rename to app/components/sidebars/settings/status_label/status_label.js diff --git a/app/components/settings_drawer/user_info/index.js b/app/components/sidebars/settings/user_info/index.js similarity index 100% rename from app/components/settings_drawer/user_info/index.js rename to app/components/sidebars/settings/user_info/index.js diff --git a/app/components/settings_drawer/user_info/user_info.js b/app/components/sidebars/settings/user_info/user_info.js similarity index 100% rename from app/components/settings_drawer/user_info/user_info.js rename to app/components/sidebars/settings/user_info/user_info.js diff --git a/app/components/swiper.js b/app/components/swiper.js index b4a152c23..2fa264c58 100644 --- a/app/components/swiper.js +++ b/app/components/swiper.js @@ -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 ( {pages} @@ -287,7 +305,8 @@ const styles = StyleSheet.create({ flex: 1, }, slide: { - backgroundColor: 'transparent', + flex: 1, + width: '100%', }, pagination: { position: 'absolute', diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index f5aea1526..5f269e47b 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -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 ( - - @@ -300,8 +342,8 @@ export default class Channel extends PureComponent { @@ -313,10 +355,14 @@ export default class Channel extends PureComponent { navigator={navigator} /> + {LocalConfig.EnableMobileClientUpgrade && } - - + + ); } } @@ -330,5 +376,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { backgroundColor: theme.centerChannelBg, flex: 1, }, + channelLoader: { + position: 'absolute', + width: '100%', + flex: 1, + }, }; }); diff --git a/app/screens/code/code.js b/app/screens/code/code.js index 5202cb3a4..414933cd8 100644 --- a/app/screens/code/code.js +++ b/app/screens/code/code.js @@ -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; }; diff --git a/package-lock.json b/package-lock.json index e860d9c80..f9570bd81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9d09af02e..8786a2482 100644 --- a/package.json +++ b/package.json @@ -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",