diff --git a/app/AppContainer.js b/app/AppContainer.js index 76e2fee8e..4242c0cda 100644 --- a/app/AppContainer.js +++ b/app/AppContainer.js @@ -3,14 +3,12 @@ import PropTypes from 'prop-types'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { connect } from 'react-redux'; -import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'; import Navigation from './lib/Navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND } from './actions/app'; -import { ActionSheetProvider } from './containers/ActionSheet'; // Stacks import AuthLoadingView from './views/AuthLoadingView'; @@ -53,57 +51,53 @@ const App = React.memo(({ root, isMasterDetail }) => { }, []); return ( - - - { - const previousRouteName = Navigation.routeNameRef.current; - const currentRouteName = getActiveRouteName(state); - if (previousRouteName !== currentRouteName) { - setCurrentScreen(currentRouteName); - } - Navigation.routeNameRef.current = currentRouteName; - }} - > - - <> - {root === ROOT_LOADING || root === ROOT_BACKGROUND ? ( - - ) : null} - {root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? ( - - ) : null} - {root === ROOT_INSIDE && isMasterDetail ? ( - - ) : null} - {root === ROOT_INSIDE && !isMasterDetail ? ( - - ) : null} - {root === ROOT_SET_USERNAME ? ( - - ) : null} - - - - - + { + const previousRouteName = Navigation.routeNameRef.current; + const currentRouteName = getActiveRouteName(state); + if (previousRouteName !== currentRouteName) { + setCurrentScreen(currentRouteName); + } + Navigation.routeNameRef.current = currentRouteName; + }} + > + + <> + {root === ROOT_LOADING || root === ROOT_BACKGROUND ? ( + + ) : null} + {root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? ( + + ) : null} + {root === ROOT_INSIDE && isMasterDetail ? ( + + ) : null} + {root === ROOT_INSIDE && !isMasterDetail ? ( + + ) : null} + {root === ROOT_SET_USERNAME ? ( + + ) : null} + + + ); }); const mapStateToProps = state => ({ diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 78bfa4176..65c39d9c6 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -51,7 +51,6 @@ export const LOGOUT = 'LOGOUT'; // logout is always success export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']); export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); -export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']); export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT'; export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS'; export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS'; diff --git a/app/actions/notification.js b/app/actions/notification.js deleted file mode 100644 index 35fc49d87..000000000 --- a/app/actions/notification.js +++ /dev/null @@ -1,19 +0,0 @@ -import { NOTIFICATION } from './actionsTypes'; - -export function notificationReceived(params) { - return { - type: NOTIFICATION.RECEIVED, - payload: { - title: params.title, - avatar: params.avatar, - message: params.text, - payload: params.payload - } - }; -} - -export function removeNotification() { - return { - type: NOTIFICATION.REMOVE - }; -} diff --git a/app/containers/InAppNotification/NotifierComponent.js b/app/containers/InAppNotification/NotifierComponent.js new file mode 100644 index 000000000..9ad6163a8 --- /dev/null +++ b/app/containers/InAppNotification/NotifierComponent.js @@ -0,0 +1,147 @@ +import React from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import PropTypes from 'prop-types'; +import Touchable from 'react-native-platform-touchable'; +import { connect } from 'react-redux'; +import { Notifier } from 'react-native-notifier'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useDeviceOrientation } from '@react-native-community/hooks'; + +import Avatar from '../Avatar'; +import { CustomIcon } from '../../lib/Icons'; +import sharedStyles from '../../views/Styles'; +import { themes } from '../../constants/colors'; +import { useTheme } from '../../theme'; +import { getUserSelector } from '../../selectors/login'; +import { ROW_HEIGHT } from '../../presentation/RoomItem'; +import { goRoom } from '../../utils/goRoom'; +import Navigation from '../../lib/Navigation'; + +const AVATAR_SIZE = 48; +const BUTTON_HIT_SLOP = { + top: 12, right: 12, bottom: 12, left: 12 +}; + +const styles = StyleSheet.create({ + container: { + height: ROW_HEIGHT, + paddingHorizontal: 14, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginHorizontal: 10, + borderWidth: StyleSheet.hairlineWidth, + borderRadius: 4 + }, + content: { + flex: 1, + flexDirection: 'row', + alignItems: 'center' + }, + inner: { + flex: 1 + }, + avatar: { + marginRight: 10 + }, + roomName: { + fontSize: 17, + lineHeight: 20, + ...sharedStyles.textMedium + }, + message: { + fontSize: 14, + lineHeight: 17, + ...sharedStyles.textRegular + }, + close: { + marginLeft: 10 + }, + small: { + width: '50%', + alignSelf: 'center' + } +}); + +const hideNotification = () => Notifier.hideNotification(); + +const NotifierComponent = React.memo(({ + baseUrl, user, notification, isMasterDetail +}) => { + const { theme } = useTheme(); + const insets = useSafeAreaInsets(); + const { landscape } = useDeviceOrientation(); + + const { id: userId, token } = user; + const { text, payload } = notification; + const { type } = payload; + const name = type === 'd' ? payload.sender.username : payload.name; + // if sub is not on local database, title and avatar will be null, so we use payload from notification + const { title = name, avatar = name } = notification; + + const onPress = () => { + const { rid, prid } = payload; + if (!rid) { + return; + } + const item = { + rid, name: title, t: type, prid + }; + + if (isMasterDetail) { + Navigation.navigate('DrawerNavigator'); + } + goRoom({ item, isMasterDetail }); + hideNotification(); + }; + + return ( + + + <> + + + {title} + {text} + + + + + + + + ); +}); + +NotifierComponent.propTypes = { + baseUrl: PropTypes.string, + user: PropTypes.object, + notification: PropTypes.object, + isMasterDetail: PropTypes.bool +}; + +const mapStateToProps = state => ({ + user: getUserSelector(state), + baseUrl: state.server.server, + isMasterDetail: state.app.isMasterDetail +}); + +export default connect(mapStateToProps)(NotifierComponent); diff --git a/app/containers/InAppNotification/index.js b/app/containers/InAppNotification/index.js new file mode 100644 index 000000000..765f16078 --- /dev/null +++ b/app/containers/InAppNotification/index.js @@ -0,0 +1,40 @@ +import React, { memo, useEffect } from 'react'; +import { NotifierRoot, Notifier, Easing } from 'react-native-notifier'; + +import NotifierComponent from './NotifierComponent'; +import EventEmitter from '../../utils/events'; +import Navigation from '../../lib/Navigation'; +import { getActiveRoute } from '../../utils/navigation'; + +export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp'; + +const InAppNotification = memo(() => { + const show = (notification) => { + const { payload } = notification; + const state = Navigation.navigationRef.current.getRootState(); + const route = getActiveRoute(state); + if (payload.rid) { + if (route?.name === 'RoomView' && route.params?.rid === payload.rid) { + return; + } + Notifier.showNotification({ + showEasing: Easing.inOut(Easing.quad), + Component: NotifierComponent, + componentProps: { + notification + } + }); + } + }; + + useEffect(() => { + EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show); + return () => { + EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER); + }; + }, []); + + return ; +}); + +export default InAppNotification; diff --git a/app/index.js b/app/index.js index 862d1b08b..a7167e692 100644 --- a/app/index.js +++ b/app/index.js @@ -5,6 +5,7 @@ import { Provider } from 'react-redux'; import RNUserDefaults from 'rn-user-defaults'; import { KeyCommandsEmitter } from 'react-native-keycommands'; import RNScreens from 'react-native-screens'; +import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'; import { defaultTheme, @@ -30,6 +31,10 @@ import AppContainer from './AppContainer'; import TwoFactor from './containers/TwoFactor'; import ScreenLockedView from './views/ScreenLockedView'; import ChangePasscodeView from './views/ChangePasscodeView'; +import Toast from './containers/Toast'; +import InAppNotification from './containers/InAppNotification'; +import { ActionSheetProvider } from './containers/ActionSheet'; + RNScreens.enableScreens(); @@ -151,22 +156,28 @@ export default class Root extends React.Component { render() { const { themePreferences, theme } = this.state; return ( - - - - - - - - - - + + + + + + + + + + + + + + + + ); } } diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index d6ea0f7b6..722f6337a 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -9,12 +9,12 @@ import log from '../../../utils/log'; import random from '../../../utils/random'; import store from '../../createStore'; import { roomsRequest } from '../../../actions/rooms'; -import { notificationReceived } from '../../../actions/notification'; import { handlePayloadUserInteraction } from '../actions'; import buildMessage from '../helpers/buildMessage'; import RocketChat from '../../rocketchat'; -import EventEmmiter from '../../../utils/events'; +import EventEmitter from '../../../utils/events'; import { removedRoom } from '../../../actions/room'; +import { INAPP_NOTIFICATION_EMITTER } from '../../../containers/InAppNotification'; const removeListener = listener => listener.stop(); @@ -267,7 +267,7 @@ export default function subscribeRooms() { if (data.rid === roomState.rid && roomState.isDeleting) { store.dispatch(removedRoom()); } else { - EventEmmiter.emit('ROOM_REMOVED', { rid: data.rid }); + EventEmitter.emit('ROOM_REMOVED', { rid: data.rid }); } } catch (e) { log(e); @@ -320,7 +320,7 @@ export default function subscribeRooms() { } catch (e) { // do nothing } - store.dispatch(notificationReceived(notification)); + EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, notification); } if (/uiInteraction/.test(ev)) { const { type: eventType, ...args } = type; diff --git a/app/notifications/inApp/index.js b/app/notifications/inApp/index.js deleted file mode 100644 index cd77c2f19..000000000 --- a/app/notifications/inApp/index.js +++ /dev/null @@ -1,248 +0,0 @@ -import React from 'react'; -import { - View, Text, StyleSheet, TouchableOpacity, Animated, Easing -} from 'react-native'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import equal from 'deep-equal'; -import { responsive } from 'react-native-responsive-ui'; -import Touchable from 'react-native-platform-touchable'; - -import { hasNotch, isIOS, isTablet } from '../../utils/deviceInfo'; -import { CustomIcon } from '../../lib/Icons'; -import { themes } from '../../constants/colors'; -import Avatar from '../../containers/Avatar'; -import { removeNotification as removeNotificationAction } from '../../actions/notification'; -import sharedStyles from '../../views/Styles'; -import { ROW_HEIGHT } from '../../presentation/RoomItem'; -import { withTheme } from '../../theme'; -import { getUserSelector } from '../../selectors/login'; -import { getActiveRoute } from '../../utils/navigation'; -import Navigation from '../../lib/Navigation'; -import { goRoom } from '../../utils/goRoom'; - -const AVATAR_SIZE = 48; -const ANIMATION_DURATION = 300; -const NOTIFICATION_DURATION = 3000; -const BUTTON_HIT_SLOP = { - top: 12, right: 12, bottom: 12, left: 12 -}; -const ANIMATION_PROPS = { - duration: ANIMATION_DURATION, - easing: Easing.inOut(Easing.quad), - useNativeDriver: true -}; - -const styles = StyleSheet.create({ - container: { - height: ROW_HEIGHT, - paddingHorizontal: 14, - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - position: 'absolute', - zIndex: 2, - width: '100%', - borderBottomWidth: StyleSheet.hairlineWidth - }, - content: { - flex: 1, - flexDirection: 'row', - alignItems: 'center' - }, - inner: { - flex: 1 - }, - avatar: { - marginRight: 10 - }, - roomName: { - fontSize: 17, - lineHeight: 20, - ...sharedStyles.textMedium - }, - message: { - fontSize: 14, - lineHeight: 17, - ...sharedStyles.textRegular - }, - close: { - marginLeft: 10 - } -}); - -class NotificationBadge extends React.Component { - static propTypes = { - isMasterDetail: PropTypes.bool, - baseUrl: PropTypes.string, - user: PropTypes.object, - notification: PropTypes.object, - window: PropTypes.object, - removeNotification: PropTypes.func, - theme: PropTypes.string - } - - constructor(props) { - super(props); - this.animatedValue = new Animated.Value(0); - } - - shouldComponentUpdate(nextProps) { - const { notification: nextNotification } = nextProps; - const { - notification: { payload }, window, theme - } = this.props; - if (nextProps.theme !== theme) { - return true; - } - if (!equal(nextNotification.payload, payload)) { - return true; - } - if (nextProps.window.width !== window.width) { - return true; - } - return false; - } - - componentDidUpdate(prevProps) { - const { notification: { payload } } = this.props; - const { notification: { payload: prevPayload } } = prevProps; - if (!equal(prevPayload, payload)) { - const state = Navigation.navigationRef.current.getRootState(); - const route = getActiveRoute(state); - if (payload.rid) { - if (route?.name === 'RoomView' && route.params?.rid === payload.rid) { - return; - } - this.show(); - } - } - } - - componentWillUnmount() { - this.clearTimeout(); - } - - show = () => { - Animated.timing( - this.animatedValue, - { - toValue: 1, - ...ANIMATION_PROPS - } - ).start(() => { - this.clearTimeout(); - this.timeout = setTimeout(() => { - this.hide(); - }, NOTIFICATION_DURATION); - }); - } - - hide = () => { - const { removeNotification } = this.props; - Animated.timing( - this.animatedValue, - { - toValue: 0, - ...ANIMATION_PROPS - } - ).start(); - setTimeout(removeNotification, ANIMATION_DURATION); - } - - clearTimeout = () => { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - goToRoom = () => { - const { notification, isMasterDetail, baseUrl } = this.props; - const { payload } = notification; - const { rid, type, prid } = payload; - if (!rid) { - return; - } - const name = type === 'd' ? payload.sender.username : payload.name; - // if sub is not on local database, title will be null, so we use payload from notification - const { title = name } = notification; - const item = { - rid, name: title, t: type, prid, baseUrl - }; - if (isMasterDetail) { - Navigation.navigate('DrawerNavigator'); - } - goRoom({ item, isMasterDetail }); - this.hide(); - } - - render() { - const { - baseUrl, user: { id: userId, token }, notification, window, theme - } = this.props; - const { message, payload } = notification; - const { type } = payload; - const name = type === 'd' ? payload.sender.username : payload.name; - // if sub is not on local database, title and avatar will be null, so we use payload from notification - const { title = name, avatar = name } = notification; - - let top = 0; - if (isIOS) { - const portrait = window.height > window.width; - if (portrait) { - top = hasNotch ? 45 : 20; - } else { - top = isTablet ? 20 : 0; - } - } - - const translateY = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: [-top - ROW_HEIGHT, top] - }); - return ( - - - <> - - - {title} - {message} - - - - - - - - ); - } -} - -const mapStateToProps = state => ({ - user: getUserSelector(state), - baseUrl: state.server.server, - notification: state.notification, - isMasterDetail: PropTypes.bool -}); - -const mapDispatchToProps = dispatch => ({ - removeNotification: () => dispatch(removeNotificationAction()) -}); - -export default responsive(connect(mapStateToProps, mapDispatchToProps)(withTheme(NotificationBadge))); diff --git a/app/reducers/index.js b/app/reducers/index.js index dead110fb..1ac810c33 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -9,7 +9,6 @@ import selectedUsers from './selectedUsers'; import createChannel from './createChannel'; import app from './app'; import sortPreferences from './sortPreferences'; -import notification from './notification'; import share from './share'; import crashReport from './crashReport'; import customEmojis from './customEmojis'; @@ -29,7 +28,6 @@ export default combineReducers({ room, rooms, sortPreferences, - notification, share, crashReport, customEmojis, diff --git a/app/reducers/notification.js b/app/reducers/notification.js deleted file mode 100644 index 5b1d07c9b..000000000 --- a/app/reducers/notification.js +++ /dev/null @@ -1,24 +0,0 @@ -import { NOTIFICATION } from '../actions/actionsTypes'; - -const initialState = { - message: '', - payload: { - type: 'p', - name: '', - rid: '' - } -}; - -export default function notification(state = initialState, action) { - switch (action.type) { - case NOTIFICATION.RECEIVED: - return { - ...state, - ...action.payload - }; - case NOTIFICATION.REMOVE: - return initialState; - default: - return state; - } -} diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index e5d3d7b0c..147081deb 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { createStackNavigator } from '@react-navigation/stack'; import { createDrawerNavigator } from '@react-navigation/drawer'; @@ -7,9 +6,7 @@ import { ThemeContext } from '../theme'; import { defaultHeader, themedHeader, ModalAnimation, StackAnimation } from '../utils/navigation'; -import Toast from '../containers/Toast'; import Sidebar from '../views/SidebarView'; -import NotificationBadge from '../notifications/inApp'; // Chats Stack import RoomView from '../views/RoomView'; @@ -320,16 +317,4 @@ const InsideStackNavigator = () => { ); }; -const RootInsideStack = ({ navigation, route }) => ( - <> - - - - -); -RootInsideStack.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object -}; - -export default RootInsideStack; +export default InsideStackNavigator; diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.js index 3e2466c5f..40b59b09e 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.js @@ -8,8 +8,6 @@ import { ThemeContext } from '../../theme'; import { defaultHeader, themedHeader, StackAnimation, FadeFromCenterModal } from '../../utils/navigation'; -import Toast from '../../containers/Toast'; -import NotificationBadge from '../../notifications/inApp'; import { ModalContainer } from './ModalContainer'; // Chats Stack @@ -292,16 +290,4 @@ const InsideStackNavigator = React.memo(() => { ); }); -const RootInsideStack = React.memo(({ navigation, route }) => ( - <> - - - - -)); -RootInsideStack.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object -}; - -export default RootInsideStack; +export default InsideStackNavigator; diff --git a/package.json b/package.json index b95a6010f..f258cb27b 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "react-native-modal": "11.5.6", "react-native-navigation-bar-color": "2.0.1", "react-native-notifications": "2.1.7", + "react-native-notifier": "^1.3.1", "react-native-orientation-locker": "1.1.8", "react-native-picker-select": "7.0.0", "react-native-platform-touchable": "^1.1.1", diff --git a/yarn.lock b/yarn.lock index 831aa2a9d..c69d4569f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11895,6 +11895,11 @@ react-native-notifications@2.1.7: core-js "^1.0.0" uuid "^2.0.3" +react-native-notifier@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-notifier/-/react-native-notifier-1.3.1.tgz#a878c82c8ee99b04d57818401b1f084232729afd" + integrity sha512-w7KOTF5WOYzbhCXQHz6p9tbosOVxhOW+Sh7VAdIuW6r7PSoryRNkF4P6Bzq1+2NPtMK7L6xnojCdKJ+nVnwh+A== + react-native-orientation-locker@1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/react-native-orientation-locker/-/react-native-orientation-locker-1.1.8.tgz#45d1c9e002496b8d286ec8932d6e3e7d341f9c85"