stream-chat 9.10.1 → 9.12.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 +400 -21
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +413 -28
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +400 -21
  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 +5 -9
  9. package/dist/types/channel_manager.d.ts +5 -1
  10. package/dist/types/client.d.ts +7 -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 +52 -5
  22. package/package.json +1 -1
  23. package/src/LiveLocationManager.ts +297 -0
  24. package/src/channel.ts +37 -2
  25. package/src/channel_manager.ts +15 -2
  26. package/src/client.ts +14 -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/attachmentManager.ts +4 -0
  32. package/src/messageComposer/configuration/configuration.ts +8 -0
  33. package/src/messageComposer/configuration/types.ts +14 -0
  34. package/src/messageComposer/index.ts +1 -0
  35. package/src/messageComposer/messageComposer.ts +81 -9
  36. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +2 -0
  37. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +1 -5
  38. package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
  39. package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +42 -0
  40. package/src/types.ts +55 -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
+ });
package/src/types.ts CHANGED
@@ -888,6 +888,7 @@ export type UserResponse = CustomUserData & {
888
888
  teams_role?: TeamsRole;
889
889
  updated_at?: string;
890
890
  username?: string;
891
+ avg_response_time?: number;
891
892
  };
892
893
 
893
894
  export type TeamsRole = { [team: string]: string };
@@ -2254,6 +2255,7 @@ export type AppSettings = {
2254
2255
  sqs_key?: string;
2255
2256
  sqs_secret?: string;
2256
2257
  sqs_url?: string;
2258
+ user_response_time_enabled?: boolean;
2257
2259
  video_provider?: string;
2258
2260
  webhook_events?: Array<string> | null;
2259
2261
  webhook_url?: string;
@@ -2285,7 +2287,6 @@ export type Attachment = CustomAttachmentData & {
2285
2287
  original_height?: number;
2286
2288
  original_width?: number;
2287
2289
  pretext?: string;
2288
- stopped_sharing?: boolean;
2289
2290
  text?: string;
2290
2291
  thumb_url?: string;
2291
2292
  title?: string;
@@ -2346,6 +2347,7 @@ export type ChannelConfigFields = {
2346
2347
  read_events?: boolean;
2347
2348
  replies?: boolean;
2348
2349
  search?: boolean;
2350
+ shared_locations?: boolean;
2349
2351
  typing_events?: boolean;
2350
2352
  uploads?: boolean;
2351
2353
  url_enrichment?: boolean;
@@ -2703,7 +2705,7 @@ export type Logger = (
2703
2705
  export type Message = Partial<
2704
2706
  MessageBase & {
2705
2707
  mentioned_users: string[];
2706
- shared_location?: SharedLocationRequest;
2708
+ shared_location?: StaticLocationPayload | LiveLocationPayload;
2707
2709
  }
2708
2710
  >;
2709
2711
 
@@ -2748,6 +2750,12 @@ export type SendMessageOptions = {
2748
2750
 
2749
2751
  export type UpdateMessageOptions = {
2750
2752
  skip_enrich_url?: boolean;
2753
+ skip_push?: boolean;
2754
+ };
2755
+
2756
+ export type SendReactionOptions = {
2757
+ enforce_unique?: boolean;
2758
+ skip_push?: boolean;
2751
2759
  };
2752
2760
 
2753
2761
  export type GetMessageOptions = {
@@ -2839,6 +2847,7 @@ export type Reaction = CustomReactionData & {
2839
2847
  score?: number;
2840
2848
  user?: UserResponse | null;
2841
2849
  user_id?: string;
2850
+ emoji_code?: string;
2842
2851
  };
2843
2852
 
2844
2853
  export type Resource =
@@ -3957,13 +3966,14 @@ export type DraftMessage = {
3957
3966
  parent_id?: string;
3958
3967
  poll_id?: string;
3959
3968
  quoted_message_id?: string;
3969
+ shared_location?: StaticLocationPayload | LiveLocationPayload; // todo: live-location verify if possible
3960
3970
  show_in_channel?: boolean;
3961
3971
  silent?: boolean;
3962
3972
  type?: MessageLabel;
3963
3973
  };
3964
3974
 
3965
3975
  export type ActiveLiveLocationsAPIResponse = APIResponse & {
3966
- active_live_locations: SharedLocationResponse[];
3976
+ active_live_locations: SharedLiveLocationResponse[];
3967
3977
  };
3968
3978
 
3969
3979
  export type SharedLocationResponse = {
@@ -3978,11 +3988,51 @@ export type SharedLocationResponse = {
3978
3988
  user_id: string;
3979
3989
  };
3980
3990
 
3981
- export type SharedLocationRequest = {
3991
+ export type SharedStaticLocationResponse = {
3992
+ channel_cid: string;
3993
+ created_at: string;
3982
3994
  created_by_device_id: string;
3995
+ latitude: number;
3996
+ longitude: number;
3997
+ message_id: string;
3998
+ updated_at: string;
3999
+ user_id: string;
4000
+ };
4001
+
4002
+ export type SharedLiveLocationResponse = {
4003
+ channel_cid: string;
4004
+ created_at: string;
4005
+ created_by_device_id: string;
4006
+ end_at: string;
4007
+ latitude: number;
4008
+ longitude: number;
4009
+ message_id: string;
4010
+ updated_at: string;
4011
+ user_id: string;
4012
+ };
4013
+
4014
+ export type UpdateLocationPayload = {
4015
+ message_id: string;
4016
+ created_by_device_id?: string;
3983
4017
  end_at?: string;
3984
4018
  latitude?: number;
3985
4019
  longitude?: number;
4020
+ user?: { id: string };
4021
+ user_id?: string;
4022
+ };
4023
+
4024
+ export type StaticLocationPayload = {
4025
+ created_by_device_id: string;
4026
+ latitude: number;
4027
+ longitude: number;
4028
+ message_id: string;
4029
+ };
4030
+
4031
+ export type LiveLocationPayload = {
4032
+ created_by_device_id: string;
4033
+ end_at: string;
4034
+ latitude: number;
4035
+ longitude: number;
3986
4036
  message_id: string;
3987
4037
  };
3988
4038
 
@@ -4055,9 +4105,9 @@ export type ReminderResponseBase = {
4055
4105
  };
4056
4106
 
4057
4107
  export type ReminderResponse = ReminderResponseBase & {
4058
- channel: ChannelResponse;
4059
4108
  user: UserResponse;
4060
4109
  message: MessageResponse;
4110
+ channel?: ChannelResponse;
4061
4111
  };
4062
4112
 
4063
4113
  export type ReminderAPIResponse = APIResponse & {