@knocklabs/client 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ import Knock from "../../knock";
2
+
3
+ import Feed from "./feed";
4
+ import { FeedClientOptions } from "./interfaces";
5
+
6
+ class FeedClient {
7
+ private instance: Knock;
8
+ private feedInstances: Feed[] = [];
9
+
10
+ constructor(instance: Knock) {
11
+ this.instance = instance;
12
+ }
13
+
14
+ initialize(feedChannelId: string, options: FeedClientOptions = {}) {
15
+ const feedInstance = new Feed(this.instance, feedChannelId, options);
16
+ this.feedInstances.push(feedInstance);
17
+
18
+ return feedInstance;
19
+ }
20
+
21
+ removeInstance(feed: Feed) {
22
+ this.feedInstances = this.feedInstances.filter((f) => f !== feed);
23
+ }
24
+
25
+ teardownInstances() {
26
+ for (const feed of this.feedInstances) {
27
+ feed.teardown();
28
+ }
29
+ }
30
+
31
+ reinitializeInstances() {
32
+ for (const feed of this.feedInstances) {
33
+ feed.reinitialize();
34
+ }
35
+ }
36
+ }
37
+
38
+ export { Feed };
39
+ export default FeedClient;
@@ -0,0 +1,105 @@
1
+ import { GenericData, PageInfo } from "@knocklabs/types";
2
+
3
+ import { Activity, Recipient } from "../../interfaces";
4
+ import { NetworkStatus } from "../../networkStatus";
5
+
6
+ // Specific feed interfaces
7
+
8
+ export interface FeedClientOptions {
9
+ before?: string;
10
+ after?: string;
11
+ page_size?: number;
12
+ status?: "unread" | "read" | "unseen" | "seen" | "all";
13
+ // Optionally scope all notifications to a particular source only
14
+ source?: string;
15
+ // Optionally scope all requests to a particular tenant
16
+ tenant?: string;
17
+ // Optionally scope to notifications with any tenancy or no tenancy
18
+ has_tenant?: boolean;
19
+ // Optionally scope to notifications with any of the categories provided
20
+ workflow_categories?: string[];
21
+ // Optionally scope to a given archived status (defaults to `exclude`)
22
+ archived?: "include" | "exclude" | "only";
23
+ // Optionally scope all notifications that contain this argument as part of their trigger payload
24
+ trigger_data?: GenericData;
25
+ // Optionally enable cross browser feed updates for this feed
26
+ __experimentalCrossBrowserUpdates?: boolean;
27
+ // Optionally automatically manage socket connections on changes to tab visibility (defaults to `false`)
28
+ auto_manage_socket_connection?: boolean;
29
+ // Optionally set the delay amount in milliseconds when automatically disconnecting sockets from inactive tabs (defaults to `2000`)
30
+ // Requires `auto_manage_socket_connection` to be `true`
31
+ auto_manage_socket_connection_delay?: number;
32
+ }
33
+
34
+ export type FetchFeedOptions = {
35
+ __loadingType?: NetworkStatus.loading | NetworkStatus.fetchMore;
36
+ __fetchSource?: "socket" | "http";
37
+ } & Omit<FeedClientOptions, "__experimentalCrossBrowserUpdates">;
38
+
39
+ export interface ContentBlockBase {
40
+ name: string;
41
+ type: "markdown" | "text" | "button_set";
42
+ }
43
+
44
+ export interface ActionButton {
45
+ name: string;
46
+ label: string;
47
+ action: string;
48
+ }
49
+
50
+ export interface ButtonSetContentBlock extends ContentBlockBase {
51
+ type: "button_set";
52
+ buttons: ActionButton[];
53
+ }
54
+
55
+ export interface TextContentBlock extends ContentBlockBase {
56
+ type: "text";
57
+ rendered: string;
58
+ content: string;
59
+ }
60
+
61
+ export interface MarkdownContentBlock extends ContentBlockBase {
62
+ type: "markdown";
63
+ rendered: string;
64
+ content: string;
65
+ }
66
+
67
+ export interface NotificationSource {
68
+ key: string;
69
+ version_id: string;
70
+ }
71
+
72
+ export type ContentBlock =
73
+ | MarkdownContentBlock
74
+ | TextContentBlock
75
+ | ButtonSetContentBlock;
76
+
77
+ export interface FeedItem<T = GenericData> {
78
+ __cursor: string;
79
+ id: string;
80
+ activities: Activity<T>[];
81
+ actors: Recipient[];
82
+ blocks: ContentBlock[];
83
+ inserted_at: string;
84
+ updated_at: string;
85
+ read_at: string | null;
86
+ seen_at: string | null;
87
+ archived_at: string | null;
88
+ total_activities: number;
89
+ total_actors: number;
90
+ data: T | null;
91
+ source: NotificationSource;
92
+ tenant: string | null;
93
+ }
94
+
95
+ export interface FeedMetadata {
96
+ total_count: number;
97
+ unread_count: number;
98
+ unseen_count: number;
99
+ }
100
+
101
+ export interface FeedResponse {
102
+ entries: FeedItem[];
103
+ meta: FeedMetadata;
104
+ page_info: PageInfo;
105
+ }
@@ -0,0 +1,93 @@
1
+ import create from "zustand/vanilla";
2
+
3
+ import { NetworkStatus } from "../../networkStatus";
4
+
5
+ import { FeedItem } from "./interfaces";
6
+ import { FeedStoreState } from "./types";
7
+ import { deduplicateItems, sortItems } from "./utils";
8
+
9
+ function processItems(items: FeedItem[]) {
10
+ const deduped = deduplicateItems(items);
11
+ const sorted = sortItems(deduped);
12
+
13
+ return sorted;
14
+ }
15
+
16
+ const defaultSetResultOptions = {
17
+ shouldSetPage: true,
18
+ shouldAppend: false,
19
+ };
20
+
21
+ const initialStoreState = {
22
+ items: [],
23
+ metadata: {
24
+ total_count: 0,
25
+ unread_count: 0,
26
+ unseen_count: 0,
27
+ },
28
+ pageInfo: {
29
+ before: null,
30
+ after: null,
31
+ page_size: 50,
32
+ },
33
+ };
34
+
35
+ export default function createStore() {
36
+ return create<FeedStoreState>((set) => ({
37
+ // Keeps track of all of the items loaded
38
+ ...initialStoreState,
39
+ // The network status indicates what's happening with the request
40
+ networkStatus: NetworkStatus.ready,
41
+ loading: false,
42
+
43
+ setNetworkStatus: (networkStatus: NetworkStatus) =>
44
+ set(() => ({
45
+ networkStatus,
46
+ loading: networkStatus === NetworkStatus.loading,
47
+ })),
48
+
49
+ setResult: (
50
+ { entries, meta, page_info },
51
+ options = defaultSetResultOptions,
52
+ ) =>
53
+ set((state) => {
54
+ // We resort the list on set, so concating everything is fine (if a bit suboptimal)
55
+ const items = options.shouldAppend
56
+ ? processItems(state.items.concat(entries))
57
+ : entries;
58
+
59
+ return {
60
+ items,
61
+ metadata: meta,
62
+ pageInfo: options.shouldSetPage ? page_info : state.pageInfo,
63
+ loading: false,
64
+ networkStatus: NetworkStatus.ready,
65
+ };
66
+ }),
67
+
68
+ setMetadata: (metadata) => set(() => ({ metadata })),
69
+
70
+ resetStore: (metadata = initialStoreState.metadata) =>
71
+ set(() => ({ ...initialStoreState, metadata })),
72
+
73
+ setItemAttrs: (itemIds, attrs) => {
74
+ // Create a map for the items to the updates to be made
75
+ const itemUpdatesMap: { [id: string]: object } = itemIds.reduce(
76
+ (acc, itemId) => ({ ...acc, [itemId]: attrs }),
77
+ {},
78
+ );
79
+
80
+ return set((state) => {
81
+ const items = state.items.map((item) => {
82
+ if (itemUpdatesMap[item.id]) {
83
+ return { ...item, ...itemUpdatesMap[item.id] };
84
+ }
85
+
86
+ return item;
87
+ });
88
+
89
+ return { items };
90
+ });
91
+ },
92
+ }));
93
+ }
@@ -0,0 +1,64 @@
1
+ import { PageInfo } from "@knocklabs/types";
2
+
3
+ import { NetworkStatus } from "../../networkStatus";
4
+
5
+ import { FeedItem, FeedMetadata, FeedResponse } from "./interfaces";
6
+
7
+ export type StoreFeedResultOptions = {
8
+ shouldSetPage?: boolean;
9
+ shouldAppend?: boolean;
10
+ };
11
+
12
+ export type FeedStoreState = {
13
+ items: FeedItem[];
14
+ pageInfo: PageInfo;
15
+ metadata: FeedMetadata;
16
+ loading: boolean;
17
+ networkStatus: NetworkStatus;
18
+ setResult: (response: FeedResponse, opts?: StoreFeedResultOptions) => void;
19
+ setMetadata: (metadata: FeedMetadata) => void;
20
+ setNetworkStatus: (networkStatus: NetworkStatus) => void;
21
+ setItemAttrs: (itemIds: string[], attrs: object) => void;
22
+ resetStore: (metadata?: FeedMetadata) => void;
23
+ };
24
+
25
+ export type FeedMessagesReceivedPayload = {
26
+ metadata: FeedMetadata;
27
+ };
28
+
29
+ /*
30
+ Event types:
31
+ - `messages.new`: legacy event fired for all messages (feed items) received, real-time or not
32
+ - `items.received.realtime`: all real-time items received via a socket update
33
+ - `items.received.page`: invoked every time a page is fetched (like on initial load)
34
+ */
35
+ export type FeedRealTimeEvent = "messages.new";
36
+
37
+ export type FeedEvent =
38
+ | FeedRealTimeEvent
39
+ | "items.received.page"
40
+ | "items.received.realtime"
41
+ | "items.archived"
42
+ | "items.unarchived"
43
+ | "items.seen"
44
+ | "items.unseen"
45
+ | "items.read"
46
+ | "items.unread"
47
+ | "items.all_archived"
48
+ | "items.all_read"
49
+ | "items.all_seen";
50
+
51
+ // Because we can bind to wild card feed events, this is here to accomodate whatever can be bound to
52
+ export type BindableFeedEvent = FeedEvent | "items.received.*" | "items.*";
53
+
54
+ export type FeedEventPayload = {
55
+ event: Omit<FeedEvent, "messages.new">;
56
+ items: FeedItem[];
57
+ metadata: FeedMetadata;
58
+ };
59
+
60
+ export type FeedRealTimeCallback = (resp: FeedResponse) => void;
61
+
62
+ export type FeedEventCallback = (payload: FeedEventPayload) => void;
63
+
64
+ export type FeedItemOrItems = FeedItem | FeedItem[];
@@ -0,0 +1,23 @@
1
+ import { FeedItem } from "./interfaces";
2
+
3
+ export function deduplicateItems(items: FeedItem[]): FeedItem[] {
4
+ const seen: Record<string, boolean> = {};
5
+ const values: FeedItem[] = [];
6
+
7
+ return items.reduce((acc, item) => {
8
+ if (seen[item.id]) {
9
+ return acc;
10
+ }
11
+
12
+ seen[item.id] = true;
13
+ return [...acc, item];
14
+ }, values);
15
+ }
16
+
17
+ export function sortItems(items: FeedItem[]) {
18
+ return items.sort((a, b) => {
19
+ return (
20
+ new Date(b.inserted_at).getTime() - new Date(a.inserted_at).getTime()
21
+ );
22
+ });
23
+ }
@@ -0,0 +1 @@
1
+ export const TENANT_OBJECT_COLLECTION = "$tenants";
@@ -0,0 +1,61 @@
1
+ import { ApiResponse } from "../../api";
2
+ import { ChannelData } from "../../interfaces";
3
+ import Knock from "../../knock";
4
+
5
+ type GetChannelDataInput = {
6
+ objectId: string;
7
+ collection: string;
8
+ channelId: string;
9
+ };
10
+
11
+ type SetChannelDataInput = {
12
+ objectId: string;
13
+ collection: string;
14
+ channelId: string;
15
+ data: any;
16
+ };
17
+
18
+ class ObjectClient {
19
+ private instance: Knock;
20
+
21
+ constructor(instance: Knock) {
22
+ this.instance = instance;
23
+ }
24
+ async getChannelData<T = any>({
25
+ collection,
26
+ objectId,
27
+ channelId,
28
+ }: GetChannelDataInput) {
29
+ const result = await this.instance.client().makeRequest({
30
+ method: "GET",
31
+ url: `/v1/objects/${collection}/${objectId}/channel_data/${channelId}`,
32
+ });
33
+
34
+ return this.handleResponse<ChannelData<T>>(result);
35
+ }
36
+
37
+ async setChannelData({
38
+ objectId,
39
+ collection,
40
+ channelId,
41
+ data,
42
+ }: SetChannelDataInput) {
43
+ const result = await this.instance.client().makeRequest({
44
+ method: "PUT",
45
+ url: `v1/objects/${collection}/${objectId}/channel_data/${channelId}`,
46
+ data: { data },
47
+ });
48
+
49
+ return this.handleResponse(result);
50
+ }
51
+
52
+ private handleResponse<T>(response: ApiResponse) {
53
+ if (response.statusCode === "error") {
54
+ throw new Error(response.error || response.body);
55
+ }
56
+
57
+ return response.body as T;
58
+ }
59
+ }
60
+
61
+ export default ObjectClient;
@@ -0,0 +1,196 @@
1
+ import { ChannelType } from "@knocklabs/types";
2
+ import { ApiResponse } from "../../api";
3
+ import Knock from "../../knock";
4
+ import {
5
+ ChannelTypePreferences,
6
+ PreferenceOptions,
7
+ SetPreferencesProperties,
8
+ WorkflowPreferenceSetting,
9
+ WorkflowPreferences,
10
+ PreferenceSet,
11
+ } from "./interfaces";
12
+
13
+ const DEFAULT_PREFERENCE_SET_ID = "default";
14
+
15
+ function buildUpdateParam(param: WorkflowPreferenceSetting) {
16
+ if (typeof param === "object") {
17
+ return param;
18
+ }
19
+
20
+ return { subscribed: param };
21
+ }
22
+
23
+ class Preferences {
24
+ private instance: Knock;
25
+
26
+ constructor(instance: Knock) {
27
+ this.instance = instance;
28
+ }
29
+
30
+ /**
31
+ * @deprecated Use `user.getAllPreferences()` instead
32
+ */
33
+ async getAll() {
34
+ const result = await this.instance.client().makeRequest({
35
+ method: "GET",
36
+ url: `/v1/users/${this.instance.userId}/preferences`,
37
+ });
38
+
39
+ return this.handleResponse(result);
40
+ }
41
+
42
+ /**
43
+ * @deprecated Use `user.getPreferences()` instead
44
+ */
45
+ async get(options: PreferenceOptions = {}) {
46
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
47
+
48
+ const result = await this.instance.client().makeRequest({
49
+ method: "GET",
50
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}`,
51
+ });
52
+
53
+ return this.handleResponse(result);
54
+ }
55
+
56
+ /**
57
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
58
+ */
59
+ async set(
60
+ preferenceSet: SetPreferencesProperties,
61
+ options: PreferenceOptions = {},
62
+ ) {
63
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
64
+
65
+ const result = await this.instance.client().makeRequest({
66
+ method: "PUT",
67
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}`,
68
+ data: preferenceSet,
69
+ });
70
+
71
+ return this.handleResponse(result);
72
+ }
73
+
74
+ /**
75
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
76
+ */
77
+ async setChannelTypes(
78
+ channelTypePreferences: ChannelTypePreferences,
79
+ options: PreferenceOptions = {},
80
+ ) {
81
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
82
+
83
+ const result = await this.instance.client().makeRequest({
84
+ method: "PUT",
85
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/channel_types`,
86
+ data: channelTypePreferences,
87
+ });
88
+
89
+ return this.handleResponse(result);
90
+ }
91
+
92
+ /**
93
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
94
+ */
95
+ async setChannelType(
96
+ channelType: ChannelType,
97
+ setting: boolean,
98
+ options: PreferenceOptions = {},
99
+ ) {
100
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
101
+
102
+ const result = await this.instance.client().makeRequest({
103
+ method: "PUT",
104
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/channel_types/${channelType}`,
105
+ data: { subscribed: setting },
106
+ });
107
+
108
+ return this.handleResponse(result);
109
+ }
110
+
111
+ /**
112
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
113
+ */
114
+ async setWorkflows(
115
+ workflowPreferences: WorkflowPreferences,
116
+ options: PreferenceOptions = {},
117
+ ) {
118
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
119
+
120
+ const result = await this.instance.client().makeRequest({
121
+ method: "PUT",
122
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/workflows`,
123
+ data: workflowPreferences,
124
+ });
125
+
126
+ return this.handleResponse(result);
127
+ }
128
+
129
+ /**
130
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
131
+ */
132
+ async setWorkflow(
133
+ workflowKey: string,
134
+ setting: WorkflowPreferenceSetting,
135
+ options: PreferenceOptions = {},
136
+ ) {
137
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
138
+ const params = buildUpdateParam(setting);
139
+
140
+ const result = await this.instance.client().makeRequest({
141
+ method: "PUT",
142
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/workflows/${workflowKey}`,
143
+ data: params,
144
+ });
145
+
146
+ return this.handleResponse(result);
147
+ }
148
+
149
+ /**
150
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
151
+ */
152
+ async setCategories(
153
+ categoryPreferences: WorkflowPreferences,
154
+ options: PreferenceOptions = {},
155
+ ) {
156
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
157
+
158
+ const result = await this.instance.client().makeRequest({
159
+ method: "PUT",
160
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/categories`,
161
+ data: categoryPreferences,
162
+ });
163
+
164
+ return this.handleResponse(result);
165
+ }
166
+
167
+ /**
168
+ * @deprecated Use `user.setPreferences(preferenceSet, options)` instead
169
+ */
170
+ async setCategory(
171
+ categoryKey: string,
172
+ setting: WorkflowPreferenceSetting,
173
+ options: PreferenceOptions = {},
174
+ ) {
175
+ const preferenceSetId = options.preferenceSet || DEFAULT_PREFERENCE_SET_ID;
176
+ const params = buildUpdateParam(setting);
177
+
178
+ const result = await this.instance.client().makeRequest({
179
+ method: "PUT",
180
+ url: `/v1/users/${this.instance.userId}/preferences/${preferenceSetId}/categories/${categoryKey}`,
181
+ data: params,
182
+ });
183
+
184
+ return this.handleResponse(result);
185
+ }
186
+
187
+ private handleResponse(response: ApiResponse) {
188
+ if (response.statusCode === "error") {
189
+ throw new Error(response.error || response.body);
190
+ }
191
+
192
+ return response.body as PreferenceSet;
193
+ }
194
+ }
195
+
196
+ export default Preferences;
@@ -0,0 +1,34 @@
1
+ import { ChannelType } from "@knocklabs/types";
2
+
3
+ export type ChannelTypePreferences = {
4
+ [K in ChannelType]?: boolean;
5
+ };
6
+
7
+ export type WorkflowPreferenceSetting =
8
+ | boolean
9
+ | { channel_types: ChannelTypePreferences };
10
+
11
+ export type WorkflowPreferences = Partial<
12
+ Record<string, WorkflowPreferenceSetting>
13
+ >;
14
+
15
+ export interface SetPreferencesProperties {
16
+ workflows: WorkflowPreferences;
17
+ categories: WorkflowPreferences;
18
+ channel_types: ChannelTypePreferences;
19
+ }
20
+
21
+ export interface PreferenceSet {
22
+ id: string;
23
+ categories: WorkflowPreferences;
24
+ workflows: WorkflowPreferences;
25
+ channel_types: ChannelTypePreferences;
26
+ }
27
+
28
+ export interface PreferenceOptions {
29
+ preferenceSet?: string;
30
+ }
31
+
32
+ export interface GetPreferencesOptions extends PreferenceOptions {
33
+ tenant?: string;
34
+ }