stream-chat 9.11.0 → 9.13.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 (40) hide show
  1. package/dist/cjs/index.browser.cjs +437 -23
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +451 -30
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +437 -23
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/types/LiveLocationManager.d.ts +54 -0
  8. package/dist/types/channel.d.ts +3 -1
  9. package/dist/types/channel_manager.d.ts +5 -1
  10. package/dist/types/client.d.ts +14 -6
  11. package/dist/types/events.d.ts +2 -0
  12. package/dist/types/index.d.ts +1 -0
  13. package/dist/types/messageComposer/LocationComposer.d.ts +34 -0
  14. package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -1
  15. package/dist/types/messageComposer/configuration/configuration.d.ts +2 -1
  16. package/dist/types/messageComposer/configuration/types.d.ts +11 -0
  17. package/dist/types/messageComposer/index.d.ts +1 -0
  18. package/dist/types/messageComposer/messageComposer.d.ts +7 -2
  19. package/dist/types/messageComposer/middleware/messageComposer/index.d.ts +1 -0
  20. package/dist/types/messageComposer/middleware/messageComposer/sharedLocation.d.ts +3 -0
  21. package/dist/types/types.d.ts +58 -5
  22. package/package.json +1 -1
  23. package/src/LiveLocationManager.ts +297 -0
  24. package/src/channel.ts +34 -0
  25. package/src/channel_manager.ts +33 -4
  26. package/src/client.ts +27 -5
  27. package/src/events.ts +2 -0
  28. package/src/index.ts +1 -0
  29. package/src/messageComposer/LocationComposer.ts +94 -0
  30. package/src/messageComposer/attachmentIdentity.ts +8 -1
  31. package/src/messageComposer/configuration/configuration.ts +8 -0
  32. package/src/messageComposer/configuration/types.ts +14 -0
  33. package/src/messageComposer/index.ts +1 -0
  34. package/src/messageComposer/messageComposer.ts +81 -9
  35. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +2 -0
  36. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +1 -5
  37. package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
  38. package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +42 -0
  39. package/src/offline-support/offline_sync_manager.ts +16 -0
  40. package/src/types.ts +63 -5
