feat: device notification settings (#5277)

* iOS go to device notification setting to change the configuration

* go to notification settings with android

* add notifee

* add the reducer and action

* saga request done

* add the setInAlert action

* tweak at name and add focus to dispatch the request

* use the foreground inside pushTroubleShoot to request the notification and fix the icon color

* add the request at roomslistview didmount

* remove the notification modulo from android

* add patch

* minor tweak
This commit is contained in:
Reinaldo Neto 2023-10-20 10:57:12 -03:00 committed by GitHub
parent 3ec6800a34
commit 5ab5d9c945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 259 additions and 26 deletions

View File

@ -96,3 +96,8 @@ export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [
'ACCEPT_CALL',
'SET_CALLING'
]);
export const TROUBLESHOOTING_NOTIFICATION = createRequestTypes('TROUBLESHOOTING_NOTIFICATION', [
'REQUEST',
'SET',
'SET_IN_ALERT'
]);

View File

@ -0,0 +1,34 @@
import { Action } from 'redux';
import { TROUBLESHOOTING_NOTIFICATION } from './actionsTypes';
import { ITroubleshootingNotification } from '../reducers/troubleshootingNotification';
type TSetTroubleshootingNotification = Action & { payload: Partial<ITroubleshootingNotification> };
type TSetInAlertTroubleshootingNotification = Action & { payload: Pick<ITroubleshootingNotification, 'inAlertNotification'> };
export type TActionTroubleshootingNotification = Action &
TSetTroubleshootingNotification &
TSetInAlertTroubleshootingNotification;
export function requestTroubleshootingNotification(): Action {
return {
type: TROUBLESHOOTING_NOTIFICATION.REQUEST
};
}
export function setTroubleshootingNotification(payload: Partial<ITroubleshootingNotification>): TSetTroubleshootingNotification {
return {
type: TROUBLESHOOTING_NOTIFICATION.SET,
payload
};
}
export function setInAlertTroubleshootingNotification(
payload: Pick<ITroubleshootingNotification, 'inAlertNotification'>
): TSetInAlertTroubleshootingNotification {
return {
type: TROUBLESHOOTING_NOTIFICATION.SET_IN_ALERT,
payload
};
}

View File

