From 5ab5d9c9453096c831eb5f210ac3ddb8f62b637a Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:57:12 -0300 Subject: [PATCH] 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 --- app/actions/actionsTypes.ts | 5 ++ app/actions/troubleshootingNotification.ts | 34 +++++++++++++ app/definitions/redux/index.ts | 6 ++- app/reducers/index.js | 4 +- .../troubleshootingNotification.test.ts | 33 +++++++++++++ app/reducers/troubleshootingNotification.ts | 37 ++++++++++++++ app/sagas/index.js | 4 +- app/sagas/troubleshootingNotification.ts | 36 ++++++++++++++ app/views/PushTroubleshootView/index.tsx | 48 ++++++++++++++++--- app/views/RoomView/RightButtons.tsx | 19 ++++---- app/views/RoomsListView/index.tsx | 18 +++++-- ios/Podfile.lock | 9 ++++ package.json | 1 + patches/@notifee+react-native+7.8.0.patch | 20 ++++++++ yarn.lock | 11 +++-- 15 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 app/actions/troubleshootingNotification.ts create mode 100644 app/reducers/troubleshootingNotification.test.ts create mode 100644 app/reducers/troubleshootingNotification.ts create mode 100644 app/sagas/troubleshootingNotification.ts create mode 100644 patches/@notifee+react-native+7.8.0.patch diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index e05090434..e40e78148 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -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' +]); diff --git a/app/actions/troubleshootingNotification.ts b/app/actions/troubleshootingNotification.ts new file mode 100644 index 000000000..df7256d58 --- /dev/null +++ b/app/actions/troubleshootingNotification.ts @@ -0,0 +1,34 @@ +import { Action } from 'redux'; + +import { TROUBLESHOOTING_NOTIFICATION } from './actionsTypes'; +import { ITroubleshootingNotification } from '../reducers/troubleshootingNotification'; + +type TSetTroubleshootingNotification = Action & { payload: Partial }; + +type TSetInAlertTroubleshootingNotification = Action & { payload: Pick }; + +export type TActionTroubleshootingNotification = Action & + TSetTroubleshootingNotification & + TSetInAlertTroubleshootingNotification; + +export function requestTroubleshootingNotification(): Action { + return { + type: TROUBLESHOOTING_NOTIFICATION.REQUEST + }; +} + +export function setTroubleshootingNotification(payload: Partial): TSetTroubleshootingNotification { + return { + type: TROUBLESHOOTING_NOTIFICATION.SET, + payload + }; +} + +export function setInAlertTroubleshootingNotification( + payload: Pick +): TSetInAlertTroubleshootingNotification { + return { + type: TROUBLESHOOTING_NOTIFICATION.SET_IN_ALERT, + payload + }; +} diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts index ec8dea5da..5df432480 100644 --- a/app/definitions/redux/index.ts +++ b/app/definitions/redux/index.ts @@ -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; diff --git a/app/reducers/index.js b/app/reducers/index.js index a612e6f3f..61421d633 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -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 }); diff --git a/app/reducers/troubleshootingNotification.test.ts b/app/reducers/troubleshootingNotification.test.ts new file mode 100644 index 000000000..22c7c528e --- /dev/null +++ b/app/reducers/troubleshootingNotification.test.ts @@ -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 = { + inAlertNotification: !previousInAlertState + }; + mockedStore.dispatch(setInAlertTroubleshootingNotification(payload)); + const newInAlertState = mockedStore.getState().troubleshootingNotification.inAlertNotification; + expect(newInAlertState).toEqual(payload.inAlertNotification); + }); +}); diff --git a/app/reducers/troubleshootingNotification.ts b/app/reducers/troubleshootingNotification.ts new file mode 100644 index 000000000..0c4337032 --- /dev/null +++ b/app/reducers/troubleshootingNotification.ts @@ -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; + } +}; diff --git a/app/sagas/index.js b/app/sagas/index.js index 12d2ba83f..b60cc2118 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -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() ]); }; diff --git a/app/sagas/troubleshootingNotification.ts b/app/sagas/troubleshootingNotification.ts new file mode 100644 index 000000000..24ad2460e --- /dev/null +++ b/app/sagas/troubleshootingNotification.ts @@ -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(TROUBLESHOOTING_NOTIFICATION.REQUEST, request); + yield takeEvery(TROUBLESHOOTING_NOTIFICATION.SET, setNotification); +} diff --git a/app/views/PushTroubleshootView/index.tsx b/app/views/PushTroubleshootView/index.tsx index f27b22f28..370e21e27 100644 --- a/app/views/PushTroubleshootView/index.tsx +++ b/app/views/PushTroubleshootView/index.tsx @@ -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; } 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 }; diff --git a/app/views/RoomView/RightButtons.tsx b/app/views/RoomView/RightButtons.tsx index d80c55966..17a438cec 100644 --- a/app/views/RoomView/RightButtons.tsx +++ b/app/views/RoomView/RightButtons.tsx @@ -46,6 +46,7 @@ interface IRightButtonsProps extends Pick { 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 { private threadSubscription?: Subscription; private subSubscription?: Subscription; @@ -96,7 +95,7 @@ class RightButtonsContainer extends Component { 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 ({ 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)); diff --git a/app/views/RoomsListView/index.tsx b/app/views/RoomsListView/index.tsx index 897d7fd1f..2adcbe6c9 100644 --- a/app/views/RoomsListView/index.tsx +++ b/app/views/RoomsListView/index.tsx @@ -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 { 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 { 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 {canCreateRoom ? ( @@ -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)))); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aa2353fa9..337cb4fb8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/package.json b/package.json index df8e3d433..8e8203cca 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@notifee+react-native+7.8.0.patch b/patches/@notifee+react-native+7.8.0.patch new file mode 100644 index 000000000..04bfbe16c --- /dev/null +++ b/patches/@notifee+react-native+7.8.0.patch @@ -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); + } diff --git a/yarn.lock b/yarn.lock index 0e3aaeaa5..23e0b506b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"