@amityco/ts-sdk 0.0.1-e32b438.0 → 0.0.1-e556efe.0

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.
Files changed (130) hide show
  1. package/.eslintrc.json +4 -1
  2. package/dist/@types/core/events.d.ts +7 -7
  3. package/dist/@types/core/events.d.ts.map +1 -1
  4. package/dist/@types/core/live.d.ts +23 -0
  5. package/dist/@types/core/live.d.ts.map +1 -0
  6. package/dist/@types/core/paging.d.ts +1 -1
  7. package/dist/@types/core/paging.d.ts.map +1 -1
  8. package/dist/@types/core/payload.d.ts +3 -5
  9. package/dist/@types/core/payload.d.ts.map +1 -1
  10. package/dist/@types/domains/comment.d.ts +14 -0
  11. package/dist/@types/domains/comment.d.ts.map +1 -1
  12. package/dist/@types/domains/follow.d.ts +2 -1
  13. package/dist/@types/domains/follow.d.ts.map +1 -1
  14. package/dist/@types/domains/post.d.ts +17 -0
  15. package/dist/@types/domains/post.d.ts.map +1 -1
  16. package/dist/@types/domains/reaction.d.ts +10 -0
  17. package/dist/@types/domains/reaction.d.ts.map +1 -1
  18. package/dist/@types/index.d.ts +1 -0
  19. package/dist/@types/index.d.ts.map +1 -1
  20. package/dist/channel/api/banChannelMembers.d.ts +16 -0
  21. package/dist/channel/api/banChannelMembers.d.ts.map +1 -0
  22. package/dist/channel/api/index.d.ts +2 -0
  23. package/dist/channel/api/index.d.ts.map +1 -1
  24. package/dist/channel/api/unbanChannelMembers.d.ts +16 -0
  25. package/dist/channel/api/unbanChannelMembers.d.ts.map +1 -0
  26. package/dist/comment/api/queryComments.d.ts +1 -9
  27. package/dist/comment/api/queryComments.d.ts.map +1 -1
  28. package/dist/comment/observers/index.d.ts +1 -0
  29. package/dist/comment/observers/index.d.ts.map +1 -1
  30. package/dist/comment/observers/liveComments.d.ts +22 -0
  31. package/dist/comment/observers/liveComments.d.ts.map +1 -0
  32. package/dist/comment/observers/observeComments.d.ts.map +1 -1
  33. package/dist/core/query/query.d.ts.map +1 -1
  34. package/dist/core/tests/query/filtering.test.d.ts +2 -0
  35. package/dist/core/tests/query/filtering.test.d.ts.map +1 -0
  36. package/dist/core/tests/query/query.test.d.ts +2 -0
  37. package/dist/core/tests/query/query.test.d.ts.map +1 -0
  38. package/dist/follow/api/follow.d.ts.map +1 -1
  39. package/dist/follow/api/getFollowInfo.d.ts.map +1 -1
  40. package/dist/follow/api/queryFollowers.d.ts +4 -4
  41. package/dist/follow/api/queryFollowers.d.ts.map +1 -1
  42. package/dist/follow/api/queryFollowings.d.ts +4 -4
  43. package/dist/follow/api/queryFollowings.d.ts.map +1 -1
  44. package/dist/follow/api/utils.d.ts +4 -4
  45. package/dist/follow/api/utils.d.ts.map +1 -1
  46. package/dist/follow/events/utils.d.ts.map +1 -1
  47. package/dist/index.cjs.js +561 -23
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.esm.js +554 -24
  51. package/dist/index.umd.js +4 -4
  52. package/dist/post/api/queryPosts.d.ts +1 -12
  53. package/dist/post/api/queryPosts.d.ts.map +1 -1
  54. package/dist/post/observers/index.d.ts +1 -0
  55. package/dist/post/observers/index.d.ts.map +1 -1
  56. package/dist/post/observers/livePosts.d.ts +22 -0
  57. package/dist/post/observers/livePosts.d.ts.map +1 -0
  58. package/dist/post/observers/observePosts.d.ts.map +1 -1
  59. package/dist/post/tests/api/getPost.test.d.ts +2 -0
  60. package/dist/post/tests/api/getPost.test.d.ts.map +1 -0
  61. package/dist/post/tests/api/queryPosts.test.d.ts +2 -0
  62. package/dist/post/tests/api/queryPosts.test.d.ts.map +1 -0
  63. package/dist/reaction/api/queryReactions.d.ts +1 -6
  64. package/dist/reaction/api/queryReactions.d.ts.map +1 -1
  65. package/dist/reaction/events/index.d.ts +3 -0
  66. package/dist/reaction/events/index.d.ts.map +1 -1
  67. package/dist/reaction/events/onReactorAdded.d.ts +19 -0
  68. package/dist/reaction/events/onReactorAdded.d.ts.map +1 -0
  69. package/dist/reaction/events/onReactorRemoved.d.ts +19 -0
  70. package/dist/reaction/events/onReactorRemoved.d.ts.map +1 -0
  71. package/dist/reaction/observers/index.d.ts +2 -0
  72. package/dist/reaction/observers/index.d.ts.map +1 -0
  73. package/dist/reaction/observers/liveReactions.d.ts +22 -0
  74. package/dist/reaction/observers/liveReactions.d.ts.map +1 -0
  75. package/dist/utils/constants.d.ts +3 -0
  76. package/dist/utils/constants.d.ts.map +1 -0
  77. package/dist/utils/tests/client.d.ts +3 -0
  78. package/dist/utils/tests/client.d.ts.map +1 -0
  79. package/dist/utils/tests/dummy.d.ts +8 -0
  80. package/dist/utils/tests/dummy.d.ts.map +1 -0
  81. package/dist/utils/tests/index.d.ts +3 -0
  82. package/dist/utils/tests/index.d.ts.map +1 -0
  83. package/jest.config.ts +15 -0
  84. package/package.json +10 -3
  85. package/src/@types/core/events.ts +7 -7
  86. package/src/@types/core/live.ts +28 -0
  87. package/src/@types/core/paging.ts +1 -1
  88. package/src/@types/core/payload.ts +5 -6
  89. package/src/@types/domains/comment.ts +31 -0
  90. package/src/@types/domains/follow.ts +3 -1
  91. package/src/@types/domains/post.ts +37 -0
  92. package/src/@types/domains/reaction.ts +17 -0
  93. package/src/@types/index.ts +1 -0
  94. package/src/channel/api/banChannelMembers.ts +41 -0
  95. package/src/channel/api/index.ts +3 -0
  96. package/src/channel/api/unbanChannelMembers.ts +41 -0
  97. package/src/comment/api/queryComments.ts +3 -9
  98. package/src/comment/observers/index.ts +1 -0
  99. package/src/comment/observers/liveComments.ts +172 -0
  100. package/src/comment/observers/observeComments.ts +1 -11
  101. package/src/core/query/query.ts +15 -2
  102. package/src/core/tests/query/filtering.test.ts +11 -0
  103. package/src/core/tests/query/query.test.ts +19 -0
  104. package/src/follow/api/acceptFollower.ts +1 -1
  105. package/src/follow/api/declineFollower.ts +1 -1
  106. package/src/follow/api/follow.ts +1 -4
  107. package/src/follow/api/getFollowInfo.ts +8 -5
  108. package/src/follow/api/queryFollowers.ts +5 -4
  109. package/src/follow/api/queryFollowings.ts +5 -4
  110. package/src/follow/api/unfollow.ts +1 -1
  111. package/src/follow/api/utils.ts +10 -10
  112. package/src/follow/events/utils.ts +9 -6
  113. package/src/index.ts +1 -0
  114. package/src/post/api/queryPosts.ts +3 -12
  115. package/src/post/observers/index.ts +1 -0
  116. package/src/post/observers/livePosts.ts +170 -0
  117. package/src/post/observers/observePosts.ts +1 -13
  118. package/src/post/tests/api/getPost.test.ts +88 -0
  119. package/src/post/tests/api/queryPosts.test.ts +23 -0
  120. package/src/reaction/api/queryReactions.ts +14 -14
  121. package/src/reaction/events/index.ts +4 -0
  122. package/src/reaction/events/onReactorAdded.ts +80 -0
  123. package/src/reaction/events/onReactorRemoved.ts +85 -0
  124. package/src/reaction/observers/index.ts +1 -0
  125. package/src/reaction/observers/liveReactions.ts +142 -0
  126. package/src/utils/constants.ts +2 -0
  127. package/src/utils/tests/client.ts +5 -0
  128. package/src/utils/tests/dummy.ts +7 -0
  129. package/src/utils/tests/index.ts +2 -0
  130. package/tsconfig.json +23 -22
