stream-chat 9.0.1 → 9.1.1

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.
@@ -474,6 +474,17 @@ export declare class StreamChat {
474
474
  * @return {Promise<MessageFlagsResponse>} Message Flags Response
475
475
  */
476
476
  queryMessageFlags(filterConditions?: MessageFlagsFilters, options?: MessageFlagsPaginationOptions): Promise<MessageFlagsResponse>;
477
+ /**
478
+ * queryChannelsRequest - Queries channels and returns the raw response
479
+ *
480
+ * @param {ChannelFilters} filterConditions object MongoDB style filters
481
+ * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
482
+ * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
483
+ * @param {ChannelOptions} [options] Options object
484
+ *
485
+ * @return {Promise<Array<ChannelAPIResponse>>} search channels response
486
+ */
487
+ queryChannelsRequest(filterConditions: ChannelFilters, sort?: ChannelSort, options?: ChannelOptions): Promise<Omit<ChannelAPIResponse, "duration">[]>;
477
488
  /**
478
489
  * queryChannels - Query channels
479
490
  *
@@ -483,8 +494,9 @@ export declare class StreamChat {
483
494
  * @param {ChannelOptions} [options] Options object
484
495
  * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request.
485
496
  * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list.
497
+ * - stateOptions.skipHydration - Skips returning the channels as instances of the Channel class and rather returns the raw query response.
486
498
  *
487
- * @return {Promise<{ channels: Array<ChannelAPIResponse>}> } search channels response
499
+ * @return {Promise<Array<Channel>>} search channels response
488
500
  */
489
501
  queryChannels(filterConditions: ChannelFilters, sort?: ChannelSort, options?: ChannelOptions, stateOptions?: ChannelStateOptions): Promise<Channel[]>;
490
502
  /**
@@ -0,0 +1,3 @@
1
+ import type { MessageCompositionMiddleware } from './types';
2
+ import type { MessageComposer } from '../../messageComposer';
3
+ export declare const createPollOnlyCompositionMiddleware: (composer: MessageComposer) => MessageCompositionMiddleware;
@@ -43,6 +43,7 @@ export type PollComposerStateChangeMiddlewareValue = {
43
43
  targetFields: Partial<{
44
44
  [K in keyof PollComposerState['data']]: K extends 'options' ? PollComposerOptionUpdate : PollComposerState['data'][K];
45
45
  }>;
46
+ injectedFieldErrors?: PollComposerFieldErrors;
46
47
  };
47
48
  export type PollComposerStateMiddlewareValue = MiddlewareExecutionResult<PollComposerStateChangeMiddlewareValue>;
48
49
  export {};
@@ -1,5 +1,5 @@
1
- import type { ExecuteParams, MiddlewareExecutionResult, MiddlewareHandler } from '../../../middleware';
2
1
  import { MiddlewareExecutor } from '../../../middleware';
2
+ import type { ExecuteParams, MiddlewareExecutionResult, MiddlewareHandler } from '../../../middleware';
3
3
  import type { Suggestion, TextComposerMiddlewareExecutorOptions, TextComposerState } from './types';
4
4
  export type TextComposerMiddlewareExecutorState<T extends Suggestion = Suggestion> = TextComposerState<T> & {
5
5
  change?: {
@@ -2,7 +2,7 @@ import { PollComposerCompositionMiddlewareExecutor, PollComposerStateMiddlewareE
2
2
  import { StateStore } from '../store';
3
3
  import { VotingVisibility } from '../types';
4
4
  import type { MessageComposer } from './messageComposer';
5
- import type { PollComposerState, UpdateFieldsData } from './middleware/pollComposer';
5
+ import type { PollComposerFieldErrors, PollComposerState, UpdateFieldsData } from './middleware/pollComposer';
6
6
  export type PollComposerOptions = {
7
7
  composer: MessageComposer;
8
8
  };
@@ -25,7 +25,12 @@ export declare class PollComposer {
25
25
  get voting_visibility(): VotingVisibility | undefined;
26
26
  get canCreatePoll(): boolean;
27
27
  initState: () => void;
28
- updateFields: (data: UpdateFieldsData) => Promise<void>;
28
+ /**
29
+ * Updates specified fields and generates relevant errors
30
+ * @param data
31
+ * @param injectedFieldErrors - errors produced externally that will take precedence over the errors generated in the middleware chaing
32
+ */
33
+ updateFields: (data: UpdateFieldsData, injectedFieldErrors?: PollComposerFieldErrors) => Promise<void>;
29
34
  handleFieldBlur: (field: keyof PollComposerState["data"]) => Promise<void>;
30
35
  compose: () => Promise<import("..").PollComposerCompositionMiddlewareValueState | undefined>;
31
36
  }
