Skip to content

Commit 4afbbe4

Browse files
[MM-63553] Fix for: messages received on the background on iOS lose the props (#8811) (#8854)
* convert attachment fields short and disabled to bool * a more robust existence test * only perform the conversion on iOS * add tests (cherry picked from commit c983de9) Co-authored-by: Christopher Poile <[email protected]>
1 parent 2298fd5 commit 4afbbe4

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

app/actions/remote/notifications.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import {Platform} from 'react-native';
77

8+
import * as PostActions from '@actions/local/post';
89
import {SYSTEM_IDENTIFIERS} from '@constants/database';
910
import DatabaseManager from '@database/manager';
1011
import NetworkManager from '@managers/network_manager';
@@ -231,3 +232,167 @@ describe('sendTestNotification', () => {
231232
expect(mockClient.sendTestNotification).not.toHaveBeenCalled();
232233
});
233234
});
235+
236+
describe('backgroundNotification boolean conversion', () => {
237+
const postId = 'postWithBooleans';
238+
const samplePostData = {
239+
posts: [
240+
{
241+
id: postId,
242+
props: {
243+
attachments: [
244+
{
245+
id: 1,
246+
fields: [
247+
{title: 'Field1', short: 1},
248+
{title: 'Field2', short: 0},
249+
{title: 'Field3', short: true},
250+
{title: 'Field4', short: false},
251+
{title: 'Field5', short: undefined},
252+
{title: 'Field6'},
253+
],
254+
actions: [
255+
{id: 'action1', disabled: 1},
256+
{id: 'action2', disabled: 0},
257+
{id: 'action3', disabled: true},
258+
{id: 'action4', disabled: false},
259+
{id: 'action5', disabled: undefined},
260+
{id: 'action6'},
261+
],
262+
},
263+
{
264+
id: 2,
265+
},
266+
],
267+
},
268+
269+
// Other necessary post props
270+
channel_id: channelId,
271+
create_at: 12345,
272+
update_at: 12345,
273+
delete_at: 0,
274+
message: 'Test message',
275+
user_id: user1.id,
276+
type: '',
277+
metadata: {},
278+
is_pinned: false, // Add other base post props as needed
279+
reply_count: 0,
280+
281+
// Add potentially missing required props for type check, even if empty
282+
edit_at: 0,
283+
root_id: '',
284+
original_id: '',
285+
hashtags: '',
286+
pending_post_id: '',
287+
} as any, // Use 'as any' to bypass strict type checking for test data
288+
],
289+
order: [postId],
290+
previousPostId: '',
291+
} as ProcessedPosts;
292+
293+
let storePostsSpy: jest.SpyInstance;
294+
let freshNotificationWithBooleanPost: NotificationWithData;
295+
296+
beforeEach(async () => {
297+
// Reset mocks and setup basic DB state for each test
298+
jest.clearAllMocks();
299+
300+
// Reconstruct the notification object with deep-copied post data for each test
301+
const currentSamplePostData = JSON.parse(JSON.stringify(samplePostData)); // Deep copy
302+
freshNotificationWithBooleanPost = {
303+
payload: {
304+
...notificationData,
305+
post_id: postId,
306+
data: {
307+
...notificationExtraData,
308+
posts: {
309+
posts: {[postId]: currentSamplePostData.posts[0]},
310+
order: currentSamplePostData.order,
311+
312+
// Ensure previousPostId is handled if needed by processPostsFetched
313+
prev_post_id: currentSamplePostData.previousPostId,
314+
},
315+
},
316+
},
317+
318+
// Add other required NotificationWithData properties
319+
identifier: 'test-identifier',
320+
foreground: false,
321+
userInteraction: false,
322+
} as any; // Use 'as any' to bypass strict type checking for test data
323+
324+
// Spy on the actual function and provide a mock implementation that matches the return type
325+
storePostsSpy = jest.spyOn(PostActions, 'storePostsForChannel').mockImplementation(async () => ({models: []}));
326+
327+
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}, {id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: teamId}], prepareRecordsOnly: false});
328+
});
329+
330+
afterEach(() => {
331+
// Restore the original implementation after each test
332+
storePostsSpy.mockRestore();
333+
});
334+
335+
it('should convert numeric booleans to true/false on iOS', async () => {
336+
Platform.OS = 'ios';
337+
338+
await backgroundNotification(serverUrl, freshNotificationWithBooleanPost);
339+
340+
// Check the arguments passed to the spy
341+
expect(storePostsSpy).toHaveBeenCalledTimes(1);
342+
const storedPosts = storePostsSpy.mock.calls[0][2] as Post[]; // 3rd argument is posts
343+
expect(storedPosts).toHaveLength(1);
344+
345+
const attachments = storedPosts[0].props!.attachments as MessageAttachment[];
346+
expect(attachments).toHaveLength(2);
347+
348+
// Check fields conversion
349+
const fields = attachments[0].fields!;
350+
expect(fields[0].short).toBe(true); // 1 -> true
351+
expect(fields[1].short).toBe(false); // 0 -> false
352+
expect(fields[2].short).toBe(true); // true -> true
353+
expect(fields[3].short).toBe(false); // false -> false
354+
expect(fields[4].short).toBe(undefined);
355+
expect(fields[5].short).toBe(undefined);
356+
357+
// Check actions conversion
358+
const actions = attachments[0].actions!;
359+
expect(actions[0].disabled).toBe(true); // 1 -> true
360+
expect(actions[1].disabled).toBe(false); // 0 -> false
361+
expect(actions[2].disabled).toBe(true); // true -> true
362+
expect(actions[3].disabled).toBe(false); // false -> false
363+
expect(actions[4].disabled).toBe(undefined);
364+
expect(actions[5].disabled).toBe(undefined);
365+
});
366+
367+
it('should NOT convert numeric booleans on Android (it wouldnt be needed)', async () => {
368+
Platform.OS = 'android';
369+
370+
await backgroundNotification(serverUrl, freshNotificationWithBooleanPost);
371+
372+
// Check the arguments passed to the spy
373+
expect(storePostsSpy).toHaveBeenCalledTimes(1);
374+
const storedPosts = storePostsSpy.mock.calls[0][2] as Post[]; // 3rd argument is posts
375+
expect(storedPosts).toHaveLength(1);
376+
377+
const attachments = storedPosts[0].props!.attachments as MessageAttachment[];
378+
expect(attachments).toHaveLength(2);
379+
380+
// Check fields remain unchanged
381+
const fields = attachments[0].fields!;
382+
expect(fields[0].short).toBe(1); // 1 -> 1
383+
expect(fields[1].short).toBe(0); // 0 -> 0
384+
expect(fields[2].short).toBe(true);
385+
expect(fields[3].short).toBe(false);
386+
expect(fields[4].short).toBe(undefined);
387+
expect(fields[5].short).toBe(undefined);
388+
389+
// Check actions remain unchanged
390+
const actions = attachments[0].actions!;
391+
expect(actions[0].disabled).toBe(1); // 1 -> 1
392+
expect(actions[1].disabled).toBe(0); // 0 -> 0
393+
expect(actions[2].disabled).toBe(true);
394+
expect(actions[3].disabled).toBe(false);
395+
expect(actions[4].disabled).toBe(undefined);
396+
expect(actions[5].disabled).toBe(undefined);
397+
});
398+
});