@ -38,6 +38,8 @@ import { IEnterpriseModules } from '../../reducers/enterpriseModules';
import { IVideoConf } from '../../reducers/videoConf';
import { TActionUsersRoles } from '../../actions/usersRoles';
import { TUsersRoles } from '../../reducers/usersRoles';
import { ITroubleshootingNotification } from '../../reducers/troubleshootingNotification';
import { TActionTroubleshootingNotification } from '../../actions/troubleshootingNotification';
export interface IApplicationState {
settings: TSettingsState;
@ -63,6 +65,7 @@ export interface IApplicationState {
roles: IRoles;
videoConf: IVideoConf;
usersRoles: TUsersRoles;
troubleshootingNotification: ITroubleshootingNotification;
}
export type TApplicationActions = TActionActiveUsers &
@ -83,4 +86,5 @@ export type TApplicationActions = TActionActiveUsers &
TActionPermissions &
TActionEnterpriseModules &
TActionVideoConf &
TActionUsersRoles;
TActionUsersRoles &
TActionTroubleshootingNotification;

View File

@ -23,6 +23,7 @@ import permissions from './permissions';
import roles from './roles';
import videoConf from './videoConf';
import usersRoles from './usersRoles';
import troubleshootingNotification from './troubleshootingNotification';
export default combineReducers({
settings,
@ -47,5 +48,6 @@ export default combineReducers({
permissions,
roles,
videoConf,
usersRoles
usersRoles,
troubleshootingNotification
});

View File

@ -0,0 +1,33 @@
import { setInAlertTroubleshootingNotification, setTroubleshootingNotification } from '../actions/troubleshootingNotification';
import { mockedStore } from './mockedStore';
import { ITroubleshootingNotification, initialState } from './troubleshootingNotification';
describe('test troubleshootingNotification reducer', () => {
it('should return initial state', () => {
const state = mockedStore.getState().troubleshootingNotification;
expect(state).toEqual(initialState);
});
it('should return correctly value after call troubleshootingNotification action', () => {
const payload: ITroubleshootingNotification = {
consumptionPercentage: 50,
deviceNotificationEnabled: true,
inAlertNotification: false,
isCommunityEdition: true,
isCustomPushGateway: true,
isPushGatewayConnected: true
};
mockedStore.dispatch(setTroubleshootingNotification(payload));
const state = mockedStore.getState().troubleshootingNotification;
expect(state).toEqual(payload);
});
it('should return correctly the inAlert value after call setInAlert action', () => {
const previousInAlertState = mockedStore.getState().troubleshootingNotification.inAlertNotification;
const payload: Pick<ITroubleshootingNotification, 'inAlertNotification'> = {
inAlertNotification: !previousInAlertState
};
mockedStore.dispatch(setInAlertTroubleshootingNotification(payload));
const newInAlertState = mockedStore.getState().troubleshootingNotification.inAlertNotification;
expect(newInAlertState).toEqual(payload.inAlertNotification);
});
});

View File

@ -0,0 +1,37 @@
import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes';
import { TActionTroubleshootingNotification } from '../actions/troubleshootingNotification';
export interface ITroubleshootingNotification {
deviceNotificationEnabled: boolean;
isCommunityEdition: boolean;
isPushGatewayConnected: boolean;
isCustomPushGateway: boolean;
consumptionPercentage: number;
inAlertNotification: boolean;
}
export const initialState: ITroubleshootingNotification = {
consumptionPercentage: 0,
deviceNotificationEnabled: false,
isCommunityEdition: false,
isPushGatewayConnected: false,
isCustomPushGateway: false,
inAlertNotification: false
};
export default (state = initialState, action: TActionTroubleshootingNotification): ITroubleshootingNotification => {
switch (action.type) {
case TROUBLESHOOTING_NOTIFICATION.SET:
return {
...state,
...action.payload
};
case TROUBLESHOOTING_NOTIFICATION.SET_IN_ALERT:
return {
...state,
...action.payload
};
default:
return state;
}
};

View File

@ -14,6 +14,7 @@ import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion';
import encryption from './encryption';
import videoConf from './videoConf';
import troubleshootingNotification from './troubleshootingNotification';
const root = function* root() {
yield all([
@ -30,7 +31,8 @@ const root = function* root() {
createDiscussion(),
inquiry(),
encryption(),
videoConf()
videoConf(),
troubleshootingNotification()
]);
};

View File

@ -0,0 +1,36 @@
import { Action } from 'redux';
import { put, takeEvery } from 'redux-saga/effects';
import { call } from 'typed-redux-saga';
import notifee from '@notifee/react-native';
import { ITroubleshootingNotification } from '../reducers/troubleshootingNotification';
import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes';
import { setInAlertTroubleshootingNotification, setTroubleshootingNotification } from '../actions/troubleshootingNotification';
import { appSelector } from '../lib/hooks';
interface IGenericAction extends Action {
type: string;
}
type TSetGeneric = IGenericAction & {
payload: ITroubleshootingNotification;
};
function* request() {
const settings = yield* call(notifee.getNotificationSettings);
yield put(setTroubleshootingNotification({ deviceNotificationEnabled: !!settings.authorizationStatus }));
}
function* setNotification({ payload }: { payload: ITroubleshootingNotification }) {
const troubleshootingNotification = yield* appSelector(state => state.troubleshootingNotification);
const newState = { ...troubleshootingNotification, ...payload };
// TODO: add properly the conditions to set inAlertNotification bias on each expected settings
// For now there is only the deviceNotificationEnabled properly, waiting for the next settings to fix
const inAlertNotification = !newState.deviceNotificationEnabled;
yield put(setInAlertTroubleshootingNotification({ inAlertNotification }));
}
export default function* root(): Generator {
yield takeEvery<IGenericAction>(TROUBLESHOOTING_NOTIFICATION.REQUEST, request);
yield takeEvery<TSetGeneric>(TROUBLESHOOTING_NOTIFICATION.SET, setNotification);
}

View File

@ -1,6 +1,8 @@
import { StackNavigationProp } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import { Alert, Linking } from 'react-native';
import notifee from '@notifee/react-native';
import { useDispatch } from 'react-redux';
import * as List from '../../containers/List';
import SafeAreaView from '../../containers/SafeAreaView';
@ -10,20 +12,40 @@ import { SettingsStackParamList } from '../../stacks/types';
import { useTheme } from '../../theme';
import CustomListSection from './components/CustomListSection';
import ListPercentage from './components/ListPercentage';
import { isIOS, showErrorAlert } from '../../lib/methods/helpers';
import { requestTroubleshootingNotification } from '../../actions/troubleshootingNotification';
import { useAppSelector } from '../../lib/hooks';
interface IPushTroubleshootViewProps {
navigation: StackNavigationProp<SettingsStackParamList, 'PushTroubleshootView'>;
}
const PushTroubleshootView = ({ navigation }: IPushTroubleshootViewProps): JSX.Element => {
const deviceNotificationEnabled = false;
const isCommunityEdition = true;
const isPushGatewayConnected = true;
const isCustomPushGateway = true;
const consumptionPercentage = 50;
const { colors } = useTheme();
const dispatch = useDispatch();
const {
consumptionPercentage,
deviceNotificationEnabled,
isCommunityEdition,
isCustomPushGateway,
isPushGatewayConnected,
foreground
} = useAppSelector(state => ({
deviceNotificationEnabled: state.troubleshootingNotification.deviceNotificationEnabled,
isCommunityEdition: state.troubleshootingNotification.isCommunityEdition,
isPushGatewayConnected: state.troubleshootingNotification.isPushGatewayConnected,
isCustomPushGateway: state.troubleshootingNotification.isCustomPushGateway,
consumptionPercentage: state.troubleshootingNotification.consumptionPercentage,
foreground: state.app.foreground
}));
useEffect(() => {
if (foreground) {
dispatch(requestTroubleshootingNotification());
}
}, [dispatch, foreground]);
useEffect(() => {
navigation.setOptions({
title: I18n.t('Push_Troubleshooting')
@ -35,13 +57,25 @@ const PushTroubleshootView = ({ navigation }: IPushTroubleshootViewProps): JSX.E
};
const alertDeviceNotificationSettings = () => {
Alert.alert(I18n.t('Device_notifications_alert_title'), I18n.t('Device_notifications_alert_description'));
showErrorAlert(
I18n.t('Device_notifications_alert_description'),
I18n.t('Device_notifications_alert_title'),
goToNotificationSettings
);
};
const alertWorkspaceConsumption = () => {
Alert.alert(I18n.t('Push_consumption_alert_title'), I18n.t('Push_consumption_alert_description'));
};
const goToNotificationSettings = () => {
if (isIOS) {
Linking.openURL('app-settings:');
} else {
notifee.openNotificationSettings();
}
};
const handleTestPushNotification = () => {
// do nothing
};

View File

@ -46,6 +46,7 @@ interface IRightButtonsProps extends Pick<ISubscription, 't'> {
rid?: string;
theme?: TSupportedThemes;
colors?: TColors;
inAlertNotification: boolean;
}
interface IRigthButtonsState {
@ -55,8 +56,6 @@ interface IRigthButtonsState {
tunreadGroup: string[];
}
const deviceNotificationEnabled = true;
class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsState> {
private threadSubscription?: Subscription;
private subSubscription?: Subscription;
@ -96,7 +95,7 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
shouldComponentUpdate(nextProps: IRightButtonsProps, nextState: IRigthButtonsState) {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { teamId, status, joined, omnichannelPermissions, theme } = this.props;
const { teamId, status, joined, omnichannelPermissions, theme, inAlertNotification } = this.props;
if (nextProps.teamId !== teamId) {
return true;
}
@ -112,6 +111,9 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
if (nextState.isFollowingThread !== isFollowingThread) {
return true;
}
if (nextProps.inAlertNotification !== inAlertNotification) {
return true;
}
if (!dequal(nextProps.omnichannelPermissions, omnichannelPermissions)) {
return true;
}
@ -299,12 +301,12 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
goToNotification = () => {
const { room } = this;
const { rid, navigation, isMasterDetail } = this.props;
const { rid, navigation, isMasterDetail, inAlertNotification } = this.props;
if (!rid || !room) {
return;
}
if (deviceNotificationEnabled && room) {
if (!inAlertNotification && room) {
if (isMasterDetail) {
navigation.navigate('ModalStackNavigator', {
screen: 'NotificationPrefView',
@ -355,7 +357,7 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
render() {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { t, tmid, threadsEnabled, rid, colors } = this.props;
const { t, tmid, threadsEnabled, rid, colors, inAlertNotification } = this.props;
if (t === 'l') {
if (!this.isOmnichannelPreview()) {
@ -381,7 +383,7 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
return (
<HeaderButton.Container>
<HeaderButton.Item
color={deviceNotificationEnabled ? colors!.headerTintColor : colors!.fontDanger}
color={inAlertNotification ? colors!.fontDanger : colors!.headerTintColor}
iconName='notification-disabled'
onPress={this.goToNotification}
testID='room-view-push-troubleshoot'
@ -405,7 +407,8 @@ const mapStateToProps = (state: IApplicationState) => ({
userId: getUserSelector(state).id,
threadsEnabled: state.settings.Threads_enabled as boolean,
isMasterDetail: state.app.isMasterDetail,
livechatRequestComment: state.settings.Livechat_request_comment_when_closing_conversation as boolean
livechatRequestComment: state.settings.Livechat_request_comment_when_closing_conversation as boolean,
inAlertNotification: state.troubleshootingNotification.inAlertNotification
});
export default connect(mapStateToProps)(withTheme(RightButtonsContainer));

View File

@ -15,6 +15,7 @@ import RoomItem, { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from '../../containers/Roo
import log, { logEvent, events } from '../../lib/methods/helpers/log';
import I18n from '../../i18n';
import { closeSearchHeader, closeServerDropdown, openSearchHeader, roomsRequest } from '../../actions/rooms';
import { requestTroubleshootingNotification } from '../../actions/troubleshootingNotification';
import * as HeaderButton from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import ActivityIndicator from '../../containers/ActivityIndicator';
@ -90,6 +91,7 @@ interface IRoomsListViewProps {
createPrivateChannelPermission: [];
createDiscussionPermission: [];
serverVersion: string;
inAlertNotification: boolean;
}
interface IRoomsListViewState {
@ -143,7 +145,8 @@ const shouldUpdateProps = [
'createDirectMessagePermission',
'createPublicChannelPermission',
'createPrivateChannelPermission',
'createDiscussionPermission'
'createDiscussionPermission',
'inAlertNotification'
];
const sortPreferencesShouldUpdate = ['sortBy', 'groupByType', 'showFavorites', 'showUnread'];
@ -198,6 +201,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
this.handleHasPermission();
this.mounted = true;
dispatch(requestTroubleshootingNotification());
this.unsubscribeFocus = navigation.addListener('focus', () => {
this.animated = true;
// Check if there were changes with sort preference, then call getSubscription to remount the list
@ -330,7 +334,8 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
createDirectMessagePermission,
createDiscussionPermission,
showAvatar,
displayMode
displayMode,
inAlertNotification
} = this.props;
const { item } = this.state;
@ -353,7 +358,8 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
if (
insets.left !== prevProps.insets.left ||
insets.right !== prevProps.insets.right ||
notificationPresenceCap !== prevProps.notificationPresenceCap
notificationPresenceCap !== prevProps.notificationPresenceCap ||
inAlertNotification !== prevProps.inAlertNotification
) {
this.setHeader();
}
@ -406,7 +412,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
getHeader = (): StackNavigationOptions => {
const { searching, canCreateRoom } = this.state;
const { navigation, isMasterDetail, notificationPresenceCap } = this.props;
const { navigation, isMasterDetail, notificationPresenceCap, inAlertNotification, theme } = this.props;
if (searching) {
return {
headerTitleAlign: 'left',
@ -446,6 +452,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
iconName='notification-disabled'
onPress={this.goPushTroubleshoot}
testID='rooms-list-view-push-troubleshoot'
color={inAlertNotification ? themes[theme].fontDanger : themes[theme].headerTintColor}
/>
{canCreateRoom ? (
<HeaderButton.Item iconName='create' onPress={this.goToNewMessage} testID='rooms-list-view-create-channel' />
@ -986,7 +993,8 @@ const mapStateToProps = (state: IApplicationState) => ({
createPublicChannelPermission: state.permissions['create-c'],
createPrivateChannelPermission: state.permissions['create-p'],
createDiscussionPermission: state.permissions['start-discussion'],
serverVersion: state.server.version
serverVersion: state.server.version,
inAlertNotification: state.troubleshootingNotification.inAlertNotification
});
export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomsListView))));

View File

@ -536,6 +536,11 @@ PODS:
- React-Core
- RNMathView (1.0.0):
- iosMath
- RNNotifee (7.8.0):
- React-Core
- RNNotifee/NotifeeCore (= 7.8.0)
- RNNotifee/NotifeeCore (7.8.0):
- React-Core
- RNReanimated (2.8.0):
- DoubleConversion
- FBLazyVector
@ -669,6 +674,7 @@ DEPENDENCIES:
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNLocalize (from `../node_modules/react-native-localize`)
- RNMathView (from `../node_modules/react-native-math-view/ios`)
- "RNNotifee (from `../node_modules/@notifee/react-native`)"
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNRootView (from `../node_modules/rn-root-view`)
- RNScreens (from `../node_modules/react-native-screens`)
@ -862,6 +868,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-localize"
RNMathView:
:path: "../node_modules/react-native-math-view/ios"
RNNotifee:
:path: "../node_modules/@notifee/react-native"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNRootView:
@ -978,6 +986,7 @@ SPEC CHECKSUMS:
RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d
RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b
RNMathView: 4c8a3c081fa671ab3136c51fa0bdca7ffb708bd5
RNNotifee: f3c01b391dd8e98e67f539f9a35a9cbcd3bae744
RNReanimated: 64573e25e078ae6bec03b891586d50b9ec284393
RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19

View File

@ -41,6 +41,7 @@
"@codler/react-native-keyboard-aware-scroll-view": "^2.0.1",
"@gorhom/bottom-sheet": "^4.3.1",
"@hookform/resolvers": "^2.9.10",
"@notifee/react-native": "^7.8.0",
"@nozbe/watermelondb": "0.23.0",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-native-camera-roll/camera-roll": "5.6.1",

View File

@ -0,0 +1,20 @@
--- a/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
+++ b/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
@@ -238,7 +238,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
@ReactMethod
public void requestPermission(Promise promise) {
// For Android 12 and below, we return the notification settings
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT < 33) {
Notifee.getInstance()
.getNotificationSettings(
(e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
@@ -265,7 +265,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
(e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
activity.requestPermissions(
- new String[] {Manifest.permission.POST_NOTIFICATIONS},
+ new String[] {"android.permission.POST_NOTIFICATIONS"},
Notifee.REQUEST_CODE_NOTIFICATION_PERMISSION,
this);
}

View File

@ -3368,9 +3368,9 @@
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.21.0":
version "7.22.11"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4"
integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
dependencies:
regenerator-runtime "^0.14.0"
@ -5155,6 +5155,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@notifee/react-native@^7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.0.tgz#2990883753990f3585aa0cb5becc5cbdbcd87a43"
integrity sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ==
"@nozbe/simdjson@0.9.6-fix2":
version "0.9.6-fix2"
resolved "https://registry.yarnpkg.com/@nozbe/simdjson/-/simdjson-0.9.6-fix2.tgz#00d1c8ec76bfac25c022b07511c8fff4568b2973"