RN-13 Scroll to New messages indicator (#1110)
This commit is contained in:
parent
2a6b49cdfa
commit
b649dada6f
10 changed files with 42 additions and 244 deletions
1
Makefile
1
Makefile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue