[MM-26855] Ensure More Messages button works as expected when app is launched from a push notification (#4569)

* Fix more messages button with push notif

* Don't cancel in showMoreText
This commit is contained in:
Miguel Alatzar 2020-07-16 08:25:40 -07:00 committed by GitHub
parent e2e33e05cc
commit 7c41c83196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 40 deletions

View file

@ -16,7 +16,7 @@ import EventEmitter from '@mm-redux/utils/event_emitter';
import initialState from '@store/initial_state';
import {getStateForReset} from '@store/utils';
import {markChannelViewedAndRead} from './channel';
import {markAsViewedAndReadBatch} from './channel';
export function startDataCleanup() {
return async (dispatch, getState) => {
@ -107,7 +107,7 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
const {currentTeamId} = state.entities.teams;
const channel = channels[channelId];
const member = myMembers[channelId];
const actions = [];
const actions = markAsViewedAndReadBatch(state, channelId);
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
@ -124,8 +124,6 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
teamId: channel.team_id || currentTeamId,
},
});
dispatch(markChannelViewedAndRead(channelId));
}
if (actions.length) {

View file

@ -3,6 +3,8 @@
import {connect} from 'react-redux';
import {resetUnreadMessageCount} from '@actions/views/channel';
import MoreMessagesButton from './more_messages_button';
function mapStateToProps(state, ownProps) {
@ -18,4 +20,8 @@ function mapStateToProps(state, ownProps) {
};
}
export default connect(mapStateToProps)(MoreMessagesButton);
const mapDispatchToProps = {
resetUnreadMessageCount,
};
export default connect(mapStateToProps, mapDispatchToProps)(MoreMessagesButton);

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {ActivityIndicator, Animated, Text, View} from 'react-native';
import {ActivityIndicator, Animated, AppState, Text, View} from 'react-native';
import {intlShape} from 'react-intl';
import PropTypes from 'prop-types';
@ -51,6 +51,7 @@ export default class MoreMessageButton extends React.PureComponent {
scrollToIndex: PropTypes.func.isRequired,
registerViewableItemsListener: PropTypes.func.isRequired,
registerScrollEndIndexListener: PropTypes.func.isRequired,
resetUnreadMessageCount: PropTypes.func.isRequired,
deepLinkURL: PropTypes.string,
};
@ -68,12 +69,14 @@ export default class MoreMessageButton extends React.PureComponent {
}
componentDidMount() {
AppState.addEventListener('change', this.onAppStateChange);
EventEmitter.on(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
this.removeViewableItemsListener = this.props.registerViewableItemsListener(this.onViewableItemsChanged);
this.removeScrollEndIndexListener = this.props.registerScrollEndIndexListener(this.onScrollEndIndex);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange);
EventEmitter.off(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
if (this.removeViewableItemsListener) {
this.removeViewableItemsListener();
@ -114,6 +117,17 @@ export default class MoreMessageButton extends React.PureComponent {
if (unreadCount > prevProps.unreadCount && prevProps.unreadCount === 0) {
this.onViewableItemsChanged(this.viewableItems);
}
if (unreadCount === 0 && prevProps.unreadCount > 0) {
this.hide();
}
}
onAppStateChange = (appState) => {
const isActive = appState === 'active';
if (!isActive) {
this.props.resetUnreadMessageCount(this.props.channelId);
}
}
onIndicatorBarVisible = (indicatorVisible) => {
@ -201,9 +215,9 @@ export default class MoreMessageButton extends React.PureComponent {
onViewableItemsChanged = (viewableItems) => {
this.viewableItems = viewableItems;
const {newMessageLineIndex, scrollToIndex} = this.props;
const {newMessageLineIndex, scrollToIndex, unreadCount} = this.props;
if (newMessageLineIndex <= 0 || unreadCount === 0 || viewableItems.length === 0 || this.disableViewableItems) {
if (newMessageLineIndex <= 0 || viewableItems.length === 0 || this.disableViewableItems) {
return;
}
@ -248,13 +262,10 @@ export default class MoreMessageButton extends React.PureComponent {
showMoreText = (readCount) => {
const moreCount = this.props.unreadCount - readCount;
if (moreCount <= 0) {
this.cancel(true);
return;
if (moreCount > 0) {
const moreText = this.moreText(moreCount);
this.setState({moreText}, this.show);
}
const moreText = this.moreText(moreCount);
this.setState({moreText}, this.show);
}
getReadCount = (lastViewableIndex) => {

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Animated} from 'react-native';
import {Animated, AppState} from 'react-native';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from '@mm-redux/constants/preferences';
@ -28,6 +28,7 @@ describe('MoreMessagesButton', () => {
manuallyUnread: false,
newMessageLineIndex: 0,
scrollToIndex: jest.fn(),
resetUnreadMessageCount: jest.fn(),
registerViewableItemsListener: jest.fn(() => {
return jest.fn();
}),
@ -54,12 +55,16 @@ describe('MoreMessagesButton', () => {
});
describe('lifecycle methods', () => {
test('componentDidMount should register indicator visibility listener, viewable items listener, and scroll end index listener', () => {
AppState.addEventListener = jest.fn();
AppState.removeEventListener = jest.fn();
test('componentDidMount should register app state listener, indicator visibility listener, viewable items listener, and scroll end index listener', () => {
EventEmitter.on = jest.fn();
const wrapper = shallowWithIntl(
<MoreMessagesButton {...baseProps}/>,
);
const instance = wrapper.instance();
instance.onAppStateChange = jest.fn();
instance.onIndicatorBarVisible = jest.fn();
instance.onViewableItemsChanged = jest.fn();
instance.onScrollEndIndex = jest.fn();
@ -69,6 +74,7 @@ describe('MoreMessagesButton', () => {
// have not yet been mocked so we call componentDidMount again.
instance.componentDidMount();
expect(AppState.addEventListener).toHaveBeenCalledWith('change', instance.onAppStateChange);
expect(EventEmitter.on).toHaveBeenCalledWith(ViewTypes.INDICATOR_BAR_VISIBLE, instance.onIndicatorBarVisible);
expect(baseProps.registerViewableItemsListener).toHaveBeenCalledWith(instance.onViewableItemsChanged);
expect(instance.removeViewableItemsListener).toBeDefined();
@ -76,18 +82,20 @@ describe('MoreMessagesButton', () => {
expect(instance.removeScrollEndIndexListener).toBeDefined();
});
test('componentWillUnmount should remove the indicator bar visible listener, the viewable items listener, the scroll end index listener, and clear all timers', () => {
test('componentWillUnmount should remove the app state listener, the indicator bar visible listener, the viewable items listener, the scroll end index listener, and clear all timers', () => {
jest.useFakeTimers();
EventEmitter.off = jest.fn();
const wrapper = shallowWithIntl(
<MoreMessagesButton {...baseProps}/>,
);
const instance = wrapper.instance();
instance.onAppStateChange = jest.fn();
instance.onIndicatorBarVisible = jest.fn();
instance.removeViewableItemsListener = jest.fn();
instance.removeScrollEndIndexListener = jest.fn();
instance.componentWillUnmount();
expect(AppState.removeEventListener).toHaveBeenCalledWith('change', instance.onAppStateChange);
expect(EventEmitter.off).toHaveBeenCalledWith(ViewTypes.INDICATOR_BAR_VISIBLE, instance.onIndicatorBarVisible);
expect(instance.removeViewableItemsListener).toHaveBeenCalled();
expect(instance.removeScrollEndIndexListener).toHaveBeenCalled();
@ -217,6 +225,52 @@ describe('MoreMessagesButton', () => {
wrapper.setProps({unreadCount: 2});
expect(instance.onViewableItemsChanged).toHaveBeenCalledTimes(1);
});
test('componentDidUpdate should call hide when the unreadCount decreases to 0', () => {
const wrapper = shallowWithIntl(
<MoreMessagesButton {...baseProps}/>,
);
const instance = wrapper.instance();
instance.hide = jest.fn();
instance.viewableItems = [{index: 1}];
wrapper.setProps({unreadCount: 2});
expect(instance.hide).not.toHaveBeenCalled();
wrapper.setProps({unreadCount: 1});
expect(instance.hide).not.toHaveBeenCalled();
wrapper.setProps({unreadCount: 0});
expect(instance.hide).toHaveBeenCalledTimes(1);
});
});
describe('onAppStateChange', () => {
it('should call resetUnreadMessageCount when app state is not active', () => {
const wrapper = shallowWithIntl(
<MoreMessagesButton {...baseProps}/>,
);
const instance = wrapper.instance();
let appState = 'active';
instance.onAppStateChange(appState);
expect(baseProps.resetUnreadMessageCount).not.toHaveBeenCalled();
appState = 'inactive';
instance.onAppStateChange(appState);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledTimes(1);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledWith(baseProps.channelId);
appState = 'background';
instance.onAppStateChange(appState);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledTimes(2);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledWith(baseProps.channelId);
appState = '';
instance.onAppStateChange(appState);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledTimes(3);
expect(baseProps.resetUnreadMessageCount).toHaveBeenCalledWith(baseProps.channelId);
});
});
describe('onIndicatorBarVisible', () => {
@ -574,18 +628,6 @@ describe('MoreMessagesButton', () => {
expect(clearTimeout).not.toHaveBeenCalled();
});
it('should return early when unreadCount is 0', () => {
const viewableItems = [{index: 0}, {index: 1}];
wrapper.setProps({newMessageLineIndex: 1, unreadCount: 0});
instance.onViewableItemsChanged(viewableItems);
expect(clearTimeout).not.toHaveBeenCalled();
wrapper.setProps({newMessageLineIndex: -1});
instance.onViewableItemsChanged(viewableItems);
expect(clearTimeout).not.toHaveBeenCalled();
});
it('should return early when viewableItems length is 0', () => {
const viewableItems = [];
wrapper.setProps({newMessageLineIndex: 1, unreadCount: 10});
@ -732,21 +774,19 @@ describe('MoreMessagesButton', () => {
jest.clearAllMocks();
});
it('should force cancel when props.unreadCount - readCount is <= 0', () => {
it('should not set moreText when props.unreadCount - readCount is <= 0', () => {
let readCount = props.unreadCount + 1;
instance.showMoreText(readCount);
expect(instance.cancel).toHaveBeenCalledTimes(1);
expect(instance.cancel.mock.calls[0][0]).toBe(true);
expect(instance.moreText).not.toHaveBeenCalled();
expect(instance.setState).not.toHaveBeenCalled();
readCount = props.unreadCount;
instance.showMoreText(readCount);
expect(instance.cancel).toHaveBeenCalledTimes(2);
expect(instance.cancel.mock.calls[1][0]).toBe(true);
expect(instance.moreText).not.toHaveBeenCalled();
expect(instance.setState).not.toHaveBeenCalled();
});
it('should set moreTextd when props.unreadCount - readCount is > 0', () => {
it('should set moreText when props.unreadCount - readCount is > 0', () => {
const moreCount = 1;
const readCount = props.unreadCount - moreCount;
instance.showMoreText(readCount);

View file

@ -31,7 +31,6 @@ export default class ChannelBase extends PureComponent {
selectDefaultTeam: PropTypes.func.isRequired,
selectInitialChannel: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
resetUnreadMessageCount: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string,
@ -92,7 +91,6 @@ export default class ChannelBase extends PureComponent {
}
if (currentChannelId) {
actions.resetUnreadMessageCount(this.props.currentChannelId);
PushNotifications.clearChannelNotifications(currentChannelId);
requestAnimationFrame(() => {
actions.getChannelStats(currentChannelId);

View file

@ -28,7 +28,6 @@ describe('ChannelBase', () => {
recordLoadTime: jest.fn(),
selectDefaultTeam: jest.fn(),
selectInitialChannel: jest.fn(),
resetUnreadMessageCount: jest.fn(),
},
componentId: channelBaseComponentId,
theme: Preferences.THEMES.default,

View file

@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {loadChannelsForTeam, selectInitialChannel, resetUnreadMessageCount} from '@actions/views/channel';
import {loadChannelsForTeam, selectInitialChannel} from '@actions/views/channel';
import {recordLoadTime} from '@actions/views/root';
import {selectDefaultTeam} from '@actions/views/select_team';
import {ViewTypes} from '@constants';
@ -49,7 +49,6 @@ function mapDispatchToProps(dispatch) {
selectDefaultTeam,
selectInitialChannel,
recordLoadTime,
resetUnreadMessageCount,
}, dispatch),
};
}