From 78fc4b79dd9c5b76e91b9e3b156c117505b5f02a Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 10 Sep 2025 14:32:32 +0200 Subject: [PATCH 1/9] In progress --- .../handle-notification-feed-updated.test.ts | 293 ++++++++++++++++++ .../handle-notification-feed-updated.ts | 5 +- 2 files changed, 295 insertions(+), 3 deletions(-) diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts index 67ffddd7..53608ed6 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts @@ -25,6 +25,7 @@ const createMockNotificationStatus = ( ): NotificationStatusResponse => ({ unread: 0, unseen: 0, + user_count_truncated: false, ...overrides, }); @@ -415,4 +416,296 @@ describe('notification-feed-utils', () => { expect(result.notification_status).toStrictEqual(newNotificationStatus); }); }); + + describe('addAggregatedActivitiesToState', () => { + it('should add new activities when none exist', () => { + const newActivities = [ + createMockAggregatedActivity({ group: 'group1' }), + createMockAggregatedActivity({ group: 'group2' }), + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + undefined, + 'start', + ); + + expect(result.changed).toBe(true); + expect(result.aggregated_activities).toStrictEqual(newActivities); + }); + + it('should add new activities to existing ones', () => { + const existingActivities = [ + createMockAggregatedActivity({ group: 'existing1' }), + ]; + const newActivities = [ + createMockAggregatedActivity({ group: 'new1' }), + createMockAggregatedActivity({ group: 'new2' }), + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(true); + expect(result.aggregated_activities).toStrictEqual([ + ...newActivities, + ...existingActivities, + ]); + }); + + it('should add new activities at the end when position is end', () => { + const existingActivities = [ + createMockAggregatedActivity({ group: 'existing1' }), + ]; + const newActivities = [createMockAggregatedActivity({ group: 'new1' })]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'end', + ); + + expect(result.changed).toBe(true); + expect(result.aggregated_activities).toStrictEqual([ + ...existingActivities, + ...newActivities, + ]); + }); + + it('should update existing activities with same group (upsert)', () => { + const baseDate = new Date('2023-01-01'); + const existingActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activity_count: 1, + score: 10, + updated_at: baseDate, + }), + createMockAggregatedActivity({ + group: 'group2', + activity_count: 2, + score: 20, + }), + ]; + const newActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activity_count: 3, + score: 30, + updated_at: new Date('2023-01-02'), + }), + createMockAggregatedActivity({ + group: 'group3', + activity_count: 4, + score: 40, + }), + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(true); + expect(result.aggregated_activities).toHaveLength(3); + + // Check that group1 was updated + const updatedGroup1 = result.aggregated_activities.find( + (a) => a.group === 'group1', + ); + expect(updatedGroup1?.activity_count).toBe(3); + expect(updatedGroup1?.score).toBe(30); + expect(updatedGroup1?.updated_at).toEqual(new Date('2023-01-02')); + + // Check that group2 remains unchanged + const unchangedGroup2 = result.aggregated_activities.find( + (a) => a.group === 'group2', + ); + expect(unchangedGroup2?.activity_count).toBe(2); + expect(unchangedGroup2?.score).toBe(20); + + // Check that group3 was added + const newGroup3 = result.aggregated_activities.find( + (a) => a.group === 'group3', + ); + expect(newGroup3?.activity_count).toBe(4); + expect(newGroup3?.score).toBe(40); + }); + + it('should not mark as changed when updating with identical data', () => { + const baseDate = new Date('2023-01-01'); + const existingActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activity_count: 1, + score: 10, + updated_at: baseDate, + }), + ]; + const identicalActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activity_count: 1, + score: 10, + updated_at: baseDate, + }), + ]; + + const result = addAggregatedActivitiesToState( + identicalActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(false); + expect(result.aggregated_activities).toStrictEqual(existingActivities); + }); + + it('should detect changes in user_count and user_count_truncated', () => { + const existingActivities = [ + createMockAggregatedActivity({ + group: 'group1', + user_count: 5, + user_count_truncated: false, + }), + ]; + const updatedActivities = [ + createMockAggregatedActivity({ + group: 'group1', + user_count: 10, + user_count_truncated: true, + }), + ]; + + const result = addAggregatedActivitiesToState( + updatedActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(true); + const updatedActivity = result.aggregated_activities.find( + (a) => a.group === 'group1', + ); + expect(updatedActivity?.user_count).toBe(10); + expect(updatedActivity?.user_count_truncated).toBe(true); + }); + + it('should detect changes in activities array length', () => { + const existingActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activities: [{ id: 'activity1' } as any], + }), + ]; + const updatedActivities = [ + createMockAggregatedActivity({ + group: 'group1', + activities: [{ id: 'activity1' } as any, { id: 'activity2' } as any], + }), + ]; + + const result = addAggregatedActivitiesToState( + updatedActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(true); + const updatedActivity = result.aggregated_activities.find( + (a) => a.group === 'group1', + ); + expect(updatedActivity?.activities).toHaveLength(2); + }); + + it('should handle mixed new and existing activities', () => { + const existingActivities = [ + createMockAggregatedActivity({ group: 'existing1' }), + createMockAggregatedActivity({ group: 'existing2' }), + ]; + const newActivities = [ + createMockAggregatedActivity({ group: 'existing1', activity_count: 5 }), // Update existing + createMockAggregatedActivity({ group: 'new1' }), // Add new + createMockAggregatedActivity({ group: 'existing2', score: 100 }), // Update existing + createMockAggregatedActivity({ group: 'new2' }), // Add new + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'start', + ); + + expect(result.changed).toBe(true); + expect(result.aggregated_activities).toHaveLength(4); + + // Check that existing1 was updated + const updatedExisting1 = result.aggregated_activities.find( + (a) => a.group === 'existing1', + ); + expect(updatedExisting1?.activity_count).toBe(5); + + // Check that existing2 was updated + const updatedExisting2 = result.aggregated_activities.find( + (a) => a.group === 'existing2', + ); + expect(updatedExisting2?.score).toBe(100); + + // Check that new activities were added + expect( + result.aggregated_activities.find((a) => a.group === 'new1'), + ).toBeDefined(); + expect( + result.aggregated_activities.find((a) => a.group === 'new2'), + ).toBeDefined(); + }); + + it('should preserve order when adding at start', () => { + const existingActivities = [ + createMockAggregatedActivity({ group: 'existing1' }), + createMockAggregatedActivity({ group: 'existing2' }), + ]; + const newActivities = [ + createMockAggregatedActivity({ group: 'new1' }), + createMockAggregatedActivity({ group: 'new2' }), + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'start', + ); + + expect(result.aggregated_activities).toStrictEqual([ + ...newActivities, + ...existingActivities, + ]); + }); + + it('should preserve order when adding at end', () => { + const existingActivities = [ + createMockAggregatedActivity({ group: 'existing1' }), + createMockAggregatedActivity({ group: 'existing2' }), + ]; + const newActivities = [ + createMockAggregatedActivity({ group: 'new1' }), + createMockAggregatedActivity({ group: 'new2' }), + ]; + + const result = addAggregatedActivitiesToState( + newActivities, + existingActivities, + 'end', + ); + + expect(result.aggregated_activities).toStrictEqual([ + ...existingActivities, + ...newActivities, + ]); + }); + }); }); diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts index 7b2c24b6..bdd005bc 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts @@ -94,11 +94,11 @@ export const updateNotificationFeedFromEvent = ( } } - if (event.aggregated_activities && currentAggregatedActivities) { + if (event.aggregated_activities) { const aggregatedActivitiesResult = addAggregatedActivitiesToState( event.aggregated_activities, currentAggregatedActivities, - 'start', + 'start', // Add new activities at the start ); if (aggregatedActivitiesResult.changed) { @@ -126,7 +126,6 @@ export function handleNotificationFeedUpdated( const result = updateNotificationFeedFromEvent( event, this.currentState.aggregated_activities, - this.currentState.notification_status, ); if (result.changed) { this.state.partialNext({ From e8f7bb1130083fde704adf309137a51ce0c20856 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 11 Sep 2025 13:32:24 +0200 Subject: [PATCH 2/9] enable notification feed pagination --- .../__integration-tests__/utils.ts | 2 +- .../handle-notification-feed-updated.test.ts | 485 +++++++++--------- .../handle-notification-feed-updated.ts | 16 +- 3 files changed, 258 insertions(+), 245 deletions(-) diff --git a/packages/feeds-client/__integration-tests__/utils.ts b/packages/feeds-client/__integration-tests__/utils.ts index dfeac722..7b6c9bee 100644 --- a/packages/feeds-client/__integration-tests__/utils.ts +++ b/packages/feeds-client/__integration-tests__/utils.ts @@ -73,7 +73,7 @@ export const waitForEvent = ( ) => { return new Promise((resolve, reject) => { // @ts-expect-error client expects WSEvents - client.on(type, () => { + client.on(type, (e) => { resolve(undefined); clearTimeout(timeout); }); diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts index 53608ed6..24618f59 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts @@ -25,7 +25,6 @@ const createMockNotificationStatus = ( ): NotificationStatusResponse => ({ unread: 0, unseen: 0, - user_count_truncated: false, ...overrides, }); @@ -393,7 +392,7 @@ describe('notification-feed-utils', () => { }); describe('updateNotificationStatus', () => { - it('should replace old state with new one', () => { + it('should merge read_activities and seen_activities arrays correctly', () => { const newNotificationStatus = createMockNotificationStatus({ unread: 5, unseen: 3, @@ -413,298 +412,300 @@ describe('notification-feed-utils', () => { currentNotificationStatus, ); - expect(result.notification_status).toStrictEqual(newNotificationStatus); + expect(result.notification_status?.unread).toBe(5); + expect(result.notification_status?.unseen).toBe(3); + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity2', + 'activity5', + 'activity6', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity3', + 'activity4', + 'activity7', + 'activity8', + ]); }); - }); - describe('addAggregatedActivitiesToState', () => { - it('should add new activities when none exist', () => { - const newActivities = [ - createMockAggregatedActivity({ group: 'group1' }), - createMockAggregatedActivity({ group: 'group2' }), - ]; + it('should handle empty arrays in both notification statuses', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: [], + seen_activities: [], + }); - const result = addAggregatedActivitiesToState( - newActivities, - undefined, - 'start', + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: [], + seen_activities: [], + }); + + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.changed).toBe(true); - expect(result.aggregated_activities).toStrictEqual(newActivities); + expect(result.notification_status?.read_activities).toEqual([]); + expect(result.notification_status?.seen_activities).toEqual([]); }); - it('should add new activities to existing ones', () => { - const existingActivities = [ - createMockAggregatedActivity({ group: 'existing1' }), - ]; - const newActivities = [ - createMockAggregatedActivity({ group: 'new1' }), - createMockAggregatedActivity({ group: 'new2' }), - ]; + it('should handle undefined arrays by treating them as empty arrays', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: undefined, + seen_activities: undefined, + }); - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'start', + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: ['activity1'], + seen_activities: ['activity2'], + }); + + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.changed).toBe(true); - expect(result.aggregated_activities).toStrictEqual([ - ...newActivities, - ...existingActivities, + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity2', ]); }); - it('should add new activities at the end when position is end', () => { - const existingActivities = [ - createMockAggregatedActivity({ group: 'existing1' }), - ]; - const newActivities = [createMockAggregatedActivity({ group: 'new1' })]; + it('should handle when current arrays are undefined', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: ['activity1', 'activity2'], + seen_activities: ['activity3', 'activity4'], + }); - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'end', + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: undefined, + seen_activities: undefined, + }); + + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.changed).toBe(true); - expect(result.aggregated_activities).toStrictEqual([ - ...existingActivities, - ...newActivities, + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity2', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity3', + 'activity4', ]); }); - it('should update existing activities with same group (upsert)', () => { - const baseDate = new Date('2023-01-01'); - const existingActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activity_count: 1, - score: 10, - updated_at: baseDate, - }), - createMockAggregatedActivity({ - group: 'group2', - activity_count: 2, - score: 20, - }), - ]; - const newActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activity_count: 3, - score: 30, - updated_at: new Date('2023-01-02'), - }), - createMockAggregatedActivity({ - group: 'group3', - activity_count: 4, - score: 40, - }), - ]; - - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'start', - ); - - expect(result.changed).toBe(true); - expect(result.aggregated_activities).toHaveLength(3); + it('should remove duplicates when merging arrays', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: ['activity1', 'activity2', 'activity3'], + seen_activities: ['activity4', 'activity5', 'activity6'], + }); - // Check that group1 was updated - const updatedGroup1 = result.aggregated_activities.find( - (a) => a.group === 'group1', - ); - expect(updatedGroup1?.activity_count).toBe(3); - expect(updatedGroup1?.score).toBe(30); - expect(updatedGroup1?.updated_at).toEqual(new Date('2023-01-02')); + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: ['activity2', 'activity3', 'activity7'], + seen_activities: ['activity5', 'activity6', 'activity8'], + }); - // Check that group2 remains unchanged - const unchangedGroup2 = result.aggregated_activities.find( - (a) => a.group === 'group2', + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(unchangedGroup2?.activity_count).toBe(2); - expect(unchangedGroup2?.score).toBe(20); - // Check that group3 was added - const newGroup3 = result.aggregated_activities.find( - (a) => a.group === 'group3', - ); - expect(newGroup3?.activity_count).toBe(4); - expect(newGroup3?.score).toBe(40); + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity2', + 'activity3', + 'activity7', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity4', + 'activity5', + 'activity6', + 'activity8', + ]); }); - it('should not mark as changed when updating with identical data', () => { - const baseDate = new Date('2023-01-01'); - const existingActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activity_count: 1, - score: 10, - updated_at: baseDate, - }), - ]; - const identicalActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activity_count: 1, - score: 10, - updated_at: baseDate, - }), - ]; - - const result = addAggregatedActivitiesToState( - identicalActivities, - existingActivities, - 'start', - ); - - expect(result.changed).toBe(false); - expect(result.aggregated_activities).toStrictEqual(existingActivities); - }); + it('should preserve all other properties from newNotificationStatus', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 10, + unseen: 5, + last_seen_at: new Date('2023-01-01'), + read_activities: ['activity1'], + seen_activities: ['activity2'], + }); - it('should detect changes in user_count and user_count_truncated', () => { - const existingActivities = [ - createMockAggregatedActivity({ - group: 'group1', - user_count: 5, - user_count_truncated: false, - }), - ]; - const updatedActivities = [ - createMockAggregatedActivity({ - group: 'group1', - user_count: 10, - user_count_truncated: true, - }), - ]; + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + last_seen_at: new Date('2022-01-01'), + read_activities: ['activity3'], + seen_activities: ['activity4'], + }); - const result = addAggregatedActivitiesToState( - updatedActivities, - existingActivities, - 'start', + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.changed).toBe(true); - const updatedActivity = result.aggregated_activities.find( - (a) => a.group === 'group1', + expect(result.notification_status?.unread).toBe(10); + expect(result.notification_status?.unseen).toBe(5); + expect(result.notification_status?.last_seen_at).toEqual( + new Date('2023-01-01'), ); - expect(updatedActivity?.user_count).toBe(10); - expect(updatedActivity?.user_count_truncated).toBe(true); + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity3', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity2', + 'activity4', + ]); }); - it('should detect changes in activities array length', () => { - const existingActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activities: [{ id: 'activity1' } as any], - }), - ]; - const updatedActivities = [ - createMockAggregatedActivity({ - group: 'group1', - activities: [{ id: 'activity1' } as any, { id: 'activity2' } as any], - }), - ]; + it('should handle mixed undefined and defined arrays', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: ['activity1'], + seen_activities: undefined, + }); - const result = addAggregatedActivitiesToState( - updatedActivities, - existingActivities, - 'start', - ); + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: undefined, + seen_activities: ['activity2'], + }); - expect(result.changed).toBe(true); - const updatedActivity = result.aggregated_activities.find( - (a) => a.group === 'group1', + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(updatedActivity?.activities).toHaveLength(2); - }); - it('should handle mixed new and existing activities', () => { - const existingActivities = [ - createMockAggregatedActivity({ group: 'existing1' }), - createMockAggregatedActivity({ group: 'existing2' }), - ]; - const newActivities = [ - createMockAggregatedActivity({ group: 'existing1', activity_count: 5 }), // Update existing - createMockAggregatedActivity({ group: 'new1' }), // Add new - createMockAggregatedActivity({ group: 'existing2', score: 100 }), // Update existing - createMockAggregatedActivity({ group: 'new2' }), // Add new - ]; - - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'start', - ); + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity2', + ]); + }); - expect(result.changed).toBe(true); - expect(result.aggregated_activities).toHaveLength(4); + it('should handle complex activity arrays with many duplicates', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: ['a', 'b', 'c', 'd', 'e'], + seen_activities: ['f', 'g', 'h', 'i', 'j'], + }); - // Check that existing1 was updated - const updatedExisting1 = result.aggregated_activities.find( - (a) => a.group === 'existing1', - ); - expect(updatedExisting1?.activity_count).toBe(5); + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: ['c', 'd', 'e', 'f', 'g'], + seen_activities: ['h', 'i', 'j', 'k', 'l'], + }); - // Check that existing2 was updated - const updatedExisting2 = result.aggregated_activities.find( - (a) => a.group === 'existing2', + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(updatedExisting2?.score).toBe(100); - // Check that new activities were added - expect( - result.aggregated_activities.find((a) => a.group === 'new1'), - ).toBeDefined(); - expect( - result.aggregated_activities.find((a) => a.group === 'new2'), - ).toBeDefined(); + expect(result.notification_status?.read_activities).toEqual([ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + ]); }); - it('should preserve order when adding at start', () => { - const existingActivities = [ - createMockAggregatedActivity({ group: 'existing1' }), - createMockAggregatedActivity({ group: 'existing2' }), - ]; - const newActivities = [ - createMockAggregatedActivity({ group: 'new1' }), - createMockAggregatedActivity({ group: 'new2' }), - ]; + it('should handle empty new arrays with non-empty current arrays', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: [], + seen_activities: [], + }); - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'start', + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: ['activity1', 'activity2'], + seen_activities: ['activity3', 'activity4'], + }); + + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.aggregated_activities).toStrictEqual([ - ...newActivities, - ...existingActivities, + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity2', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity3', + 'activity4', ]); }); - it('should preserve order when adding at end', () => { - const existingActivities = [ - createMockAggregatedActivity({ group: 'existing1' }), - createMockAggregatedActivity({ group: 'existing2' }), - ]; - const newActivities = [ - createMockAggregatedActivity({ group: 'new1' }), - createMockAggregatedActivity({ group: 'new2' }), - ]; + it('should handle non-empty new arrays with empty current arrays', () => { + const newNotificationStatus = createMockNotificationStatus({ + unread: 1, + unseen: 1, + read_activities: ['activity1', 'activity2'], + seen_activities: ['activity3', 'activity4'], + }); - const result = addAggregatedActivitiesToState( - newActivities, - existingActivities, - 'end', + const currentNotificationStatus = createMockNotificationStatus({ + unread: 0, + unseen: 0, + read_activities: [], + seen_activities: [], + }); + + const result = updateNotificationStatus( + newNotificationStatus, + currentNotificationStatus, ); - expect(result.aggregated_activities).toStrictEqual([ - ...existingActivities, - ...newActivities, + expect(result.notification_status?.read_activities).toEqual([ + 'activity1', + 'activity2', + ]); + expect(result.notification_status?.seen_activities).toEqual([ + 'activity3', + 'activity4', ]); }); }); diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts index bdd005bc..75de8fd4 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts @@ -62,6 +62,16 @@ export const updateNotificationStatus = ( changed: true, notification_status: { ...newNotificationStatus, + read_activities: uniqueArrayMerge( + newNotificationStatus?.read_activities ?? [], + currentNotificationStatus?.read_activities ?? [], + (a) => a, + ), + seen_activities: uniqueArrayMerge( + newNotificationStatus?.seen_activities ?? [], + currentNotificationStatus?.seen_activities ?? [], + (a) => a, + ), }, }; } @@ -94,11 +104,11 @@ export const updateNotificationFeedFromEvent = ( } } - if (event.aggregated_activities) { + if (event.aggregated_activities && currentAggregatedActivities) { const aggregatedActivitiesResult = addAggregatedActivitiesToState( event.aggregated_activities, currentAggregatedActivities, - 'start', // Add new activities at the start + 'start', ); if (aggregatedActivitiesResult.changed) { @@ -126,8 +136,10 @@ export function handleNotificationFeedUpdated( const result = updateNotificationFeedFromEvent( event, this.currentState.aggregated_activities, + this.currentState.notification_status, ); if (result.changed) { + console.log('result changed', result.data?.notification_status); this.state.partialNext({ notification_status: result.data?.notification_status, aggregated_activities: result.data?.aggregated_activities, From b4ba49dead3f17c24ecf239ac02fbcd893f4194f Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 11 Sep 2025 17:00:09 +0200 Subject: [PATCH 3/9] implement notification feed pagination --- .../handle-notification-feed-updated.test.ts | 298 +----------------- .../handle-notification-feed-updated.ts | 10 - 2 files changed, 2 insertions(+), 306 deletions(-) diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts index 24618f59..67ffddd7 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts @@ -392,7 +392,7 @@ describe('notification-feed-utils', () => { }); describe('updateNotificationStatus', () => { - it('should merge read_activities and seen_activities arrays correctly', () => { + it('should replace old state with new one', () => { const newNotificationStatus = createMockNotificationStatus({ unread: 5, unseen: 3, @@ -412,301 +412,7 @@ describe('notification-feed-utils', () => { currentNotificationStatus, ); - expect(result.notification_status?.unread).toBe(5); - expect(result.notification_status?.unseen).toBe(3); - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity2', - 'activity5', - 'activity6', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity3', - 'activity4', - 'activity7', - 'activity8', - ]); - }); - - it('should handle empty arrays in both notification statuses', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: [], - seen_activities: [], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: [], - seen_activities: [], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([]); - expect(result.notification_status?.seen_activities).toEqual([]); - }); - - it('should handle undefined arrays by treating them as empty arrays', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: undefined, - seen_activities: undefined, - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: ['activity1'], - seen_activities: ['activity2'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity2', - ]); - }); - - it('should handle when current arrays are undefined', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: ['activity1', 'activity2'], - seen_activities: ['activity3', 'activity4'], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: undefined, - seen_activities: undefined, - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity2', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity3', - 'activity4', - ]); - }); - - it('should remove duplicates when merging arrays', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: ['activity1', 'activity2', 'activity3'], - seen_activities: ['activity4', 'activity5', 'activity6'], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: ['activity2', 'activity3', 'activity7'], - seen_activities: ['activity5', 'activity6', 'activity8'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity2', - 'activity3', - 'activity7', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity4', - 'activity5', - 'activity6', - 'activity8', - ]); - }); - - it('should preserve all other properties from newNotificationStatus', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 10, - unseen: 5, - last_seen_at: new Date('2023-01-01'), - read_activities: ['activity1'], - seen_activities: ['activity2'], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - last_seen_at: new Date('2022-01-01'), - read_activities: ['activity3'], - seen_activities: ['activity4'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.unread).toBe(10); - expect(result.notification_status?.unseen).toBe(5); - expect(result.notification_status?.last_seen_at).toEqual( - new Date('2023-01-01'), - ); - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity3', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity2', - 'activity4', - ]); - }); - - it('should handle mixed undefined and defined arrays', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: ['activity1'], - seen_activities: undefined, - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: undefined, - seen_activities: ['activity2'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity2', - ]); - }); - - it('should handle complex activity arrays with many duplicates', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: ['a', 'b', 'c', 'd', 'e'], - seen_activities: ['f', 'g', 'h', 'i', 'j'], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: ['c', 'd', 'e', 'f', 'g'], - seen_activities: ['h', 'i', 'j', 'k', 'l'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - ]); - }); - - it('should handle empty new arrays with non-empty current arrays', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: [], - seen_activities: [], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: ['activity1', 'activity2'], - seen_activities: ['activity3', 'activity4'], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity2', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity3', - 'activity4', - ]); - }); - - it('should handle non-empty new arrays with empty current arrays', () => { - const newNotificationStatus = createMockNotificationStatus({ - unread: 1, - unseen: 1, - read_activities: ['activity1', 'activity2'], - seen_activities: ['activity3', 'activity4'], - }); - - const currentNotificationStatus = createMockNotificationStatus({ - unread: 0, - unseen: 0, - read_activities: [], - seen_activities: [], - }); - - const result = updateNotificationStatus( - newNotificationStatus, - currentNotificationStatus, - ); - - expect(result.notification_status?.read_activities).toEqual([ - 'activity1', - 'activity2', - ]); - expect(result.notification_status?.seen_activities).toEqual([ - 'activity3', - 'activity4', - ]); + expect(result.notification_status).toStrictEqual(newNotificationStatus); }); }); }); diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts index 75de8fd4..ca2d9926 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts @@ -62,16 +62,6 @@ export const updateNotificationStatus = ( changed: true, notification_status: { ...newNotificationStatus, - read_activities: uniqueArrayMerge( - newNotificationStatus?.read_activities ?? [], - currentNotificationStatus?.read_activities ?? [], - (a) => a, - ), - seen_activities: uniqueArrayMerge( - newNotificationStatus?.seen_activities ?? [], - currentNotificationStatus?.seen_activities ?? [], - (a) => a, - ), }, }; } From 3cc5f371e434014dc4792e390885f34235d5d05a Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 11 Sep 2025 17:09:16 +0200 Subject: [PATCH 4/9] Remove unnecessary change --- packages/feeds-client/__integration-tests__/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feeds-client/__integration-tests__/utils.ts b/packages/feeds-client/__integration-tests__/utils.ts index 7b6c9bee..dfeac722 100644 --- a/packages/feeds-client/__integration-tests__/utils.ts +++ b/packages/feeds-client/__integration-tests__/utils.ts @@ -73,7 +73,7 @@ export const waitForEvent = ( ) => { return new Promise((resolve, reject) => { // @ts-expect-error client expects WSEvents - client.on(type, (e) => { + client.on(type, () => { resolve(undefined); clearTimeout(timeout); }); From 1314c7550c60795d7361df798526eb06587fc3b4 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 11 Sep 2025 17:10:53 +0200 Subject: [PATCH 5/9] Remove console.log --- .../notification-feed/handle-notification-feed-updated.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts index ca2d9926..7b2c24b6 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts @@ -129,7 +129,6 @@ export function handleNotificationFeedUpdated( this.currentState.notification_status, ); if (result.changed) { - console.log('result changed', result.data?.notification_status); this.state.partialNext({ notification_status: result.data?.notification_status, aggregated_activities: result.data?.aggregated_activities, From bd1dc929b2e8bc8496839712129714d77e0285af Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Thu, 11 Sep 2025 13:32:24 +0200 Subject: [PATCH 6/9] enable notification feed pagination --- packages/feeds-client/__integration-tests__/utils.ts | 2 +- .../notification-feed/handle-notification-feed-updated.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/feeds-client/__integration-tests__/utils.ts b/packages/feeds-client/__integration-tests__/utils.ts index dfeac722..7b6c9bee 100644 --- a/packages/feeds-client/__integration-tests__/utils.ts +++ b/packages/feeds-client/__integration-tests__/utils.ts @@ -73,7 +73,7 @@ export const waitForEvent = ( ) => { return new Promise((resolve, reject) => { // @ts-expect-error client expects WSEvents - client.on(type, () => { + client.on(type, (e) => { resolve(undefined); clearTimeout(timeout); }); diff --git a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts index 7b2c24b6..ca2d9926 100644 --- a/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +++ b/packages/feeds-client/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts @@ -129,6 +129,7 @@ export function handleNotificationFeedUpdated( this.currentState.notification_status, ); if (result.changed) { + console.log('result changed', result.data?.notification_status); this.state.partialNext({ notification_status: result.data?.notification_status, aggregated_activities: result.data?.aggregated_activities, From 531dc499d206d279310eecd654821ea8f547e17e Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Mon, 15 Sep 2025 16:29:09 +0200 Subject: [PATCH 7/9] feat: new aggregation format for notifications --- .../aggregated-feed-pagination.test.ts | 12 +++- .../__integration-tests__/utils.ts | 2 +- .../app/components/NewActivity.tsx | 2 +- .../Search/SearchResults/SearchResultItem.tsx | 9 ++- .../components/notifications/Notification.tsx | 64 +++++++++++++------ 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/packages/feeds-client/__integration-tests__/aggregated-feed-pagination.test.ts b/packages/feeds-client/__integration-tests__/aggregated-feed-pagination.test.ts index d189fbca..b6779550 100644 --- a/packages/feeds-client/__integration-tests__/aggregated-feed-pagination.test.ts +++ b/packages/feeds-client/__integration-tests__/aggregated-feed-pagination.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import type { FeedsClient, UserRequest } from '../src'; +import type { + FeedsClient, + NotificationFeedUpdatedEvent, + UserRequest, +} from '../src'; import { createTestClient, createTestTokenGenerator, @@ -58,7 +62,7 @@ describe('Aggregated Feed Pagination Integration Tests', () => { .aggregated_activities!.map((a) => a.group), }); - await waitForEvent(feed, 'feeds.notification_feed.updated', { + const event = await waitForEvent(feed, 'feeds.notification_feed.updated', { shouldReject: true, }); @@ -66,6 +70,10 @@ describe('Aggregated Feed Pagination Integration Tests', () => { expect( feed.state.getLatestValue().notification_status?.seen_activities?.length, ).toBe(2); + expect( + (event as NotificationFeedUpdatedEvent)?.notification_status + ?.seen_activities?.length, + ).toBe(2); }); it('should fetch next page of notifications', async () => { diff --git a/packages/feeds-client/__integration-tests__/utils.ts b/packages/feeds-client/__integration-tests__/utils.ts index 7b6c9bee..ad478ec1 100644 --- a/packages/feeds-client/__integration-tests__/utils.ts +++ b/packages/feeds-client/__integration-tests__/utils.ts @@ -74,7 +74,7 @@ export const waitForEvent = ( return new Promise((resolve, reject) => { // @ts-expect-error client expects WSEvents client.on(type, (e) => { - resolve(undefined); + resolve(e); clearTimeout(timeout); }); const timeout = setTimeout(() => { diff --git a/sample-apps/react-sample-app/app/components/NewActivity.tsx b/sample-apps/react-sample-app/app/components/NewActivity.tsx index aa23edea..74975206 100644 --- a/sample-apps/react-sample-app/app/components/NewActivity.tsx +++ b/sample-apps/react-sample-app/app/components/NewActivity.tsx @@ -91,7 +91,7 @@ export const NewActivity = ({ feed }: { feed: Feed }) => { diff --git a/sample-apps/react-sample-app/app/components/Search/SearchResults/SearchResultItem.tsx b/sample-apps/react-sample-app/app/components/Search/SearchResults/SearchResultItem.tsx index 13dbeda6..fa9f9d0c 100644 --- a/sample-apps/react-sample-app/app/components/Search/SearchResults/SearchResultItem.tsx +++ b/sample-apps/react-sample-app/app/components/Search/SearchResults/SearchResultItem.tsx @@ -34,8 +34,9 @@ export const FeedSearchResultItem = ({ item }: FeedSearchResultItemProps) => { ); const isFollowing = - ownFollows.some((follow) => follow.source_feed.feed === ownTimeline?.feed) ?? - false; + ownFollows.some( + (follow) => follow.source_feed.feed === ownTimeline?.feed, + ) ?? false; return (
{ if (isFollowing) { ownTimeline?.unfollow(item.feed); } else { - ownTimeline?.follow(item.feed); + ownTimeline?.follow(item.feed, { + create_notification_activity: true, + }); } }} > diff --git a/sample-apps/react-sample-app/app/components/notifications/Notification.tsx b/sample-apps/react-sample-app/app/components/notifications/Notification.tsx index b137e8ba..1f1b19db 100644 --- a/sample-apps/react-sample-app/app/components/notifications/Notification.tsx +++ b/sample-apps/react-sample-app/app/components/notifications/Notification.tsx @@ -12,52 +12,74 @@ export const Notification = ({ isSeen: boolean; onMarkRead: () => {}; }) => { - const notificationText = useMemo(() => { + const notification = useMemo(() => { const verb = group.activities[0].type; - let text = ''; + const targetActivity = group.activities[0].notification_context?.target; + const notification = { + text: '', + image: targetActivity?.text + ? undefined + : targetActivity?.attachments?.[0]?.image_url, + }; + + const targetActivityTruncatedText = targetActivity?.text + ? ` "${ + targetActivity.text.length > 20 + ? targetActivity?.text?.slice(0, 20) + '...' + : targetActivity?.text + }"` + : ''; + const previewCount = 5; + const previewActors = Array.from( + new Set(group.activities.map(({ user }) => user.name)), + ).slice(0, previewCount); + notification.text = previewActors.join(', '); + const remainingActors = group.user_count - previewActors.length; + + if (remainingActors > 1) { + notification.text += ` and ${remainingActors}${group.user_count_truncated ? '+' : ''} more people`; + } else if (remainingActors === 1) { + notification.text += ' and 1 more person'; + } switch (verb) { case 'comment': { - text += `${group.activity_count} new comments`; + notification.text += ` commented on your post${targetActivityTruncatedText}`; break; } case 'reaction': { - text += `${group.activity_count} likes`; + notification.text += ` reacted to your post${targetActivityTruncatedText}`; break; } case 'follow': { - const previewCount = 5; - text = Array.from( - new Set(group.activities.map(({ user }) => user.name)), - ) - .slice(0, previewCount) - .join(', '); - const remainingActors = group.user_count - previewCount; - if (remainingActors > 1) { - text += ` and ${remainingActors}${group.user_count_truncated ? '+' : ''} more people`; - } else if (remainingActors === 1) { - text += ' and 1 more person'; - } - text += ` started following you`; + notification.text += ` started following you`; break; } case 'comment_reaction': { - text += `${group.activity_count} new reactions to your comment`; + notification.text += ` reacted to your comment on post${targetActivityTruncatedText}`; break; } default: { - text += 'Unknown type'; + notification.text += 'Unknown type'; break; } } - return text; + return notification; }, [group]); return (
-
{notificationText}
+ {notification.text &&
{notification.text}
} + {notification.image && ( + Notification image + )}
{!isRead && (