diff --git a/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap b/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap index 35d2060fe..cc7ef5ad8 100644 --- a/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap +++ b/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap @@ -230,6 +230,7 @@ exports[`Reactions Should match snapshot with default emojis 1`] = ` /> {list} - + { 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; diff --git a/app/screens/add_reaction/index.js b/app/screens/add_reaction/index.js index 72df14589..e0a62ec02 100644 --- a/app/screens/add_reaction/index.js +++ b/app/screens/add_reaction/index.js @@ -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(); } } diff --git a/app/screens/index.js b/app/screens/index.js index 7157a2411..924b41f7f 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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); diff --git a/app/screens/notification/__snapshots__/notification.test.js.snap b/app/screens/notification/__snapshots__/notification.test.js.snap deleted file mode 100644 index 87b26a683..000000000 --- a/app/screens/notification/__snapshots__/notification.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Notification should match snapshot 1`] = `null`; diff --git a/app/screens/notification/index.js b/app/screens/notification/index.js deleted file mode 100644 index 46f22496e..000000000 --- a/app/screens/notification/index.js +++ /dev/null @@ -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); diff --git a/app/screens/notification/index.tsx b/app/screens/notification/index.tsx new file mode 100644 index 000000000..5add8a6a8 --- /dev/null +++ b/app/screens/notification/index.tsx @@ -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(initialAnimation); + const dispatch = useDispatch(); + const dismissTimerRef = useRef(null); + const tapped = useRef(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 ( + + + + + + + + + + {message} + + + + + + + + ); +}; + +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; diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js deleted file mode 100644 index 5da0aa0ff..000000000 --- a/app/screens/notification/notification.js +++ /dev/null @@ -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 = ( - - ); - - 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 = ( - - ); - } else { - icon = ( - - ); - } - } else if (user) { - icon = ( - - ); - } - - return icon; - }; - - getNotificationTitle = (notification) => { - const {channel} = this.props; - const {message, data} = notification; - - if (data.version === 'v2') { - if (data.channel_name) { - return ( - - {data.channel_name} - - ); - } - - return null; - } - - const msg = message.split(':'); - const titleText = msg.shift(); - - let title = ( - - {titleText} - - ); - - const userName = this.getNotificationUserName(); - if (userName && channel) { - let channelName; - let inText; - if (!isDirectChannel(channel)) { - inText = ( - - ); - - channelName = ( - - {channel.display_name} - - ); - } - - title = ( - - {userName} - {inText} - {channelName} - - ); - } - - return title; - }; - - getNotificationUserName = () => { - const {config, notification, teammateNameDisplay, user} = this.props; - const {data} = notification; - - let userName; - if (data.override_username && config.EnablePostUsernameOverride === 'true') { - userName = ( - - {data.override_username} - - ); - } else if (user) { - userName = ( - - {displayUsername(user, teammateNameDisplay)} - - ); - } - - 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 ( - - - - - - {icon} - - - {title} - - - {messageText} - - - - - - - - ); - } - - 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, - }, -}); diff --git a/app/screens/notification/notification.test.js b/app/screens/notification/notification.test.js deleted file mode 100644 index 9026e2999..000000000 --- a/app/screens/notification/notification.test.js +++ /dev/null @@ -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( - , - ); - - expect(wrapper.getElement()).toMatchSnapshot(); - }); - - test('should call loadFromPushNotification on notification tap for non-local notification', () => { - const props = { - ...baseProps, - notification: { - localNotification: true, - }, - }; - const wrapper = shallow( - , - ); - 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); - }); -}); diff --git a/app/screens/notification/notification_icon.tsx b/app/screens/notification/notification_icon.tsx new file mode 100644 index 000000000..e6e271df0 --- /dev/null +++ b/app/screens/notification/notification_icon.tsx @@ -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 = ( + + ); + } else { + icon = ( + + ); + } + } else if (user) { + icon = ( + + ); + } else { + icon = ( + + ); + } + + return ( + + {icon} + + ); +}; + +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; \ No newline at end of file diff --git a/app/screens/notification/notification_title.tsx b/app/screens/notification/notification_title.tsx new file mode 100644 index 000000000..6940d5038 --- /dev/null +++ b/app/screens/notification/notification_title.tsx @@ -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 ( + + {channelName} + + ); +}; + +const styles = StyleSheet.create({ + title: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default NotificationTitle; \ No newline at end of file diff --git a/app/types/push_notification.d.ts b/app/types/push_notification.d.ts new file mode 100644 index 000000000..373356c53 --- /dev/null +++ b/app/types/push_notification.d.ts @@ -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; +} \ No newline at end of file diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 111480740..7d1aa9fa9 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/detox/e2e/test/channel_info/header.e2e.js b/detox/e2e/test/channel_info/header.e2e.js index 0f3bdd243..f8c77357b 100644 --- a/detox/e2e/test/channel_info/header.e2e.js +++ b/detox/e2e/test/channel_info/header.e2e.js @@ -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', () => { diff --git a/detox/e2e/test/in_app_notification/notification.e2e.js b/detox/e2e/test/in_app_notification/notification.e2e.js new file mode 100644 index 000000000..dc53e86d5 --- /dev/null +++ b/detox/e2e/test/in_app_notification/notification.e2e.js @@ -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(); + }); +}); diff --git a/detox/e2e/test/messaging/message_posting.e2e.js b/detox/e2e/test/messaging/message_posting.e2e.js index c5eece06a..7f583c6ec 100644 --- a/detox/e2e/test/messaging/message_posting.e2e.js +++ b/detox/e2e/test/messaging/message_posting.e2e.js @@ -8,7 +8,6 @@ // ******************************************************************* import {logoutUser, toChannelScreen} from '@support/ui/screen'; - import {Setup} from '@support/server_api'; describe('Messaging', () => { diff --git a/patches/react-native-notifications+2.1.7.patch b/patches/react-native-notifications+2.1.7.patch index 48213c4dd..417e02ed8 100644 --- a/patches/react-native-notifications+2.1.7.patch +++ b/patches/react-native-notifications+2.1.7.patch @@ -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 diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 6e451be29..847a0def8 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -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