diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 29545dc22..6e704861b 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -66,4 +66,5 @@ 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_MARKDOWN = 'TOGGLE_MARKDOWN';
diff --git a/app/actions/notification.js b/app/actions/notification.js
new file mode 100644
index 000000000..44f03d8a0
--- /dev/null
+++ b/app/actions/notification.js
@@ -0,0 +1,17 @@
+import { NOTIFICATION } from './actionsTypes';
+
+export function notificationReceived(params) {
+ return {
+ type: NOTIFICATION.RECEIVED,
+ payload: {
+ message: params.text,
+ payload: params.payload
+ }
+ };
+}
+
+export function removeNotification() {
+ return {
+ type: NOTIFICATION.REMOVE
+ };
+}
diff --git a/app/constants/colors.js b/app/constants/colors.js
index 57d5a0f92..e572bd278 100644
--- a/app/constants/colors.js
+++ b/app/constants/colors.js
@@ -10,6 +10,7 @@ export const COLOR_TEXT = '#2F343D';
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
export const COLOR_SEPARATOR = '#A7A7AA';
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
+export const COLOR_BACKGROUND_NOTIFICATION = '#f8f8f8';
export const COLOR_BORDER = '#e1e5e8';
export const COLOR_UNREAD = '#e1e5e8';
export const COLOR_TOAST = '#0C0D0F';
diff --git a/app/index.js b/app/index.js
index 33c491561..7a08db17d 100644
--- a/app/index.js
+++ b/app/index.js
@@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
import { Linking } from 'react-native';
import firebase from 'react-native-firebase';
+import PropTypes from 'prop-types';
import { appInit } from './actions';
import { deepLinkingOpen } from './actions/deepLinking';
@@ -39,8 +40,9 @@ import OAuthView from './views/OAuthView';
import SetUsernameView from './views/SetUsernameView';
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
import parseQuery from './lib/methods/helpers/parseQuery';
-import { initializePushNotifications, onNotification } from './push';
+import { initializePushNotifications, onNotification } from './notifications/push';
import store from './lib/createStore';
+import NotificationBadge from './notifications/inApp';
useScreens();
@@ -195,10 +197,28 @@ const SetUsernameStack = createStackNavigator({
SetUsernameView
});
+class CustomInsideStack extends React.Component {
+ static router = InsideStackModal.router;
+
+ static propTypes = {
+ navigation: PropTypes.object
+ }
+
+ render() {
+ const { navigation } = this.props;
+ return (
+
+
+
+
+ );
+ }
+}
+
const App = createAppContainer(createSwitchNavigator(
{
OutsideStack: OutsideStackModal,
- InsideStack: InsideStackModal,
+ InsideStack: CustomInsideStack,
AuthLoading: AuthLoadingView,
SetUsernameStack
},
diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js
index aee57b1ee..50a35c551 100644
--- a/app/lib/methods/subscriptions/rooms.js
+++ b/app/lib/methods/subscriptions/rooms.js
@@ -6,6 +6,7 @@ import log from '../../../utils/log';
import random from '../../../utils/random';
import store from '../../createStore';
import { roomsRequest } from '../../../actions/rooms';
+import { notificationReceived } from '../../../actions/notification';
const removeListener = listener => listener.stop();
@@ -120,6 +121,10 @@ export default async function subscribeRooms() {
}
});
}
+ if (/notification/.test(ev)) {
+ const [notification] = ddpMessage.fields.args;
+ store.dispatch(notificationReceived(notification));
+ }
});
const stop = () => {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 3c7f622e5..737e58b8f 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -35,7 +35,7 @@ import loadThreadMessages from './methods/loadThreadMessages';
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
-import { getDeviceToken } from '../push';
+import { getDeviceToken } from '../notifications/push';
import { roomsRequest } from '../actions/rooms';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
diff --git a/app/notifications/inApp/index.js b/app/notifications/inApp/index.js
new file mode 100644
index 000000000..f99cdc5ab
--- /dev/null
+++ b/app/notifications/inApp/index.js
@@ -0,0 +1,229 @@
+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 { isNotch, isIOS } from '../../utils/deviceInfo';
+import { CustomIcon } from '../../lib/Icons';
+import { COLOR_BACKGROUND_NOTIFICATION, COLOR_SEPARATOR, COLOR_TEXT } 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';
+
+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,
+ backgroundColor: COLOR_BACKGROUND_NOTIFICATION,
+ width: '100%',
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderColor: COLOR_SEPARATOR
+ },
+ content: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ avatar: {
+ marginRight: 10
+ },
+ roomName: {
+ fontSize: 17,
+ lineHeight: 20,
+ ...sharedStyles.textColorNormal,
+ ...sharedStyles.textMedium
+ },
+ message: {
+ fontSize: 14,
+ lineHeight: 17,
+ ...sharedStyles.textRegular,
+ ...sharedStyles.textColorNormal
+ },
+ close: {
+ color: COLOR_TEXT,
+ marginLeft: 10
+ }
+});
+
+@responsive
+@connect(
+ state => ({
+ userId: state.login.user && state.login.user.id,
+ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
+ token: state.login.user && state.login.user.token,
+ notification: state.notification
+ }),
+ dispatch => ({
+ removeNotification: () => dispatch(removeNotificationAction())
+ })
+)
+export default class NotificationBadge extends React.Component {
+ static propTypes = {
+ navigation: PropTypes.object,
+ baseUrl: PropTypes.string,
+ token: PropTypes.string,
+ userId: PropTypes.string,
+ notification: PropTypes.object,
+ window: PropTypes.object,
+ removeNotification: PropTypes.func
+ }
+
+ constructor(props) {
+ super(props);
+ this.animatedValue = new Animated.Value(0);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { notification: nextNotification } = nextProps;
+ const {
+ notification: { payload }, window
+ } = this.props;
+ if (!equal(nextNotification.payload, payload)) {
+ return true;
+ }
+ if (nextProps.window.width !== window.width) {
+ return true;
+ }
+ return false;
+ }
+
+ componentDidUpdate() {
+ const { notification: { payload }, navigation } = this.props;
+ const navState = this.getNavState(navigation.state);
+ if (payload.rid) {
+ if (navState && navState.routeName === 'RoomView' && navState.params && navState.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);
+ }
+ }
+
+ getNavState = (routes) => {
+ if (!routes.routes) {
+ return routes;
+ }
+ return this.getNavState(routes.routes[routes.index]);
+ }
+
+ goToRoom = async() => {
+ const { notification: { payload }, navigation } = this.props;
+ const { rid, type, prid } = payload;
+ if (!rid) {
+ return;
+ }
+ const name = type === 'p' ? payload.name : payload.sender.username;
+ await navigation.navigate('RoomsListView');
+ navigation.navigate('RoomView', {
+ rid, name, t: type, prid
+ });
+ this.hide();
+ }
+
+ render() {
+ const {
+ baseUrl, token, userId, notification, window
+ } = this.props;
+ const { message, payload } = notification;
+ const { type } = payload;
+ const name = type === 'p' ? payload.name : payload.sender.username;
+
+ let top = 0;
+ if (isIOS) {
+ const portrait = window.height > window.width;
+ if (portrait) {
+ top = isNotch ? 45 : 20;
+ } else {
+ top = 0;
+ }
+ }
+
+ const maxWidthMessage = window.width - 110;
+
+ const translateY = this.animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [-top - ROW_HEIGHT, top]
+ });
+ return (
+
+
+
+
+
+ {name}
+ {message}
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/push/index.js b/app/notifications/push/index.js
similarity index 89%
rename from app/push/index.js
rename to app/notifications/push/index.js
index d78af4c7b..37b2b0391 100644
--- a/app/push/index.js
+++ b/app/notifications/push/index.js
@@ -1,8 +1,8 @@
import EJSON from 'ejson';
import PushNotification from './push';
-import store from '../lib/createStore';
-import { deepLinkingOpen } from '../actions/deepLinking';
+import store from '../../lib/createStore';
+import { deepLinkingOpen } from '../../actions/deepLinking';
export const onNotification = (notification) => {
if (notification) {
diff --git a/app/push/push.android.js b/app/notifications/push/push.android.js
similarity index 100%
rename from app/push/push.android.js
rename to app/notifications/push/push.android.js
diff --git a/app/push/push.ios.js b/app/notifications/push/push.ios.js
similarity index 100%
rename from app/push/push.ios.js
rename to app/notifications/push/push.ios.js
diff --git a/app/reducers/index.js b/app/reducers/index.js
index d33c26abc..e7bfb41d1 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -9,6 +9,7 @@ import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
import sortPreferences from './sortPreferences';
+import notification from './notification';
import markdown from './markdown';
export default combineReducers({
@@ -22,5 +23,6 @@ export default combineReducers({
app,
rooms,
sortPreferences,
+ notification,
markdown
});
diff --git a/app/reducers/notification.js b/app/reducers/notification.js
new file mode 100644
index 000000000..5b1d07c9b
--- /dev/null
+++ b/app/reducers/notification.js
@@ -0,0 +1,24 @@
+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/sagas/state.js b/app/sagas/state.js
index 27b09483b..7d5ece8c1 100644
--- a/app/sagas/state.js
+++ b/app/sagas/state.js
@@ -2,7 +2,7 @@ import { takeLatest, select } from 'redux-saga/effects';
import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate';
import RocketChat from '../lib/rocketchat';
-import { setBadgeCount } from '../push';
+import { setBadgeCount } from '../notifications/push';
import log from '../utils/log';
const appHasComeBackToForeground = function* appHasComeBackToForeground() {