@@ -0,0 +1,170 @@
1
+ /* eslint-disable no-use-before-define */
2
+ import { getResolver } from '~/core/model';
3
+ import { getActiveClient } from '~/client/api';
4
+ import { pushToCache, pullFromCache } from '~/cache/api';
5
+ import {
6
+ createQuery,
7
+ runQuery,
8
+ queryOptions,
9
+ filterByPropEquality,
10
+ sortByFirstCreated,
11
+ sortByLastCreated,
12
+ } from '~/core/query';
13
+
14
+ import {
15
+ COLLECTION_DEFAULT_CACHING_POLICY,
16
+ COLLECTION_DEFAULT_PAGINATION_LIMIT,
17
+ } from '~/utils/constants';
18
+
19
+ import {
20
+ onPostCreated,
21
+ onPostUpdated,
22
+ onPostDeleted,
23
+ onPostApproved,
24
+ onPostDeclined,
25
+ onPostFlagged,
26
+ onPostUnflagged,
27
+ onPostReactionAdded,
28
+ onPostReactionRemoved,
29
+ } from '../events';
30
+ import { queryPosts } from '../api';
31
+
32
+ /**
33
+ * ```js
34
+ * import { livePosts } from '@amityco/ts-sdk'
35
+ *
36
+ * let posts = []
37
+ * const unsub = livePosts({
38
+ * targetType: Amity.PostTargetType,
39
+ * targetId: Amity.Post['targetId'],
40
+ * }, response => merge(posts, response.data))
41
+ * ```
42
+ *
43
+ * Observe all mutations on a list of {@link Amity.Post} for a given target object
44
+ *
45
+ * @param targetType the type of the target
46
+ * @param targetId the ID of the target
47
+ * @param callback the function to call when new data are available
48
+ * @returns An {@link Amity.Unsubscriber} function to run when willing to stop observing the messages
49
+ *
50
+ * @category Posts Live Collection
51
+ */
52
+ export const livePosts = (
53
+ params: Amity.PostLiveCollection,
54
+ callback: Amity.LiveCollectionCallback<Amity.Post>,
55
+ config?: Amity.LiveCollectionConfig,
56
+ ): Amity.Unsubscriber => {
57
+ const { log, cache } = getActiveClient();
58
+
59
+ if (!cache) {
60
+ console.log('For using Live Collection feature you need to enable Cache!');
61
+ }
62
+
63
+ const timestamp = Date.now();
64
+ log(`livePosts(tmpid: ${timestamp}) > listen`);
65
+
66
+ const { limit: queryLimit, ...queryParams } = params;
67
+
68
+ const limit = queryLimit ?? COLLECTION_DEFAULT_PAGINATION_LIMIT;
69
+ const { policy = COLLECTION_DEFAULT_CACHING_POLICY } = config ?? {};
70
+
71
+ const disposers: Amity.Unsubscriber[] = [];
72
+ const cacheKey = [
73
+ 'post',
74
+ 'collection',
75
+ { targetId: params.targetId, targetType: params.targetType },
76
+ ];
77
+
78
+ const responder = (data: Amity.PostLiveCollectionCache) => {
79
+ let posts: Amity.Post[] =
80
+ data.data
81
+ .map(postId => pullFromCache<Amity.Post>(['post', 'get', postId])!)
82
+ .filter(Boolean)
83
+ .map(({ data }) => data) ?? [];
84
+
85
+ posts = filterByPropEquality(posts, 'isDeleted', params.isDeleted);
86
+
87
+ const sortBy = params.sortBy ? params.sortBy : 'lastCreated';
88
+
89
+ posts = posts.sort(sortBy === 'lastCreated' ? sortByLastCreated : sortByFirstCreated);
90
+
91
+ callback({
92
+ onNextPage: onFetch,
93
+ data: posts,
94
+ hasNextPage: !!data.params?.page,
95
+ loading: data.loading,
96
+ error: data.error,
97
+ });
98
+ };
99
+
100
+ const realtimeRouter = (post: Amity.Post, action: Amity.PostActionType) => {
101
+ const collection = pullFromCache<Amity.PostLiveCollectionCache>(cacheKey)?.data;
102
+
103
+ if (params.targetId !== post.targetId || params.targetType !== post.targetType || !collection)
104
+ return;
105
+
106
+ if (action === 'onCreate') {
107
+ collection.data = [...new Set([post.postId, ...collection.data])];
108
+ } else if (['onDelete', 'onDeclined'].includes(action)) {
109
+ collection.data = collection.data.filter(p => p !== post.postId);
110
+ }
111
+
112
+ pushToCache(cacheKey, collection);
113
+
114
+ responder(collection);
115
+ };
116
+
117
+ const onFetch = () => {
118
+ const collection = pullFromCache<Amity.PostLiveCollectionCache>(cacheKey)?.data;
119
+
120
+ const posts = collection?.data ?? [];
121
+
122
+ if (posts.length > 0 && !collection?.params?.page) return;
123
+
124
+ const query = createQuery(queryPosts, {
125
+ ...queryParams,
126
+ page: collection?.params?.page ?? { limit },
127
+ });
128
+
129
+ runQuery(
130
+ query,
131
+ ({ data: result, error, loading, nextPage: page }) => {
132
+ const data = {
133
+ loading,
134
+ error,
135
+ params: { page },
136
+ data: posts,
137
+ };
138
+
139
+ if (result) {
140
+ data.data = [...new Set([...posts, ...result.map(getResolver('post'))])];
141
+ }
142
+
143
+ pushToCache(cacheKey, data);
144
+
145
+ responder(data);
146
+ },
147
+ queryOptions(policy),
148
+ );
149
+ };
150
+
151
+ disposers.push(
152
+ onPostCreated(post => realtimeRouter(post, 'onCreate')),
153
+ onPostApproved(post => realtimeRouter(post, 'onApproved')),
154
+ onPostDeclined(post => realtimeRouter(post, 'onDeclined')),
155
+ onPostDeleted(post => realtimeRouter(post, 'onDelete')),
156
+
157
+ onPostUpdated(post => realtimeRouter(post, 'onUpdate')),
158
+ onPostFlagged(post => realtimeRouter(post, 'onFlagged')),
159
+ onPostUnflagged(post => realtimeRouter(post, 'onUnflagged')),
160
+ onPostReactionAdded(post => realtimeRouter(post, 'onReactionAdded')),
161
+ onPostReactionRemoved(post => realtimeRouter(post, 'onReactionRemoved')),
162
+ );
163
+
164
+ onFetch();
165
+
166
+ return () => {
167
+ log(`livePosts(tmpid: ${timestamp}) > dispose`);
168
+ disposers.forEach(fn => fn());
169
+ };
170
+ };
@@ -59,19 +59,7 @@ export const observePosts = (
59
59
 
60
60
  const disposers: Amity.Unsubscriber[] = [];
61
61
 
62
- const router = (
63
- post: Amity.Post,
64
- action:
65
- | 'onCreate'
66
- | 'onUpdate'
67
- | 'onDelete'
68
- | 'onApproved'
69
- | 'onDeclined'
70
- | 'onFlagged'
71
- | 'onUnflagged'
72
- | 'onReactionAdded'
73
- | 'onReactionRemoved',
74
- ) => {
62
+ const router = (post: Amity.Post, action: Exclude<Amity.PostActionType, 'onFetch'>) => {
75
63
  if (params.targetId !== post.targetId || params.targetType !== post.targetType) return;
76
64
 
77
65
  if (callback instanceof Function) return callback(post);
@@ -0,0 +1,88 @@
1
+ import { ASCApiError } from '~/core/errors';
2
+ import { client, userPosts } from '~/utils/tests';
3
+ import { pushToCache, enableCache, disableCache } from '~/cache/api';
4
+
5
+ import { getPost } from '~/post/api';
6
+
7
+ describe('post/api', () => {
8
+ const post = { postId: userPosts.page1[0] };
9
+ const post2 = { postId: userPosts.page1[1] };
10
+
11
+ describe('getPost.locally', () => {
12
+ test('should return undefined if cache is turned off', () => {
13
+ expect(getPost.locally(post.postId)).toBeUndefined();
14
+ });
15
+
16
+ test('should return undefined if post not in cache', () => {
17
+ enableCache();
18
+
19
+ const cachedAt = Date.now();
20
+
21
+ pushToCache(['post', 'get', post.postId], post, { cachedAt });
22
+
23
+ expect(getPost.locally(post2.postId)).toBeUndefined();
24
+
25
+ disableCache();
26
+ });
27
+
28
+ test('should return post if post in cache', () => {
29
+ enableCache();
30
+
31
+ const cachedAt = Date.now();
32
+
33
+ pushToCache(['post', 'get', post.postId], post, { cachedAt });
34
+
35
+ const data = getPost.locally(post.postId);
36
+
37
+ expect(data).toBeDefined();
38
+ expect(data?.data).toEqual(post);
39
+ expect(data?.cachedAt).toEqual(cachedAt);
40
+
41
+ disableCache();
42
+ });
43
+ });
44
+
45
+ describe('getPost', () => {
46
+ test('should return a post', async () => {
47
+ client.http.get = jest.fn().mockReturnValueOnce({ data: { posts: [post] } });
48
+
49
+ const data = await getPost(post.postId);
50
+
51
+ expect(data).toBeDefined();
52
+ expect(data?.data).toEqual(post);
53
+ });
54
+
55
+ test('should put post into the cache after fetch', async () => {
56
+ enableCache();
57
+ client.http.get = jest.fn().mockReturnValueOnce({ data: { posts: [post] } });
58
+
59
+ await getPost(post.postId);
60
+
61
+ expect(getPost.locally(post.postId)?.data).toEqual(post);
62
+
63
+ disableCache();
64
+ });
65
+
66
+ test('should return an error', async () => {
67
+ client.http.get = jest.fn().mockRejectedValueOnce(new Error('error'));
68
+
69
+ await expect(getPost(post.postId)).rejects.toThrow('error');
70
+ });
71
+
72
+ test('should return an error if post in cache but api throws not found', async () => {
73
+ enableCache();
74
+ pushToCache(['post', 'get', post.postId], post, { cachedAt: Date.now() });
75
+
76
+ client.http.get = jest
77
+ .fn()
78
+ .mockRejectedValueOnce(
79
+ new ASCApiError('not found!', Amity.ServerError.ITEM_NOT_FOUND, Amity.ErrorLevel.ERROR),
80
+ );
81
+
82
+ await expect(getPost(post.postId)).rejects.toThrow();
83
+
84
+ expect(getPost.locally(post.postId)).toBeUndefined();
85
+ disableCache();
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,23 @@
1
+ import { queryPosts } from '~/post/api';
2
+ import { client, userPosts } from '~/utils/tests';
3
+
4
+ describe('post/api', () => {
5
+ test('should return a post query', async () => {
6
+ const returnValue = [{ postId: userPosts.page1[0] }];
7
+
8
+ client.http.get = jest.fn().mockResolvedValue({
9
+ data: {
10
+ posts: returnValue,
11
+ paging: {},
12
+ },
13
+ });
14
+
15
+ const data = await queryPosts({
16
+ targetId: userPosts.targetId,
17
+ targetType: userPosts.targetType,
18
+ });
19
+
20
+ expect(data).toBeDefined();
21
+ expect(data?.data).toEqual(returnValue);
22
+ });
23
+ });
@@ -1,6 +1,7 @@
1
1
  import { getActiveClient } from '~/client/api/activeClient';
2
2
 
3
- import { toToken, toPage } from '~/core/query';
3
+ import { toToken, toPageRaw } from '~/core/query';
4
+ import { ingestInCache } from '~/cache/api/ingestInCache';
4
5
 
5
6
  /**
6
7
  * ```js
@@ -19,34 +20,33 @@ import { toToken, toPage } from '~/core/query';
19
20
  * @reaction Reaction API
20
21
  * @async
21
22
  * */
22
- export const queryReactions = async (query: {
23
- referenceId: Amity.Reaction['referenceId'];
24
- referenceType: Amity.Reaction['referenceType'];
25
- reactionName?: Amity.Reaction['reactionName'];
26
- page?: Amity.Page;
27
- }): Promise<Amity.Paged<Amity.ReactionQuery, Amity.Page>> => {
23
+ export const queryReactions = async (
24
+ query: Amity.QueryReactions,
25
+ ): Promise<Amity.Paged<Amity.ReactionQuery, Amity.Page<string>>> => {
28
26
  const client = getActiveClient();
29
27
  client.log('reaction/queryReactions', query);
30
28
 
31
29
  const { page = { limit: 10 }, ...params } = query ?? {};
32
30
 
33
- const { data } = await client.http.get<{ results: Amity.ReactionPayload } & Amity.Pagination>(
34
- `/api/v2/reactions`,
31
+ const { data } = await client.http.get<Amity.ReactionPayload & Amity.Pagination>(
32
+ `/api/v3/reactions`,
35
33
  {
36
34
  params: {
37
35
  ...params,
38
36
  options: {
39
- token: toToken(page, 'skiplimit'),
37
+ token: toToken(page, 'afterbeforeraw'),
40
38
  },
41
39
  },
42
40
  },
43
41
  );
44
42
 
45
- const { paging, results } = data;
46
- const { reactions } = results;
43
+ const { paging, ...payload } = data;
44
+ const { reactions } = payload;
47
45
 
48
- const nextPage = toPage(paging.next);
49
- const prevPage = toPage(paging.previous);
46
+ ingestInCache({ ...payload, reactions: reactions[0].reactors });
47
+
48
+ const nextPage = toPageRaw(paging.next);
49
+ const prevPage = toPageRaw(paging.previous);
50
50
 
51
51
  return { data: reactions, prevPage, nextPage };
52
52
  };
@@ -1 +1,5 @@
1
+ export * from './onReactionAdded';
1
2
  export * from './onReactionRemoved';
3
+
4
+ export * from './onReactorAdded';
5
+ export * from './onReactorRemoved';
@@ -0,0 +1,80 @@
1
+ import { getActiveClient } from '~/client/api';
2
+ import { ingestInCache } from '~/cache/api/ingestInCache';
3
+ import { createEventSubscriber } from '~/core/events';
4
+
5
+ /**
6
+ * ```js
7
+ * import { onReactorAdded } from '@amityco/ts-sdk'
8
+ * const dispose = onReactorAdded('post', postId, reactor => {
9
+ * // ...
10
+ * })
11
+ * ```
12
+ *
13
+ * Fired when an {@link Amity.Reactor} has been added
14
+ *
15
+ * @param {@link Amity.ReactableType} referenceType
16
+ * @param {string} referenceId
17
+ * @param callback The function to call when the event was fired
18
+ * @returns an {@link Amity.Unsubscriber} function to stop listening
19
+ *
20
+ * @category Events
21
+ * */
22
+ export const onReactorAdded = (
23
+ referenceType: Amity.ReactableType,
24
+ referenceId: Amity.Reaction['referenceId'],
25
+ callback: Amity.Listener<Amity.Reactor>,
26
+ ): Amity.Unsubscriber => {
27
+ const client = getActiveClient();
28
+
29
+ const callbackWrapper = (
30
+ referenceType_: Amity.ReactableType,
31
+ referenceId_: Amity.Reaction['referenceId'],
32
+ reaction: Amity.Reactor,
33
+ ) => {
34
+ if (referenceType_ === referenceType && referenceId_ === referenceId) {
35
+ callback(reaction);
36
+ }
37
+ };
38
+
39
+ if (referenceType === 'message') {
40
+ const filter = (payload: Amity.MessagePayload) => {
41
+ if (!payload.messages[0].latestReaction) return;
42
+
43
+ const { eventName, referenceId, referenceType, userDisplayName, ...reactor } =
44
+ payload.messages[0].latestReaction;
45
+
46
+ if (eventName !== 'add') return;
47
+
48
+ ingestInCache(payload);
49
+ ingestInCache({ reactions: [reactor as Amity.Reactor] });
50
+
51
+ callbackWrapper('message', payload.messages[0].messageId, reactor);
52
+ };
53
+
54
+ return createEventSubscriber(client, 'reaction/onReactorAdded', 'v3.message.didUpdate', filter);
55
+ }
56
+
57
+ if (referenceType === 'post') {
58
+ const filter = (payload: Amity.PostPayload & { reactor: Amity.Reactor }) => {
59
+ const { reactor, ...rest } = payload;
60
+
61
+ ingestInCache(rest as Amity.PostPayload);
62
+ ingestInCache({ reactions: [reactor] });
63
+
64
+ callbackWrapper('post', payload.posts[0].postId, reactor);
65
+ };
66
+
67
+ return createEventSubscriber(client, 'post.addReaction', 'post.addReaction', filter);
68
+ }
69
+
70
+ const filter = (payload: Amity.CommentPayload & { reactor: Amity.Reactor }) => {
71
+ const { reactor, ...rest } = payload;
72
+
73
+ ingestInCache(rest as Amity.CommentPayload);
74
+ ingestInCache({ reactions: [reactor] });
75
+
76
+ callbackWrapper('comment', payload.comments[0].commentId, reactor);
77
+ };
78
+
79
+ return createEventSubscriber(client, 'comment.addReaction', 'comment.addReaction', filter);
80
+ };
@@ -0,0 +1,85 @@
1
+ import { getActiveClient } from '~/client/api';
2
+ import { ingestInCache } from '~/cache/api/ingestInCache';
3
+ import { createEventSubscriber } from '~/core/events';
4
+
5
+ /**
6
+ * ```js
7
+ * import { onReactorRemoved } from '@amityco/ts-sdk'
8
+ * const dispose = onReactorRemoved('post', postId, reactor => {
9
+ * // ...
10
+ * })
11
+ * ```
12
+ *
13
+ * Fired when an {@link Amity.Reactor} has been removed
14
+ *
15
+ * @param {@link Amity.ReactableType} referenceType
16
+ * @param {string} referenceId
17
+ * @param callback The function to call when the event was fired
18
+ * @returns an {@link Amity.Unsubscriber} function to stop listening
19
+ *
20
+ * @category Events
21
+ * */
22
+ export const onReactorRemoved = (
23
+ referenceType: Amity.ReactableType,
24
+ referenceId: Amity.Reaction['referenceId'],
25
+ callback: Amity.Listener<Amity.Reactor>,
26
+ ): Amity.Unsubscriber => {
27
+ const client = getActiveClient();
28
+
29
+ const callbackWrapper = (
30
+ referenceType_: Amity.ReactableType,
31
+ referenceId_: Amity.Reaction['referenceId'],
32
+ reaction: Amity.Reactor,
33
+ ) => {
34
+ if (referenceType_ === referenceType && referenceId_ === referenceId) {
35
+ callback(reaction);
36
+ }
37
+ };
38
+
39
+ if (referenceType === 'message') {
40
+ const filter = (payload: Amity.MessagePayload) => {
41
+ if (!payload.messages[0].latestReaction) return;
42
+
43
+ const { eventName, referenceId, referenceType, ...reactor } =
44
+ payload.messages[0].latestReaction;
45
+
46
+ if (eventName !== 'remove') return;
47
+
48
+ ingestInCache(payload);
49
+ ingestInCache({ reactions: [reactor as Amity.Reactor] });
50
+
51
+ callbackWrapper('message', payload.messages[0].messageId, reactor);
52
+ };
53
+
54
+ return createEventSubscriber(
55
+ client,
56
+ 'reaction/onReactorRemoved',
57
+ 'v3.message.didUpdate',
58
+ filter,
59
+ );
60
+ }
61
+
62
+ if (referenceType === 'post') {
63
+ const filter = (payload: Amity.PostPayload & { reactor: Amity.Reactor }) => {
64
+ const { reactor, ...rest } = payload;
65
+
66
+ ingestInCache(rest as Amity.PostPayload);
67
+ ingestInCache({ reactions: [reactor] });
68
+
69
+ callbackWrapper('post', payload.posts[0].postId, reactor);
70
+ };
71
+
72
+ return createEventSubscriber(client, 'post.removeReaction', 'post.removeReaction', filter);
73
+ }
74
+
75
+ const filter = (payload: Amity.CommentPayload & { reactor: Amity.Reactor }) => {
76
+ const { reactor, ...rest } = payload;
77
+
78
+ ingestInCache(rest as Amity.CommentPayload);
79
+ ingestInCache({ reactions: [reactor] });
80
+
81
+ callbackWrapper('comment', payload.comments[0].commentId, reactor);
82
+ };
83
+
84
+ return createEventSubscriber(client, 'comment.removeReaction', 'comment.removeReaction', filter);
85
+ };
@@ -0,0 +1 @@
1
+ export * from './liveReactions';
@@ -0,0 +1,142 @@
1
+ /* eslint-disable no-use-before-define */
2
+ import { getResolver } from '~/core/model';
3
+ import { getActiveClient } from '~/client/api';
4
+ import { pushToCache, pullFromCache } from '~/cache/api';
5
+ import { createQuery, runQuery, queryOptions } from '~/core/query';
6
+
7
+ import {
8
+ COLLECTION_DEFAULT_CACHING_POLICY,
9
+ COLLECTION_DEFAULT_PAGINATION_LIMIT,
10
+ } from '~/utils/constants';
11
+
12
+ import { onReactorAdded, onReactorRemoved } from '../events';
13
+ import { queryReactions } from '../api';
14
+
15
+ /**
16
+ * ```js
17
+ * import { liveReactions } from '@amityco/ts-sdk'
18
+ *
19
+ * let reactions = []
20
+ * const unsub = liveReactions({
21
+ * referenceId: Amity.Reaction['referenceId'],
22
+ * referenceType: Amity.Reaction['referenceType'],
23
+ * }, response => merge(reactions, response.data))
24
+ * ```
25
+ *
26
+ * Observe all mutations on a list of {@link Amity.Reaction} for a given target object
27
+ *
28
+ * @param referenceType the type of the target
29
+ * @param referenceId the ID of the target
30
+ * @param callback the function to call when new data are available
31
+ * @returns An {@link Amity.Unsubscriber} function to run when willing to stop observing the messages
32
+ *
33
+ * @category Reactions Live Collection
34
+ */
35
+ export const liveReactions = (
36
+ params: Amity.ReactionLiveCollection,
37
+ callback: Amity.LiveCollectionCallback<Amity.Reactor>,
38
+ config?: Amity.LiveCollectionConfig,
39
+ ): Amity.Unsubscriber => {
40
+ const { log, cache } = getActiveClient();
41
+
42
+ if (!cache) {
43
+ console.log('For using Live Collection feature you need to enable Cache!');
44
+ }
45
+
46
+ const timestamp = Date.now();
47
+ log(`liveReactions(tmpid: ${timestamp}) > listen`);
48
+
49
+ const { limit: queryLimit, ...queryParams } = params;
50
+
51
+ const limit = queryLimit ?? COLLECTION_DEFAULT_PAGINATION_LIMIT;
52
+ const { policy = COLLECTION_DEFAULT_CACHING_POLICY } = config ?? {};
53
+
54
+ const disposers: Amity.Unsubscriber[] = [];
55
+ const cacheKey = [
56
+ 'reaction',
57
+ 'collection',
58
+ { referenceId: params.referenceId, referenceType: params.referenceType },
59
+ ];
60
+
61
+ const responder = (data: Amity.ReactionLiveCollectionCache) => {
62
+ const reactions: Amity.Reactor[] =
63
+ data.data
64
+ .map(reactorId => pullFromCache<Amity.Reactor>(['reaction', 'get', reactorId])!)
65
+ .filter(Boolean)
66
+ .map(({ data }) => data) ?? [];
67
+
68
+ callback({
69
+ onNextPage: onFetch,
70
+ data: reactions,
71
+ hasNextPage: !!data.params?.page,
72
+ loading: data.loading,
73
+ error: data.error,
74
+ });
75
+ };
76
+
77
+ const realtimeRouter = (reaction: Amity.Reactor, action: Amity.ReactionActionType) => {
78
+ const collection = pullFromCache<Amity.ReactionLiveCollectionCache>(cacheKey)?.data;
79
+ if (!collection) return;
80
+
81
+ if (action === 'onAdded') {
82
+ collection.data = [...new Set([reaction.reactionId, ...collection.data])];
83
+ } else if (action === 'onRemoved') {
84
+ collection.data = collection.data.filter(p => p !== reaction.reactionId);
85
+ }
86
+
87
+ pushToCache(cacheKey, collection);
88
+ responder(collection);
89
+ };
90
+
91
+ const onFetch = () => {
92
+ const collection = pullFromCache<Amity.ReactionLiveCollectionCache>(cacheKey)?.data;
93
+
94
+ const reactions = collection?.data ?? [];
95
+
96
+ if (reactions.length > 0 && !collection?.params?.page) return;
97
+
98
+ const query = createQuery(queryReactions, {
99
+ ...queryParams,
100
+ page: collection?.params?.page ?? { limit },
101
+ });
102
+
103
+ runQuery(
104
+ query,
105
+ ({ data: result, error, loading, nextPage: page }) => {
106
+ const data = {
107
+ loading,
108
+ error,
109
+ params: { page },
110
+ data: reactions,
111
+ };
112
+
113
+ if (result) {
114
+ data.data = [
115
+ ...new Set([...reactions, ...result[0].reactors.map(getResolver('reaction'))]),
116
+ ];
117
+ }
118
+
119
+ pushToCache(cacheKey, data);
120
+
121
+ responder(data);
122
+ },
123
+ queryOptions(policy),
124
+ );
125
+ };
126
+
127
+ disposers.push(
128
+ onReactorAdded(queryParams.referenceType, queryParams.referenceId, reaction =>
129
+ realtimeRouter(reaction, 'onAdded'),
130
+ ),
131
+ onReactorRemoved(queryParams.referenceType, queryParams.referenceId, reaction =>
132
+ realtimeRouter(reaction, 'onRemoved'),
133
+ ),
134
+ );
135
+
136
+ onFetch();
137
+
138
+ return () => {
139
+ log(`liveReactions(tmpid: ${timestamp}) > dispose`);
140
+ disposers.forEach(fn => fn());
141
+ };
142
+ };
@@ -0,0 +1,2 @@
1
+ export const COLLECTION_DEFAULT_PAGINATION_LIMIT = 5;
2
+ export const COLLECTION_DEFAULT_CACHING_POLICY: Amity.QueryPolicy = 'cache_then_server';
@@ -0,0 +1,5 @@
1
+ import { createClient } from '~/client/api';
2
+
3
+ const client = createClient('key', 'sg');
4
+
5
+ export default client;
@@ -0,0 +1,7 @@
1
+ export const userPosts = {
2
+ targetId: 'userId',
3
+ targetType: 'user' as Amity.PostTargetType,
4
+ page1: ['postId11', 'postId12', 'postId13'],
5
+ page2: ['postId21', 'postId22', 'postId23'],
6
+ page3: ['postId31', 'postId32', 'postId33'],
7
+ };