Entry screen and Channel Loader improvements (perf part 4) (#1707)

* Entry screen and Channel Loader improvements

* Feedback review

* Removed unnecessary style array
This commit is contained in:
Elias Nahum 2018-05-29 10:58:06 -04:00 committed by Harrison Healey
parent b500df868e
commit cf3683272e
11 changed files with 100 additions and 182 deletions

View file

@ -6,152 +6,56 @@ import PropTypes from 'prop-types';
import {
Platform,
View,
Animated,
Easing,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import Placeholder from 'rn-placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const GRADIENT_START_END = 0;
const GRADIENT_MIDDLE = 0.8;
const START_VALUE = 0;
const MIDDLE_VALUE = 50;
const END_VALUE = 100;
const DURATION = 2000;
export default class ChannelLoader extends PureComponent {
static propTypes = {
backgroundColor: PropTypes.string,
channelIsLoading: PropTypes.bool.isRequired,
deviceWidth: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
};
constructor() {
super();
this.animation = new Animated.Value(0);
}
componentDidMount() {
if (this.props.channelIsLoading) {
this.start();
}
}
componentWillUnmount() {
this.animation.stopAnimation();
}
componentWillReceiveProps(nextProps) {
if (nextProps.channelIsLoading !== this.props.channelIsLoading) {
if (nextProps.channelIsLoading) {
this.start();
} else {
this.animation.stopAnimation();
}
}
}
start() {
this.animation.setValue(START_VALUE);
Animated.sequence([
Animated.timing(this.animation, {
toValue: END_VALUE,
duration: DURATION,
easing: Easing.linear,
}),
]).start((animation) => {
if (animation.finished) {
this.start();
}
});
}
buildSections({key, style, top, bg}) {
const left = this.animation.interpolate({
inputRange: [START_VALUE, END_VALUE],
outputRange: ['-10%', '50%'],
});
const opacity = this.animation.interpolate({
inputRange: [START_VALUE, MIDDLE_VALUE, END_VALUE],
outputRange: [GRADIENT_START_END, GRADIENT_MIDDLE, GRADIENT_START_END],
});
const bodyPosLeft = this.animation.interpolate({
inputRange: [START_VALUE, END_VALUE],
outputRange: ['-20%', '80%'],
});
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
buildSections({key, style, bg, color}) {
return (
<View
key={key}
style={[style.section, (top && {marginTop: Platform.OS === 'android' ? 0 : -15, paddingTop: 10})]}
style={[style.section, {backgroundColor: bg}]}
>
<View style={style.avatar}/>
<View style={style.sectionMessage}>
<View>
<View style={[style.messageText]}/>
<AnimatedGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity(bg, GRADIENT_START_END),
changeOpacity(bg, GRADIENT_MIDDLE),
changeOpacity(bg, GRADIENT_START_END),
]}
locations={[0, 0.5, 1]}
style={[style.gradientText, {left, opacity}]}
/>
</View>
<View>
<View style={[style.messageText, {width: '100%'}]}/>
<AnimatedGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity(bg, GRADIENT_START_END),
changeOpacity(bg, GRADIENT_MIDDLE),
changeOpacity(bg, GRADIENT_START_END),
]}
locations={[0, 0.5, 1]}
style={[style.gradientText, {left: bodyPosLeft, opacity}]}
/>
</View>
<View>
<View style={[style.messageText, {width: '100%'}]}/>
<AnimatedGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity(bg, GRADIENT_START_END),
changeOpacity(bg, GRADIENT_MIDDLE),
changeOpacity(bg, GRADIENT_START_END),
]}
locations={[0, 0.5, 1]}
style={[style.gradientText, {left: bodyPosLeft, opacity}]}
/>
</View>
</View>
<Placeholder.ImageContent
size={32}
animate='fade'
lineNumber={3}
lineSpacing={5}
firstLineWidth='80%'
hasRadius={true}
textSize={14}
color={changeOpacity(color, 0.2)}
/>
</View>
);
}
render() {
const {channelIsLoading, deviceWidth, theme} = this.props;
const {channelIsLoading, theme} = this.props;
if (!channelIsLoading) {
return null;
}
const style = getStyleSheet(theme);
const bg = this.props.backgroundColor || theme.centerChannelBg;
return (
<View style={[style.container, {width: deviceWidth}]}>
{Array(20).fill().map((item, index) => this.buildSections({key: index, style, top: index === 0, bg: theme.centerChannelBg}))}
<View style={style.container}>
{Array(6).fill().map((item, index) => this.buildSections({
key: index,
style,
bg,
color: theme.centerChannelColor,
}))}
</View>
);
}
@ -162,7 +66,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
container: {
backgroundColor: theme.centerChannelBg,
flex: 1,
position: 'absolute',
...Platform.select({
android: {
top: 0,
@ -172,35 +75,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
}),
},
avatar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRadius: 16,
height: 32,
width: 32,
},
gradientText: {
backgroundColor: changeOpacity(theme.centerChannelBg, 0.2),
height: 10,
marginBottom: 10,
position: 'absolute',
width: '40%',
},
section: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
flex: 1,
paddingLeft: 12,
paddingRight: 20,
marginVertical: 10,
},
sectionMessage: {
marginLeft: 12,
flex: 1,
},
messageText: {
width: '80%',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 10,
marginBottom: 10,
},
};
});

