[IMPROVEMENT] Use react-native-notifier for in-app notifications (#2139)
Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com> Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
089e4bf3eb
commit
2632ef50f5
|
@ -3,14 +3,12 @@ import PropTypes from 'prop-types';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import Navigation from './lib/Navigation';
|
import Navigation from './lib/Navigation';
|
||||||
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
|
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
|
||||||
import {
|
import {
|
||||||
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
|
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
|
||||||
} from './actions/app';
|
} from './actions/app';
|
||||||
import { ActionSheetProvider } from './containers/ActionSheet';
|
|
||||||
|
|
||||||
// Stacks
|
// Stacks
|
||||||
import AuthLoadingView from './views/AuthLoadingView';
|
import AuthLoadingView from './views/AuthLoadingView';
|
||||||
|
@ -53,57 +51,53 @@ const App = React.memo(({ root, isMasterDetail }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
<NavigationContainer
|
||||||
<ActionSheetProvider>
|
theme={navTheme}
|
||||||
<NavigationContainer
|
ref={Navigation.navigationRef}
|
||||||
theme={navTheme}
|
onStateChange={(state) => {
|
||||||
ref={Navigation.navigationRef}
|
const previousRouteName = Navigation.routeNameRef.current;
|
||||||
onStateChange={(state) => {
|
const currentRouteName = getActiveRouteName(state);
|
||||||
const previousRouteName = Navigation.routeNameRef.current;
|
if (previousRouteName !== currentRouteName) {
|
||||||
const currentRouteName = getActiveRouteName(state);
|
setCurrentScreen(currentRouteName);
|
||||||
if (previousRouteName !== currentRouteName) {
|
}
|
||||||
setCurrentScreen(currentRouteName);
|
Navigation.routeNameRef.current = currentRouteName;
|
||||||
}
|
}}
|
||||||
Navigation.routeNameRef.current = currentRouteName;
|
>
|
||||||
}}
|
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
||||||
>
|
<>
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
|
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
|
||||||
<>
|
<Stack.Screen
|
||||||
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
|
name='AuthLoading'
|
||||||
<Stack.Screen
|
component={AuthLoadingView}
|
||||||
name='AuthLoading'
|
/>
|
||||||
component={AuthLoadingView}
|
) : null}
|
||||||
/>
|
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
|
||||||
) : null}
|
<Stack.Screen
|
||||||
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
|
name='OutsideStack'
|
||||||
<Stack.Screen
|
component={OutsideStack}
|
||||||
name='OutsideStack'
|
/>
|
||||||
component={OutsideStack}
|
) : null}
|
||||||
/>
|
{root === ROOT_INSIDE && isMasterDetail ? (
|
||||||
) : null}
|
<Stack.Screen
|
||||||
{root === ROOT_INSIDE && isMasterDetail ? (
|
name='MasterDetailStack'
|
||||||
<Stack.Screen
|
component={MasterDetailStack}
|
||||||
name='MasterDetailStack'
|
/>
|
||||||
component={MasterDetailStack}
|
) : null}
|
||||||
/>
|
{root === ROOT_INSIDE && !isMasterDetail ? (
|
||||||
) : null}
|
<Stack.Screen
|
||||||
{root === ROOT_INSIDE && !isMasterDetail ? (
|
name='InsideStack'
|
||||||
<Stack.Screen
|
component={InsideStack}
|
||||||
name='InsideStack'
|
/>
|
||||||
component={InsideStack}
|
) : null}
|
||||||
/>
|
{root === ROOT_SET_USERNAME ? (
|
||||||
) : null}
|
<Stack.Screen
|
||||||
{root === ROOT_SET_USERNAME ? (
|
name='SetUsernameStack'
|
||||||
<Stack.Screen
|
component={SetUsernameStack}
|
||||||
name='SetUsernameStack'
|
/>
|
||||||
component={SetUsernameStack}
|
) : null}
|
||||||
/>
|
</>
|
||||||
) : null}
|
</Stack.Navigator>
|
||||||
</>
|
</NavigationContainer>
|
||||||
</Stack.Navigator>
|
|
||||||
</NavigationContainer>
|
|
||||||
</ActionSheetProvider>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -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 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_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
||||||
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
||||||
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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 (
|
||||||
|
<View style={[
|
||||||
|
styles.container,
|
||||||
|
(isMasterDetail || landscape) && styles.small,
|
||||||
|
{
|
||||||
|
backgroundColor: themes[theme].focusedBackground,
|
||||||
|
borderColor: themes[theme].separatorColor,
|
||||||
|
marginTop: insets.top
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Touchable
|
||||||
|
style={styles.content}
|
||||||
|
onPress={onPress}
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
background={Touchable.SelectableBackgroundBorderless()}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Avatar text={avatar} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
|
||||||
|
<View style={styles.inner}>
|
||||||
|
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
|
||||||
|
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
</Touchable>
|
||||||
|
<Touchable
|
||||||
|
onPress={hideNotification}
|
||||||
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
|
background={Touchable.SelectableBackgroundBorderless()}
|
||||||
|
>
|
||||||
|
<CustomIcon name='Cross' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
|
||||||
|
</Touchable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
|
@ -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 <NotifierRoot />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default InAppNotification;
|
43
app/index.js
43
app/index.js
|
@ -5,6 +5,7 @@ import { Provider } from 'react-redux';
|
||||||
import RNUserDefaults from 'rn-user-defaults';
|
import RNUserDefaults from 'rn-user-defaults';
|
||||||
import { KeyCommandsEmitter } from 'react-native-keycommands';
|
import { KeyCommandsEmitter } from 'react-native-keycommands';
|
||||||
import RNScreens from 'react-native-screens';
|
import RNScreens from 'react-native-screens';
|
||||||
|
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
|
@ -30,6 +31,10 @@ import AppContainer from './AppContainer';
|
||||||
import TwoFactor from './containers/TwoFactor';
|
import TwoFactor from './containers/TwoFactor';
|
||||||
import ScreenLockedView from './views/ScreenLockedView';
|
import ScreenLockedView from './views/ScreenLockedView';
|
||||||
import ChangePasscodeView from './views/ChangePasscodeView';
|
import ChangePasscodeView from './views/ChangePasscodeView';
|
||||||
|
import Toast from './containers/Toast';
|
||||||
|
import InAppNotification from './containers/InAppNotification';
|
||||||
|
import { ActionSheetProvider } from './containers/ActionSheet';
|
||||||
|
|
||||||
|
|
||||||
RNScreens.enableScreens();
|
RNScreens.enableScreens();
|
||||||
|
|
||||||
|
@ -151,22 +156,28 @@ export default class Root extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { themePreferences, theme } = this.state;
|
const { themePreferences, theme } = this.state;
|
||||||
return (
|
return (
|
||||||
<AppearanceProvider>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
<Provider store={store}>
|
<AppearanceProvider>
|
||||||
<ThemeContext.Provider
|
<Provider store={store}>
|
||||||
value={{
|
<ThemeContext.Provider
|
||||||
theme,
|
value={{
|
||||||
themePreferences,
|
theme,
|
||||||
setTheme: this.setTheme
|
themePreferences,
|
||||||
}}
|
setTheme: this.setTheme
|
||||||
>
|
}}
|
||||||
<AppContainer />
|
>
|
||||||
<TwoFactor />
|
<ActionSheetProvider>
|
||||||
<ScreenLockedView />
|
<AppContainer />
|
||||||
<ChangePasscodeView />
|
<TwoFactor />
|
||||||
</ThemeContext.Provider>
|
<ScreenLockedView />
|
||||||
</Provider>
|
<ChangePasscodeView />
|
||||||
</AppearanceProvider>
|
<InAppNotification />
|
||||||
|
<Toast />
|
||||||
|
</ActionSheetProvider>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
</Provider>
|
||||||
|
</AppearanceProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ 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';
|
|
||||||
import { handlePayloadUserInteraction } from '../actions';
|
import { handlePayloadUserInteraction } from '../actions';
|
||||||
import buildMessage from '../helpers/buildMessage';
|
import buildMessage from '../helpers/buildMessage';
|
||||||
import RocketChat from '../../rocketchat';
|
import RocketChat from '../../rocketchat';
|
||||||
import EventEmmiter from '../../../utils/events';
|
import EventEmitter from '../../../utils/events';
|
||||||
import { removedRoom } from '../../../actions/room';
|
import { removedRoom } from '../../../actions/room';
|
||||||
|
import { INAPP_NOTIFICATION_EMITTER } from '../../../containers/InAppNotification';
|
||||||
|
|
||||||
const removeListener = listener => listener.stop();
|
const removeListener = listener => listener.stop();
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ export default function subscribeRooms() {
|
||||||
if (data.rid === roomState.rid && roomState.isDeleting) {
|
if (data.rid === roomState.rid && roomState.isDeleting) {
|
||||||
store.dispatch(removedRoom());
|
store.dispatch(removedRoom());
|
||||||
} else {
|
} else {
|
||||||
EventEmmiter.emit('ROOM_REMOVED', { rid: data.rid });
|
EventEmitter.emit('ROOM_REMOVED', { rid: data.rid });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
|
@ -320,7 +320,7 @@ export default function subscribeRooms() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
store.dispatch(notificationReceived(notification));
|
EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, notification);
|
||||||
}
|
}
|
||||||
if (/uiInteraction/.test(ev)) {
|
if (/uiInteraction/.test(ev)) {
|
||||||
const { type: eventType, ...args } = type;
|
const { type: eventType, ...args } = type;
|
||||||
|
|
|
@ -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 (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
{
|
|
||||||
transform: [{ translateY }],
|
|
||||||
backgroundColor: themes[theme].focusedBackground,
|
|
||||||
borderColor: themes[theme].separatorColor
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Touchable
|
|
||||||
style={styles.content}
|
|
||||||
onPress={this.goToRoom}
|
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
|
||||||
background={Touchable.SelectableBackgroundBorderless()}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<Avatar text={avatar} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
|
|
||||||
<View style={styles.inner}>
|
|
||||||
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
|
|
||||||
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{message}</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
</Touchable>
|
|
||||||
<TouchableOpacity onPress={this.hide}>
|
|
||||||
<CustomIcon name='cancel' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)));
|
|
|
@ -9,7 +9,6 @@ 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 share from './share';
|
import share from './share';
|
||||||
import crashReport from './crashReport';
|
import crashReport from './crashReport';
|
||||||
import customEmojis from './customEmojis';
|
import customEmojis from './customEmojis';
|
||||||
|
@ -29,7 +28,6 @@ export default combineReducers({
|
||||||
room,
|
room,
|
||||||
rooms,
|
rooms,
|
||||||
sortPreferences,
|
sortPreferences,
|
||||||
notification,
|
|
||||||
share,
|
share,
|
||||||
crashReport,
|
crashReport,
|
||||||
customEmojis,
|
customEmojis,
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { createDrawerNavigator } from '@react-navigation/drawer';
|
import { createDrawerNavigator } from '@react-navigation/drawer';
|
||||||
|
|
||||||
|
@ -7,9 +6,7 @@ import { ThemeContext } from '../theme';
|
||||||
import {
|
import {
|
||||||
defaultHeader, themedHeader, ModalAnimation, StackAnimation
|
defaultHeader, themedHeader, ModalAnimation, StackAnimation
|
||||||
} from '../utils/navigation';
|
} from '../utils/navigation';
|
||||||
import Toast from '../containers/Toast';
|
|
||||||
import Sidebar from '../views/SidebarView';
|
import Sidebar from '../views/SidebarView';
|
||||||
import NotificationBadge from '../notifications/inApp';
|
|
||||||
|
|
||||||
// Chats Stack
|
// Chats Stack
|
||||||
import RoomView from '../views/RoomView';
|
import RoomView from '../views/RoomView';
|
||||||
|
@ -320,16 +317,4 @@ const InsideStackNavigator = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RootInsideStack = ({ navigation, route }) => (
|
export default InsideStackNavigator;
|
||||||
<>
|
|
||||||
<InsideStackNavigator navigation={navigation} />
|
|
||||||
<NotificationBadge navigation={navigation} route={route} />
|
|
||||||
<Toast />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
RootInsideStack.propTypes = {
|
|
||||||
navigation: PropTypes.object,
|
|
||||||
route: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootInsideStack;
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { ThemeContext } from '../../theme';
|
||||||
import {
|
import {
|
||||||
defaultHeader, themedHeader, StackAnimation, FadeFromCenterModal
|
defaultHeader, themedHeader, StackAnimation, FadeFromCenterModal
|
||||||
} from '../../utils/navigation';
|
} from '../../utils/navigation';
|
||||||
import Toast from '../../containers/Toast';
|
|
||||||
import NotificationBadge from '../../notifications/inApp';
|
|
||||||
import { ModalContainer } from './ModalContainer';
|
import { ModalContainer } from './ModalContainer';
|
||||||
|
|
||||||
// Chats Stack
|
// Chats Stack
|
||||||
|
@ -292,16 +290,4 @@ const InsideStackNavigator = React.memo(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const RootInsideStack = React.memo(({ navigation, route }) => (
|
export default InsideStackNavigator;
|
||||||
<>
|
|
||||||
<InsideStackNavigator navigation={navigation} />
|
|
||||||
<NotificationBadge navigation={navigation} route={route} />
|
|
||||||
<Toast />
|
|
||||||
</>
|
|
||||||
));
|
|
||||||
RootInsideStack.propTypes = {
|
|
||||||
navigation: PropTypes.object,
|
|
||||||
route: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootInsideStack;
|
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"react-native-modal": "11.5.6",
|
"react-native-modal": "11.5.6",
|
||||||
"react-native-navigation-bar-color": "2.0.1",
|
"react-native-navigation-bar-color": "2.0.1",
|
||||||
"react-native-notifications": "2.1.7",
|
"react-native-notifications": "2.1.7",
|
||||||
|
"react-native-notifier": "^1.3.1",
|
||||||
"react-native-orientation-locker": "1.1.8",
|
"react-native-orientation-locker": "1.1.8",
|
||||||
"react-native-picker-select": "7.0.0",
|
"react-native-picker-select": "7.0.0",
|
||||||
"react-native-platform-touchable": "^1.1.1",
|
"react-native-platform-touchable": "^1.1.1",
|
||||||
|
|
|
@ -11895,6 +11895,11 @@ react-native-notifications@2.1.7:
|
||||||
core-js "^1.0.0"
|
core-js "^1.0.0"
|
||||||
uuid "^2.0.3"
|
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:
|
react-native-orientation-locker@1.1.8:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-native-orientation-locker/-/react-native-orientation-locker-1.1.8.tgz#45d1c9e002496b8d286ec8932d6e3e7d341f9c85"
|
||||||
|
|
Loading…
Reference in New Issue