app/actions/remote/notifications.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,37 @@ export const backgroundNotification = async (serverUrl: string, notification: No
119119

120120
if (posts) {
121121
const postsData = processPostsFetched(posts);
122+
123+
// We need to convert the following properties from 1/0 to true/false, because
124+
// we validate message attachments before displaying them.
125+
// This is a problem from the NSJSONSerialization call in AppDelegate.mm -- there
126+
// is no true/false in Objective-C, only NSNumber with value 1 or 0
127+
if (Platform.OS === 'ios') {
128+
// Convert attachment.fields.short, and attachment.actions.disabled
129+
postsData.posts.forEach((post) => {
130+
if (post.props?.attachments) {
131+
(post.props.attachments as MessageAttachment[])?.forEach((attachment) => {
132+
if (attachment.fields?.length) {
133+
// eslint-disable-next-line max-nested-callbacks
134+
attachment.fields.forEach((field) => {
135+
if (field.short !== undefined) {
136+
field.short = Boolean(field.short);
137+
}
138+
});
139+
}
140+
if (attachment.actions?.length) {
141+
// eslint-disable-next-line max-nested-callbacks
142+
attachment.actions.forEach((action) => {
143+
if (action.disabled !== undefined) {
144+
action.disabled = Boolean(action.disabled);
145+
}
146+
});
147+
}
148+
});
149+
}
150+
});
151+
}
152+
122153
const isThreadNotification = isCRTEnabled && Boolean(notification.payload.root_id);
123154
const actionType = isThreadNotification ? ActionType.POSTS.RECEIVED_IN_THREAD : ActionType.POSTS.RECEIVED_IN_CHANNEL;
124155

0 commit comments

Comments
 (0)