@stream-io/feeds-client 0.3.47 → 0.3.49

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 (49) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/react-bindings.js +1 -1
  5. package/dist/es/index.mjs +2 -2
  6. package/dist/es/index.mjs.map +1 -1
  7. package/dist/es/react-bindings.mjs +1 -1
  8. package/dist/{feeds-client-ykIZW9Hi.mjs → feeds-client-B9b7zUcW.mjs} +182 -196
  9. package/dist/feeds-client-B9b7zUcW.mjs.map +1 -0
  10. package/dist/{feeds-client-CxjZlEtX.js → feeds-client-BDvUG9yF.js} +182 -196
  11. package/dist/feeds-client-BDvUG9yF.js.map +1 -0
  12. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  13. package/dist/types/common/Poll.d.ts +15 -6
  14. package/dist/types/common/Poll.d.ts.map +1 -1
  15. package/dist/types/common/real-time/StableWSConnection.d.ts +3 -3
  16. package/dist/types/common/real-time/StableWSConnection.d.ts.map +1 -1
  17. package/dist/types/feed/event-handlers/activity-updater.d.ts +1 -1
  18. package/dist/types/feed/event-handlers/comment/handle-comment-added.d.ts.map +1 -1
  19. package/dist/types/feed/feed.d.ts +2 -2
  20. package/dist/types/feed/feed.d.ts.map +1 -1
  21. package/dist/types/feeds-client/feeds-client.d.ts +17 -3
  22. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  23. package/dist/types/gen/feeds/FeedApi.d.ts +2 -1
  24. package/dist/types/gen/feeds/FeedApi.d.ts.map +1 -1
  25. package/dist/types/gen/feeds/FeedsApi.d.ts +5 -1
  26. package/dist/types/gen/feeds/FeedsApi.d.ts.map +1 -1
  27. package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts.map +1 -1
  28. package/dist/types/gen/models/index.d.ts +209 -422
  29. package/dist/types/gen/models/index.d.ts.map +1 -1
  30. package/dist/types/gen/moderation/ModerationApi.d.ts.map +1 -1
  31. package/dist/types/types.d.ts +2 -2
  32. package/dist/types/types.d.ts.map +1 -1
  33. package/dist/types/utils/constants.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/src/common/Poll.ts +59 -40
  36. package/src/feed/event-handlers/comment/handle-comment-added.ts +14 -2
  37. package/src/feed/feed.ts +24 -9
  38. package/src/feeds-client/feeds-client.ts +70 -1
  39. package/src/gen/feeds/FeedApi.ts +12 -0
  40. package/src/gen/feeds/FeedsApi.ts +37 -0
  41. package/src/gen/model-decoders/decoders.ts +47 -251
  42. package/src/gen/model-decoders/event-decoder-mapping.ts +3 -2
  43. package/src/gen/models/index.ts +314 -723
  44. package/src/gen/moderation/ModerationApi.ts +1 -0
  45. package/src/test-utils/response-generators.ts +3 -2
  46. package/src/types.ts +2 -2
  47. package/src/utils/constants.ts +1 -1
  48. package/dist/feeds-client-CxjZlEtX.js.map +0 -1
  49. package/dist/feeds-client-ykIZW9Hi.mjs.map +0 -1
@@ -3,38 +3,21 @@ import type { FeedsClient } from '../feeds-client';
3
3
  import type {
4
4
  PollVoteResponseData,
5
5
  QueryPollVotesRequest,
6
- PollUpdatedFeedEvent,
7
- WSEvent,
8
- PollClosedFeedEvent,
9
- PollVoteCastedFeedEvent,
10
- PollVoteChangedFeedEvent,
11
- PollVoteRemovedFeedEvent,
12
6
  PollResponseData,
13
7
  } from '../gen/models';
14
8
 