@@ -821,6 +821,7 @@ export type ChannelQueryOptions = {
821
821
  export type ChannelStateOptions = {
822
822
  offlineMode?: boolean;
823
823
  skipInitialization?: string[];
824
+ skipHydration?: boolean;
824
825
  };
825
826
  export type CreateChannelOptions = {
826
827
  automod?: ChannelConfigAutomod;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.0.1",
3
+ "version": "9.1.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/client.ts CHANGED
@@ -1735,22 +1735,19 @@ export class StreamChat {
1735
1735
  }
1736
1736
 
1737
1737
  /**
1738
- * queryChannels - Query channels
1738
+ * queryChannelsRequest - Queries channels and returns the raw response
1739
1739
  *
1740
1740
  * @param {ChannelFilters} filterConditions object MongoDB style filters
1741
1741
  * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
1742
1742
  * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
1743
1743
  * @param {ChannelOptions} [options] Options object
1744
- * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request.
1745
- * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list.
1746
1744
  *
1747
- * @return {Promise<{ channels: Array<ChannelAPIResponse>}> } search channels response
1745
+ * @return {Promise<Array<ChannelAPIResponse>>} search channels response
1748
1746
  */
1749
- async queryChannels(
1747
+ async queryChannelsRequest(
1750
1748
  filterConditions: ChannelFilters,
1751
1749
  sort: ChannelSort = [],
1752
1750
  options: ChannelOptions = {},
1753
- stateOptions: ChannelStateOptions = {},
1754
1751
  ) {
1755
1752
  const defaultOptions: ChannelOptions = {
1756
1753
  state: true,
@@ -1777,15 +1774,39 @@ export class StreamChat {
1777
1774
  payload,
1778
1775
  );
1779
1776
 
1777
+ return data.channels;
1778
+ }
1779
+
1780
+ /**
1781
+ * queryChannels - Query channels
1782
+ *
1783
+ * @param {ChannelFilters} filterConditions object MongoDB style filters
1784
+ * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
1785
+ * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
1786
+ * @param {ChannelOptions} [options] Options object
1787
+ * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request.
1788
+ * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list.
1789
+ * - stateOptions.skipHydration - Skips returning the channels as instances of the Channel class and rather returns the raw query response.
1790
+ *
1791
+ * @return {Promise<Array<Channel>>} search channels response
1792
+ */
1793
+ async queryChannels(
1794
+ filterConditions: ChannelFilters,
1795
+ sort: ChannelSort = [],
1796
+ options: ChannelOptions = {},
1797
+ stateOptions: ChannelStateOptions = {},
1798
+ ) {
1799
+ const channels = await this.queryChannelsRequest(filterConditions, sort, options);
1800
+
1780
1801
  this.dispatchEvent({
1781
1802
  type: 'channels.queried',
1782
1803
  queriedChannels: {
1783
- channels: data.channels,
1804
+ channels,
1784
1805
  isLatestMessageSet: true,
1785
1806
  },
1786
1807
  });
1787
1808
 
1788
- return this.hydrateActiveChannels(data.channels, stateOptions, options);
1809
+ return this.hydrateActiveChannels(channels, stateOptions, options);
1789
1810
  }
1790
1811
 
1791
1812
  /**
@@ -88,16 +88,18 @@ const initState = (
88
88
  const quotedMessage = composition.quoted_message;
89
89
  let message;
90
90
  let draftId = null;
91
+ let id = MessageComposer.generateId(); // do not use draft id for messsage id
91
92
  if (compositionIsDraftResponse(composition)) {
92
93
  message = composition.message;
93
94
  draftId = composition.message.id;
94
95
  } else {
95
96
  message = composition;
97
+ id = composition.id;
96
98
  }
97
99
 
98
100
  return {
99
101
  draftId,
100
- id: message.id,
102
+ id,
101
103
  quotedMessage: quotedMessage
102
104
  ? formatMessage(quotedMessage as MessageResponseBase)
103
105
  : null,
@@ -318,6 +320,7 @@ export class MessageComposer {
318
320
  this.attachmentManager.initState({ message });
319
321
  this.linkPreviewsManager.initState({ message });
320
322
  this.textComposer.initState({ message });
323
+ this.pollComposer.initState();
321
324
  this.customDataManager.initState({ message });
322
325
  this.state.next(initState(composition));
323
326
  if (
@@ -543,11 +546,6 @@ export class MessageComposer {
543
546
  };
544
547
 
545
548
  clear = () => {
546
- this.attachmentManager.initState();
547
- this.linkPreviewsManager.initState();
548
- this.textComposer.initState();
549
- this.pollComposer.initState();
550
- this.customDataManager.initState();
551
549
  this.initState();
552
550
  };
553
551
 
@@ -630,6 +628,7 @@ export class MessageComposer {
630
628
  try {
631
629
  const { poll } = await this.client.createPoll(composition.data);
632
630
  this.state.partialNext({ pollId: poll.id });
631
+ this.pollComposer.initState();
633
632
  } catch (error) {
634
633
  this.client.notifications.add({
635
634
  message: 'Failed to create the poll',
@@ -30,6 +30,7 @@ import {
30
30
  createCustomDataCompositionMiddleware,
31
31
  createDraftCustomDataCompositionMiddleware,
32
32
  } from './customData';
33
+ import { createPollOnlyCompositionMiddleware } from './pollOnly';
33
34
 
34
35
  export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
35
36
  MessageComposerMiddlewareState,
@@ -40,6 +41,7 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
40
41
  // todo: document how to add custom data to a composed message using middleware
41
42
  // or adding custom composer components (apart from AttachmentsManager, TextComposer etc.)
42
43
  this.use([
44
+ createPollOnlyCompositionMiddleware(composer),
43
45
  createTextComposerCompositionMiddleware(composer),
44
46
  createAttachmentsCompositionMiddleware(composer),
45
47
  createLinkPreviewsCompositionMiddleware(composer),
@@ -0,0 +1,49 @@
1
+ import type {
2
+ MessageComposerMiddlewareState,
3
+ MessageCompositionMiddleware,
4
+ } from './types';
5
+ import type { MessageComposer } from '../../messageComposer';
6
+ import type { MiddlewareHandlerParams } from '../../../middleware';
7
+ import type { LocalMessage } from '../../../types';
8
+
9
+ const pollLocalMessageNullifiedFields: Pick<
10
+ LocalMessage,
11
+ 'attachments' | 'mentioned_users' | 'parent_id' | 'quoted_message' | 'text'
12
+ > = {
13
+ attachments: [],
14
+ mentioned_users: [],
15
+ parent_id: undefined,
16
+ quoted_message: undefined,
17
+ text: '',
18
+ };
19
+
20
+ export const createPollOnlyCompositionMiddleware = (
21
+ composer: MessageComposer,
22
+ ): MessageCompositionMiddleware => ({
23
+ id: 'stream-io/message-composer-middleware/poll-only',
24
+ handlers: {
25
+ compose: ({
26
+ state,
27
+ complete,
28
+ forward,
29
+ }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
30
+ const pollId = composer.pollId;
31
+ const isEditingMessage = !!composer.editedMessage;
32
+ const isComposingThreadReply = !!composer.threadId;
33
+ if (!pollId || isComposingThreadReply || isEditingMessage) return forward();
34
+
35
+ return complete({
36
+ ...state,
37
+ localMessage: {
38
+ ...state.localMessage,
39
+ ...pollLocalMessageNullifiedFields,
40
+ poll_id: pollId,
41
+ },
42
+ message: {
43
+ id: state.localMessage.id,
44
+ poll_id: pollId,
45
+ },
46
+ });
47
+ },
48
+ },
49
+ });
@@ -31,6 +31,10 @@ export const pollStateChangeValidators: Partial<
31
31
  max_votes_allowed: ({ data, value }) => {
32
32
  if (data.enforce_unique_vote && value)
33
33
  return { max_votes_allowed: 'Enforce unique vote is enabled' };
34
+ const numericMatch = value.match(/^[0-9]+$/);
35
+ if (!numericMatch && value) {
36
+ return { max_votes_allowed: 'Only numbers are allowed' };
37
+ }
34
38
  if (value?.length > 1 && !value.match(VALID_MAX_VOTES_VALUE_REGEX))
35
39
  return { max_votes_allowed: 'Type a number from 2 to 10' };
36
40
  return { max_votes_allowed: undefined };
@@ -225,7 +229,7 @@ export const createPollComposerStateMiddleware = ({
225
229
  forward,
226
230
  }: MiddlewareHandlerParams<PollComposerStateChangeMiddlewareValue>) => {
227
231
  if (!state.targetFields) return forward();
228
- const { previousState } = state;
232
+ const { previousState, injectedFieldErrors } = state;
229
233
  const finalValidators = {
230
234
  ...pollStateChangeValidators,
231
235
  ...defaultPollFieldChangeEventValidators,
@@ -247,7 +251,7 @@ export const createPollComposerStateMiddleware = ({
247
251
  nextState: {
248
252
  ...previousState,
249
253
  data: { ...previousState.data, ...newData },
250
- errors: { ...previousState.errors, ...newErrors },
254
+ errors: { ...previousState.errors, ...newErrors, ...injectedFieldErrors },
251
255
  },
252
256
  });
253
257
  },
@@ -275,7 +279,11 @@ export const createPollComposerStateMiddleware = ({
275
279
  nextState: {
276
280
  ...previousState,
277
281
  data: { ...previousState.data, ...newData },
278
- errors: { ...previousState.errors, ...newErrors },
282
+ errors: {
283
+ ...previousState.errors,
284
+ ...newErrors,
285
+ ...state.injectedFieldErrors,
286
+ },
279
287
  },
280
288
  });
281
289
  },
@@ -60,6 +60,7 @@ export type PollComposerStateChangeMiddlewareValue = {
60
60
  ? PollComposerOptionUpdate
61
61
  : PollComposerState['data'][K];
62
62
  }>;
63
+ injectedFieldErrors?: PollComposerFieldErrors;
63
64
  };
64
65
 
65
66
  export type PollComposerStateMiddlewareValue =
@@ -1,15 +1,15 @@
1
1
  import { createCommandsMiddleware } from './commands';
2
2
  import { createMentionsMiddleware } from './mentions';
3
3
  import { createTextComposerPreValidationMiddleware } from './validation';
4
+ import { MiddlewareExecutor } from '../../../middleware';
4
5
  import type {
5
6
  ExecuteParams,
6
7
  MiddlewareExecutionResult,
7
8
  MiddlewareHandler,
8
9
  } from '../../../middleware';
9
- import { MiddlewareExecutor } from '../../../middleware';
10
- import { withCancellation } from '../../../utils/concurrency';
11
10
  import type {
12
11
  Suggestion,
12
+ Suggestions,
13
13
  TextComposerMiddlewareExecutorOptions,
14
14
  TextComposerState,
15
15
  } from './types';
@@ -58,28 +58,14 @@ export class TextComposerMiddlewareExecutor<
58
58
  initialValue: initialState,
59
59
  });
60
60
 
61
- if (result && result.state.suggestions) {
62
- try {
63
- const searchResult = await withCancellation(
64
- 'textComposer-suggestions-search',
65
- async () => {
66
- await result.state.suggestions?.searchSource.search(
67
- result.state.suggestions?.query,
68
- );
69
- },
70
- );
71
- if (searchResult === 'canceled') return { ...result, status: 'discard' };
72
- } catch (error) {
73
- // Clear suggestions on search error
74
- return {
75
- ...result,
76
- state: {
77
- ...result.state,
78
- suggestions: undefined,
79
- },
80
- };
81
- }
82
- }
61
+ const { query, searchSource } = result.state.suggestions ?? ({} as Suggestions);
62
+ /**
63
+ * Catching error just for sanity purposes.
64
+ * The BaseSearchSource.search() method returns debounced result.
65
+ * That means the result of the previous search call as the debounced call result is unknown at the moment.
66
+ * Custom search source implementation should handle errors meaningfully internally.
67
+ */
68
+ searchSource?.search(query)?.catch(console.error);
83
69
 
84
70
  return result;
85
71
  }
@@ -7,7 +7,11 @@ import { StateStore } from '../store';
7
7
  import { VotingVisibility } from '../types';
8
8
  import { generateUUIDv4 } from '../utils';
9
9
  import type { MessageComposer } from './messageComposer';
10
- import type { PollComposerState, UpdateFieldsData } from './middleware/pollComposer';
10
+ import type {
11
+ PollComposerFieldErrors,
12
+ PollComposerState,
13
+ UpdateFieldsData,
14
+ } from './middleware/pollComposer';
11
15
 
12
16
  export type PollComposerOptions = {
13
17
  composer: MessageComposer;
@@ -102,13 +106,23 @@ export class PollComposer {
102
106
  this.state.next(this.initialState);
103
107
  };
104
108
 
105
- updateFields = async (data: UpdateFieldsData) => {
109
+ /**
110
+ * Updates specified fields and generates relevant errors
111
+ * @param data
112
+ * @param injectedFieldErrors - errors produced externally that will take precedence over the errors generated in the middleware chaing
113
+ */
114
+ // FIXME: change method params to a single object with the next major release
115
+ updateFields = async (
116
+ data: UpdateFieldsData,
117
+ injectedFieldErrors?: PollComposerFieldErrors,
118
+ ) => {
106
119
  const { state, status } = await this.stateMiddlewareExecutor.execute({
107
120
  eventName: 'handleFieldChange',
108
121
  initialValue: {
109
122
  nextState: { ...this.state.getLatestValue() },
110
123
  previousState: { ...this.state.getLatestValue() },
111
124
  targetFields: data,
125
+ injectedFieldErrors,
112
126
  },
113
127
  });
114
128
 
package/src/types.ts CHANGED
@@ -968,6 +968,7 @@ export type ChannelQueryOptions = {
968
968
  export type ChannelStateOptions = {
969
969
  offlineMode?: boolean;
970
970
  skipInitialization?: string[];
971
+ skipHydration?: boolean;
971
972
  };
972
973
 
973
974
  export type CreateChannelOptions = {