Skip to content

Commit 2af3f5e

Browse files
authored
feat(KNO-11394): initialize feed clients in compact mode by default (#851)
1 parent 61fccaa commit 2af3f5e

5 files changed

Lines changed: 168 additions & 5 deletions

File tree

.changeset/clever-friends-carry.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"@knocklabs/client": minor
3+
"@knocklabs/react-core": minor
4+
"@knocklabs/expo": minor
5+
"@knocklabs/react": minor
6+
"@knocklabs/react-native": minor
7+
---
8+
9+
Initialize feeds in `"compact"` mode by default
10+
11+
The feed client can now be initialized with a `mode` option, set to either `"compact"` or `"rich"`. When `mode` is `"compact"`, the following restrictions will apply when the feed is fetched:
12+
13+
- The `activities` and `total_activities` fields will _not_ be present on feed items
14+
- The `data` field will _not_ include nested arrays and objects
15+
- The `actors` field will only have up to one actor
16+
17+
**By default, feeds are initialized in `"compact"` mode. If you need to access `activities`, `total_activities`, the complete `data`, or the complete array of `actors`, you must initialize your feed in `"rich"` mode.**
18+
19+
If you are using the feed client via `@knocklabs/client` directly:
20+
21+
```js
22+
const knockFeed = knockClient.feeds.initialize(
23+
process.env.KNOCK_FEED_CHANNEL_ID,
24+
{ mode: "rich" },
25+
);
26+
```
27+
28+
If you are using `<KnockFeedProvider>` via `@knocklabs/react`, `@knocklabs/react-native`, or `@knocklabs/expo`:
29+
30+
```tsx
31+
<KnockFeedProvider
32+
feedId={process.env.KNOCK_FEED_CHANNEL_ID}
33+
defaultFeedOptions={{ mode: "rich" }}
34+
/>
35+
```
36+
37+
If you are using the `useNotifications` hook via `@knocklabs/react-core`:
38+
39+
```js
40+
const feedClient = useNotifications(
41+
knockClient,
42+
process.env.KNOCK_FEED_CHANNEL_ID,
43+
{ mode: "rich" },
44+
);
45+
```

packages/client/src/clients/feed/feed.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ import {
3636
import { getFormattedTriggerData, mergeDateRangeParams } from "./utils";
3737

3838
// Default options to apply
39-
const feedClientDefaults: Pick<FeedClientOptions, "archived"> = {
39+
const feedClientDefaults: Pick<FeedClientOptions, "archived" | "mode"> = {
4040
archived: "exclude",
41+
mode: "compact",
4142
};
4243

4344
const DEFAULT_DISCONNECT_DELAY = 2000;

packages/client/src/clients/feed/interfaces.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ export interface FeedClientOptions {
4848
// Optionally set whether to be inclusive of the start and end dates
4949
inclusive?: boolean;
5050
};
51+
/**
52+
* The mode to render the feed items in. When `mode` is `compact`:
53+
*
54+
* - The `activities` and `total_activities` fields will _not_ be present on feed items
55+
* - The `data` field will _not_ include nested arrays and objects
56+
* - The `actors` field will only have up to one actor
57+
*
58+
* @default "compact"
59+
*/
60+
mode?: "rich" | "compact";
5161
}
5262

5363
export type FetchFeedOptions = {
@@ -107,7 +117,11 @@ export type ContentBlock =
107117
export interface FeedItem<T = GenericData> {
108118
__cursor: string;
109119
id: string;
110-
activities: Activity<T>[];
120+
/**
121+
* List of activities associated with this feed item.
122+
* Only present in "rich" mode.
123+
*/
124+
activities?: Activity<T>[];
111125
actors: Recipient[];
112126
blocks: ContentBlock[];
113127
inserted_at: string;
@@ -118,7 +132,11 @@ export interface FeedItem<T = GenericData> {
118132
interacted_at: string | null;
119133
link_clicked_at: string | null;
120134
archived_at: string | null;
121-
total_activities: number;
135+
/**
136+
* Total number of activities related to this feed item.
137+
* Only present in "rich" mode.
138+
*/
139+
total_activities?: number;
122140
total_actors: number;
123141
data: T | null;
124142
source: NotificationSource;

packages/client/test/clients/feed/feed.test.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test, vi } from "vitest";
22

3+
import type { FetchFeedOptions } from "../../../src";
34
import ApiClient from "../../../src/api";
45
import Feed from "../../../src/clients/feed/feed";
56
import { FeedSocketManager } from "../../../src/clients/feed/socket-manager";
@@ -83,6 +84,7 @@ describe("Feed", () => {
8384
);
8485

8586
expect(feed.defaultOptions.archived).toBe("exclude");
87+
expect(feed.defaultOptions.mode).toBe("compact");
8688
} finally {
8789
cleanup();
8890
}
@@ -546,7 +548,7 @@ describe("Feed", () => {
546548
expect(mockApiClient.makeRequest).toHaveBeenCalledWith({
547549
method: "GET",
548550
url: "/v1/users/user_123/feeds/01234567-89ab-cdef-0123-456789abcdef",
549-
params: { archived: "exclude" },
551+
params: { archived: "exclude", mode: "compact" },
550552
});
551553
expect(result).toBeDefined();
552554
if (result && "entries" in result) {
@@ -579,10 +581,11 @@ describe("Feed", () => {
579581
undefined,
580582
);
581583

582-
const options = {
584+
const options: FetchFeedOptions = {
583585
page_size: 25,
584586
source: "workflow_123",
585587
tenant: "tenant_456",
588+
mode: "rich",
586589
};
587590

588591
await feed.fetch(options);
@@ -595,6 +598,7 @@ describe("Feed", () => {
595598
page_size: 25,
596599
source: "workflow_123",
597600
tenant: "tenant_456",
601+
mode: "rich",
598602
},
599603
});
600604
} finally {
@@ -650,6 +654,7 @@ describe("Feed", () => {
650654
url: "/v1/users/user_123/feeds/01234567-89ab-cdef-0123-456789abcdef",
651655
params: {
652656
archived: "exclude",
657+
mode: "compact",
653658
after: "cursor_123",
654659
},
655660
});
@@ -1582,4 +1587,94 @@ describe("Feed", () => {
15821587
}
15831588
});
15841589
});
1590+
1591+
describe("Feed Mode", () => {
1592+
test("sets mode query param to rich when initialized in rich mode", async () => {
1593+
const { knock, mockApiClient, cleanup } = getTestSetup();
1594+
1595+
try {
1596+
const mockFeedResponse = {
1597+
entries: [],
1598+
meta: { total_count: 0, unread_count: 0, unseen_count: 0 },
1599+
page_info: { before: null, after: null, page_size: 50 },
1600+
};
1601+
1602+
mockApiClient.makeRequest.mockResolvedValue({
1603+
statusCode: "ok",
1604+
body: mockFeedResponse,
1605+
});
1606+
1607+
const feed = new Feed(
1608+
knock,
1609+
"01234567-89ab-cdef-0123-456789abcdef",
1610+
{ mode: "rich" },
1611+
undefined,
1612+
);
1613+
1614+
await feed.fetch();
1615+
1616+
expect(mockApiClient.makeRequest).toHaveBeenCalledWith({
1617+
method: "GET",
1618+
url: "/v1/users/user_123/feeds/01234567-89ab-cdef-0123-456789abcdef",
1619+
params: {
1620+
archived: "exclude",
1621+
mode: "rich",
1622+
},
1623+
});
1624+
} finally {
1625+
cleanup();
1626+
}
1627+
});
1628+
1629+
test("handles lack of activities and total_activities in compact mode", async () => {
1630+
const { knock, mockApiClient, cleanup } = getTestSetup();
1631+
1632+
try {
1633+
// Create a compact mode feed item (no activities or total_activities)
1634+
const compactFeedItem = {
1635+
__cursor: "cursor_123",
1636+
id: "msg_123",
1637+
actors: [],
1638+
blocks: [],
1639+
archived_at: null,
1640+
inserted_at: new Date().toISOString(),
1641+
read_at: null,
1642+
seen_at: null,
1643+
clicked_at: null,
1644+
interacted_at: null,
1645+
link_clicked_at: null,
1646+
source: { key: "workflow", version_id: "v1", categories: [] },
1647+
tenant: null,
1648+
total_actors: 1,
1649+
updated_at: new Date().toISOString(),
1650+
data: { message: "Hello" },
1651+
};
1652+
1653+
const mockFeedResponse = {
1654+
entries: [compactFeedItem],
1655+
meta: { total_count: 1, unread_count: 1, unseen_count: 1 },
1656+
page_info: { before: null, after: null, page_size: 50 },
1657+
};
1658+
1659+
mockApiClient.makeRequest.mockResolvedValue({
1660+
statusCode: "ok",
1661+
body: mockFeedResponse,
1662+
});
1663+
1664+
const feed = new Feed(
1665+
knock,
1666+
"01234567-89ab-cdef-0123-456789abcdef",
1667+
{ mode: "compact" },
1668+
undefined,
1669+
);
1670+
1671+
const result = await feed.fetch();
1672+
1673+
expect(result).toBeDefined();
1674+
expect(result!.data).toEqual(mockFeedResponse);
1675+
} finally {
1676+
cleanup();
1677+
}
1678+
});
1679+
});
15851680
});

packages/react-core/test/feed/useNotifications.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("useNotifications", () => {
2020
archived: "include",
2121
page_size: 10,
2222
status: "all",
23+
mode: "rich",
2324
};
2425

2526
const { result } = renderHook(
@@ -127,6 +128,7 @@ describe("useNotifications", () => {
127128
archived: "include",
128129
page_size: 10,
129130
status: "all",
131+
mode: "rich",
130132
};
131133

132134
const { result, rerender } = renderHook(
@@ -162,12 +164,14 @@ describe("useNotifications", () => {
162164
archived: "include",
163165
page_size: 10,
164166
status: "all",
167+
mode: "compact",
165168
};
166169

167170
const options2: FeedClientOptions = {
168171
archived: "exclude",
169172
page_size: 10,
170173
status: "read",
174+
mode: "rich",
171175
};
172176

173177
const { result, rerender } = renderHook(

0 commit comments

Comments
 (0)