Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ export default class Root extends React.Component<{}, IState> {
return;
}

await getInitialNotification();
// Open app from video conf notification (Android)
const handledVideoConf = await getInitialNotification();
if (handledVideoConf) {
// Video conf saga will handle initialization
return;
}

// Open app from deep linking
const deepLinking = await Linking.getInitialURL();
Expand Down
19 changes: 18 additions & 1 deletion app/lib/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import EJSON from 'ejson';
import { appInit } from '../../actions/app';
import { deepLinkingClickCallPush, deepLinkingOpen } from '../../actions/deepLinking';
import { type INotification, SubscriptionType } from '../../definitions';
import { isIOS } from '../methods/helpers';
import { store } from '../store/auxStore';
import { deviceToken, pushNotificationConfigure, removeAllNotifications, setNotificationsBadgeCount } from './push';

Expand All @@ -13,17 +14,33 @@ interface IEjson {
type: string;
host: string;
messageId: string;
notificationType?: string;
}

export const onNotification = (push: INotification): void => {
const identifier = String(push?.payload?.action?.identifier);

if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') {
if (push?.payload && push?.payload?.ejson) {
const notification = EJSON.parse(push?.payload?.ejson);
store.dispatch(deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' }));
const event = identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline';
store.dispatch(deepLinkingClickCallPush({ ...notification, event }));
return;
}
}

// On iOS cold start, video conf notifications come without action info initially.
// Skip processing here - the registerNotificationOpened handler will handle it with action info.
if (isIOS && push?.payload?.ejson) {
try {
const ejsonData: IEjson = EJSON.parse(push.payload.ejson);
if (ejsonData.notificationType === 'videoconf') {
return;
}
} catch {
// Continue with normal processing if ejson parsing fails
}
}
if (push?.payload) {
try {
const notification = push?.payload;
Expand Down
24 changes: 16 additions & 8 deletions app/lib/notifications/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,25 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
}
);

Notifications.events().registerNotificationOpened((notification: Notification, completion: () => void) => {
if (isIOS) {
const { background } = reduxStore.getState().app;
if (background) {
Notifications.events().registerNotificationOpened(
(notification: Notification, completion: () => void, actionResponse?: { identifier: string; text?: string }) => {
if (actionResponse && notification.payload) {
notification.payload.action = actionResponse;
}

if (isIOS) {
const hasExplicitAction =
actionResponse?.identifier === 'ACCEPT_ACTION' || actionResponse?.identifier === 'DECLINE_ACTION';
const { background } = reduxStore.getState().app;
if (background || hasExplicitAction) {
onNotification(notification);
}
} else {
onNotification(notification);
}
} else {
onNotification(notification);
completion();
}
completion();
});
);

Notifications.events().registerNotificationReceivedBackground(
(notification: Notification, completion: (response: any) => void) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const createChannel = () =>
const handleBackgroundEvent = async (event: Event) => {
const { pressAction, notification } = event.detail;
const notificationData = notification?.data;

if (
typeof notificationData?.caller === 'object' &&
(notificationData.caller as Caller)?._id &&
Expand All @@ -44,7 +45,7 @@ const handleBackgroundEvent = async (event: Event) => {
if (store?.getState()?.app.ready) {
store.dispatch(deepLinkingClickCallPush({ ...notificationData, event: pressAction?.id }));
} else {
AsyncStorage.setItem('pushNotification', JSON.stringify({ ...notificationData, event: pressAction?.id }));
await AsyncStorage.setItem('pushNotification', JSON.stringify({ ...notificationData, event: pressAction?.id }));
}
await notifee.cancelNotification(
`${notificationData.rid}${(notificationData.caller as Caller)._id}`.replace(/[^A-Za-z0-9]/g, '')
Expand Down
8 changes: 7 additions & 1 deletion app/lib/notifications/videoConf/getInitialNotification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
import { isAndroid } from '../../methods/helpers';
import { store } from '../../store/auxStore';

export const getInitialNotification = async (): Promise<void> => {
/**
* Checks for and handles initial video conference notifications on Android.
* @returns true if a video conf notification was handled, false otherwise
*/
export const getInitialNotification = async (): Promise<boolean> => {
if (isAndroid) {
const notifee = require('@notifee/react-native').default;
const initialNotification = await notifee.getInitialNotification();
if (initialNotification?.notification?.data?.notificationType === 'videoconf') {
store.dispatch(
deepLinkingClickCallPush({ ...initialNotification?.notification?.data, event: initialNotification?.pressAction?.id })
);
return true;
}
}
return false;
};
2 changes: 2 additions & 0 deletions app/sagas/deepLinking.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,15 @@ const handleClickCallPush = function* handleClickCallPush({ params }) {
yield localAuthenticate(host);
yield put(selectServerRequest(host, serverRecord.version, true));
yield take(types.LOGIN.SUCCESS);
yield put(appReady({}));
}
yield handleNavigateCallRoom({ params });
} else {
if (user && serverRecord) {
yield localAuthenticate(host);
yield put(selectServerRequest(host, serverRecord.version, true, true));
yield take(types.LOGIN.SUCCESS);
yield put(appReady({}));
yield handleNavigateCallRoom({ params });
return;
}
Expand Down
4 changes: 2 additions & 2 deletions app/sagas/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ const restore = function* restore() {
yield put(appReady({}));
const pushNotification = yield call(AsyncStorage.getItem, 'pushNotification');
if (pushNotification) {
const pushNotification = yield call(AsyncStorage.removeItem, 'pushNotification');
yield call(deepLinkingClickCallPush, JSON.parse(pushNotification));
yield call(AsyncStorage.removeItem, 'pushNotification');
yield put(deepLinkingClickCallPush(JSON.parse(pushNotification)));
}
} catch (e) {
log(e);
Expand Down
16 changes: 13 additions & 3 deletions app/views/JitsiMeetView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import CookieManager from '@react-native-cookies/cookies';
import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, BackHandler, Linking, SafeAreaView, StyleSheet, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
BackHandler,
type NativeEventSubscription,
Linking,
SafeAreaView,
StyleSheet,
View
} from 'react-native';
import WebView, { type WebViewNavigation } from 'react-native-webview';

import { userAgent } from '../../lib/constants/userAgent';
Expand All @@ -25,6 +33,7 @@ const JitsiMeetView = (): React.ReactElement => {

const [authModal, setAuthModal] = useState(false);
const [cookiesSet, setCookiesSet] = useState(false);
const backHandlerSubscription = useRef<NativeEventSubscription | null>(null);

const setCookies = async () => {
const date = new Date();
Expand Down Expand Up @@ -53,7 +62,7 @@ const JitsiMeetView = (): React.ReactElement => {
goBack();
} catch (error) {
// As the jitsi app was not opened, disable the backhandler on android
BackHandler.addEventListener('hardwareBackPress', () => true);
backHandlerSubscription.current = BackHandler.addEventListener('hardwareBackPress', () => true);
}
}, [goBack, url]);

Expand Down Expand Up @@ -94,6 +103,7 @@ const JitsiMeetView = (): React.ReactElement => {
logEvent(videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
if (!videoConf) endVideoConfTimer();
deactivateKeepAwake();
backHandlerSubscription.current?.remove();
};
}, [handleJitsiApp, onConferenceJoined, videoConf]);

Expand Down
14 changes: 14 additions & 0 deletions ios/NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ class NotificationService: UNNotificationServiceExtension {
if let bestAttemptContent = bestAttemptContent {
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)!
guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else {
// If decoding fails, still deliver the original notification
contentHandler(bestAttemptContent)
return
}

// Video conference notifications should be delivered immediately with VIDEOCONF category
if data.notificationType == .videoconf {
bestAttemptContent.categoryIdentifier = "VIDEOCONF"
contentHandler(bestAttemptContent)
return
}

Expand Down Expand Up @@ -47,6 +56,11 @@ class NotificationService: UNNotificationServiceExtension {
}

func processPayload(payload: Payload) {
// Handle video conference notifications
if payload.notificationType == .videoconf {
bestAttemptContent?.categoryIdentifier = "VIDEOCONF"
}

// If is a encrypted message
if payload.messageType == .e2e {
if let rid = payload.rid {
Expand Down
8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2385,10 +2385,10 @@ PODS:
- React-Core
- RNLocalize (2.1.1):
- React-Core
- RNNotifee (7.8.2):
- RNNotifee (9.1.8):
- React-Core
- RNNotifee/NotifeeCore (= 7.8.2)
- RNNotifee/NotifeeCore (7.8.2):
- RNNotifee/NotifeeCore (= 9.1.8)
- RNNotifee/NotifeeCore (9.1.8):
- React-Core
- RNReanimated (3.17.1):
- DoubleConversion
Expand Down Expand Up @@ -3185,7 +3185,7 @@ SPEC CHECKSUMS:
RNImageCropPicker: b219389d3a300679b396e81d501e8c8169ffa3c0
RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4
RNLocalize: ca86348d88b9a89da0e700af58d428ab3f343c4e
RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6
RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168
RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642
RNScreens: b13e4c45f0406f33986a39c0d8da0324bff94435
RNSVG: 680e961f640e381aab730a04b2371969686ed9f7
Expand Down
1 change: 1 addition & 0 deletions ios/Shared/Models/NotificationType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ import Foundation
enum NotificationType: String, Codable {
case message = "message"
case messageIdOnly = "message-id-only"
case videoconf = "videoconf"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@expo/vector-icons": "^14.1.0",
"@hookform/resolvers": "^2.9.10",
"@notifee/react-native": "7.8.2",
"@notifee/react-native": "^9.1.8",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@notifee/react-native v9 v8 breaking changes changelog release notes

💡 Result:

Here are the main breaking / upgrade notes when moving from v8 → v9 of @notifee/react-native:

  • Minimum Android compileSdk requirement increased to 34 (you must build with compileSdk 34+). [1]
  • Notifee removed uses of Google Play Services (GMS) so it no longer depends on GMS libs (improves compatibility with F‑Droid / open distributions). [1]
  • Android FOREGROUND_SERVICE changes:
    • USE_FULL_SCREEN_INTENT permission was removed from the package manifest (apps that need it must now add it themselves). [1]
    • Foreground service types support changed — you must add required foregroundServiceTypes in your AndroidManifest and may need to replace the service definition; new API parameter available to set types dynamically. [1]
  • New Android 15 FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING flag added (if relevant). [1]
  • v8.0.0 had an alpha publish and the project moved versioning to v9 (note about version skip). [1]
  • Misc: several Android/iOS bugfixes and compatibility fixes for RN new‑architecture; some API/type tweaks (see changelog for full list). [1][2]

Sources:

  1. Notifee release notes (v9.x changelog). [docs.page / Notifee release notes]. [1]
  2. Notifee GitHub issues / releases (examples of v9.x fixes and RN compatibility notes). [2]

If you want, I can copy the exact changelog entries for v9.x (v9.0.0 → latest) or produce a migration checklist tailored to your app (AndroidManifest, Gradle, RN version, Expo), which files to edit, and sample manifest snippets.


🏁 Script executed:

#!/bin/bash
# Search for notification-related tests and @notifee imports
echo "=== Test files mentioning notification/notifee ==="
fd \( -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.test.js" -o -name "*.test.jsx" -o -name "*.spec.ts" -o -name "*.spec.tsx" -o -name "*.spec.js" \) | head -20

echo -e "\n=== Searching for @notifee imports in test files ==="
rg "@notifee" --type ts --type tsx --type js --type jsx -l | head -20

echo -e "\n=== Searching for notification-related tests ==="
rg -i "notif" --type ts --type tsx --type js --type jsx -l | head -20

echo -e "\n=== Check package.json location and context ==="
cat -n package.json | grep -A 5 -B 5 "@notifee"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1150


🏁 Script executed:

#!/bin/bash
echo "=== Checking AndroidManifest.xml for FOREGROUND_SERVICE configuration ==="
fd -name "AndroidManifest.xml" -exec cat {} \; 2>/dev/null | head -100

echo -e "\n=== Checking build.gradle for compileSdk version ==="
fd -name "build.gradle" | head -5 | xargs cat 2>/dev/null | grep -i "compileSdk\|targetSdk\|minSdk" | head -20

echo -e "\n=== Checking if GMS dependencies are still present ==="
rg "google.*play.*services|gms" package.json gradle.properties

echo -e "\n=== Checking for notifee initialization/usage in codebase ==="
rg "@notifee" --type js --type jsx -A 2 -B 2 | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 658


🏁 Script executed:

#!/bin/bash
echo "=== Finding all directories and key Android files ==="
find . -maxdepth 3 -type d -name "android" 2>/dev/null

echo -e "\n=== Locating AndroidManifest.xml ==="
find . -name "AndroidManifest.xml" 2>/dev/null

echo -e "\n=== Locating build.gradle files ==="
find . -name "build.gradle" 2>/dev/null | head -10

echo -e "\n=== Checking for notifee imports in entire codebase ==="
rg "notifee|@notifee" -i | head -30

echo -e "\n=== Checking directory structure ==="
ls -la | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4616


🏁 Script executed:

#!/bin/bash
echo "=== Main AndroidManifest.xml ==="
cat ./android/app/src/main/AndroidManifest.xml

echo -e "\n=== build.gradle (app level) ==="
cat ./android/app/build.gradle | head -100

echo -e "\n=== Check root build.gradle ==="
cat ./android/build.gradle | head -50

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 10422


🏁 Script executed:

#!/bin/bash
echo "=== Check backgroundNotificationHandler for foreground service usage ==="
cat ./app/lib/notifications/videoConf/backgroundNotificationHandler.ts

echo -e "\n=== Search for foreground service references ==="
rg -i "foreground|FOREGROUND" --type ts --type tsx | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3828


Add FOREGROUND_SERVICE configuration to AndroidManifest.xml and verify manifest compatibility with v9 breaking changes.

This major version upgrade (7.8.2 → ^9.1.8) includes breaking changes requiring Android manifest updates:

  • Foreground service types: v9 requires foregroundServiceTypes attribute in AndroidManifest.xml. Your notification code uses ongoing: true, which may trigger foreground service behavior. Add the required attribute to your service definition.
  • USE_FULL_SCREEN_INTENT permission: v9 removed this from the package manifest. If your app needs full-screen intent notifications, add this permission explicitly to AndroidManifest.xml.
  • Google Play Services removal: v9 no longer depends on GMS libraries (improves F-Droid compatibility).

While your build configuration is correct (compileSdkVersion = 35 meets the v9 requirement of 34+), the Android manifest has not been updated to reflect these breaking changes. This can cause runtime crashes on Android 12+ when foreground services are triggered.

Additionally, the caret range (^9.1.8) allows automatic minor and patch updates. Consider pinning to "9.1.8" until manifest migration is fully tested.

🤖 Prompt for AI Agents
In package.json around line 31, the dependency upgrade to
"@notifee/react-native": "^9.1.8" requires AndroidManifest.xml changes and a
pinned version; update AndroidManifest.xml to add the required
foregroundServiceTypes attribute on your service declaration (matching any
ongoing/foreground notifications used), explicitly add the
USE_FULL_SCREEN_INTENT permission if your app uses full-screen intents, confirm
no GMS libraries are referenced/needed, ensure your AndroidManifest and service
entries are compatible with Android 12+ (compileSdkVersion 35 is fine), and pin
the dependency to "9.1.8" (remove the caret) until you finish the manifest
migration and testing.

"@nozbe/watermelondb": "^0.28.1-0",
"@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-camera-roll/camera-roll": "^7.10.0",
Expand Down
41 changes: 0 additions & 41 deletions patches/@notifee+react-native+7.8.2.patch

This file was deleted.

33 changes: 33 additions & 0 deletions patches/@notifee+react-native+9.1.8.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
diff --git a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
index beaa360..0ef1054 100644
--- a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
+++ b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m
@@ -180,17 +180,17 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center

_notificationOpenedAppID = notifeeNotification[@"id"];

- // handle notification outside of notifee
- if (notifeeNotification == nil) {
- if (_originalDelegate != nil && originalUNCDelegateRespondsTo.didReceiveNotificationResponse) {
- [_originalDelegate userNotificationCenter:center
- didReceiveNotificationResponse:response
- withCompletionHandler:completionHandler];
- } else {
- notifeeNotification =
- [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
- }
- }
+ // // handle notification outside of notifee
+ // if (notifeeNotification == nil) {
+ // if (_originalDelegate != nil && originalUNCDelegateRespondsTo.didReceiveNotificationResponse) {
+ // [_originalDelegate userNotificationCenter:center
+ // didReceiveNotificationResponse:response
+ // withCompletionHandler:completionHandler];
+ // } else {
+ // notifeeNotification =
+ // [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
+ // }
+ // }

if (notifeeNotification != nil) {
if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3825,10 +3825,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"

"@notifee/react-native@7.8.2":
version "7.8.2"
resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.2.tgz#72d3199ae830b4128ddaef3c1c2f11604759c9c4"
integrity sha512-VG4IkWJIlOKqXwa3aExC3WFCVCGCC9BA55Ivg0SMRfEs+ruvYy/zlLANcrVGiPtgkUEryXDhA8SXx9+JcO8oLA==
"@notifee/react-native@^9.1.8":
version "9.1.8"
resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-9.1.8.tgz#3d55cb3fbcc21f9e35931e366afdf64b294da891"
integrity sha512-Az/dueoPerJsbbjRxu8a558wKY+gONUrfoy3Hs++5OqbeMsR0dYe6P+4oN6twrLFyzAhEA1tEoZRvQTFDRmvQg==

"@nozbe/[email protected]":
version "3.9.4"
Expand Down