View file

@ -7,10 +7,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelLoader from './channel_loader';
function mapStateToProps(state, ownProps) {
const {deviceWidth} = state.device.dimension;
return {
channelIsLoading: ownProps.channelIsLoading || state.views.channel.loading,
deviceWidth,
theme: getTheme(state),
};
}

View file

@ -13,6 +13,7 @@ import {
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 KeyboardLayout from 'app/components/layout/keyboard_layout';
import OfflineIndicator from 'app/components/offline_indicator';
@ -47,6 +48,7 @@ export default class Channel extends PureComponent {
currentChannelId: PropTypes.string,
channelsRequestFailed: PropTypes.bool,
currentTeamId: PropTypes.string,
isLandscape: PropTypes.bool,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
};
@ -257,9 +259,15 @@ export default class Channel extends PureComponent {
const Loading = require('app/components/channel_loader').default;
return (
<View style={style.loading}>
<Loading channelIsLoading={true}/>
</View>
<SafeAreaView navigator={navigator}>
<View style={style.loading}>
<EmptyToolbar
theme={theme}
isLandscape={this.props.isLandscape}
/>
<Loading channelIsLoading={true}/>
</View>
</SafeAreaView>
);
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
@ -9,6 +9,7 @@ import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/a
import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {
loadChannelsIfNecessary,
@ -18,7 +19,7 @@ import {
import {connection} from 'app/actions/device';
import {recordLoadTime} from 'app/actions/views/root';
import {selectDefaultTeam} from 'app/actions/views/select_team';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isLandscape} from 'app/selectors/device';
import Channel from './channel';
@ -29,6 +30,7 @@ function mapStateToProps(state) {
channelsRequestFailed: channelsRequest.status === RequestStatus.FAILURE,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
isLandscape: isLandscape(state),
theme: getTheme(state),
};
}

View file

@ -22,6 +22,7 @@ import {loadFromPushNotification} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import PushNotifications from 'app/push_notifications';
import {stripTrailingSlashes} from 'app/utils/url';
import {wrapWithContextProvider} from 'app/utils/wrap_context_provider';
import ChannelLoader from 'app/components/channel_loader';
import EmptyToolbar from 'app/components/start/empty_toolbar';
@ -212,22 +213,21 @@ export default class Entry extends PureComponent {
renderLogin = () => {
const SelectServer = lazyLoadSelectServer();
return (
<SelectServer
navigator={this.props.navigator}
allowOtherServers={app.allowOtherServers}
/>
);
const props = {
allowOtherServers: app.allowOtherServers,
navigator: this.props.navigator,
};
return wrapWithContextProvider(SelectServer)(props);
};
renderChannel = () => {
const ChannelScreen = lazyLoadChannel();
const props = {
navigator: this.props.navigator,
};
return (
<ChannelScreen
navigator={this.props.navigator}
/>
);
return wrapWithContextProvider(ChannelScreen, true)(props);
};
render() {
@ -264,9 +264,10 @@ export default class Entry extends PureComponent {
);
loading = (
<View>
<ChannelLoader channelIsLoading={true}/>
</View>
<ChannelLoader
backgroundColor={backgroundColor}
channelIsLoading={true}
/>
);
} else {
loading = <Loading/>;

View file

@ -1,27 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {Navigation} from 'react-native-navigation';
import Channel from 'app/screens/channel';
import Entry from 'app/screens/entry';
import SelectServer from 'app/screens/select_server';
import IntlWrapper from 'app/components/root';
function wrapWithContextProvider(Comp, excludeEvents = true) {
return (props) => { //eslint-disable-line react/display-name
const {navigator} = props; //eslint-disable-line react/prop-types
return (
<IntlWrapper
navigator={navigator}
excludeEvents={excludeEvents}
>
<Comp {...props}/>
</IntlWrapper>
);
};
}
import {wrapWithContextProvider} from 'app/utils/wrap_context_provider';
export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapWithContextProvider(require('app/screens/about').default), store, Provider);
@ -40,7 +25,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(require('app/screens/edit_channel').default), store, Provider);
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(require('app/screens/edit_post').default), store, Provider);
Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(require('app/screens/edit_profile').default), store, Provider);
Navigation.registerComponent('Entry', () => wrapWithContextProvider(Entry), store, Provider);
Navigation.registerComponent('Entry', () => Entry, store, Provider);
Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider);

