MM-28105 do not crash when in-app notification shows (#4915)

* MM-28105 do not crash when in-app notification shows

* Use index.tsx to register the notification screen

* Fix notification e2e and some import cleanup

* Update iOS notification json

* e2e: Show the add reaction screen before receiving a notification

* Update snapshots

* Patch react-native-notifications to prevent iOS crash when opening

* Apply TM4J id

Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com>
This commit is contained in:
Elias Nahum 2020-10-22 14:34:49 -03:00
parent 8dac3ce9b9
commit 0569479398
No known key found for this signature in database
GPG key ID: E038DB71E0B61702
19 changed files with 510 additions and 526 deletions

View file

@ -230,6 +230,7 @@ exports[`Reactions Should match snapshot with default emojis 1`] = `
/>
<TouchableWithoutFeedback
onPress={[MockFunction]}
testID="reaction_picker.open"
>
<View
style={
@ -491,6 +492,7 @@ exports[`Reactions Should match snapshot with default emojis 2`] = `
/>
<TouchableWithoutFeedback
onPress={[MockFunction]}
testID="reaction_picker.open"
>
<View
style={

View file

@ -81,7 +81,10 @@ export default class ReactionPicker extends PureComponent {
return (
<View style={[style.reactionListContainer, paddingRes]}>
{list}
<TouchableWithoutFeedback onPress={this.props.openReactionScreen}>
<TouchableWithoutFeedback
onPress={this.props.openReactionScreen}
testID='reaction_picker.open'
>
<View
style={[
style.reactionContainer,

View file

@ -67,10 +67,11 @@ class PushNotificationUtils {
onPushNotification = async (deviceNotification) => {
const {dispatch} = Store.redux;
const {data, foreground, message, userInteraction} = deviceNotification;
const {data, foreground, message, userInfo, userInteraction} = deviceNotification;
const notification = {
data,
message,
userInfo,
};
waitForHydration(Store.redux, () => {
@ -84,7 +85,7 @@ class PushNotificationUtils {
if (foreground) {
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
} else if (userInteraction && !notification?.data?.localNotification) {
} else if (userInteraction && !notification?.userInfo?.localNotification && !notification?.data?.localNotification) {
this.loadFromNotification(notification);
}
break;

View file

@ -26,7 +26,8 @@ export default class AddReaction extends PureComponent {
};
leftButton = {
id: 'close-edit-post',
id: 'close-add-reaction',
testID: 'screen.add_reaction.close',
};
constructor(props) {
@ -42,7 +43,7 @@ export default class AddReaction extends PureComponent {
}
navigationButtonPressed({buttonId}) {
if (buttonId === 'close-edit-post') {
if (buttonId === 'close-add-reaction') {
this.close();
}
}

View file

@ -54,9 +54,9 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('MoreChannels', () => wrapper(require('app/screens/more_channels').default), () => require('app/screens/more_channels').default);
Navigation.registerComponent('MoreDirectMessages', () => wrapper(require('app/screens/more_dms').default), () => require('app/screens/more_dms').default);
if (Platform.OS === 'android') {
Navigation.registerComponent('Notification', () => gestureHandlerRootHOC(wrapper(require('app/screens/notification').default), {flex: undefined, height: 100}), () => require('app/screens/notification').default);
Navigation.registerComponent('Notification', () => gestureHandlerRootHOC(wrapper(require('app/screens/notification/index.tsx').default), {flex: undefined, height: 100}), () => require('app/screens/notification/index.tsx').default);
} else {
Navigation.registerComponent('Notification', () => wrapper(require('app/screens/notification').default), () => require('app/screens/notification').default);
Navigation.registerComponent('Notification', () => wrapper(require('app/screens/notification/index.tsx').default), () => require('app/screens/notification/index.tsx').default);
}
Navigation.registerComponent('NotificationSettings', () => wrapper(require('app/screens/settings/notification_settings').default), () => require('app/screens/settings/notification_settings').default);
Navigation.registerComponent('NotificationSettingsAutoResponder', () => wrapper(require('app/screens/settings/notification_settings_auto_responder').default), () => require('app/screens/settings/notification_settings_auto_responder').default);

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Notification should match snapshot 1`] = `null`;

View file

@ -1,45 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {loadFromPushNotification} from 'app/actions/views/root';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
import {getUser} from '@mm-redux/selectors/entities/users';
import {getConfig} from '@mm-redux/selectors/entities/general';
import Notification from './notification';
function mapStateToProps(state, ownProps) {
const {data} = ownProps.notification;
let user;
if (data.sender_id) {
user = getUser(state, data.sender_id);
}
let channel;
if (data.channel_id) {
channel = getChannel(state, data.channel_id);
}
const config = getConfig(state);
return {
config,
channel,
user,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadFromPushNotification,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Notification);

View file

@ -0,0 +1,210 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useRef, useState} from 'react';
import {Platform, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import {Navigation} from 'react-native-navigation';
import {PanGestureHandler} from 'react-native-gesture-handler';
import {useDispatch} from 'react-redux';
import {popToRoot, dismissAllModals, dismissOverlay} from '@actions/navigation';
import {loadFromPushNotification} from '@actions/views/root';
import {NavigationTypes} from '@constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {changeOpacity} from '@utils/theme';
import NotificationIcon from './notification_icon';
import NotificationTitle from './notification_title';
interface NotificationProps {
componentId: string;
notification: PushNotificationData;
}
interface SlideAnimation extends Animatable.CustomAnimation {
from: {
translateY: number;
};
to: {
translateY: number;
};
}
const AUTO_DISMISS_TIME_MILLIS = 5000;
const initialAnimation: SlideAnimation = {
from: {
translateY: -100,
},
to: {
translateY: 0,
},
};
const Notification = ({componentId, notification}: NotificationProps) => {
const [animation, setAnimation] = useState<SlideAnimation>(initialAnimation);
const dispatch = useDispatch();
const dismissTimerRef = useRef<NodeJS.Timeout | null>(null);
const tapped = useRef<boolean>(false);
const animateDismissOverlay = () => {
cancelDismissTimer();
setAnimation({
from: {
translateY: 0,
},
to: {
translateY: -100,
},
});
dismissTimerRef.current = setTimeout(dismiss, 1000);
};
const cancelDismissTimer = () => {
if (dismissTimerRef.current) {
clearTimeout(dismissTimerRef.current);
}
};
const dismiss = () => {
cancelDismissTimer();
dismissOverlay(componentId);
};
const notificationTapped = () => {
tapped.current = true;
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
EventEmitter.emit(NavigationTypes.CLOSE_SETTINGS_SIDEBAR);
dismiss();
if (!notification.userInfo?.localNotification) {
dispatch(loadFromPushNotification(notification));
}
};
useEffect(() => {
dismissTimerRef.current = setTimeout(() => {
if (!tapped.current) {
animateDismissOverlay();
}
}, AUTO_DISMISS_TIME_MILLIS);
return cancelDismissTimer;
}, []);
useEffect(() => {
const didDismissListener = Navigation.events().registerComponentDidDisappearListener(async ({componentId: screen}) => {
if (componentId === screen && tapped.current) {
await dismissAllModals();
await popToRoot();
}
});
return () => {
didDismissListener.remove();
};
}, []);
useEffect(() => {
EventEmitter.on(NavigationTypes.NAVIGATION_SHOW_OVERLAY, dismiss);
return () => EventEmitter.off(NavigationTypes.NAVIGATION_SHOW_OVERLAY, dismiss);
}, []);
let message = notification.message;
if (typeof notification.message === 'object') {
message = notification.message.body;
}
return (
<PanGestureHandler
onGestureEvent={animateDismissOverlay}
minOffsetY={-20}
>
<Animatable.View
duration={250}
style={styles.container}
useNativeDriver={true}
animation={animation}
testID='in_app_notification'
>
<View style={styles.flex}>
<TouchableOpacity
style={styles.touchable}
onPress={notificationTapped}
activeOpacity={1}
>
<NotificationIcon
fromWebhook={notification.data?.from_webhook === 'true'}
senderId={notification.data?.sender_id || ''}
useUserIcon={notification.data?.use_user_icon === 'true'}
overrideIconUrl={notification.data?.override_icon_url}
/>
<View style={styles.titleContainer}>
<NotificationTitle channelName={notification.data?.channel_name || ''}/>
<View style={styles.flex}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.message}
testID='in_app_notification.message'
>
{message}
</Text>
</View>
</View>
</TouchableOpacity>
</View>
</Animatable.View>
</PanGestureHandler>
);
};
const styles = StyleSheet.create({
container: {
alignItems: 'flex-start',
backgroundColor: changeOpacity('#000', 1),
flexDirection: 'row',
justifyContent: 'flex-start',
paddingHorizontal: 10,
width: '100%',
...Platform.select({
android: {
height: 68,
},
ios: {
height: 88,
},
}),
},
flex: {
flex: 1,
},
message: {
color: '#FFFFFF',
fontSize: 14,
},
titleContainer: {
flex: 1,
flexDirection: 'column',
alignSelf: 'stretch',
alignItems: 'flex-start',
marginLeft: 10,
...Platform.select({
android: {
marginTop: 17,
height: 50,
},
ios: {
paddingTop: 37,
},
}),
},
touchable: {
flex: 1,
flexDirection: 'row',
},
});
export default Notification;

View file

@ -1,407 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
Platform,
StyleSheet,
TouchableOpacity,
Text,
View,
} from 'react-native';
import FastImage from 'react-native-fast-image';
import {Navigation} from 'react-native-navigation';
import * as Animatable from 'react-native-animatable';
import {PanGestureHandler} from 'react-native-gesture-handler';
import {popToRoot, dismissAllModals, dismissOverlay} from '@actions/navigation';
import logo from '@assets/images/icon.png';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {NavigationTypes} from '@constants';
import {Client4} from '@mm-redux/client';
import {isDirectChannel} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {changeOpacity} from '@utils/theme';
const IMAGE_SIZE = 32;
const AUTO_DISMISS_TIME_MILLIS = 5000;
export default class Notification extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadFromPushNotification: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string.isRequired,
channel: PropTypes.object,
config: PropTypes.object,
notification: PropTypes.object.isRequired,
teammateNameDisplay: PropTypes.string,
user: PropTypes.object,
};
state = {
keyFrames: {
from: {
translateY: -100,
},
to: {
translateY: 0,
},
},
}
tapped = false;
componentDidMount() {
this.setDismissTimer();
this.setDidDisappearListener();
this.setShowOverlayListener();
}
componentWillUnmount() {
this.clearDismissTimer();
this.clearDidDisappearListener();
this.clearShowOverlayListener();
}
setDismissTimer = () => {
this.dismissTimer = setTimeout(() => {
if (!this.tapped) {
this.animateDismissOverlay();
}
}, AUTO_DISMISS_TIME_MILLIS);
}
clearDismissTimer = () => {
if (this.dismissTimer) {
clearTimeout(this.dismissTimer);
this.dismissTimer = null;
}
}
setDidDisappearListener = () => {
this.didDismissListener = Navigation.events().registerComponentDidDisappearListener(async ({componentId}) => {
if (componentId === this.props.componentId && this.tapped) {
await dismissAllModals();
await popToRoot();
}
});
}
clearDidDisappearListener = () => {
this.didDismissListener.remove();
}
setShowOverlayListener = () => {
EventEmitter.on(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay);
}
clearShowOverlayListener = () => {
EventEmitter.off(NavigationTypes.NAVIGATION_SHOW_OVERLAY, this.onNewOverlay);
}
onNewOverlay = () => {
// Dismiss this overlay so that there is only ever one.
this.dismissOverlay();
}
dismissOverlay = () => {
this.clearDismissTimer();
const {componentId} = this.props;
dismissOverlay(componentId);
}
animateDismissOverlay = () => {
this.clearDismissTimer();
this.setState({
keyFrames: {
from: {
translateY: 0,
},
to: {
translateY: -100,
},
},
});
setTimeout(() => this.dismissOverlay(), 1000);
}
notificationTapped = () => {
this.tapped = true;
this.clearDismissTimer();
const {actions, notification} = this.props;
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEABR);
EventEmitter.emit(NavigationTypes.CLOSE_SETTINGS_SIDEBAR);
this.dismissOverlay();
InteractionManager.runAfterInteractions(() => {
if (!notification.localNotification) {
actions.loadFromPushNotification(notification);
}
});
};
getNotificationIcon = () => {
const {config, notification, user} = this.props;
const {data} = notification;
let icon = (
<FastImage
source={logo}
style={style.icon}
/>
);
if (data.from_webhook && config.EnablePostIconOverride === 'true' && data.use_user_icon !== 'true') {
if (data.override_icon_url) { // eslint-disable-line camelcase
const source = {uri: Client4.getAbsoluteUrl(data.override_icon_url)}; // eslint-disable-line camelcase
icon = (
<FastImage
source={source}
style={style.icon}
/>
);
} else {
icon = (
<CompassIcon
name='webhook'
style={style.icon}
/>
);
}
} else if (user) {
icon = (
<ProfilePicture
userId={user.id}
size={IMAGE_SIZE}
iconSize={24}
/>
);
}
return icon;
};
getNotificationTitle = (notification) => {
const {channel} = this.props;
const {message, data} = notification;
if (data.version === 'v2') {
if (data.channel_name) {
return (
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={style.title}
>
{data.channel_name}
</Text>
);
}
return null;
}
const msg = message.split(':');
const titleText = msg.shift();
let title = (
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={style.title}
>
{titleText}
</Text>
);
const userName = this.getNotificationUserName();
if (userName && channel) {
let channelName;
let inText;
if (!isDirectChannel(channel)) {
inText = (
<FormattedText
id='mobile.notification.in'
defaultMessage=' in '
style={style.message}
/>
);
channelName = (
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={[style.title, style.channelName]}
>
{channel.display_name}
</Text>
);
}
title = (
<View style={{flex: 1, flexDirection: 'row'}}>
{userName}
{inText}
{channelName}
</View>
);
}
return title;
};
getNotificationUserName = () => {
const {config, notification, teammateNameDisplay, user} = this.props;
const {data} = notification;
let userName;
if (data.override_username && config.EnablePostUsernameOverride === 'true') {
userName = (
<Text style={style.title}>
{data.override_username}
</Text>
);
} else if (user) {
userName = (
<Text style={style.title}>
{displayUsername(user, teammateNameDisplay)}
</Text>
);
}
return userName;
};
render() {
const {notification} = this.props;
const {data, message} = notification;
if (message) {
let messageText;
if (data.version !== 'v2') {
const msg = message.split(':');
messageText = msg.join('').trim();
} else if (Platform.OS === 'ios') {
messageText = message.body || message;
} else {
messageText = message;
}
const title = this.getNotificationTitle(notification);
const icon = this.getNotificationIcon();
return (
<PanGestureHandler
onGestureEvent={this.animateDismissOverlay}
minOffsetY={-20}
>
<Animatable.View
duration={250}
style={style.container}
useNativeDriver={true}
animation={this.state.keyFrames}
>
<View style={{flex: 1}}>
<TouchableOpacity
style={{flex: 1, flexDirection: 'row'}}
onPress={this.notificationTapped}
activeOpacity={1}
>
<View style={style.iconContainer}>
{icon}
</View>
<View style={style.textContainer}>
{title}
<View style={{flex: 1}}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={style.message}
>
{messageText}
</Text>
</View>
</View>
</TouchableOpacity>
</View>
</Animatable.View>
</PanGestureHandler>
);
}
return null;
}
}
const style = StyleSheet.create({
container: {
alignItems: 'flex-start',
backgroundColor: changeOpacity('#000', 1),
flexDirection: 'row',
justifyContent: 'flex-start',
paddingHorizontal: 10,
width: '100%',
...Platform.select({
android: {
height: 68,
},
ios: {
height: 88,
},
}),
},
iconContainer: {
...Platform.select({
android: {
paddingTop: 17,
},
ios: {
paddingTop: 37,
},
}),
},
icon: {
borderRadius: (IMAGE_SIZE / 2),
height: IMAGE_SIZE,
width: IMAGE_SIZE,
},
textContainer: {
flex: 1,
flexDirection: 'column',
alignSelf: 'stretch',
alignItems: 'flex-start',
marginLeft: 10,
...Platform.select({
android: {
marginTop: 17,
height: 50,
},
ios: {
paddingTop: 37,
},
}),
},
title: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
channelName: {
alignSelf: 'stretch',
alignItems: 'flex-start',
flex: 1,
},
message: {
color: '#FFFFFF',
fontSize: 14,
},
});

View file

@ -1,60 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from '@mm-redux/constants/preferences';
import Notification from './notification.js';
jest.mock('react-native-navigation', () => ({
Navigation: {
events: jest.fn(() => ({
registerComponentDidDisappearListener: jest.fn(),
})),
},
}));
describe('Notification', () => {
const baseProps = {
actions: {
loadFromPushNotification: jest.fn(),
},
componentId: 'component-id',
deviceWidth: 100,
notification: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallow(
<Notification {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call loadFromPushNotification on notification tap for non-local notification', () => {
const props = {
...baseProps,
notification: {
localNotification: true,
},
};
const wrapper = shallow(
<Notification {...props}/>,
);
const instance = wrapper.instance();
instance.notificationTapped();
expect(baseProps.actions.loadFromPushNotification).not.toHaveBeenCalled();
const notification = {
localNotification: false,
};
wrapper.setProps({notification});
instance.notificationTapped();
expect(baseProps.actions.loadFromPushNotification).toHaveBeenCalledWith(notification);
});
});

View file

@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, StyleSheet, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {useSelector} from 'react-redux';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import {Client4} from '@mm-redux/client';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getUser} from '@mm-redux/selectors/entities/users';
import type {GlobalState} from '@mm-redux/types/store';
interface NotificationIconProps {
fromWebhook: boolean;
senderId: string;
useUserIcon: boolean;
overrideIconUrl?: string;
}
const IMAGE_SIZE = 32;
const logo = require('@assets/images/icon.png');
const NotificationIcon = ({fromWebhook, overrideIconUrl, senderId, useUserIcon}: NotificationIconProps) => {
const config = useSelector(getConfig);
const user = useSelector((state: GlobalState) => getUser(state, senderId));
let icon;
if (fromWebhook && useUserIcon && config.EnablePostIconOverride === 'true') {
if (overrideIconUrl) {
const source = {uri: Client4.getAbsoluteUrl(overrideIconUrl)};
icon = (
<FastImage
source={source}
style={styles.icon}
/>
);
} else {
icon = (
<CompassIcon
name='webhook'
style={styles.icon}
/>
);
}
} else if (user) {
icon = (
<ProfilePicture
userId={user.id}
size={IMAGE_SIZE}
iconSize={24}
/>
);
} else {
icon = (
<FastImage
source={logo}
style={styles.icon}
/>
);
}
return (
<View
style={styles.iconContainer}
testID='in_app_notification.icon'
>
{icon}
</View>
);
};
const styles = StyleSheet.create({
iconContainer: {
...Platform.select({
android: {
paddingTop: 17,
},
ios: {
paddingTop: 37,
},
}),
},
icon: {
borderRadius: (IMAGE_SIZE / 2),
height: IMAGE_SIZE,
width: IMAGE_SIZE,
},
});
export default NotificationIcon;

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet, Text} from 'react-native';
interface NotificationTitleProps {
channelName: string;
}
const NotificationTitle = ({channelName}: NotificationTitleProps) => {
return (
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.title}
testID='in_app_notification.title'
>
{channelName}
</Text>
);
};
const styles = StyleSheet.create({
title: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
});
export default NotificationTitle;

36
app/types/push_notification.d.ts vendored Normal file
View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
interface NotificationUserInfo {
localNotification: boolean;
localTest: boolean;
channel_id?: string;
}
interface NotificationData {
channel_id: string;
channel_name?: string;
from_webhook?: string;
post_id: string;
root_id?: string;
type: string;
use_user_icon?: string;
override_icon_url?: string;
override_username?: string;
version: string;
sender_id?: string;
}
interface NotificationMessage {
body: string;
}
interface PushNotificationData {
date?: Date;
data?: NotificationData;
fireDate?: string;
foreground: boolean;
message: NotificationMessage | string;
userInfo?: NotificationUserInfo;
userInteraction: boolean;
}

View file

@ -368,7 +368,6 @@
"mobile.notification_settings.ooo_auto_responder": "Automatic Direct Message Replies",
"mobile.notification_settings.save_failed_description": "The notification settings failed to save due to a connection issue, please try again.",
"mobile.notification_settings.save_failed_title": "Connection issue",
"mobile.notification.in": " in ",
"mobile.offlineIndicator.connected": "Connected",
"mobile.offlineIndicator.connecting": "Connecting...",
"mobile.offlineIndicator.offline": "No internet connection",

View file

@ -9,7 +9,6 @@
import {logoutUser, toChannelScreen} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {Setup} from '@support/server_api';
describe('Channel Info Header', () => {

View file

@ -0,0 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {logoutUser, toChannelScreen} from '@support/ui/screen';
import {isAndroid, timeouts, wait} from '@support/utils';
import {Setup} from '@support/server_api';
const DetoxConstants = require('detox').DetoxConstants;
function getNotification(channel, team, user) {
if (isAndroid()) {
return {
payload: {
ack_id: 'ack-e2e-test-id',
type: 'message',
badge: 1,
version: '2',
channel_id: channel.id,
channel_name: channel.name,
team_id: team.id,
sender_id: user.id,
sender_name: user.name,
message: 'This is an e2e test message',
post_id: 'post-e2e-test-id',
root_id: '',
},
};
}
return {
trigger: {
type: DetoxConstants.userNotificationTriggers.push,
},
badge: 1,
body: 'This is an e2e test message',
payload: {
ack_id: 'ack-e2e-test-id',
type: 'message',
version: '2',
channel_id: channel.id,
channel_name: channel.name,
team_id: team.id,
sender_id: user.id,
sender_name: user.name,
post_id: 'post-e2e-test-id',
root_id: '',
},
};
}
describe('in-app Notification', () => {
let testNotification;
let testChannel;
beforeAll(async () => {
const {channel, team, user} = await Setup.apiInit();
testChannel = channel;
testNotification = getNotification(channel, team, user);
await toChannelScreen(user);
});
afterAll(async () => {
await logoutUser();
});
it('MM-T3440 should render an in-app notification', async () => {
const message = Date.now().toString();
// # Type a message
const postInput = await element(by.id('post_input'));
await postInput.tap();
await postInput.typeText(message);
// # Tap the send button
await element(by.id('send_button')).tap();
// # Open Add reaction screen
await element(by.text(message)).longPress();
await element(by.id('reaction_picker.open')).tap();
await element(by.id('screen.add_reaction.close')).tap();
if (isAndroid()) {
// eslint-disable-next-line no-console
console.log('Skipping on Android until https://github.com/wix/Detox/issues/2141');
return;
}
// # When a push notification is received
await device.sendUserNotification(testNotification);
await wait(timeouts.HALF_SEC);
// * in-app notification shows
await expect(element(by.id('in_app_notification'))).toBeVisible();
await expect(element(by.id('in_app_notification.icon'))).toBeVisible();
await expect(element(by.id('in_app_notification.title'))).toHaveText(testChannel.name);
await expect(element(by.id('in_app_notification.message'))).toHaveText('This is an e2e test message');
// # Wait for some profiles to load
await wait(5 * timeouts.ONE_SEC);
// * in-app notification hides
await expect(element(by.id('in_app_notification'))).not.toBeVisible();
});
});

View file

@ -8,7 +8,6 @@
// *******************************************************************
import {logoutUser, toChannelScreen} from '@support/ui/screen';
import {Setup} from '@support/server_api';
describe('Messaging', () => {

View file

@ -1,3 +1,18 @@
diff --git a/node_modules/react-native-notifications/RNNotifications/RNNotificationsStore.m b/node_modules/react-native-notifications/RNNotifications/RNNotificationsStore.m
index d5e953a..08d4d46 100644
--- a/node_modules/react-native-notifications/RNNotifications/RNNotificationsStore.m
+++ b/node_modules/react-native-notifications/RNNotifications/RNNotificationsStore.m
@@ -40,7 +40,9 @@ - (void)setPresentationCompletionHandler:(void (^)(UNNotificationPresentationOpt
- (void)completeAction:(NSString *)completionKey {
void (^completionHandler)() = (void (^)())[_actionCompletionHandlers valueForKey:completionKey];
if (completionHandler) {
- completionHandler();
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completionHandler();
+ });
[_actionCompletionHandlers removeObjectForKey:completionKey];
}
}
diff --git a/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml b/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml
index 7053040..ad63eff 100644
--- a/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml

View file

@ -1,6 +1,6 @@
#!/bin/sh
jsfiles=$(git diff --cached --name-only --diff-filter=ACM | grep -E '.js$|.ts$')
jsfiles=$(git diff --cached --name-only --diff-filter=ACM | grep -E '.js$|.ts$|.tsx$')
if [ -z "jsfiles" ]; then
exit 0