Skip to content

Commit

Permalink
[Android] Scroll to first unread message. (#1389)
Browse files Browse the repository at this point in the history
* [Android] Scroll to first unread message.

* Position first unread message at the middle of the screen.

* Send scroll events when scrollView does not occupy full screen.

As user as seen those messages so mark them as read.

* Scroll to end if first unread message is not found.

Currently assuming that if first unread message is not found than all are read.

* Completed.

*Added comments.

*Reverted unwanted changes.

* Fix: not scrolled to first unread message just after app launch.
  • Loading branch information
jainkuniya authored and borisyankov committed Nov 6, 2017
1 parent 19ec5fb commit 45b3aef
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 14 deletions.
61 changes: 53 additions & 8 deletions android/app/src/main/java/com/zulipmobile/AnchorScrollView.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public class AnchorScrollView extends ScrollView implements ReactClippingViewGro
private String mAnchorTag;
private int mLastAnchorY;
private ScrollView scrollView;
private boolean autoScrollToBottom = false;
private int anchor;

public AnchorScrollView(ReactContext context) {
this(context, null);
Expand Down Expand Up @@ -122,13 +124,13 @@ public AnchorScrollView(ReactContext context, @Nullable FpsListener fpsListener)

this.setOnTouchListener(new OnTouchListener() {
private ViewTreeObserver observer;

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (observer == null) {
observer = getViewTreeObserver();
observer.addOnScrollChangedListener(onScrollChangedListener);
}
else if (!observer.isAlive()) {
} else if (!observer.isAlive()) {
observer.removeOnScrollChangedListener(onScrollChangedListener);
observer = getViewTreeObserver();
observer.addOnScrollChangedListener(onScrollChangedListener);
Expand Down Expand Up @@ -507,7 +509,7 @@ public void onLayoutChange(View v, int left, int top, int right, int bottom, int
}

// Zulip changes
if (mAnchorTag != null) {
if (mAnchorTag != null && autoScrollToBottom) {
View mAnchorView = null;

if (mRemoveClippedSubviews && !(mContentView instanceof ReactViewGroup)) {
Expand All @@ -523,23 +525,66 @@ public void onLayoutChange(View v, int left, int top, int right, int bottom, int
}
}

int arrLength = mRemoveClippedSubviews && children != null ? children.length : mContentView.getChildCount();
int arrLength = children != null ? children.length : mContentView.getChildCount();
View previousChild = null; //adjust scroll position is such a way that last read message and first unread message both are visible at same time
for (int i = 0; i < arrLength; i++) {
View child = mRemoveClippedSubviews && children != null ? children[i] : mContentView.getChildAt(i);
View child = children != null ? children[i] : mContentView.getChildAt(i);

if (child != null && mAnchorTag.equals(child.getTag())) {
mAnchorView = child;
if (child != null && String.valueOf(anchor).equals(child.getTag())) {
mAnchorView = previousChild != null ? previousChild : child;
break;
}
previousChild = child;
}
if (mAnchorView != null) {
int anchorChange = mAnchorView.getTop() - mLastAnchorY;
scrollTo(getScrollX(), currentScrollY + anchorChange);
scrollTo(getScrollX(), currentScrollY + anchorChange - getHeight() / 2);
} else {
//first unread message not found
//one case may be no message is unread
//scroll to end
scrollTo(getScrollX(), getMaxScrollY());
}
//send events to fetch more if whole screen is not occupied
if (canNotScroll()) {
AnchorScrollViewHelper.emitScrollEvent(AnchorScrollView.this, getVisibleIds());
}
}
findAnchorView();
}

private int getScrollRange() {
int scrollRange = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);
scrollRange = Math.max(0,
child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
}
return scrollRange;
}

private boolean canNotScroll() {
try {
return getHeight() > getChildAt(0).getHeight();
} catch (Exception e) {
int scrollRange = getScrollRange();
int maxScrollY = getMaxScrollY();
if (scrollRange == 0 && maxScrollY == 0) {
return true;
}
}
return false;
}

public void flashScrollIndicators() {
awakenScrollBars();
}

public void setAutoScrollToBottom(boolean autoScrollToBottom) {
this.autoScrollToBottom = autoScrollToBottom;
}

public void setAnchor(int anchor) {
this.anchor = anchor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ public void setScrollPerfTag(AnchorScrollView view, String scrollPerfTag) {
view.setScrollPerfTag(scrollPerfTag);
}

/**
* boolean used to check whether we need to adjust scroll position after render
* @param view
* @param autoScrollToBottom
*/
@ReactProp(name = "autoScrollToBottom", defaultBoolean = false)
public void setAutoScrollToBottom(AnchorScrollView view, boolean autoScrollToBottom) {
view.setAutoScrollToBottom(autoScrollToBottom);
}

/**
* id of the first unread message, to which we need to scroll
* @param view
* @param anchor
*/
@ReactProp(name = "anchor", defaultInt = -1)
public void setAnchor(AnchorScrollView view, int anchor) {
view.setAnchor(anchor);
}

/**
* When set, fills the rest of the scrollview with a color to avoid setting a background and
* creating unnecessary overdraw.
Expand Down
2 changes: 1 addition & 1 deletion src/app/appReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const initialState: AppState = {
},
debug: {
htmlMessages: false,
unreadMessages: false,
unreadMessages: true,
splitMessageText: false,
},
};
Expand Down
38 changes: 35 additions & 3 deletions src/message/InfiniteScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
import type { ChildrenArray } from 'react';
import { Platform } from 'react-native';

import type { StyleObj } from '../types';
import type { StyleObj, Narrow } from '../types';
import config from '../config';
import { nullFunction } from '../nullObjects';
import AnchorScrollView from '../native/AnchorScrollView';
Expand All @@ -15,6 +15,8 @@ type Props = {
contentContainerStyle?: Object,
style: StyleObj,
stickyHeaderIndices: [],
anchor?: number,
narrow?: Narrow,
autoScrollToBottom?: boolean,
children?: $ReadOnlyArray<ChildrenArray<*>>,
listRef?: (component: any) => void,
Expand All @@ -23,8 +25,21 @@ type Props = {
onScroll: (e: Event) => void,
};

export default class InfiniteScrollView extends PureComponent<Props> {
type State = {
autoScrollToBottom: boolean,
};

export default class InfiniteScrollView extends PureComponent<Props, State> {
props: Props;
nextProps: Props;
state: State;

// we only need to adjust scroll position after first render
// for subsequent fetch we don't need to adjust scroll
// this info is captured in autoScrollToBottom
state = {
autoScrollToBottom: true,
};

static defaultProps = {
onStartReached: nullFunction,
Expand Down Expand Up @@ -60,6 +75,9 @@ export default class InfiniteScrollView extends PureComponent<Props> {
) {
this._sentStartForContentHeight = this._contentHeight;
this.props.onStartReached();
this.setState({
autoScrollToBottom: false,
});
}
}

Expand All @@ -72,6 +90,9 @@ export default class InfiniteScrollView extends PureComponent<Props> {
) {
this._sentEndForContentHeight = this._contentHeight;
this.props.onEndReached();
this.setState({
autoScrollToBottom: false,
});
}
}

Expand Down Expand Up @@ -100,10 +121,21 @@ export default class InfiniteScrollView extends PureComponent<Props> {
this.props.onScroll(e.nativeEvent);
};

componentWillReceiveProps(nextProps: Props) {
if (this.props.narrow !== nextProps.narrow) {
this.setState({
autoScrollToBottom: true,
});
}
}

render() {
const { autoScrollToBottom } = this.state;

return (
<AnchorScrollView
style={this.props.style}
anchor={this.props.anchor}
contentContainerStyle={this.props.contentContainerStyle}
automaticallyAdjustContentInset={false}
scrollsToTop
Expand All @@ -112,7 +144,7 @@ export default class InfiniteScrollView extends PureComponent<Props> {
onScroll={this._onScroll}
scrollEventThrottle={config.scrollCallbackThrottle}
// stickyHeaderIndices={Platform.OS === 'ios' ? this.props.stickyHeaderIndices : undefined}
autoScrollToBottom={this.props.autoScrollToBottom}
autoScrollToBottom={autoScrollToBottom}
removeClippedSubviews
ref={(component: any) => {
const { listRef } = this.props;
Expand Down
10 changes: 8 additions & 2 deletions src/message/MessageList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* @flow */
import React, { PureComponent } from 'react';

import type { Actions, TypingState } from '../types';
import type { Actions, TypingState, Narrow } from '../types';
import { nullFunction } from '../nullObjects';
import { LoadingIndicator } from '../common';
import MessageTyping from '../message/MessageTyping';
Expand All @@ -13,9 +13,11 @@ type Props = {
fetchingOlder: boolean,
fetchingNewer: boolean,
singleFetchProgress: boolean,
renderedMessages: Object[],
anchor?: number,
narrow?: Narrow,
typingUsers?: TypingState,
listRef?: (component: any) => void,
renderedMessages: Object[],
onReplySelect: () => void,
onScroll: (e: Event) => void,
};
Expand All @@ -35,6 +37,7 @@ export default class MessageList extends PureComponent<Props> {
render() {
const { styles } = this.context;
const {
anchor,
actions,
fetchingOlder,
fetchingNewer,
Expand All @@ -44,6 +47,7 @@ export default class MessageList extends PureComponent<Props> {
onScroll,
typingUsers,
renderedMessages,
narrow,
} = this.props;

const { messageList, stickyHeaderIndices } = cachedMessageRender(
Expand All @@ -60,6 +64,8 @@ export default class MessageList extends PureComponent<Props> {
onEndReached={actions.fetchNewer}
listRef={listRef}
onScroll={onScroll}
narrow={narrow}
anchor={anchor}
>
<LoadingIndicator active={fetchingOlder} backgroundColor={styles.backgroundColor} />
{messageList}
Expand Down
4 changes: 4 additions & 0 deletions src/message/MessageListContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getActiveNarrow,
getFlags,
getCaughtUpForActiveNarrow,
getAnchorForCurrentNarrow,
getFetchingForActiveNarrow,
getSubscriptions,
} from '../selectors';
Expand Down Expand Up @@ -53,6 +54,7 @@ class MessageListContainer extends PureComponent<Props> {

render() {
const {
anchor,
actions,
caughtUp,
fetching,
Expand All @@ -70,6 +72,7 @@ class MessageListContainer extends PureComponent<Props> {
return (
<MessageListComponent
auth={this.props.auth}
anchor={anchor}
subscriptions={this.props.subscriptions}
isFetching={false}
actions={actions}
Expand All @@ -95,6 +98,7 @@ export default connectWithActions(state => ({
fetching: getFetchingForActiveNarrow(state),
typingUsers: getCurrentTypingUsers(state),
renderedMessages: getRenderedMessages(state),
anchor: getAnchorForCurrentNarrow(state),
subscriptions: getSubscriptions(state),
narrow: getActiveNarrow(state),
auth: getAuth(state),
Expand Down
6 changes: 6 additions & 0 deletions src/message/messageSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { createSelector } from 'reselect';
import { getActiveNarrow } from '../directSelectors';
import { getShownMessagesInActiveNarrow } from '../chat/chatSelectors';
import renderMessages from './renderMessages';
import { findFirstUnread } from '../utils/message';

export const getRenderedMessages = createSelector(
getShownMessagesInActiveNarrow,
getActiveNarrow,
(messages, narrow) => renderMessages(messages, narrow),
);

export const getAnchorForCurrentNarrow = createSelector(
getShownMessagesInActiveNarrow,
messages => findFirstUnread(messages).id,
);

0 comments on commit 45b3aef

Please sign in to comment.