15
- const isPollUpdatedEvent = (
16
- e: WSEvent,
17
- ): e is { type: 'feeds.poll.updated' } & PollUpdatedFeedEvent =>
18
- e.type === 'feeds.poll.updated';
19
- const isPollClosedEventEvent = (
20
- e: WSEvent,
21
- ): e is { type: 'feeds.poll.closed' } & PollClosedFeedEvent =>
22
- e.type === 'feeds.poll.closed';
23
- const isPollVoteCastedEvent = (
24
- e: WSEvent,
25
- ): e is { type: 'feeds.poll.vote_casted' } & PollVoteCastedFeedEvent =>
26
- e.type === 'feeds.poll.vote_casted';
27
- const isPollVoteChangedEvent = (
28
- e: WSEvent,
29
- ): e is { type: 'feeds.poll.vote_changed' } & PollVoteChangedFeedEvent =>
30
- e.type === 'feeds.poll.vote_changed';
31
- const isPollVoteRemovedEvent = (
32
- e: WSEvent,
33
- ): e is { type: 'feeds.poll.vote_removed' } & PollVoteRemovedFeedEvent =>
34
- e.type === 'feeds.poll.vote_removed';
35
-
36
- export const isVoteAnswer = (vote: PollVoteResponseData) =>
37
- !!vote?.answer_text;
9
+ export type PollUpdatePayload = {
10
+ poll: PollResponseData;
11
+ created_at: Date;
12
+ };
13
+
14
+ export type PollVotePayload = {
15
+ poll: PollResponseData;
16
+ poll_vote: PollVoteResponseData;
17
+ created_at: Date;
18
+ };
19
+
20
+ export const isVoteAnswer = (vote: PollVoteResponseData) => !!vote?.answer_text;
38
21
 
