RN-13 Scroll to New messages indicator (#1110)

This commit is contained in:
enahum 2017-11-09 11:50:04 -03:00 committed by Jarred Witt
parent 2a6b49cdfa
commit b649dada6f
10 changed files with 42 additions and 244 deletions

View file

@ -122,6 +122,7 @@ post-install:
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
@sed -i'' -e 's^getItemLayout || index <= this._highestMeasuredFrameIndex,^!getItemLayout || index <= this._highestMeasuredFrameIndex,^g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
@cd ./node_modules/mattermost-redux && yarn run build

View file

@ -8,6 +8,7 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
markChannelAsRead,
selectChannel,
leaveChannel as serviceLeaveChannel,
unfavoriteChannel
@ -236,20 +237,27 @@ export function selectInitialChannel(teamId) {
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
}
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
dispatch(setChannelDisplayName(''));
handleSelectChannel(channel.id)(dispatch, getState);
channelId = channel.id;
} else {
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
channelId = firstChannel.id;
}
if (channelId) {
dispatch(setChannelDisplayName(''));
handleSelectChannel(firstChannel.id)(dispatch, getState);
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId)(dispatch, getState);
}
};
}

View file

@ -28,9 +28,10 @@ export function handleTeamChange(teamId, selectChannel = true) {
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
const lastChannelId = lastChannels[0] || '';
const currentChannelId = getCurrentChannelId(state);
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
viewChannel(currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
}

View file

@ -1,157 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Platform, ScrollView, StyleSheet, View} from 'react-native';
import RefreshList from 'app/components/refresh_list';
import VirtualList from './virtual_list';
export default class InvertibleFlatList extends PureComponent {
static propTypes = {
horizontal: PropTypes.bool,
inverted: PropTypes.bool,
ListFooterComponent: PropTypes.func,
renderItem: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
horizontal: false,
inverted: true
};
constructor(props) {
super(props);
this.inversionDirection = props.horizontal ? styles.horizontal : styles.vertical;
}
getMetrics = () => {
return this.flatListRef.getMetrics();
};
recordInteraction = () => {
this.flatListRef.recordInteraction();
};
renderFooter = () => {
const {ListFooterComponent: footer} = this.props;
if (!footer) {
return null;
}
return (
<View style={[styles.container, this.inversionDirection]}>
{footer()}
</View>
);
};
renderItem = (info) => {
return (
<View style={[styles.container, this.inversionDirection]}>
{this.props.renderItem(info)}
</View>
);
};
renderScrollComponent = (props) => {
const {theme} = this.props;
if (props.onRefresh) {
return (
<ScrollView
{...props}
refreshControl={
<RefreshList
refreshing={props.refreshing}
onRefresh={props.onRefresh}
tintColor={theme.centerChannelColor}
colors={[theme.centerChannelColor]}
style={this.inversionDirection}
/>
}
/>
);
}
return <ScrollView {...props}/>;
};
scrollToEnd = (params) => {
this.flatListRef.scrollToEnd(params);
};
scrollToIndex = (params) => {
this.flatListRef.scrollToIndex(params);
};
scrollToItem = (params) => {
this.flatListRef.scrollToItem(params);
};
scrollToOffset = (params) => {
this.flatListRef.scrollToOffset(params);
};
setFlatListRef = (flatListRef) => {
this.flatListRef = flatListRef;
};
render() {
const {inverted, ...forwardedProps} = this.props;
// If not inverted, render as an ordinary FlatList
if (!inverted) {
return (
<FlatList
{...forwardedProps}
/>
);
}
return (
<View style={[styles.container, this.inversionDirection]}>
<VirtualList
ref={this.setFlatListRef}
{...forwardedProps}
ListFooterComponent={this.renderFooter}
renderItem={this.renderItem}
renderScrollComponent={this.renderScrollComponent}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
vertical: Platform.select({
android: {
transform: [
{perspective: 1},
{scaleY: -1}
]
},
ios: {
transform: [{scaleY: -1}]
}
}),
horizontal: Platform.select({
android: {
transform: [
{perspective: 1},
{scaleY: -1}
]
},
ios: {
transform: [{scaleX: -1}]
}
})
});

View file

@ -1,54 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {VirtualizedList} from 'react-native';
/* eslint-disable */
export default class Virtualized extends VirtualizedList {
_onScroll = (e) => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
const timestamp = e.timeStamp;
const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
const contentLength = this._selectLength(e.nativeEvent.contentSize);
const offset = this._selectOffset(e.nativeEvent.contentOffset);
const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp);
const dOffset = offset - this._scrollMetrics.offset;
const velocity = dOffset / dt;
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
this._updateViewableItems(data);
if (!data) {
return;
}
const distanceFromEnd = contentLength - visibleLength - offset;
const itemCount = getItemCount(data);
if (this.state.last === itemCount - 1 &&
distanceFromEnd <= onEndReachedThreshold &&
(this._hasDataChangedSinceEndReached ||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
// Only call onEndReached once for a given dataset + content length.
this._hasDataChangedSinceEndReached = false;
this._sentEndForContentLength = this._scrollMetrics.contentLength;
onEndReached({distanceFromEnd});
}
const {first, last} = this.state;
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
const distanceToContentEdge = Math.min(
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)),
);
const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4);
if (hiPri) {
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
// as possible.
this._updateCellsToRenderBatcher.dispose({abort: true});
this._updateCellsToRender();
return;
}
}
this._updateCellsToRenderBatcher.schedule();
};
}

