PLT-5836 Smooth transitions and add activity indicators (#339)

This commit is contained in:
enahum 2017-03-10 20:52:52 -03:00 committed by GitHub
parent f5745cbc0e
commit f3dbb72f65
17 changed files with 289 additions and 244 deletions

View file

@ -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

View file

@ -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);
};
}

View file

@ -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 = (
<View style={{flex: 1, flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}>
<TouchableOpacity
onPress={this.onPress}
style={{paddingHorizontal: 15}}
>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
style={{color}}
/>
</TouchableOpacity>
</View>
);
} else {
contents = (
<View style={{flex: 1, flexDirection: 'column', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 15}}>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
style={{color}}
/>
</View>
);
}
if (loading) {
@ -88,20 +113,7 @@ class ActionButton extends PureComponent {
);
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={this.onPress}
style={{paddingHorizontal: 15}}
>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
style={{color}}
/>
</TouchableOpacity>
</View>
);
return contents;
}
}

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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 <Text>{'Waiting on team'}</Text>;
} else if (!currentChannel) {
return <Text>{'Waiting on channel'}</Text>;
if (!currentTeam || !currentChannel) {
return (
<View style={{flex: 1, backgroundColor: theme.centerChannelBg}}>
<Loading/>
</View>
);
}
let teamId = currentChannel.team_id;

View file

@ -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 <Text>{'waiting on posts'}</Text>;
if (!posts || (postsRequests.getPosts.status === RequestStatus.STARTED && !this.state.didInitialPostsLoad)) {
return <Loading/>;
}
return (

View file

@ -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 (
<TouchableOpacity
style={{flexDirection: 'row', flex: 1, alignItems: 'center'}}
style={{flexDirection: 'row', flex: 1}}
onPress={() => props.emitter('show_channel_info')}
>
<View>
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center'}}>
<Text style={{color: props.theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
{channelName}
</Text>
<Icon
style={{marginHorizontal: 6}}
size={12}
name='chevron-down'
color={props.theme.sidebarHeaderTextColor}
/>
</View>
</TouchableOpacity>
);

View file

@ -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() {

View file

@ -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)
};
}

View file

@ -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 (
<View style={style.container}>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
height={27}
fontSize={14}
textColor={this.props.theme.centerChannelColor}
hideBackground={true}
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
onChange={this.searchProfiles}
onSearchButtonPress={this.onSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
let content;
if (this.state.adding) {
content = (
<View style={style.container}>
<Loading/>
</View>
);
} else {
content = (
<View style={style.container}>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
height={27}
fontSize={14}
textColor={this.props.theme.centerChannelColor}
hideBackground={true}
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
onChange={this.searchProfiles}
onSearchButtonPress={this.onSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
/>
</View>
<ChannelList
data={this.state.channels}
theme={this.props.theme}
searching={this.state.searching}
onListEndReached={more}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
showSections={false}
renderRow={this.renderChannelRow}
onRowPress={this.onSelectChannel}
loadingText={{id: 'mobile.loading_channels', defaultMessage: 'Loading Channels...'}}
/>
</View>
<ChannelList
data={this.state.channels}
theme={this.props.theme}
searching={this.state.searching}
onListEndReached={more}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
showSections={false}
renderRow={this.renderChannelRow}
onRowPress={this.onSelectChannel}
loadingText={{id: 'mobile.loading_channels', defaultMessage: 'Loading Channels...'}}
/>
</View>
);
);
}
return content;
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});
export default injectIntl(MoreChannels);

View file

@ -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 (
<View style={style.container}>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
height={27}
fontSize={14}
textColor={this.props.theme.centerChannelColor}
hideBackground={true}
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
onChange={this.searchProfiles}
onSearchButtonPress={this.onSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
let content;
if (this.state.adding) {
content = (
<View style={style.container}>
<Loading/>
</View>
);
} else {
content = (
<View style={style.container}>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
height={27}
fontSize={14}
textColor={this.props.theme.centerChannelColor}
hideBackground={true}
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
onChange={this.searchProfiles}
onSearchButtonPress={this.onSearchButtonPress}
onCancelButtonPress={this.cancelSearch}
/>
</View>
<MemberList
data={this.state.profiles}
theme={this.props.theme}
searching={this.state.searching}
onListEndReached={more}
preferences={this.props.preferences}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
createSections={createMembersSections}
renderRow={renderMemberRow}
onRowPress={this.onSelectMember}
loadingText={loadingText}
/>
</View>
<MemberList
data={this.state.profiles}
theme={this.props.theme}
searching={this.state.searching}
onListEndReached={more}
preferences={this.props.preferences}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
createSections={createMembersSections}
renderRow={renderMemberRow}
onRowPress={this.onSelectMember}
loadingText={loadingText}
/>
</View>
);
);
}
return content;
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});
export default injectIntl(MoreDirectMessages);

View file

@ -22,15 +22,11 @@ const defaults = {
onPress={props.onNavigateBack}
>
<Icon
style={{fontWeight: 'bold'}}
name='angle-left'
size={18}
size={35}
color={theme.sidebarHeaderTextColor}
/>
<FormattedText
id='mobile.routes.back'
defaultMessage='Back'
style={{color: theme.sidebarHeaderTextColor, marginLeft: 10}}
/>
</TouchableOpacity>
);
},

View file

@ -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 <View {...props}/>;
}
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
}
});
});

View file

@ -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() {

View file

@ -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)
};