diff --git a/app/soapbox/entity-store/__tests__/reducer.test.ts b/app/soapbox/entity-store/__tests__/reducer.test.ts index 7d4e6db9c..4b5a9752d 100644 --- a/app/soapbox/entity-store/__tests__/reducer.test.ts +++ b/app/soapbox/entity-store/__tests__/reducer.test.ts @@ -42,7 +42,8 @@ test('import entities into a list', () => { const cache = result.TestEntity as EntityCache; expect(cache.store['2']!.msg).toBe('benis'); - expect(cache.lists.thingies?.ids.size).toBe(3); + expect(cache.lists.thingies!.ids.size).toBe(3); + expect(cache.lists.thingies!.state.totalCount).toBe(3); // Now try adding an additional item. const entities2: TestEntity[] = [ @@ -54,7 +55,8 @@ test('import entities into a list', () => { const cache2 = result2.TestEntity as EntityCache; expect(cache2.store['4']!.msg).toBe('hehe'); - expect(cache2.lists.thingies?.ids.size).toBe(4); + expect(cache2.lists.thingies!.ids.size).toBe(4); + expect(cache2.lists.thingies!.state.totalCount).toBe(4); // Finally, update an item. const entities3: TestEntity[] = [ @@ -66,7 +68,8 @@ test('import entities into a list', () => { const cache3 = result3.TestEntity as EntityCache; expect(cache3.store['2']!.msg).toBe('yolofam'); - expect(cache3.lists.thingies?.ids.size).toBe(4); // unchanged + expect(cache3.lists.thingies!.ids.size).toBe(4); // unchanged + expect(cache3.lists.thingies!.state.totalCount).toBe(4); }); test('fetching updates the list state', () => { @@ -92,7 +95,7 @@ test('deleting items', () => { lists: { '': { ids: new Set(['1', '2', '3']), - state: createListState(), + state: { ...createListState(), totalCount: 3 }, }, }, }, @@ -103,6 +106,7 @@ test('deleting items', () => { expect(result.TestEntity!.store).toMatchObject({ '2': { id: '2' } }); expect([...result.TestEntity!.lists['']!.ids]).toEqual(['2']); + expect(result.TestEntity!.lists['']!.state.totalCount).toBe(1); }); test('dismiss items', () => { @@ -112,7 +116,7 @@ test('dismiss items', () => { lists: { 'yolo': { ids: new Set(['1', '2', '3']), - state: createListState(), + state: { ...createListState(), totalCount: 3 }, }, }, }, @@ -123,4 +127,5 @@ test('dismiss items', () => { expect(result.TestEntity!.store).toMatchObject(state.TestEntity!.store); expect([...result.TestEntity!.lists.yolo!.ids]).toEqual(['2']); + expect(result.TestEntity!.lists.yolo!.state.totalCount).toBe(1); }); \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 309accf1b..0f345d675 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -47,6 +47,7 @@ function useEntities( const lastFetchedAt = useListState(path, 'lastFetchedAt'); const isFetched = useListState(path, 'fetched'); const isError = !!useListState(path, 'error'); + const totalCount = useListState(path, 'totalCount'); const next = useListState(path, 'next'); const prev = useListState(path, 'prev'); @@ -61,10 +62,12 @@ function useEntities( const response = await api.get(url); const schema = opts.schema || z.custom(); const entities = filteredArray(schema).parse(response.data); + const numItems = (selectList(getState(), path)?.ids.size || 0) + entities.length; dispatch(entitiesFetchSuccess(entities, entityType, listKey, { next: getNextLink(response), prev: getPrevLink(response), + totalCount: Number(response.headers['x-total-count'] ?? numItems) || 0, fetching: false, fetched: true, error: null, @@ -108,6 +111,7 @@ function useEntities( fetchPreviousPage, hasNextPage: !!next, hasPreviousPage: !!prev, + totalCount, isError, isFetched, isFetching, diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts index 448de33ab..4f9c1e4d2 100644 --- a/app/soapbox/entity-store/reducer.ts +++ b/app/soapbox/entity-store/reducer.ts @@ -60,7 +60,10 @@ const deleteEntities = ( if (!opts?.preserveLists) { for (const list of Object.values(cache.lists)) { - list?.ids.delete(id); + if (list) { + list.ids.delete(id); + list.state.totalCount--; + } } } } @@ -82,6 +85,7 @@ const dismissEntities = ( if (list) { for (const id of ids) { list.ids.delete(id); + list.state.totalCount--; } draft[entityType] = cache; diff --git a/app/soapbox/entity-store/types.ts b/app/soapbox/entity-store/types.ts index 0e34b62a5..09e6c0174 100644 --- a/app/soapbox/entity-store/types.ts +++ b/app/soapbox/entity-store/types.ts @@ -23,6 +23,8 @@ interface EntityListState { next: string | undefined /** Previous URL for pagination, if any. */ prev: string | undefined + /** Total number of items according to the API. */ + totalCount: number /** Error returned from the API, if any. */ error: any /** Whether data has already been fetched */ diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts index 9d56ceb42..0040ae674 100644 --- a/app/soapbox/entity-store/utils.ts +++ b/app/soapbox/entity-store/utils.ts @@ -11,9 +11,14 @@ const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => { /** Update the list with new entity IDs. */ const updateList = (list: EntityList, entities: Entity[]): EntityList => { const newIds = entities.map(entity => entity.id); + const ids = new Set([...Array.from(list.ids), ...newIds]); + + const sizeDiff = ids.size - list.ids.size; + list.state.totalCount += sizeDiff; + return { ...list, - ids: new Set([...Array.from(list.ids), ...newIds]), + ids, }; }; @@ -33,6 +38,7 @@ const createList = (): EntityList => ({ const createListState = (): EntityListState => ({ next: undefined, prev: undefined, + totalCount: 0, error: null, fetched: false, fetching: false,