Skip to content

Commit 120ce2f

Browse files
committed
fix(videoconf): add ios push deduplication state checks
Fixes stale duplicate notification handling on cold boot causing users to blindly navigate into ended video conference sessions infinitely. Closes #7015
1 parent 48c5fc2 commit 120ce2f

5 files changed

Lines changed: 56 additions & 6 deletions

File tree

app/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ export default class Root extends React.Component<{}, IState> {
139139
if ('configured' in notification) {
140140
return;
141141
}
142-
onNotification(notification);
142+
const result = onNotification(notification);
143+
if (result?.catch) {
144+
result.catch(e => console.warn('app/index.tsx: onNotification error', e));
145+
}
143146
return;
144147
}
145148

app/lib/notifications/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import EJSON from 'ejson';
22
import { Platform } from 'react-native';
3+
import AsyncStorage from '@react-native-async-storage/async-storage';
34

45
import { appInit } from '../../actions/app';
56
import { deepLinkingClickCallPush, deepLinkingOpen } from '../../actions/deepLinking';
@@ -16,14 +17,22 @@ interface IEjson {
1617
messageId: string;
1718
}
1819

19-
export const onNotification = (push: INotification): void => {
20+
export const onNotification = async (push: INotification): Promise<void> => {
2021
const identifier = String(push?.payload?.action?.identifier);
2122

2223
// Handle video conf notification actions (Accept/Decline buttons)
2324
if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') {
2425
if (push?.payload?.ejson) {
2526
try {
2627
const notification = EJSON.parse(push.payload.ejson);
28+
const lastId = await AsyncStorage.getItem('lastProcessedVideoConfNotificationId');
29+
const currentId = push.identifier || push.payload?.notId;
30+
if (currentId && lastId === currentId) {
31+
return;
32+
}
33+
if (currentId) {
34+
await AsyncStorage.setItem('lastProcessedVideoConfNotificationId', currentId);
35+
}
2736
store.dispatch(
2837
deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' })
2938
);
@@ -40,6 +49,14 @@ export const onNotification = (push: INotification): void => {
4049

4150
// Handle video conf notification tap (default action) - treat as accept
4251
if (notification?.notificationType === 'videoconf') {
52+
const lastId = await AsyncStorage.getItem('lastProcessedVideoConfNotificationId');
53+
const currentId = push.identifier || push.payload?.notId;
54+
if (currentId && lastId === currentId) {
55+
return;
56+
}
57+
if (currentId) {
58+
await AsyncStorage.setItem('lastProcessedVideoConfNotificationId', currentId);
59+
}
4360
store.dispatch(deepLinkingClickCallPush({ ...notification, event: 'accept' }));
4461
return;
4562
}
@@ -122,7 +139,10 @@ export const checkPendingNotification = async (): Promise<void> => {
122139
},
123140
identifier: notificationData.notId || ''
124141
};
125-
onNotification(notification);
142+
const result = onNotification(notification);
143+
if (result?.catch) {
144+
result.catch(e => console.warn('[notifications/index.ts] onNotification error:', e));
145+
}
126146
} catch (e) {
127147
console.warn('[notifications/index.ts] Failed to parse pending notification:', e);
128148
}

app/lib/notifications/push.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ const registerForPushNotifications = async (): Promise<string | null> => {
159159
}
160160
};
161161

162-
export const pushNotificationConfigure = (onNotification: (notification: INotification) => void): Promise<any> => {
162+
export const pushNotificationConfigure = (
163+
onNotification: (notification: INotification) => Promise<void> | void
164+
): Promise<any> => {
163165
if (configured) {
164166
return Promise.resolve({ configured: true });
165167
}
@@ -207,10 +209,16 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
207209
if (isIOS) {
208210
const { background } = reduxStore.getState().app;
209211
if (background) {
210-
onNotification(notification);
212+
const result = onNotification(notification);
213+
if (result?.catch) {
214+
result.catch((e: any) => console.warn('[push.ts] Notification handler error:', e));
215+
}
211216
}
212217
} else {
213-
onNotification(notification);
218+
const result = onNotification(notification);
219+
if (result?.catch) {
220+
result.catch((e: any) => console.warn('[push.ts] Notification handler error:', e));
221+
}
214222
}
215223
});
216224

app/lib/notifications/videoConf/getInitialNotification.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as Notifications from 'expo-notifications';
22
import EJSON from 'ejson';
33
import { DeviceEventEmitter, Platform } from 'react-native';
4+
import AsyncStorage from '@react-native-async-storage/async-storage';
45

56
import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
67
import { store } from '../../store/auxStore';
@@ -66,6 +67,14 @@ export const getInitialNotification = async (): Promise<boolean> => {
6667
if (payload.ejson) {
6768
const ejsonData = EJSON.parse(payload.ejson);
6869
if (ejsonData?.notificationType === 'videoconf') {
70+
const notificationId = notification.request.identifier;
71+
const lastId = await AsyncStorage.getItem('lastProcessedVideoConfNotificationId');
72+
if (notificationId && lastId === notificationId) {
73+
return false;
74+
}
75+
if (notificationId) {
76+
await AsyncStorage.setItem('lastProcessedVideoConfNotificationId', notificationId);
77+
}
6978
// Accept/Decline actions or default tap (treat as accept)
7079
let event = 'accept';
7180
if (actionIdentifier === 'DECLINE_ACTION') {

app/sagas/deepLinking.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { all, call, delay, put, select, take, takeLatest } from 'redux-saga/effects';
2+
import AsyncStorage from '@react-native-async-storage/async-storage';
23

34
import { shareSetParams } from '../actions/share';
45
import * as types from '../actions/actionsTypes';
@@ -245,11 +246,20 @@ const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) {
245246

246247
const handleClickCallPush = function* handleClickCallPush({ params }) {
247248
let { host } = params;
249+
const notId = params.notId || params.identifier || params.payload?.notId || params.push?.identifier || params.push?.payload?.notId;
248250

249251
if (!host) {
250252
return;
251253
}
252254

255+
const lastId = yield call(AsyncStorage.getItem, 'lastProcessedVideoConfNotificationId');
256+
if (notId && lastId === notId) {
257+
return;
258+
}
259+
if (notId) {
260+
yield call(AsyncStorage.setItem, 'lastProcessedVideoConfNotificationId', notId);
261+
}
262+
253263
if (host.slice(-1) === '/') {
254264
host = host.slice(0, host.length - 1);
255265
}

0 commit comments

Comments
 (0)