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"