View file

@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import IntlWrapper from 'app/components/root';
export function wrapWithContextProvider(Comp, excludeEvents = true) {
return (props) => { //eslint-disable-line react/display-name
const {navigator} = props; //eslint-disable-line react/prop-types
return (
<IntlWrapper
navigator={navigator}
excludeEvents={excludeEvents}
>
<Comp {...props}/>
</IntlWrapper>
);
};
}

7
package-lock.json generated
View file

@ -15494,6 +15494,13 @@
"integrity": "sha1-JC124vpIXEjXUUFuZbfM5ZaWnpE=",
"dev": true
},
"rn-placeholder": {
"version": "github:enahum/rn-placeholder#4b065f892d7a7f9d921a969f2e72e609ebc0a212",
"from": "github:enahum/rn-placeholder",
"requires": {
"prop-types": "^15.5.10"
}
},
"rst-selector-parser": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",

View file

@ -61,6 +61,7 @@
"redux-persist-transform-filter": "0.0.16",
"redux-thunk": "2.2.0",
"reselect": "3.0.1",
"rn-placeholder": "github:enahum/rn-placeholder#4b065f892d7a7f9d921a969f2e72e609ebc0a212",
"semver": "5.5.0",
"shallow-equals": "1.0.0",
"tinycolor2": "1.4.1",

File diff suppressed because one or more lines are too long

View file

@ -337,6 +337,7 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/app/utils/time_tracker.js',
'./node_modules/app/utils/tooltip.js',
'./node_modules/app/utils/url.js',
'./node_modules/app/utils/wrap_context_provider.js',
'./node_modules/react-native/Libraries/Utilities/binaryToBase64.js',
'./node_modules/react-native/Libraries/Network/convertRequestBody.js',
'./node_modules/react-native/Libraries/Animated/src/createAnimatedComponent.js',
@ -994,6 +995,21 @@ module.exports = ['./node_modules/react-native/Libraries/ART/ARTSerializablePath
'./node_modules/node_modules/requires-port/index.js',
'./node_modules/node_modules/reselect/lib/index.js',
'./node_modules/node_modules/rn-host-detect/index.js',
'./node_modules/node_modules/rn-placeholder/index.js',
'./node_modules/node_modules/rn-placeholder/src/animation/animations.js',
'./node_modules/node_modules/rn-placeholder/src/animation/fade.js',
'./node_modules/node_modules/rn-placeholder/src/animation/shine.js',
'./node_modules/node_modules/rn-placeholder/src/box/box.js',
'./node_modules/node_modules/rn-placeholder/src/box/box.style.js',
'./node_modules/node_modules/rn-placeholder/src/imageContent/imageContent.js',
'./node_modules/node_modules/rn-placeholder/src/line/line.js',
'./node_modules/node_modules/rn-placeholder/src/line/line.style.js',
'./node_modules/node_modules/rn-placeholder/src/media/media.js',
'./node_modules/node_modules/rn-placeholder/src/media/media.style.js',
'./node_modules/node_modules/rn-placeholder/src/multiWords/multiWords.js',
'./node_modules/node_modules/rn-placeholder/src/paragraph/paragraph.js',
'./node_modules/node_modules/rn-placeholder/src/placeholder.js',
'./node_modules/node_modules/rn-placeholder/src/placeholderContainer.js',
'./node_modules/node_modules/sc-channel/index.js',
'./node_modules/node_modules/sc-emitter/index.js',
'./node_modules/node_modules/sc-emitter/node_modules/component-emitter/index.js',