View file

@ -4,7 +4,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
View,
ViewPropTypes
} from 'react-native';
@ -47,7 +46,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
line: {
flex: 1,
height: StyleSheet.hairlineWidth,
height: 1,
backgroundColor: theme.newMessageSeparator
},
text: {

View file

@ -3,10 +3,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet} from 'react-native';
import {
StyleSheet,
FlatList
} from 'react-native';
import ChannelIntro from 'app/components/channel_intro';
import FlatList from 'app/components/inverted_flat_list';
import Post from 'app/components/post';
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import mattermostManaged from 'app/mattermost_managed';
@ -44,6 +46,7 @@ export default class PostList extends PureComponent {
constructor(props) {
super(props);
this.newMessagesIndex = -1;
this.state = {
managedConfig: {}
};
@ -55,15 +58,27 @@ export default class PostList extends PureComponent {
componentDidMount() {
this.setManagedConfig();
this.scrollList();
}
componentDidUpdate(prevProps) {
if (prevProps.channelId !== this.props.channelId && this.refs.list) {
// When switching channels make sure we start from the bottom
this.refs.list.scrollToOffset({y: 0, animated: false});
const initialPosts = !prevProps.postIds.length && prevProps.postIds !== this.props.postIds;
if ((prevProps.channelId !== this.props.channelId || initialPosts) && this.refs.list) {
this.scrollList();
}
}
scrollList = () => {
requestAnimationFrame(() => {
if (this.props.postIds.length && this.newMessagesIndex !== -1) {
this.refs.list.scrollToIndex({index: this.newMessagesIndex, viewPosition: 1, viewOffset: -10, animated: true});
this.newMessagesIndex = -1;
} else {
this.refs.list.scrollToOffset({y: 0, animated: false});
}
});
};
setManagedConfig = async (config) => {
let nextConfig = config;
if (!nextConfig) {
@ -73,11 +88,7 @@ export default class PostList extends PureComponent {
this.setState({
managedConfig: nextConfig
});
}
getItem = (data, index) => data[index];
getItemCount = (data) => data.length;
};
keyExtractor = (item) => {
// All keys are strings (either post IDs or special keys)
@ -102,6 +113,7 @@ export default class PostList extends PureComponent {
renderItem = ({item, index}) => {
if (item === START_OF_NEW_MESSAGES) {
this.newMessagesIndex = index;
return (
<NewMessagesDivider
theme={this.props.theme}
@ -180,8 +192,7 @@ export default class PostList extends PureComponent {
channelId,
highlightPostId,
loadMore,
postIds,
theme
postIds
} = this.props;
const refreshControl = {
@ -205,9 +216,6 @@ export default class PostList extends PureComponent {
onEndReachedThreshold={0}
{...refreshControl}
renderItem={this.renderItem}
theme={theme}
getItem={this.getItem}
getItemCount={this.getItemCount}
contentContainerStyle={styles.postListContent}
/>
);

View file

@ -64,14 +64,6 @@ class Channel extends PureComponent {
}
}
componentDidMount() {
// Mark current channel as read when opening app while logged in
if (this.props.currentChannelId) {
this.props.actions.markChannelAsRead(this.props.currentChannelId);
this.props.actions.viewChannel(this.props.currentChannelId);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeamId && this.props.currentTeamId !== nextProps.currentTeamId) {
this.loadChannels(nextProps.currentTeamId);

View file

@ -62,8 +62,8 @@ export function makePreparePostIdsForPostList() {
}
// Only add the new messages line if a lastViewedAt time is set
const postIsUnread = post.create_at > lastViewedAt;
if (lastViewedAt != null && !addedNewMessagesIndicator && postIsUnread) {
const postIsUnread = post.create_at > lastViewedAt && post.user_id !== currentUserId;
if (lastViewedAt !== null && !addedNewMessagesIndicator && postIsUnread) {
out.push(START_OF_NEW_MESSAGES);
addedNewMessagesIndicator = true;
}

View file

@ -3938,7 +3938,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "1.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/ae530a32d0d4c5b0f3c8200f7d7890439e6cf9ca"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/3c8c5c315e44daec2cbe0d6d88a79555e011c8d4"
dependencies:
deep-equal "1.0.1"
form-data "2.3.1"