39
22
  export type PollAnswersQueryParams = QueryPollVotesRequest & {
40
23
  poll_id: string;
@@ -107,9 +90,16 @@ export class StreamPoll {
107
90
  return this.state.getLatestValue();
108
91
  }
109
92
 
110
- public handlePollUpdated = (event: PollUpdatedFeedEvent) => {
93
+ public handlePollUpdated = (event: PollUpdatePayload) => {
111
94
  if (event.poll?.id && event.poll.id !== this.id) return;
112
- if (!isPollUpdatedEvent(event as WSEvent)) return;
95
+ const currentState = this.data;
96
+ if (
97
+ currentState.updated_at &&
98
+ event.poll.updated_at &&
99
+ event.poll.updated_at <= currentState.updated_at
100
+ ) {
101
+ return;
102
+ }
113
103
  const { id, ...pollData } = event.poll;
114
104
  this.state.partialNext({
115
105
  ...pollData,
@@ -117,22 +107,32 @@ export class StreamPoll {
117
107
  });
118
108
  };
119
109
 
120
- public handlePollClosed = (event: PollClosedFeedEvent) => {
110
+ public handlePollClosed = (event: PollUpdatePayload) => {
121
111
  if (event.poll?.id && event.poll.id !== this.id) return;
122
- if (!isPollClosedEventEvent(event as WSEvent)) return;
112
+ const currentState = this.data;
113
+ if (currentState.is_closed) return;
123
114
  this.state.partialNext({
124
115
  is_closed: true,
125
116
  last_activity_at: new Date(event.created_at),
126
117
  });
127
118
  };
128
119
 
129
- public handleVoteCasted = (event: PollVoteCastedFeedEvent) => {
120
+ public handleVoteCasted = (event: PollVotePayload) => {
130
121
  if (event.poll?.id && event.poll.id !== this.id) return;
131
- if (!isPollVoteCastedEvent(event as WSEvent)) return;
132
122
  const currentState = this.data;
133
123
  const isOwnVote =
134
124
  event.poll_vote.user_id ===
135
125
  this.client.state.getLatestValue().connected_user?.id;
126
+
127
+ if (isOwnVote) {
128
+ const alreadyApplied = isVoteAnswer(event.poll_vote)
129
+ ? currentState.own_answer?.id === event.poll_vote.id
130
+ : !!event.poll_vote.option_id &&
131
+ currentState.own_votes_by_option_id[event.poll_vote.option_id]?.id ===
132
+ event.poll_vote.id;
133
+ if (alreadyApplied) return;
134
+ }
135
+
136
136
  let latestAnswers = [...currentState.latest_answers];
137
137
  let ownAnswer = currentState.own_answer;
138
138
  let ownVotesByOptionId = currentState.own_votes_by_option_id;
@@ -150,7 +150,10 @@ export class StreamPoll {
150
150
  }
151
151
 
152
152
  if (isVoteAnswer(event.poll_vote)) {
153
- latestAnswers = [event.poll_vote, ...latestAnswers];
153
+ latestAnswers = [
154
+ event.poll_vote,
155
+ ...latestAnswers.filter((a) => a.id !== event.poll_vote.id),
156
+ ];
154
157
  } else {
155
158
  maxVotedOptionIds = getMaxVotedOptionIds(
156
159
  event.poll.vote_counts_by_option,
@@ -176,14 +179,23 @@ export class StreamPoll {
176
179
  });
177
180
  };
178
181
 
179
- public handleVoteChanged = (event: PollVoteChangedFeedEvent) => {
182
+ public handleVoteChanged = (event: PollVotePayload) => {
180
183
  // this event is triggered only when event.poll.enforce_unique_vote === true
181
184
  if (event.poll?.id && event.poll.id !== this.id) return;
182
- if (!isPollVoteChangedEvent(event as WSEvent)) return;
183
185
  const currentState = this.data;
184
186
  const isOwnVote =
185
187
  event.poll_vote.user_id ===
186
188
  this.client.state.getLatestValue().connected_user?.id;
189
+
190
+ if (isOwnVote) {
191
+ const alreadyApplied = isVoteAnswer(event.poll_vote)
192
+ ? currentState.own_answer?.id === event.poll_vote.id
193
+ : !!event.poll_vote.option_id &&
194
+ currentState.own_votes_by_option_id[event.poll_vote.option_id]?.id ===
195
+ event.poll_vote.id;
196
+ if (alreadyApplied) return;
197
+ }
198
+
187
199
  let latestAnswers = [...currentState.latest_answers];
188
200
  let ownAnswer = currentState.own_answer;
189
201
  let ownVotesByOptionId = currentState.own_votes_by_option_id;
@@ -249,13 +261,20 @@ export class StreamPoll {
249
261
  });
250
262
  };
251
263
 
252
- public handleVoteRemoved = (event: PollVoteRemovedFeedEvent) => {
264
+ public handleVoteRemoved = (event: PollVotePayload) => {
253
265
  if (event.poll?.id && event.poll.id !== this.id) return;
254
- if (!isPollVoteRemovedEvent(event as WSEvent)) return;
255
266
  const currentState = this.data;
256
267
  const isOwnVote =
257
268
  event.poll_vote.user_id ===
258
269
  this.client.state.getLatestValue().connected_user?.id;
270
+
271
+ if (isOwnVote) {
272
+ const alreadyApplied = isVoteAnswer(event.poll_vote)
273
+ ? !currentState.own_answer
274
+ : !!event.poll_vote.option_id &&
275
+ !(event.poll_vote.option_id in currentState.own_votes_by_option_id);
276
+ if (alreadyApplied) return;
277
+ }
259
278
  let latestAnswers = [...currentState.latest_answers];
260
279
  let ownAnswer = currentState.own_answer;
261
280
  const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
@@ -36,11 +36,23 @@ export function handleCommentAdded(
36
36
  }
37
37
 
38
38
  const newComments = entityState?.comments ? [...entityState.comments] : [];
39
+ const sort = entityState.pagination?.sort;
40
+ const hasMorePages = entityState.pagination?.next !== undefined;
41
+ const isFromCurrentUser = eventTriggeredByConnectedUser.call(this, payload);
39
42
 
40
- if (entityState.pagination?.sort === 'last') {
43
+ if (sort === 'last') {
41
44
  newComments.unshift(comment);
45
+ } else if (sort === 'first') {
46
+ if (isFromCurrentUser) {
47
+ newComments.push(comment);
48
+ } else {
49
+ if (!hasMorePages) {
50
+ newComments.push(comment);
51
+ } else {
52
+ return currentState;
53
+ }
54
+ }
42
55
  } else {
43
- // 'first' and other sort options
44
56
  newComments.push(comment);
45
57
  }
46
58
 
package/src/feed/feed.ts CHANGED
@@ -216,11 +216,11 @@ export class Feed extends FeedApi {
216
216
  'app.updated': Feed.noop,
217
217
  'user.banned': Feed.noop,
218
218
  'user.deactivated': Feed.noop,
219
- 'user.muted': Feed.noop,
220
219
  'user.reactivated': Feed.noop,
221
220
  'user.updated': Feed.noop,
222
221
  'feeds.activity.feedback': handleActivityFeedback.bind(this),
223
222
  'feeds.activity.restored': Feed.noop,
223
+ 'user.unbanned': Feed.noop,
224
224
  };
225
225
 
226
226
  protected eventDispatcher: EventDispatcher<WSEvent['type'], WSEvent> =
@@ -295,7 +295,9 @@ export class Feed extends FeedApi {
295
295
  const { last_get_or_create_request_config } = this.state.getLatestValue();
296
296
  if (last_get_or_create_request_config?.watch) {
297
297
  this.inProgressGetOrCreate = undefined;
298
- await withRetry(() => this.getOrCreate(last_get_or_create_request_config));
298
+ await withRetry(() =>
299
+ this.getOrCreate(last_get_or_create_request_config),
300
+ );
299
301
  }
300
302
  }
301
303
 
@@ -509,6 +511,25 @@ export class Feed extends FeedApi {
509
511
  );
510
512
 
511
513
  const existingComments = newCommentsByEntityId[entityId]?.comments;
514
+ let comments: CommentResponse[] = existingComments
515
+ ? uniqueArrayMerge(
516
+ existingComments,
517
+ newComments,
518
+ (comment) => comment.id,
519
+ )
520
+ : newComments;
521
+ // Only sort when merging pages (existing + new); initial load order comes from API
522
+ if (
523
+ data.sort === 'first' &&
524
+ existingComments &&
525
+ existingComments.length > 0
526
+ ) {
527
+ comments = [...comments].sort(
528
+ (a, b) =>
529
+ new Date(a.created_at).getTime() -
530
+ new Date(b.created_at).getTime(),
531
+ );
532
+ }
512
533
 
513
534
  newCommentsByEntityId[entityId] = {
514
535
  ...newCommentsByEntityId[entityId],
@@ -518,13 +539,7 @@ export class Feed extends FeedApi {
518
539
  next: item.next,
519
540
  sort: data.sort,
520
541
  },
521
- comments: existingComments
522
- ? uniqueArrayMerge(
523
- existingComments,
524
- newComments,
525
- (comment) => comment.id,
526
- )
527
- : newComments,
542
+ comments,
528
543
  };
529
544
  }
530
545
 
@@ -7,6 +7,7 @@ import type {
7
7
  AddCommentRequest,
8
8
  AddCommentResponse,
9
9
  AddReactionRequest,
10
+ CastPollVoteRequest,
10
11
  CreateGuestResponse,
11
12
  DeleteActivityReactionResponse,
12
13
  DeleteCommentReactionResponse,
@@ -22,6 +23,7 @@ import type {
22
23
  ImageUploadRequest,
23
24
  OwnBatchRequest,
24
25
  PollResponse,
26
+ PollVoteResponse,
25
27
  PollVotesResponse,
26
28
  QueryFeedsRequest,
27
29
  QueryPollVotesRequest,
@@ -31,6 +33,8 @@ import type {
31
33
  UpdateCommentRequest,
32
34
  UpdateCommentResponse,
33
35
  UpdateFollowRequest,
36
+ UpdatePollPartialRequest,
37
+ UpdatePollRequest,
34
38
  UserRequest,
35
39
  WSEvent,
36
40
  } from '../gen/models';
@@ -450,15 +454,80 @@ export class FeedsClient extends FeedsApi {
450
454
  return streamDevToken(userId);
451
455
  };
452
456
 
457
+ updatePoll = async (request: UpdatePollRequest) => {
458
+ const poll = this.pollFromState(request.id);
459
+ const response = await super.updatePoll(request);
460
+ if (response.poll && poll) {
461
+ poll.handlePollUpdated({ poll: response.poll, created_at: new Date() });
462
+ }
463
+ return response;
464
+ };
465
+
466
+ updatePollPartial = async (
467
+ request: UpdatePollPartialRequest & { poll_id: string },
468
+ ) => {
469
+ const poll = this.pollFromState(request.poll_id);
470
+ const response = await super.updatePollPartial(request);
471
+ if (response.poll && poll) {
472
+ poll.handlePollUpdated({ poll: response.poll, created_at: new Date() });
473
+ }
474
+ return response;
475
+ };
476
+
453
477
  closePoll = async (request: {
454
478
  poll_id: string;
455
479
  }): Promise<StreamResponse<PollResponse>> => {
456
- return await this.updatePollPartial({
480
+ const poll = this.pollFromState(request.poll_id);
481
+ const response = await super.updatePollPartial({
457
482
  poll_id: request.poll_id,
458
483
  set: {
459
484
  is_closed: true,
460
485
  },
461
486
  });
487
+ if (response.poll && poll) {
488
+ poll.handlePollClosed({ poll: response.poll, created_at: new Date() });
489
+ }
490
+ return response;
491
+ };
492
+
493
+ castPollVote = async (
494
+ request: CastPollVoteRequest & { activity_id: string; poll_id: string },
495
+ ): Promise<StreamResponse<PollVoteResponse>> => {
496
+ const poll = this.pollFromState(request.poll_id);
497
+ const response = await super.castPollVote(request);
498
+ if (response.poll && response.vote && poll) {
499
+ const payload = {
500
+ poll: response.poll,
501
+ poll_vote: response.vote,
502
+ created_at: new Date(),
503
+ };
504
+ if (
505
+ poll.data.enforce_unique_vote &&
506
+ Object.keys(poll.data.own_votes_by_option_id).length > 0
507
+ ) {
508
+ poll.handleVoteChanged(payload);
509
+ } else {
510
+ poll.handleVoteCasted(payload);
511
+ }
512
+ }
513
+ return response;
514
+ };
515
+
516
+ deletePollVote = async (request: {
517
+ activity_id: string;
518
+ poll_id: string;
519
+ vote_id: string;
520
+ user_id?: string;
521
+ }): Promise<StreamResponse<PollVoteResponse>> => {
522
+ const response = await super.deletePollVote(request);
523
+ if (response.poll && response.vote) {
524
+ this.pollFromState(request.poll_id)?.handleVoteRemoved({
525
+ poll: response.poll,
526
+ poll_vote: response.vote,
527
+ created_at: new Date(),
528
+ });
529
+ }
530
+ return response;
462
531
  };
463
532
 
464
533
  uploadFile = (
@@ -10,6 +10,8 @@ import type {
10
10
  PinActivityResponse,
11
11
  QueryFeedMembersRequest,
12
12
  QueryFeedMembersResponse,
13
+ QueryPinnedActivitiesRequest,
14
+ QueryPinnedActivitiesResponse,
13
15
  RejectFeedMemberInviteRequest,
14
16
  RejectFeedMemberInviteResponse,
15
17
  Response,
@@ -127,6 +129,16 @@ export class FeedApi {
127
129
  });
128
130
  }
129
131
 
132
+ queryPinnedActivities(
133
+ request?: QueryPinnedActivitiesRequest,
134
+ ): Promise<StreamResponse<QueryPinnedActivitiesResponse>> {
135
+ return this.feedsApi.queryPinnedActivities({
136
+ feed_id: this.id,
137
+ feed_group_id: this.group,
138
+ ...request,
139
+ });
140
+ }
141
+
130
142
  stopWatching(request?: {
131
143
  connection_id?: string;
132
144
  }): Promise<StreamResponse<Response>> {
@@ -88,6 +88,8 @@ import type {
88
88
  QueryFeedsResponse,
89
89
  QueryFollowsRequest,
90
90
  QueryFollowsResponse,
91
+ QueryPinnedActivitiesRequest,
92
+ QueryPinnedActivitiesResponse,
91
93
  QueryPollVotesRequest,
92
94
  QueryPollsRequest,
93
95
  QueryPollsResponse,
@@ -773,6 +775,7 @@ export class FeedsApi {
773
775
  mentioned_user_ids: request?.mentioned_user_ids,
774
776
  custom: request?.custom,
775
777
  location: request?.location,
778
+ search_data: request?.search_data,
776
779
  };
777
780
 
778
781
  const response = await this.apiClient.sendRequest<
@@ -1611,6 +1614,40 @@ export class FeedsApi {
1611
1614
  return { ...response.body, metadata: response.metadata };
1612
1615
  }
1613
1616
 
1617
+ async queryPinnedActivities(
1618
+ request: QueryPinnedActivitiesRequest & {
1619
+ feed_group_id: string;
1620
+ feed_id: string;
1621
+ },
1622
+ ): Promise<StreamResponse<QueryPinnedActivitiesResponse>> {
1623
+ const pathParams = {
1624
+ feed_group_id: request?.feed_group_id,
1625
+ feed_id: request?.feed_id,
1626
+ };
1627
+ const body = {
1628
+ limit: request?.limit,
1629
+ next: request?.next,
1630
+ prev: request?.prev,
1631
+ sort: request?.sort,
1632
+ filter: request?.filter,
1633
+ };
1634
+
1635
+ const response = await this.apiClient.sendRequest<
1636
+ StreamResponse<QueryPinnedActivitiesResponse>
1637
+ >(
1638
+ 'POST',
1639
+ '/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}/pinned_activities/query',
1640
+ pathParams,
1641
+ undefined,
1642
+ body,
1643
+ 'application/json',
1644
+ );
1645
+
1646
+ decoders.QueryPinnedActivitiesResponse?.(response.body);
1647
+
1648
+ return { ...response.body, metadata: response.metadata };
1649
+ }
1650
+
1614
1651
  async stopWatchingFeed(request: {
1615
1652
  feed_group_id: string;
1616
1653
  feed_id: string;