From f3dbb72f658aae8ce1079bb7db5886f2a0e13014 Mon Sep 17 00:00:00 2001 From: enahum Date: Fri, 10 Mar 2017 20:52:52 -0300 Subject: [PATCH] PLT-5836 Smooth transitions and add activity indicators (#339) --- app/actions/navigation/index.js | 4 + app/actions/views/channel.js | 5 - app/components/action_button.js | 40 ++++-- .../channel_drawer_list.js | 123 ++++++++---------- .../channel_drawer_list_container.js | 3 - app/navigation/router.js | 10 +- app/scenes/channel/channel.js | 13 +- .../channel_post_list/channel_post_list.js | 5 +- app/scenes/channel/channel_title.js | 11 +- app/scenes/channel_drawer/channel_drawer.js | 37 ++++-- .../channel_drawer_container.js | 6 +- app/scenes/more_channels/more_channels.js | 96 ++++++++------ app/scenes/more_dms/more_dms.js | 97 ++++++++------ app/scenes/navigationSceneConnect.js | 8 +- .../right_menu_drawer/right_menu_drawer.js | 66 +++++----- app/scenes/select_team/select_team.js | 6 +- .../select_team/select_team_container.js | 3 +- 17 files changed, 289 insertions(+), 244 deletions(-) diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js index 2c32ee497..1d7986a1b 100644 --- a/app/actions/navigation/index.js +++ b/app/actions/navigation/index.js @@ -69,6 +69,7 @@ export function goToLoadTeam() { export function goToModalSelectTeam() { return async (dispatch, getState) => { + closeDrawers()(dispatch, getState); dispatch({ type: NavigationTypes.NAVIGATION_MODAL, route: Routes.SelectTeam @@ -168,6 +169,7 @@ export function showOptionsModal(title, options) { export function showMoreChannelsModal() { return async (dispatch, getState) => { + closeDrawers()(dispatch, getState); dispatch({ type: NavigationTypes.NAVIGATION_MODAL, route: Routes.MoreChannels @@ -177,6 +179,7 @@ export function showMoreChannelsModal() { export function showDirectMessagesModal() { return async (dispatch, getState) => { + closeDrawers()(dispatch, getState); dispatch({ type: NavigationTypes.NAVIGATION_MODAL, route: Routes.MoreDirectMessages @@ -194,6 +197,7 @@ export function closeModal() { export function goToModalAccountSettings() { return async (dispatch, getState) => { + closeDrawers()(dispatch, getState); dispatch({ type: NavigationTypes.NAVIGATION_MODAL, route: Routes.AccountSettings diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index a6b0524ff..a49879b1a 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -5,7 +5,6 @@ import {batchActions} from 'redux-batched-actions'; import {ViewTypes} from 'app/constants'; import {updateStorage} from 'app/actions/storage'; -import {closeDrawers} from 'app/actions/navigation'; import { fetchMyChannelsAndMembers, @@ -154,10 +153,6 @@ export function handleSelectChannel(channelId) { await updateStorage(currentTeamId, {currentChannelId: channelId}); await selectChannel(channelId)(dispatch, getState); await getChannelStats(currentTeamId, channelId)(dispatch, getState); - - setTimeout(async () => { - await closeDrawers()(dispatch, getState); // trying to smooth out channel switch transitions - }, 200); }; } diff --git a/app/components/action_button.js b/app/components/action_button.js index 8f6120ca3..38f6352b9 100644 --- a/app/components/action_button.js +++ b/app/components/action_button.js @@ -69,8 +69,33 @@ class ActionButton extends PureComponent { const {labelDefaultMessage, labelId, theme} = this.props; const {enabled, loading} = this.state; let color = changeOpacity(theme.sidebarHeaderTextColor, 0.4); + let contents; if (enabled) { color = theme.sidebarHeaderTextColor; + contents = ( + + + + + + ); + } else { + contents = ( + + + + ); } if (loading) { @@ -88,20 +113,7 @@ class ActionButton extends PureComponent { ); } - return ( - - - - - - ); + return contents; } } diff --git a/app/components/channel_drawer_list/channel_drawer_list.js b/app/components/channel_drawer_list/channel_drawer_list.js index d26e45af3..c6a540abf 100644 --- a/app/components/channel_drawer_list/channel_drawer_list.js +++ b/app/components/channel_drawer_list/channel_drawer_list.js @@ -21,61 +21,6 @@ import UnreadIndicator from './unread_indicator'; import deepEqual from 'deep-equal'; import Icon from 'react-native-vector-icons/FontAwesome'; -const Styles = StyleSheet.create({ - container: { - flex: 1 - }, - scrollContainer: { - flex: 1 - }, - headerContainer: { - flexDirection: 'column', - ...Platform.select({ - ios: { - height: 64, - justifyContent: 'flex-end' - }, - android: { - height: 56, - justifyContent: 'center', - paddingTop: 10 - } - }), - width: 300, - paddingLeft: 10, - paddingBottom: 12 - }, - header: { - fontSize: 18, - fontWeight: 'bold' - }, - title: { - paddingTop: 10, - paddingRight: 10, - paddingLeft: 10, - paddingBottom: 5, - fontSize: 15, - opacity: 0.6 - }, - indicatorText: { - paddingVertical: 2, - paddingHorizontal: 4, - backgroundColor: 'transparent', - fontSize: 14, - textAlign: 'center', - textAlignVertical: 'center' - }, - more: { - position: 'absolute', - opacity: 0.6, - right: 0, - width: 50, - height: 30, - alignItems: 'center', - justifyContent: 'center' - } -}); - class ChannelDrawerList extends Component { static propTypes = { intl: intlShape.isRequired, @@ -86,8 +31,6 @@ class ChannelDrawerList extends Component { theme: PropTypes.object.isRequired, onSelectChannel: PropTypes.func.isRequired, actions: PropTypes.shape({ - viewChannel: PropTypes.func.isRequired, - markChannelAsRead: PropTypes.func.isRequired, closeDMChannel: PropTypes.func.isRequired, goToCreateChannel: PropTypes.func.isRequired, leaveChannel: PropTypes.func.isRequired, @@ -167,15 +110,8 @@ class ChannelDrawerList extends Component { } }; - onSelectChannel = async (channel) => { - const { - currentChannel, - currentTeam - } = this.props; - - await this.props.onSelectChannel(channel.id); - this.props.actions.viewChannel(currentTeam.id, channel.id); - this.props.actions.markChannelAsRead(channel.id, currentChannel.id); + onSelectChannel = (channel) => { + this.props.onSelectChannel(channel.id); }; onLayout = (event) => { @@ -547,4 +483,59 @@ class ChannelDrawerList extends Component { } } +const Styles = StyleSheet.create({ + container: { + flex: 1 + }, + scrollContainer: { + flex: 1 + }, + headerContainer: { + flexDirection: 'column', + ...Platform.select({ + ios: { + height: 64, + justifyContent: 'flex-end' + }, + android: { + height: 56, + justifyContent: 'center', + paddingTop: 10 + } + }), + width: 300, + paddingLeft: 10, + paddingBottom: 12 + }, + header: { + fontSize: 18, + fontWeight: 'bold' + }, + title: { + paddingTop: 10, + paddingRight: 10, + paddingLeft: 10, + paddingBottom: 5, + fontSize: 15, + opacity: 0.6 + }, + indicatorText: { + paddingVertical: 2, + paddingHorizontal: 4, + backgroundColor: 'transparent', + fontSize: 14, + textAlign: 'center', + textAlignVertical: 'center' + }, + more: { + position: 'absolute', + opacity: 0.6, + right: 0, + width: 50, + height: 30, + alignItems: 'center', + justifyContent: 'center' + } +}); + export default injectIntl(ChannelDrawerList); diff --git a/app/components/channel_drawer_list/channel_drawer_list_container.js b/app/components/channel_drawer_list/channel_drawer_list_container.js index 57fd07e32..95853e70b 100644 --- a/app/components/channel_drawer_list/channel_drawer_list_container.js +++ b/app/components/channel_drawer_list/channel_drawer_list_container.js @@ -19,7 +19,6 @@ import { unmarkFavorite } from 'app/actions/views/channel'; -import {viewChannel, markChannelAsRead} from 'service/actions/channels'; import ChannelDrawerList from './channel_drawer_list'; function mapStateToProps(state, ownProps) { @@ -31,8 +30,6 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - viewChannel, - markChannelAsRead, closeDMChannel, goToCreateChannel, leaveChannel, diff --git a/app/navigation/router.js b/app/navigation/router.js index 3382b1f6e..7e26fbfce 100644 --- a/app/navigation/router.js +++ b/app/navigation/router.js @@ -38,17 +38,17 @@ class Router extends React.Component { if (subscription) { subscription(); } - } + }; subscribeToHeaderEvent = (key) => (event, callback) => { this.headerEventSubscriptions[`${key}-${event}`] = callback; - } + }; unsubscribeFromHeaderEvent = (key) => (event) => { if (this.headerEventSubscriptions[event]) { Reflect.deleteProperty(this.headerEventSubscriptions, `${key}-${event}`); } - } + }; wrapHeaderComponent = (fx, emitter) => (props) => { if (fx && props.scene.isActive) { @@ -56,11 +56,11 @@ class Router extends React.Component { } return null; - } + }; extractNavigationProps = (route) => { return Object.assign({}, route.navigationProps, route.component.navigationProps); - } + }; renderTransition = (transitionProps) => { const navigationProps = this.extractNavigationProps(transitionProps.scene.route); diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js index 609e7c800..ab752fc9c 100644 --- a/app/scenes/channel/channel.js +++ b/app/scenes/channel/channel.js @@ -5,10 +5,11 @@ import React from 'react'; import { Platform, StatusBar, - Text + View } from 'react-native'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; +import Loading from 'app/components/loading'; import PostTextbox from 'app/components/post_textbox'; import {Constants} from 'service/constants'; @@ -120,10 +121,12 @@ export default class Channel extends React.PureComponent { theme } = this.props; - if (!currentTeam) { - return {'Waiting on team'}; - } else if (!currentChannel) { - return {'Waiting on channel'}; + if (!currentTeam || !currentChannel) { + return ( + + + + ); } let teamId = currentChannel.team_id; diff --git a/app/scenes/channel/channel_post_list/channel_post_list.js b/app/scenes/channel/channel_post_list/channel_post_list.js index 80359820f..48fea5353 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list.js +++ b/app/scenes/channel/channel_post_list/channel_post_list.js @@ -5,6 +5,7 @@ import React, {PropTypes, PureComponent} from 'react'; import {Constants, RequestStatus} from 'service/constants'; +import Loading from 'app/components/loading'; import PostList from 'app/components/post_list'; export default class ChannelPostList extends PureComponent { @@ -84,8 +85,8 @@ export default class ChannelPostList extends PureComponent { render() { const {posts, postsRequests} = this.props; - if (!posts) { - return {'waiting on posts'}; + if (!posts || (postsRequests.getPosts.status === RequestStatus.STARTED && !this.state.didInitialPostsLoad)) { + return ; } return ( diff --git a/app/scenes/channel/channel_title.js b/app/scenes/channel/channel_title.js index 63de2ab8c..39b2e6320 100644 --- a/app/scenes/channel/channel_title.js +++ b/app/scenes/channel/channel_title.js @@ -8,6 +8,7 @@ import { TouchableOpacity, View } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; import {getCurrentChannel} from 'service/selectors/entities/channels'; import {getTheme} from 'service/selectors/entities/preferences'; @@ -16,13 +17,19 @@ function ChannelTitle(props) { const channelName = props.currentChannel.display_name; return ( props.emitter('show_channel_info')} > - + {channelName} + ); diff --git a/app/scenes/channel_drawer/channel_drawer.js b/app/scenes/channel_drawer/channel_drawer.js index e293abe00..c95bd438b 100644 --- a/app/scenes/channel_drawer/channel_drawer.js +++ b/app/scenes/channel_drawer/channel_drawer.js @@ -1,30 +1,43 @@ // Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import React, {PropTypes, PureComponent} from 'react'; +import {InteractionManager} from 'react-native'; import ChannelDrawerList from 'app/components/channel_drawer_list'; -export default class ChannelDrawer extends React.PureComponent { +export default class ChannelDrawer extends PureComponent { static propTypes = { - actions: React.PropTypes.shape({ - closeDrawers: React.PropTypes.func.isRequired, - handleSelectChannel: React.PropTypes.func.isRequired + actions: PropTypes.shape({ + closeDrawers: PropTypes.func.isRequired, + handleSelectChannel: PropTypes.func.isRequired, + viewChannel: PropTypes.func.isRequired, + markChannelAsRead: PropTypes.func.isRequired }).isRequired, - currentTeam: React.PropTypes.object, - currentChannel: React.PropTypes.object, - channels: React.PropTypes.object, - channelMembers: React.PropTypes.object, - theme: React.PropTypes.object.isRequired + currentTeam: PropTypes.object, + currentChannel: PropTypes.object, + channels: PropTypes.object, + channelMembers: PropTypes.object, + theme: PropTypes.object.isRequired }; static defaultProps = { currentTeam: {}, currentChannel: {} - } + }; selectChannel = (id) => { - return this.props.actions.handleSelectChannel(id); + const { + currentChannel, + currentTeam + } = this.props; + + this.props.actions.closeDrawers(); + InteractionManager.runAfterInteractions(() => { + this.props.actions.markChannelAsRead(id, currentChannel.id); + this.props.actions.handleSelectChannel(id); + this.props.actions.viewChannel(currentTeam.id, id); + }); }; render() { diff --git a/app/scenes/channel_drawer/channel_drawer_container.js b/app/scenes/channel_drawer/channel_drawer_container.js index 13a6ae2a8..0d8410b6b 100644 --- a/app/scenes/channel_drawer/channel_drawer_container.js +++ b/app/scenes/channel_drawer/channel_drawer_container.js @@ -11,6 +11,8 @@ import {getChannelsByCategory, getCurrentChannel} from 'service/selectors/entiti import {getTheme} from 'service/selectors/entities/preferences'; import {getCurrentTeam} from 'service/selectors/entities/teams'; +import {viewChannel, markChannelAsRead} from 'service/actions/channels'; + import ChannelDrawer from './channel_drawer.js'; function mapStateToProps(state) { @@ -27,7 +29,9 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ closeDrawers, - handleSelectChannel + handleSelectChannel, + viewChannel, + markChannelAsRead }, dispatch) }; } diff --git a/app/scenes/more_channels/more_channels.js b/app/scenes/more_channels/more_channels.js index 1369e4992..12b594558 100644 --- a/app/scenes/more_channels/more_channels.js +++ b/app/scenes/more_channels/more_channels.js @@ -10,9 +10,10 @@ import { View } from 'react-native'; -import FormattedText from 'app/components/formatted_text'; import ChannelList from 'app/components/custom_list'; import ChannelListRow from 'app/components/custom_list/channel_list_row'; +import FormattedText from 'app/components/formatted_text'; +import Loading from 'app/components/loading'; import SearchBar from 'app/components/search_bar'; import {Constants, RequestStatus} from 'service/constants'; @@ -20,15 +21,6 @@ import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; import CreateButton from './create_button'; -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: theme.centerChannelBg - } - }); -}); - class MoreChannels extends PureComponent { static propTypes = { intl: intlShape.isRequired, @@ -77,6 +69,7 @@ class MoreChannels extends PureComponent { this.state = { channels: [], page: 0, + adding: false, next: true, searching: false }; @@ -193,6 +186,7 @@ class MoreChannels extends PureComponent { }; onSelectChannel = async (id) => { + this.setState({adding: true}); this.searchBar.blur(); await this.props.actions.joinChannel( this.props.currentUserId, @@ -215,40 +209,60 @@ class MoreChannels extends PureComponent { const style = getStyleFromTheme(this.props.theme); const more = this.state.searching ? () => true : this.loadMoreChannels; - return ( - - - + + + ); + } else { + content = ( + + + + + - - - ); + ); + } + + return content; } } +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.centerChannelBg + } + }); +}); + export default injectIntl(MoreChannels); diff --git a/app/scenes/more_dms/more_dms.js b/app/scenes/more_dms/more_dms.js index adfa7e981..6fe634dfd 100644 --- a/app/scenes/more_dms/more_dms.js +++ b/app/scenes/more_dms/more_dms.js @@ -11,21 +11,13 @@ import { } from 'react-native'; import FormattedText from 'app/components/formatted_text'; +import Loading from 'app/components/loading'; import MemberList from 'app/components/custom_list'; import SearchBar from 'app/components/search_bar'; import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list'; import {Constants, RequestStatus} from 'service/constants'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: theme.centerChannelBg - } - }); -}); - class MoreDirectMessages extends PureComponent { static propTypes = { intl: intlShape.isRequired, @@ -70,6 +62,7 @@ class MoreDirectMessages extends PureComponent { this.state = { profiles: [], page: 0, + adding: false, next: true, searching: false }; @@ -151,9 +144,9 @@ class MoreDirectMessages extends PureComponent { }; onSelectMember = async (id) => { + this.setState({adding: true}); this.searchBar.blur(); await this.props.actions.makeDirectChannel(id); - InteractionManager.runAfterInteractions(() => { this.props.actions.goBack(); }); @@ -166,41 +159,61 @@ class MoreDirectMessages extends PureComponent { const style = getStyleFromTheme(this.props.theme); const more = this.state.searching ? () => true : this.loadMoreProfiles; - return ( - - - + + + ); + } else { + content = ( + + + + + - - - ); + ); + } + + return content; } } +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.centerChannelBg + } + }); +}); + export default injectIntl(MoreDirectMessages); diff --git a/app/scenes/navigationSceneConnect.js b/app/scenes/navigationSceneConnect.js index eec943786..6cd806215 100644 --- a/app/scenes/navigationSceneConnect.js +++ b/app/scenes/navigationSceneConnect.js @@ -22,15 +22,11 @@ const defaults = { onPress={props.onNavigateBack} > - ); }, diff --git a/app/scenes/right_menu_drawer/right_menu_drawer.js b/app/scenes/right_menu_drawer/right_menu_drawer.js index c1f27d719..d48dad7a8 100644 --- a/app/scenes/right_menu_drawer/right_menu_drawer.js +++ b/app/scenes/right_menu_drawer/right_menu_drawer.js @@ -15,38 +15,6 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; import RightMenuDrawerItem from './right_menu_drawer_item'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - backgroundColor: theme.sidebarBg, - flex: 1, - ...Platform.select({ - ios: { - paddingTop: 20 - }, - android: { - paddingTop: 5 - } - }) - }, - itemText: { - color: 'white' - }, - icon: { - color: 'white', - width: 25 - }, - mentionIcon: { - fontSize: 17, - fontWeight: 'bold' - }, - divider: { - borderColor: 'rgba(255, 255, 255, 0.6)', - borderTopWidth: StyleSheet.hairlineWidth - } - }); -}); - export default class RightMenuDrawer extends React.Component { static propTypes = { actions: React.PropTypes.shape({ @@ -55,7 +23,7 @@ export default class RightMenuDrawer extends React.Component { logout: React.PropTypes.func.isRequired }).isRequired, theme: React.PropTypes.object - } + }; render() { const Styles = getStyleSheet(this.props.theme); @@ -140,3 +108,35 @@ export default class RightMenuDrawer extends React.Component { function Divider(props) { return ; } + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + backgroundColor: theme.sidebarBg, + flex: 1, + ...Platform.select({ + ios: { + paddingTop: 20 + }, + android: { + paddingTop: 5 + } + }) + }, + itemText: { + color: 'white' + }, + icon: { + color: 'white', + width: 25 + }, + mentionIcon: { + fontSize: 17, + fontWeight: 'bold' + }, + divider: { + borderColor: 'rgba(255, 255, 255, 0.6)', + borderTopWidth: StyleSheet.hairlineWidth + } + }); +}); diff --git a/app/scenes/select_team/select_team.js b/app/scenes/select_team/select_team.js index f714db81f..a5171ea11 100644 --- a/app/scenes/select_team/select_team.js +++ b/app/scenes/select_team/select_team.js @@ -26,7 +26,6 @@ export default class SelectTeam extends React.Component { unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired, actions: React.PropTypes.shape({ goBackToChannelView: React.PropTypes.func.isRequired, - closeDrawers: React.PropTypes.func.isRequired, handleTeamChange: React.PropTypes.func.isRequired }).isRequired }; @@ -59,14 +58,11 @@ export default class SelectTeam extends React.Component { onSelectTeam = async (team) => { const { goBackToChannelView, - closeDrawers, handleTeamChange - } = this.props.actions; await handleTeamChange(team); - await InteractionManager.runAfterInteractions(goBackToChannelView); - InteractionManager.runAfterInteractions(() => setTimeout(closeDrawers, 200)); + InteractionManager.runAfterInteractions(goBackToChannelView); }; render() { diff --git a/app/scenes/select_team/select_team_container.js b/app/scenes/select_team/select_team_container.js index e21eed828..06f10e629 100644 --- a/app/scenes/select_team/select_team_container.js +++ b/app/scenes/select_team/select_team_container.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import navigationSceneConnect from '../navigationSceneConnect'; -import {goBack, closeDrawers} from 'app/actions/navigation'; +import {goBack} from 'app/actions/navigation'; import {handleTeamChange} from 'app/actions/views/select_team'; import {getCurrentTeam} from 'service/selectors/entities/teams'; @@ -26,7 +26,6 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ goBackToChannelView: goBack, - closeDrawers, handleTeamChange }, dispatch) };