@@ -11,6 +11,7 @@ export type UploadRequestFn = (
11
11
  export type DraftsConfiguration = {
12
12
  enabled: boolean;
13
13
  };
14
+
14
15
  export type TextComposerConfig = {
15
16
  /** If false, the text input, change and selection events are disabled */
16
17
  enabled: boolean;
@@ -23,6 +24,7 @@ export type TextComposerConfig = {
23
24
  /** Prevents sending a message longer than this length */
24
25
  maxLengthOnSend?: number;
25
26
  };
27
+
26
28
  export type AttachmentManagerConfig = {
27
29
  // todo: document removal of noFiles prop showing how to achieve the same with custom fileUploadFilter function
28
30
  /**
@@ -53,6 +55,16 @@ export type LinkPreviewsManagerConfig = {
53
55
  onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void;
54
56
  };
55
57
 
58
+ export type LocationComposerConfig = {
59
+ /**
60
+ * Allows for toggling the location addition.
61
+ * By default, the feature is enabled but has to be enabled also on channel level config via shared_locations.
62
+ */
63
+ enabled: boolean;
64
+ /** Function that provides a stable id for a device from which the location is shared */
65
+ getDeviceId: () => string;
66
+ };
67
+
56
68
  export type MessageComposerConfig = {
57
69
  /** If true, enables creating drafts on the server */
58
70
  drafts: DraftsConfiguration;
@@ -60,6 +72,8 @@ export type MessageComposerConfig = {
60
72
  attachments: AttachmentManagerConfig;
61
73
  /** Configuration for the link previews manager */
62
74
  linkPreviews: LinkPreviewsManagerConfig;
75
+ /** Configuration for the location composer */
76
+ location: LocationComposerConfig;
63
77
  /** Maximum number of characters in a message */
64
78
  text: TextComposerConfig;
65
79
  };
@@ -4,6 +4,7 @@ export * from './configuration';
4
4
  export * from './CustomDataManager';
5
5
  export * from './fileUtils';
6
6
  export * from './linkPreviewsManager';
7
+ export * from './LocationComposer';
7
8
  export * from './messageComposer';
8
9
  export * from './middleware';
9
10
  export * from './pollComposer';
@@ -1,14 +1,16 @@
1
1
  import { AttachmentManager } from './attachmentManager';
2
2
  import { CustomDataManager } from './CustomDataManager';
3
3
  import { LinkPreviewsManager } from './linkPreviewsManager';
4
+ import { LocationComposer } from './LocationComposer';
4
5
  import { PollComposer } from './pollComposer';
5
6
  import { TextComposer } from './textComposer';
6
- import { DEFAULT_COMPOSER_CONFIG } from './configuration/configuration';
7
+ import { DEFAULT_COMPOSER_CONFIG } from './configuration';
7
8
  import type { MessageComposerMiddlewareValue } from './middleware';
8
9
  import {
9
10
  MessageComposerMiddlewareExecutor,
10
11
  MessageDraftComposerMiddlewareExecutor,
11
12
  } from './middleware';
13
+ import type { Unsubscribe } from '../store';
12
14
  import { StateStore } from '../store';
13
15
  import { formatMessage, generateUUIDv4, isLocalMessage, unformatMessage } from '../utils';
14
16
  import { mergeWith } from '../utils/mergeWith';
@@ -24,11 +26,11 @@ import type {
24
26
  MessageResponse,
25
27
  MessageResponseBase,
26
28
  } from '../types';
29
+ import { WithSubscriptions } from '../utils/WithSubscriptions';
27
30
  import type { StreamChat } from '../client';
28
31
  import type { MessageComposerConfig } from './configuration/types';
29
32
  import type { DeepPartial } from '../types.utility';
30
- import type { Unsubscribe } from '../store';
31
- import { WithSubscriptions } from '../utils/WithSubscriptions';
33
+ import type { MergeWithCustomizer } from '../utils/mergeWith/mergeWithCore';
32
34
 
33
35
  type UnregisterSubscriptions = Unsubscribe;
34
36
 
@@ -129,6 +131,7 @@ export class MessageComposer extends WithSubscriptions {
129
131
  linkPreviewsManager: LinkPreviewsManager;
130
132
  textComposer: TextComposer;
131
133
  pollComposer: PollComposer;
134
+ locationComposer: LocationComposer;
132
135
  customDataManager: CustomDataManager;
133
136
  // todo: mediaRecorder: MediaRecorderController;
134
137
 
@@ -142,10 +145,6 @@ export class MessageComposer extends WithSubscriptions {
142
145
 
143
146
  this.compositionContext = compositionContext;
144
147
 
145
- this.configState = new StateStore<MessageComposerConfig>(
146
- mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
147
- );
148
-
149
148
  // channel is easily inferable from the context
150
149
  if (compositionContext instanceof Channel) {
151
150
  this.channel = compositionContext;
@@ -160,6 +159,32 @@ export class MessageComposer extends WithSubscriptions {
160
159
  );
161
160
  }
162
161
 
162
+ const mergeChannelConfigCustomizer: MergeWithCustomizer<
163
+ DeepPartial<MessageComposerConfig>
164
+ > = (originalVal, channelConfigVal, key) =>
165
+ typeof originalVal === 'object'
166
+ ? undefined
167
+ : originalVal === false && key === 'enabled' // prevent enabling features that are disabled client-side
168
+ ? false
169
+ : ['string', 'number', 'bigint', 'boolean', 'symbol'].includes(
170
+ // prevent enabling features that are disabled server-side
171
+ typeof channelConfigVal,
172
+ )
173
+ ? channelConfigVal // scalar values get overridden by server-side config
174
+ : originalVal;
175
+
176
+ this.configState = new StateStore<MessageComposerConfig>(
177
+ mergeWith(
178
+ mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
179
+ {
180
+ location: {
181
+ enabled: this.channel.getConfig()?.shared_locations,
182
+ },
183
+ },
184
+ mergeChannelConfigCustomizer,
185
+ ),
186
+ );
187
+
163
188
  let message: LocalMessage | DraftMessage | undefined = undefined;
164
189
  if (compositionIsDraftResponse(composition)) {
165
190
  message = composition.message;
@@ -170,6 +195,7 @@ export class MessageComposer extends WithSubscriptions {
170
195
 
171
196
  this.attachmentManager = new AttachmentManager({ composer: this, message });
172
197
  this.linkPreviewsManager = new LinkPreviewsManager({ composer: this, message });
198
+ this.locationComposer = new LocationComposer({ composer: this, message });
173
199
  this.textComposer = new TextComposer({ composer: this, message });
174
200
  this.pollComposer = new PollComposer({ composer: this });
175
201
  this.customDataManager = new CustomDataManager({ composer: this, message });
@@ -289,7 +315,8 @@ export class MessageComposer extends WithSubscriptions {
289
315
  (!this.attachmentManager.uploadsInProgressCount &&
290
316
  (!this.textComposer.textIsEmpty ||
291
317
  this.attachmentManager.successfulUploadsCount > 0)) ||
292
- this.pollId
318
+ this.pollId ||
319
+ !!this.locationComposer.validLocation
293
320
  );
294
321
  }
295
322
 
@@ -298,7 +325,8 @@ export class MessageComposer extends WithSubscriptions {
298
325
  !this.quotedMessage &&
299
326
  this.textComposer.textIsEmpty &&
300
327
  !this.attachmentManager.attachments.length &&
301
- !this.pollId
328
+ !this.pollId &&
329
+ !this.locationComposer.validLocation
302
330
  );
303
331
  }
304
332
 
@@ -320,6 +348,10 @@ export class MessageComposer extends WithSubscriptions {
320
348
 
321
349
  static generateId = generateUUIDv4;
322
350
 
351
+ refreshId = () => {
352
+ this.state.partialNext({ id: MessageComposer.generateId() });
353
+ };
354
+
323
355
  initState = ({
324
356
  composition,
325
357
  }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}) => {
@@ -333,6 +365,7 @@ export class MessageComposer extends WithSubscriptions {
333
365
  : formatMessage(composition);
334
366
  this.attachmentManager.initState({ message });
335
367
  this.linkPreviewsManager.initState({ message });
368
+ this.locationComposer.initState({ message });
336
369
  this.textComposer.initState({ message });
337
370
  this.pollComposer.initState();
338
371
  this.customDataManager.initState({ message });
@@ -403,6 +436,7 @@ export class MessageComposer extends WithSubscriptions {
403
436
  this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
404
437
  this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
405
438
  this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
439
+ this.addUnsubscribeFunction(this.subscribeLocationComposerStateChanged());
406
440
  this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
407
441
  this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
408
442
  this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
@@ -535,6 +569,18 @@ export class MessageComposer extends WithSubscriptions {
535
569
  }
536
570
  });
537
571
 
572
+ private subscribeLocationComposerStateChanged = () =>
573
+ this.locationComposer.state.subscribe((_, previousValue) => {
574
+ if (typeof previousValue === 'undefined') return;
575
+
576
+ this.logStateUpdateTimestamp();
577
+
578
+ if (this.compositionIsEmpty) {
579
+ this.deleteDraft();
580
+ return;
581
+ }
582
+ });
583
+
538
584
  private subscribeLinkPreviewsManagerStateChanged = () =>
539
585
  this.linkPreviewsManager.state.subscribe((_, previousValue) => {
540
586
  if (typeof previousValue === 'undefined') return;
@@ -800,4 +846,30 @@ export class MessageComposer extends WithSubscriptions {
800
846
  throw error;
801
847
  }
802
848
  };
849
+
850
+ sendLocation = async () => {
851
+ const location = this.locationComposer.validLocation;
852
+ if (this.threadId || !location) return;
853
+ try {
854
+ await this.channel.sendSharedLocation(location);
855
+ this.refreshId();
856
+ this.locationComposer.initState();
857
+ } catch (error) {
858
+ this.client.notifications.addError({
859
+ message: 'Failed to share the location',
860
+ origin: {
861
+ emitter: 'MessageComposer',
862
+ context: { composer: this },
863
+ },
864
+ options: {
865
+ type: 'api:location:create:failed',
866
+ metadata: {
867
+ reason: (error as Error).message,
868
+ },
869
+ originalError: error instanceof Error ? error : undefined,
870
+ },
871
+ });
872
+ throw error;
873
+ }
874
+ };
803
875
  }
@@ -32,6 +32,7 @@ import {
32
32
  } from './customData';
33
33
  import { createUserDataInjectionMiddleware } from './userDataInjection';
34
34
  import { createPollOnlyCompositionMiddleware } from './pollOnly';
35
+ import { createSharedLocationCompositionMiddleware } from './sharedLocation';
35
36
 
36
37
  export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
37
38
  MessageComposerMiddlewareState,
@@ -47,6 +48,7 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
47
48
  createTextComposerCompositionMiddleware(composer),
48
49
  createAttachmentsCompositionMiddleware(composer),
49
50
  createLinkPreviewsCompositionMiddleware(composer),
51
+ createSharedLocationCompositionMiddleware(composer),
50
52
  createMessageComposerStateCompositionMiddleware(composer),
51
53
  createCustomDataCompositionMiddleware(composer),
52
54
  createCompositionValidationMiddleware(composer),
@@ -20,15 +20,11 @@ export const createCompositionValidationMiddleware = (
20
20
  }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
21
21
  const { maxLengthOnSend } = composer.config.text ?? {};
22
22
  const inputText = state.message.text ?? '';
23
- const isEmptyMessage =
24
- textIsEmpty(inputText) &&
25
- !state.message.attachments?.length &&
26
- !state.message.poll_id;
27
23
 
28
24
  const hasExceededMaxLength =
29
25
  typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend;
30
26
 
31
- if (isEmptyMessage || hasExceededMaxLength) {
27
+ if (composer.compositionIsEmpty || hasExceededMaxLength) {
32
28
  return await discard();
33
29
  }
34
30
 
@@ -5,6 +5,7 @@ export * from './compositionValidation';
5
5
  export * from './linkPreviews';
6
6
  export * from './MessageComposerMiddlewareExecutor';
7
7
  export * from './messageComposerState';
8
+ export * from './sharedLocation';
8
9
  export * from './textComposer';
9
10
  export * from './types';
10
11
  export * from './commandInjection';
@@ -0,0 +1,42 @@
1
+ import type { MiddlewareHandlerParams } from '../../../middleware';
2
+ import type { MessageComposer } from '../../messageComposer';
3
+ import type {
4
+ MessageComposerMiddlewareState,
5
+ MessageCompositionMiddleware,
6
+ } from './types';
7
+
8
+ export const createSharedLocationCompositionMiddleware = (
9
+ composer: MessageComposer,
10
+ ): MessageCompositionMiddleware => ({
11
+ id: 'stream-io/message-composer-middleware/shared-location',
12
+ handlers: {
13
+ compose: ({
14
+ state,
15
+ next,
16
+ forward,
17
+ }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
18
+ const { locationComposer } = composer;
19
+ const location = locationComposer.validLocation;
20
+ if (!locationComposer || !location || !composer.client.user) return forward();
21
+ const timestamp = new Date().toISOString();
22
+
23
+ return next({
24
+ ...state,
25
+ localMessage: {
26
+ ...state.localMessage,
27
+ shared_location: {
28
+ ...location,
29
+ channel_cid: composer.channel.cid,
30
+ created_at: timestamp,
31
+ updated_at: timestamp,
32
+ user_id: composer.client.user.id,
33
+ },
34
+ },
35
+ message: {
36
+ ...state.message,
37
+ shared_location: location,
38
+ },
39
+ });
40
+ },
41
+ },
42
+ });
@@ -1,6 +1,9 @@
1
1
  import type { ExecuteBatchDBQueriesType } from './types';
2
2
  import type { StreamChat } from '../client';
3
3
  import type { AbstractOfflineDB } from './offline_support_api';
4
+ import type { AxiosError } from 'axios';
5
+ import { isAxiosError } from 'axios';
6
+ import type { APIErrorResponse } from '../types';
4
7
 
5
8
  /**
6
9
  * Manages synchronization between the local offline database and the Stream backend.
@@ -174,8 +177,21 @@ export class OfflineDBSyncManager {
174
177
  });
175
178
  } catch (e) {
176
179
  console.log('An error has occurred while syncing the DB.', e);
180
+
181
+ if (isAxiosError(e) && e.code === 'ECONNABORTED') {
182
+ // If the sync was aborted due to timeout, we can simply return
183
+ return;
184
+ }
185
+
186
+ const error = e as AxiosError<APIErrorResponse>;
187
+
188
+ if (error.response?.data?.code === 23) {
189
+ return;
190
+ }
191
+
177
192
  // Error will be raised by the sync API if there are too many events.
178
193
  // In that case reset the entire DB and start fresh.
194
+ // We avoid resetting the DB if the error is due to timeout.
179
195
  await this.offlineDb.resetDB();
180
196
  }
181
197
  };
package/src/types.ts CHANGED
@@ -576,6 +576,23 @@ export type GetRateLimitsResponse = APIResponse & {
576
576
  web?: RateLimitsMap;
577
577
  };
578
578
 
579
+ export enum Product {
580
+ Chat = 'chat',
581
+ Video = 'video',
582
+ Moderation = 'moderation',
583
+ Feeds = 'feeds',
584
+ }
585
+
586
+ export type HookEvent = {
587
+ name: string;
588
+ description: string;
589
+ products: Product[];
590
+ };
591
+
592
+ export type GetHookEventsResponse = APIResponse & {
593
+ events: HookEvent[];
594
+ };
595
+
579
596
  export type GetReactionsAPIResponse = APIResponse & {
580
597
  reactions: ReactionResponse[];
581
598
  };
@@ -2287,7 +2304,6 @@ export type Attachment = CustomAttachmentData & {
2287
2304
  original_height?: number;
2288
2305
  original_width?: number;
2289
2306
  pretext?: string;
2290
- stopped_sharing?: boolean;
2291
2307
  text?: string;
2292
2308
  thumb_url?: string;
2293
2309
  title?: string;
@@ -2348,6 +2364,7 @@ export type ChannelConfigFields = {
2348
2364
  read_events?: boolean;
2349
2365
  replies?: boolean;
2350
2366
  search?: boolean;
2367
+ shared_locations?: boolean;
2351
2368
  typing_events?: boolean;
2352
2369
  uploads?: boolean;
2353
2370
  url_enrichment?: boolean;
@@ -2705,7 +2722,7 @@ export type Logger = (
2705
2722
  export type Message = Partial<
2706
2723
  MessageBase & {
2707
2724
  mentioned_users: string[];
2708
- shared_location?: SharedLocationRequest;
2725
+ shared_location?: StaticLocationPayload | LiveLocationPayload;
2709
2726
  }
2710
2727
  >;
2711
2728
 
@@ -3966,13 +3983,14 @@ export type DraftMessage = {
3966
3983
  parent_id?: string;
3967
3984
  poll_id?: string;
3968
3985
  quoted_message_id?: string;
3986
+ shared_location?: StaticLocationPayload | LiveLocationPayload; // todo: live-location verify if possible
3969
3987
  show_in_channel?: boolean;
3970
3988
  silent?: boolean;
3971
3989
  type?: MessageLabel;
3972
3990
  };
3973
3991
 
3974
3992
  export type ActiveLiveLocationsAPIResponse = APIResponse & {
3975
- active_live_locations: SharedLocationResponse[];
3993
+ active_live_locations: SharedLiveLocationResponse[];
3976
3994
  };
3977
3995
 
3978
3996
  export type SharedLocationResponse = {
@@ -3987,11 +4005,51 @@ export type SharedLocationResponse = {
3987
4005
  user_id: string;
3988
4006
  };
3989
4007
 
3990
- export type SharedLocationRequest = {
4008
+ export type SharedStaticLocationResponse = {
4009
+ channel_cid: string;
4010
+ created_at: string;
3991
4011
  created_by_device_id: string;
4012
+ latitude: number;
4013
+ longitude: number;
4014
+ message_id: string;
4015
+ updated_at: string;
4016
+ user_id: string;
4017
+ };
4018
+
4019
+ export type SharedLiveLocationResponse = {
4020
+ channel_cid: string;
4021
+ created_at: string;
4022
+ created_by_device_id: string;
4023
+ end_at: string;
4024
+ latitude: number;
4025
+ longitude: number;
4026
+ message_id: string;
4027
+ updated_at: string;
4028
+ user_id: string;
4029
+ };
4030
+
4031
+ export type UpdateLocationPayload = {
4032
+ message_id: string;
4033
+ created_by_device_id?: string;
3992
4034
  end_at?: string;
3993
4035
  latitude?: number;
3994
4036
  longitude?: number;
4037
+ user?: { id: string };
4038
+ user_id?: string;
4039
+ };
4040
+
4041
+ export type StaticLocationPayload = {
4042
+ created_by_device_id: string;
4043
+ latitude: number;
4044
+ longitude: number;
4045
+ message_id: string;
4046
+ };
4047
+
4048
+ export type LiveLocationPayload = {
4049
+ created_by_device_id: string;
4050
+ end_at: string;
4051
+ latitude: number;
4052
+ longitude: number;
3995
4053
  message_id: string;
3996
4054
  };
3997
4055
 
@@ -4064,9 +4122,9 @@ export type ReminderResponseBase = {
4064
4122
  };
4065
4123
 
4066
4124
  export type ReminderResponse = ReminderResponseBase & {
4067
- channel: ChannelResponse;
4068
4125
  user: UserResponse;
4069
4126
  message: MessageResponse;
4127
+ channel?: ChannelResponse;
4070
4128
  };
4071
4129
 
4072
4130
  export type ReminderAPIResponse = APIResponse & {