[NEW] In-app notification (#964)
* added Notification badge * added notification to state * added condition not see notification of current room * fixed lint * fixed some bugs * fixed some bugs * removed navigation prop * fixed navigation bug * removed unessary changes * done requested chamges * made separate notification for ios and android * merged notification * Removed unnecessary sub * Animation * Layout changes * Refactor
This commit is contained in:
parent
b7e6d3615f
commit
467a2d4002
|
@ -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 SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
||||||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||||
|
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
|
||||||
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
|
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ export const COLOR_TEXT = '#2F343D';
|
||||||
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
|
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
|
||||||
export const COLOR_SEPARATOR = '#A7A7AA';
|
export const COLOR_SEPARATOR = '#A7A7AA';
|
||||||
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
||||||
|
export const COLOR_BACKGROUND_NOTIFICATION = '#f8f8f8';
|
||||||
export const COLOR_BORDER = '#e1e5e8';
|
export const COLOR_BORDER = '#e1e5e8';
|
||||||
export const COLOR_UNREAD = '#e1e5e8';
|
export const COLOR_UNREAD = '#e1e5e8';
|
||||||
export const COLOR_TOAST = '#0C0D0F';
|
export const COLOR_TOAST = '#0C0D0F';
|
||||||
|
|
24
app/index.js
24
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 { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
|
||||||
import { Linking } from 'react-native';
|
import { Linking } from 'react-native';
|
||||||
import firebase from 'react-native-firebase';
|
import firebase from 'react-native-firebase';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { appInit } from './actions';
|
import { appInit } from './actions';
|
||||||
import { deepLinkingOpen } from './actions/deepLinking';
|
import { deepLinkingOpen } from './actions/deepLinking';
|
||||||
|
@ -39,8 +40,9 @@ import OAuthView from './views/OAuthView';
|
||||||
import SetUsernameView from './views/SetUsernameView';
|
import SetUsernameView from './views/SetUsernameView';
|
||||||
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
|
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
|
||||||
import parseQuery from './lib/methods/helpers/parseQuery';
|
import parseQuery from './lib/methods/helpers/parseQuery';
|
||||||
import { initializePushNotifications, onNotification } from './push';
|
import { initializePushNotifications, onNotification } from './notifications/push';
|
||||||
import store from './lib/createStore';
|
import store from './lib/createStore';
|
||||||
|
import NotificationBadge from './notifications/inApp';
|
||||||
|
|
||||||
useScreens();
|
useScreens();
|
||||||
|
|
||||||
|
@ -195,10 +197,28 @@ const SetUsernameStack = createStackNavigator({
|
||||||
SetUsernameView
|
SetUsernameView
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class CustomInsideStack extends React.Component {
|
||||||
|
static router = InsideStackModal.router;
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
navigation: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<InsideStackModal navigation={navigation} />
|
||||||
|
<NotificationBadge navigation={navigation} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const App = createAppContainer(createSwitchNavigator(
|
const App = createAppContainer(createSwitchNavigator(
|
||||||
{
|
{
|
||||||
OutsideStack: OutsideStackModal,
|
OutsideStack: OutsideStackModal,
|
||||||
InsideStack: InsideStackModal,
|
InsideStack: CustomInsideStack,
|
||||||
AuthLoading: AuthLoadingView,
|
AuthLoading: AuthLoadingView,
|
||||||
SetUsernameStack
|
SetUsernameStack
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,6 +6,7 @@ import log from '../../../utils/log';
|
||||||
import random from '../../../utils/random';
|
import random from '../../../utils/random';
|
||||||
import store from '../../createStore';
|
import store from '../../createStore';
|
||||||
import { roomsRequest } from '../../../actions/rooms';
|
import { roomsRequest } from '../../../actions/rooms';
|
||||||
|
import { notificationReceived } from '../../../actions/notification';
|
||||||
|
|
||||||
const removeListener = listener => listener.stop();
|
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 = () => {
|
const stop = () => {
|
||||||
|
|
|
@ -35,7 +35,7 @@ import loadThreadMessages from './methods/loadThreadMessages';
|
||||||
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
|
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
|
||||||
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
||||||
|
|
||||||
import { getDeviceToken } from '../push';
|
import { getDeviceToken } from '../notifications/push';
|
||||||
import { roomsRequest } from '../actions/rooms';
|
import { roomsRequest } from '../actions/rooms';
|
||||||
|
|
||||||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Animated.View style={[styles.container, { transform: [{ translateY }] }]}>
|
||||||
|
<Touchable
|
||||||
|
style={styles.content}
|
||||||
|
onPress={this.goToRoom}
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
background={Touchable.SelectableBackgroundBorderless()}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
<Avatar text={name} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
|
||||||
|
<View>
|
||||||
|
<Text style={styles.roomName}>{name}</Text>
|
||||||
|
<Text style={[styles.message, { maxWidth: maxWidthMessage }]} numberOfLines={1}>{message}</Text>
|
||||||
|
</View>
|
||||||
|
</React.Fragment>
|
||||||
|
</Touchable>
|
||||||
|
<TouchableOpacity onPress={this.hide}>
|
||||||
|
<CustomIcon name='circle-cross' style={styles.close} size={20} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import EJSON from 'ejson';
|
import EJSON from 'ejson';
|
||||||
|
|
||||||
import PushNotification from './push';
|
import PushNotification from './push';
|
||||||
import store from '../lib/createStore';
|
import store from '../../lib/createStore';
|
||||||
import { deepLinkingOpen } from '../actions/deepLinking';
|
import { deepLinkingOpen } from '../../actions/deepLinking';
|
||||||
|
|
||||||
export const onNotification = (notification) => {
|
export const onNotification = (notification) => {
|
||||||
if (notification) {
|
if (notification) {
|
|
@ -9,6 +9,7 @@ import selectedUsers from './selectedUsers';
|
||||||
import createChannel from './createChannel';
|
import createChannel from './createChannel';
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import sortPreferences from './sortPreferences';
|
import sortPreferences from './sortPreferences';
|
||||||
|
import notification from './notification';
|
||||||
import markdown from './markdown';
|
import markdown from './markdown';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
@ -22,5 +23,6 @@ export default combineReducers({
|
||||||
app,
|
app,
|
||||||
rooms,
|
rooms,
|
||||||
sortPreferences,
|
sortPreferences,
|
||||||
|
notification,
|
||||||
markdown
|
markdown
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { takeLatest, select } from 'redux-saga/effects';
|
||||||
import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate';
|
import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate';
|
||||||
|
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import { setBadgeCount } from '../push';
|
import { setBadgeCount } from '../notifications/push';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
|
|
||||||
const appHasComeBackToForeground = function* appHasComeBackToForeground() {
|
const appHasComeBackToForeground = function* appHasComeBackToForeground() {
|
||||||
|
|
Loading…
Reference in New Issue