stream-chat 9.6.1 → 9.8.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 (59) hide show
  1. package/dist/cjs/index.browser.cjs +532 -57
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +538 -57
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +532 -57
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/types/channel.d.ts +36 -4
  8. package/dist/types/client.d.ts +38 -0
  9. package/dist/types/messageComposer/messageComposer.d.ts +4 -1
  10. package/dist/types/messageComposer/middleware/messageComposer/commandInjection.d.ts +4 -0
  11. package/dist/types/messageComposer/middleware/messageComposer/index.d.ts +1 -0
  12. package/dist/types/messageComposer/middleware/textComposer/activeCommandGuard.d.ts +4 -0
  13. package/dist/types/messageComposer/middleware/textComposer/commandStringExtraction.d.ts +5 -0
  14. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +5 -5
  15. package/dist/types/messageComposer/middleware/textComposer/index.d.ts +2 -0
  16. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +1 -2
  17. package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +6 -0
  18. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +3 -2
  19. package/dist/types/messageComposer/textComposer.d.ts +6 -3
  20. package/dist/types/offline-support/offline_support_api.d.ts +39 -0
  21. package/dist/types/offline-support/types.d.ts +36 -2
  22. package/dist/types/pagination/BasePaginator.d.ts +1 -1
  23. package/dist/types/pagination/ReminderPaginator.d.ts +6 -2
  24. package/dist/types/reminders/ReminderManager.d.ts +3 -3
  25. package/dist/types/search/BaseSearchSource.d.ts +37 -31
  26. package/dist/types/search/ChannelSearchSource.d.ts +1 -1
  27. package/dist/types/search/MessageSearchSource.d.ts +1 -1
  28. package/dist/types/search/UserSearchSource.d.ts +1 -1
  29. package/dist/types/search/index.d.ts +1 -0
  30. package/dist/types/search/types.d.ts +20 -0
  31. package/dist/types/types.d.ts +6 -2
  32. package/dist/types/utils.d.ts +11 -2
  33. package/package.json +1 -1
  34. package/src/channel.ts +85 -10
  35. package/src/client.ts +61 -3
  36. package/src/messageComposer/messageComposer.ts +120 -14
  37. package/src/messageComposer/middleware/messageComposer/commandInjection.ts +72 -0
  38. package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
  39. package/src/messageComposer/middleware/textComposer/activeCommandGuard.ts +20 -0
  40. package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +56 -0
  41. package/src/messageComposer/middleware/textComposer/commands.ts +28 -11
  42. package/src/messageComposer/middleware/textComposer/index.ts +2 -0
  43. package/src/messageComposer/middleware/textComposer/mentions.ts +1 -2
  44. package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +14 -0
  45. package/src/messageComposer/middleware/textComposer/types.ts +3 -2
  46. package/src/messageComposer/textComposer.ts +23 -3
  47. package/src/offline-support/offline_support_api.ts +79 -0
  48. package/src/offline-support/types.ts +41 -1
  49. package/src/pagination/BasePaginator.ts +1 -1
  50. package/src/pagination/ReminderPaginator.ts +20 -2
  51. package/src/reminders/ReminderManager.ts +16 -2
  52. package/src/search/BaseSearchSource.ts +123 -52
  53. package/src/search/ChannelSearchSource.ts +1 -1
  54. package/src/search/MessageSearchSource.ts +1 -1
  55. package/src/search/UserSearchSource.ts +1 -1
  56. package/src/search/index.ts +1 -0
  57. package/src/search/types.ts +20 -0
  58. package/src/types.ts +9 -2
  59. package/src/utils.ts +31 -2
@@ -0,0 +1,20 @@
1
+ export type SearchSourceState<T = any> = {
2
+ hasNext: boolean;
3
+ isActive: boolean;
4
+ isLoading: boolean;
5
+ items: T[] | undefined;
6
+ searchQuery: string;
7
+ lastQueryError?: Error;
8
+ next?: string | null;
9
+ offset?: number;
10
+ };
11
+ export type SearchSourceOptions = {
12
+ /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
13
+ debounceMs?: number;
14
+ pageSize?: number;
15
+ };
16
+ export type SearchSourceType = 'channels' | 'users' | 'messages' | (string & {});
17
+ export type QueryReturnValue<T> = {
18
+ items: T[];
19
+ next?: string | null;
20
+ };
@@ -2931,7 +2931,7 @@ export type ReminderAPIResponse = APIResponse & {
2931
2931
  };
2932
2932
  export type CreateReminderOptions = {
2933
2933
  messageId: string;
2934
- remind_at?: string;
2934
+ remind_at?: string | null;
2935
2935
  user_id?: string;
2936
2936
  };
2937
2937
  export type UpdateReminderOptions = CreateReminderOptions;
@@ -2952,7 +2952,7 @@ export type QueryRemindersResponse = {
2952
2952
  prev?: string;
2953
2953
  next?: string;
2954
2954
  };
2955
- export type HookType = 'webhook' | 'sqs' | 'sns';
2955
+ export type HookType = 'webhook' | 'sqs' | 'sns' | 'pending_message';
2956
2956
  export type EventHook = {
2957
2957
  id?: string;
2958
2958
  hook_type?: HookType;
@@ -2971,6 +2971,10 @@ export type EventHook = {
2971
2971
  sns_key?: string;
2972
2972
  sns_secret?: string;
2973
2973
  sns_role_arn?: string;
2974
+ timeout_ms?: number;
2975
+ callback?: {
2976
+ mode: 'CALLBACK_MODE_NONE' | 'CALLBACK_MODE_REST' | 'CALLBACK_MODE_TWIRP';
2977
+ };
2974
2978
  created_at?: string;
2975
2979
  updated_at?: string;
2976
2980
  };
@@ -47,11 +47,20 @@ export declare function removeConnectionEventListeners(cb: (e: Event) => void):
47
47
  export declare const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'];
48
48
  /**
49
49
  * Takes the message object, parses the dates, sets `__html`
50
- * and sets the status to `received` if missing; returns a new message object.
50
+ * and sets the status to `received` if missing; returns a new LocalMessage object.
51
51
  *
52
- * @param {MessageResponse} message `MessageResponse` object
52
+ * @param {LocalMessage} message `LocalMessage` object
53
53
  */
54
54
  export declare function formatMessage(message: MessageResponse | MessageResponseBase | LocalMessage): LocalMessage;
55
+ /**
56
+ * @private
57
+ *
58
+ * Takes a LocalMessage, parses the dates back to strings,
59
+ * and converts the message back to a MessageResponse.
60
+ *
61
+ * @param {MessageResponse} message `MessageResponse` object
62
+ */
63
+ export declare function unformatMessage(message: LocalMessage): MessageResponse;
55
64
  export declare const localMessageToNewMessagePayload: (localMessage: LocalMessage) => Message;
56
65
  export declare const toUpdatedMessagePayload: (message: LocalMessage | Partial<MessageResponse>) => UpdatedMessage;
57
66
  export declare const findIndexInSortedArray: <T, L>({ needle, sortedArray, selectKey, selectValueToCompare, sortDirection, }: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.6.1",
3
+ "version": "9.8.0",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/channel.ts CHANGED
@@ -406,6 +406,17 @@ export class Channel {
406
406
  );
407
407
  }
408
408
 
409
+ /**
410
+ * sendReaction - Sends a reaction to a message. If offline support is enabled, it will make sure
411
+ * that sending the reaction is queued up if it fails due to bad internet conditions and executed
412
+ * later.
413
+ *
414
+ * @param {string} messageID the message id
415
+ * @param {Reaction} reaction the reaction object for instance {type: 'love'}
416
+ * @param {{ enforce_unique?: boolean, skip_push?: boolean }} [options] Option object, {enforce_unique: true, skip_push: true} to override any existing reaction or skip sending push notifications
417
+ *
418
+ * @return {Promise<ReactionAPIResponse>} The Server Response
419
+ */
409
420
  async sendReaction(
410
421
  messageID: string,
411
422
  reaction: Reaction,
@@ -421,7 +432,7 @@ export class Channel {
421
432
  try {
422
433
  const offlineDb = this.getClient().offlineDb;
423
434
  if (offlineDb) {
424
- return (await offlineDb.queueTask({
435
+ return await offlineDb.queueTask<ReactionAPIResponse>({
425
436
  task: {
426
437
  channelId: this.id as string,
427
438
  channelType: this.type,
@@ -429,7 +440,7 @@ export class Channel {
429
440
  payload: [messageID, reaction, options],
430
441
  type: 'send-reaction',
431
442
  },
432
- })) as ReactionAPIResponse;
443
+ });
433
444
  }
434
445
  } catch (error) {
435
446
  this._client.logger('error', `offlineDb:send-reaction`, {
@@ -1463,9 +1474,7 @@ export class Channel {
1463
1474
  this.getClient().polls.hydratePollCache(state.messages, true);
1464
1475
  this.getClient().reminders.hydrateState(state.messages);
1465
1476
 
1466
- if (state.draft) {
1467
- this.messageComposer.initState({ composition: state.draft });
1468
- }
1477
+ this.messageComposer.initStateFromChannelResponse(state);
1469
1478
 
1470
1479
  const areCapabilitiesChanged =
1471
1480
  [...(state.channel.own_capabilities || [])].sort().join() !==
@@ -1613,13 +1622,11 @@ export class Channel {
1613
1622
  /**
1614
1623
  * createDraft - Creates or updates a draft message in a channel
1615
1624
  *
1616
- * @param {string} channelType The channel type
1617
- * @param {string} channelID The channel ID
1618
1625
  * @param {DraftMessagePayload} message The draft message to create or update
1619
1626
  *
1620
1627
  * @return {Promise<CreateDraftResponse>} Response containing the created draft
1621
1628
  */
1622
- async createDraft(message: DraftMessagePayload) {
1629
+ async _createDraft(message: DraftMessagePayload) {
1623
1630
  return await this.getClient().post<CreateDraftResponse>(
1624
1631
  this._channelURL() + '/draft',
1625
1632
  {
@@ -1629,19 +1636,87 @@ export class Channel {
1629
1636
  }
1630
1637
 
1631
1638
  /**
1632
- * deleteDraft - Deletes a draft message from a channel
1639
+ * createDraft - Creates or updates a draft message in a channel. If offline support is
1640
+ * enabled, it will make sure that creating the draft is queued up if it fails due to
1641
+ * bad internet conditions and executed later.
1642
+ *
1643
+ * @param {DraftMessagePayload} message The draft message to create or update
1644
+ *
1645
+ * @return {Promise<CreateDraftResponse>} Response containing the created draft
1646
+ */
1647
+ async createDraft(message: DraftMessagePayload) {
1648
+ try {
1649
+ const offlineDb = this.getClient().offlineDb;
1650
+ if (offlineDb) {
1651
+ return await offlineDb.queueTask<CreateDraftResponse>({
1652
+ task: {
1653
+ channelId: this.id as string,
1654
+ channelType: this.type,
1655
+ threadId: message.parent_id,
1656
+ payload: [message],
1657
+ type: 'create-draft',
1658
+ },
1659
+ });
1660
+ }
1661
+ } catch (error) {
1662
+ this._client.logger('error', `offlineDb:create-draft`, {
1663
+ tags: ['channel', 'offlineDb'],
1664
+ error,
1665
+ });
1666
+ }
1667
+
1668
+ return this._createDraft(message);
1669
+ }
1670
+
1671
+ /**
1672
+ * deleteDraft - Deletes a draft message from a channel or a thread.
1633
1673
  *
1634
1674
  * @param {Object} options
1635
1675
  * @param {string} options.parent_id Optional parent message ID for drafts in threads
1636
1676
  *
1637
1677
  * @return {Promise<APIResponse>} API response
1638
1678
  */
1639
- async deleteDraft({ parent_id }: { parent_id?: string } = {}) {
1679
+ async _deleteDraft({ parent_id }: { parent_id?: string } = {}) {
1640
1680
  return await this.getClient().delete<APIResponse>(this._channelURL() + '/draft', {
1641
1681
  parent_id,
1642
1682
  });
1643
1683
  }
1644
1684
 
1685
+ /**
1686
+ * deleteDraft - Deletes a draft message from a channel or a thread. If offline support is
1687
+ * enabled, it will make sure that deleting the draft is queued up if it fails due to
1688
+ * bad internet conditions and executed later.
1689
+ *
1690
+ * @param {Object} options
1691
+ * @param {string} options.parent_id Optional parent message ID for drafts in threads
1692
+ *
1693
+ * @return {Promise<APIResponse>} API response
1694
+ */
1695
+ async deleteDraft(options: { parent_id?: string } = {}) {
1696
+ const { parent_id } = options;
1697
+ try {
1698
+ const offlineDb = this.getClient().offlineDb;
1699
+ if (offlineDb) {
1700
+ return await offlineDb.queueTask<APIResponse>({
1701
+ task: {
1702
+ channelId: this.id as string,
1703
+ channelType: this.type,
1704
+ threadId: parent_id,
1705
+ payload: [options],
1706
+ type: 'delete-draft',
1707
+ },
1708
+ });
1709
+ }
1710
+ } catch (error) {
1711
+ this._client.logger('error', `offlineDb:delete-draft`, {
1712
+ tags: ['channel', 'offlineDb'],
1713
+ error,
1714
+ });
1715
+ }
1716
+
1717
+ return this._deleteDraft(options);
1718
+ }
1719
+
1645
1720
  /**
1646
1721
  * getDraft - Retrieves a draft message from a channel
1647
1722
  *
package/src/client.ts CHANGED
@@ -1969,9 +1969,7 @@ export class StreamChat {
1969
1969
  this.reminders.hydrateState(channelState.messages);
1970
1970
  }
1971
1971
 
1972
- if (channelState.draft) {
1973
- c.messageComposer.initState({ composition: channelState.draft });
1974
- }
1972
+ c.messageComposer.initStateFromChannelResponse(channelState);
1975
1973
 
1976
1974
  channels.push(c);
1977
1975
  }
@@ -4570,4 +4568,64 @@ export class StreamChat {
4570
4568
  ...rest,
4571
4569
  });
4572
4570
  }
4571
+
4572
+ /**
4573
+ * uploadFile - Uploads a file to the configured storage (defaults to Stream CDN)
4574
+ *
4575
+ * @param {string|NodeJS.ReadableStream|Buffer|File} uri The file to upload
4576
+ * @param {string} [name] The name of the file
4577
+ * @param {string} [contentType] The content type of the file
4578
+ * @param {UserResponse} [user] Optional user information
4579
+ *
4580
+ * @return {Promise<SendFileAPIResponse>} Response containing the file URL
4581
+ */
4582
+ uploadFile(
4583
+ uri: string | NodeJS.ReadableStream | Buffer | File,
4584
+ name?: string,
4585
+ contentType?: string,
4586
+ user?: UserResponse,
4587
+ ) {
4588
+ return this.sendFile(`${this.baseURL}/uploads/file`, uri, name, contentType, user);
4589
+ }
4590
+
4591
+ /**
4592
+ * uploadImage - Uploads an image to the configured storage (defaults to Stream CDN)
4593
+ *
4594
+ * @param {string|NodeJS.ReadableStream|File} uri The image to upload
4595
+ * @param {string} [name] The name of the image
4596
+ * @param {string} [contentType] The content type of the image
4597
+ * @param {UserResponse} [user] Optional user information
4598
+ *
4599
+ * @return {Promise<SendFileAPIResponse>} Response containing the image URL
4600
+ */
4601
+ uploadImage(
4602
+ uri: string | NodeJS.ReadableStream | File,
4603
+ name?: string,
4604
+ contentType?: string,
4605
+ user?: UserResponse,
4606
+ ) {
4607
+ return this.sendFile(`${this.baseURL}/uploads/image`, uri, name, contentType, user);
4608
+ }
4609
+
4610
+ /**
4611
+ * deleteFile - Deletes a file from the configured storage
4612
+ *
4613
+ * @param {string} url The URL of the file to delete
4614
+ *
4615
+ * @return {Promise<APIResponse>} The server response
4616
+ */
4617
+ deleteFile(url: string) {
4618
+ return this.delete<APIResponse>(`${this.baseURL}/uploads/file`, { url });
4619
+ }
4620
+
4621
+ /**
4622
+ * deleteImage - Deletes an image from the configured storage
4623
+ *
4624
+ * @param {string} url The URL of the image to delete
4625
+ *
4626
+ * @return {Promise<APIResponse>} The server response
4627
+ */
4628
+ deleteImage(url: string) {
4629
+ return this.delete<APIResponse>(`${this.baseURL}/uploads/image`, { url });
4630
+ }
4573
4631
  }
@@ -10,11 +10,12 @@ import {
10
10
  MessageDraftComposerMiddlewareExecutor,
11
11
  } from './middleware';
12
12
  import { StateStore } from '../store';
13
- import { formatMessage, generateUUIDv4, isLocalMessage } from '../utils';
13
+ import { formatMessage, generateUUIDv4, isLocalMessage, unformatMessage } from '../utils';
14
14
  import { mergeWith } from '../utils/mergeWith';
15
15
  import { Channel } from '../channel';
16
16
  import { Thread } from '../thread';
17
17
  import type {
18
+ ChannelAPIResponse,
18
19
  DraftMessage,
19
20
  DraftResponse,
20
21
  EventTypes,
@@ -280,6 +281,10 @@ export class MessageComposer extends WithSubscriptions {
280
281
  }
281
282
 
282
283
  get hasSendableData() {
284
+ // If the offline mode is enabled, we allow sending a message if the composition is not empty.
285
+ if (this.client.offlineDb) {
286
+ return !this.compositionIsEmpty;
287
+ }
283
288
  return !!(
284
289
  (!this.attachmentManager.uploadsInProgressCount &&
285
290
  (!this.textComposer.textIsEmpty ||
@@ -342,6 +347,25 @@ export class MessageComposer extends WithSubscriptions {
342
347
  }
343
348
  };
344
349
 
350
+ initStateFromChannelResponse = (channelApiResponse: ChannelAPIResponse) => {
351
+ if (this.channel.cid !== channelApiResponse.channel.cid) {
352
+ return;
353
+ }
354
+ if (channelApiResponse.draft) {
355
+ this.initState({ composition: channelApiResponse.draft });
356
+ } else if (this.state.getLatestValue().draftId) {
357
+ this.clear();
358
+ this.client.offlineDb?.executeQuerySafely(
359
+ (db) =>
360
+ db.deleteDraft({
361
+ cid: this.channel.cid,
362
+ parent_id: undefined, // makes sure that we don't delete thread drafts while upserting channels
363
+ }),
364
+ { method: 'deleteDraft' },
365
+ );
366
+ }
367
+ };
368
+
345
369
  initEditingAuditState = (
346
370
  composition?: DraftResponse | MessageResponse | LocalMessage,
347
371
  ) => initEditingAuditState(composition);
@@ -360,6 +384,16 @@ export class MessageComposer extends WithSubscriptions {
360
384
  });
361
385
  }
362
386
 
387
+ public registerDraftEventSubscriptions = () => {
388
+ const unsubscribeDraftUpdated = this.subscribeDraftUpdated();
389
+ const unsubscribeDraftDeleted = this.subscribeDraftDeleted();
390
+
391
+ return () => {
392
+ unsubscribeDraftUpdated();
393
+ unsubscribeDraftDeleted();
394
+ };
395
+ };
396
+
363
397
  public registerSubscriptions = (): UnregisterSubscriptions => {
364
398
  if (!this.hasSubscriptions) {
365
399
  this.addUnsubscribeFunction(this.subscribeMessageComposerSetupStateChange());
@@ -438,7 +472,7 @@ export class MessageComposer extends WithSubscriptions {
438
472
  const draft = event.draft as DraftResponse;
439
473
  if (
440
474
  !draft ||
441
- !!draft.parent_id !== !!this.threadId ||
475
+ (draft.parent_id ?? null) !== (this.threadId ?? null) ||
442
476
  draft.channel_cid !== this.channel.cid
443
477
  )
444
478
  return;
@@ -450,7 +484,7 @@ export class MessageComposer extends WithSubscriptions {
450
484
  const draft = event.draft as DraftResponse;
451
485
  if (
452
486
  !draft ||
453
- !!draft.parent_id !== !!this.threadId ||
487
+ (draft.parent_id ?? null) !== (this.threadId ?? null) ||
454
488
  draft.channel_cid !== this.channel.cid
455
489
  ) {
456
490
  return;
@@ -548,7 +582,7 @@ export class MessageComposer extends WithSubscriptions {
548
582
  });
549
583
 
550
584
  private subscribeMessageComposerConfigStateChanged = () => {
551
- let draftUnsubscribeFunctions: Unsubscribe[] | null;
585
+ let draftUnsubscribeFunction: Unsubscribe | null;
552
586
 
553
587
  const unsubscribe = this.configState.subscribeWithSelector(
554
588
  (currentValue) => ({
@@ -563,20 +597,17 @@ export class MessageComposer extends WithSubscriptions {
563
597
  });
564
598
  }
565
599
 
566
- if (draftsEnabled && !draftUnsubscribeFunctions) {
567
- draftUnsubscribeFunctions = [
568
- this.subscribeDraftUpdated(),
569
- this.subscribeDraftDeleted(),
570
- ];
571
- } else if (!draftsEnabled && draftUnsubscribeFunctions) {
572
- draftUnsubscribeFunctions.forEach((fn) => fn());
573
- draftUnsubscribeFunctions = null;
600
+ if (draftsEnabled && !draftUnsubscribeFunction) {
601
+ draftUnsubscribeFunction = this.registerDraftEventSubscriptions();
602
+ } else if (!draftsEnabled && draftUnsubscribeFunction) {
603
+ draftUnsubscribeFunction();
604
+ draftUnsubscribeFunction = null;
574
605
  }
575
606
  },
576
607
  );
577
608
 
578
609
  return () => {
579
- draftUnsubscribeFunctions?.forEach((unsubscribe) => unsubscribe());
610
+ draftUnsubscribeFunction?.();
580
611
  unsubscribe();
581
612
  };
582
613
  };
@@ -658,6 +689,25 @@ export class MessageComposer extends WithSubscriptions {
658
689
  if (!composition) return;
659
690
  const { draft } = composition;
660
691
  this.state.partialNext({ draftId: draft.id });
692
+ if (this.client.offlineDb) {
693
+ try {
694
+ const optimisticDraftResponse = {
695
+ channel_cid: this.channel.cid,
696
+ created_at: new Date().toISOString(),
697
+ message: draft as DraftMessage,
698
+ parent_id: draft.parent_id,
699
+ quoted_message: this.quotedMessage
700
+ ? unformatMessage(this.quotedMessage)
701
+ : undefined,
702
+ };
703
+ await this.client.offlineDb.upsertDraft({ draft: optimisticDraftResponse });
704
+ } catch (error) {
705
+ this.client.logger('error', `offlineDb:upsertDraft`, {
706
+ tags: ['channel', 'offlineDb'],
707
+ error,
708
+ });
709
+ }
710
+ }
661
711
  this.logDraftUpdateTimestamp();
662
712
  await this.channel.createDraft(draft);
663
713
  };
@@ -665,8 +715,64 @@ export class MessageComposer extends WithSubscriptions {
665
715
  deleteDraft = async () => {
666
716
  if (this.editedMessage || !this.config.drafts.enabled || !this.draftId) return;
667
717
  this.state.partialNext({ draftId: null }); // todo: should we clear the whole state?
718
+ const parentId = this.threadId ?? undefined;
719
+ if (this.client.offlineDb) {
720
+ try {
721
+ await this.client.offlineDb.deleteDraft({
722
+ cid: this.channel.cid,
723
+ parent_id: parentId,
724
+ });
725
+ } catch (error) {
726
+ this.client.logger('error', `offlineDb:deleteDraft`, {
727
+ tags: ['channel', 'offlineDb'],
728
+ error,
729
+ });
730
+ }
731
+ }
668
732
  this.logDraftUpdateTimestamp();
669
- await this.channel.deleteDraft({ parent_id: this.threadId ?? undefined });
733
+ await this.channel.deleteDraft({ parent_id: parentId });
734
+ };
735
+
736
+ getDraft = async () => {
737
+ if (this.editedMessage || !this.config.drafts.enabled || !this.client.userID) return;
738
+
739
+ const draftFromOfflineDB = await this.client.offlineDb?.getDraft({
740
+ cid: this.channel.cid,
741
+ userId: this.client.userID,
742
+ parent_id: this.threadId ?? undefined,
743
+ });
744
+
745
+ if (draftFromOfflineDB) {
746
+ this.initState({ composition: draftFromOfflineDB });
747
+ }
748
+
749
+ try {
750
+ const response = await this.channel.getDraft({
751
+ parent_id: this.threadId ?? undefined,
752
+ });
753
+
754
+ const { draft } = response;
755
+
756
+ if (!draft) return;
757
+
758
+ this.client.offlineDb?.executeQuerySafely(
759
+ (db) =>
760
+ db.upsertDraft({
761
+ draft,
762
+ }),
763
+ { method: 'upsertDraft' },
764
+ );
765
+
766
+ this.initState({ composition: draft });
767
+ } catch (error) {
768
+ this.client.notifications.add({
769
+ message: 'Failed to get the draft',
770
+ origin: {
771
+ emitter: 'MessageComposer',
772
+ context: { composer: this },
773
+ },
774
+ });
775
+ }
670
776
  };
671
777
 
672
778
  createPoll = async () => {
@@ -0,0 +1,72 @@
1
+ import type { MessageComposer } from '../../messageComposer';
2
+ import type {
3
+ MessageComposerMiddlewareState,
4
+ MessageCompositionMiddleware,
5
+ MessageDraftComposerMiddlewareValueState,
6
+ MessageDraftCompositionMiddleware,
7
+ } from '../messageComposer/types';
8
+ import type { MiddlewareHandlerParams } from '../../../middleware';
9
+
10
+ export const createCommandInjectionMiddleware = (
11
+ composer: MessageComposer,
12
+ ): MessageCompositionMiddleware => ({
13
+ handlers: {
14
+ compose: ({
15
+ forward,
16
+ next,
17
+ state,
18
+ }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
19
+ const command = composer.textComposer.command;
20
+ if (!command) {
21
+ return forward();
22
+ }
23
+ const { text } = state.localMessage;
24
+
25
+ const injection = `/${command?.name}`;
26
+ const enrichedText = `${injection} ${text}`;
27
+
28
+ return next({
29
+ ...state,
30
+ localMessage: {
31
+ ...state.localMessage,
32
+ text: enrichedText,
33
+ },
34
+ message: {
35
+ ...state.message,
36
+ text: enrichedText,
37
+ },
38
+ });
39
+ },
40
+ },
41
+ id: 'stream-io/message-composer-middleware/command-string-injection',
42
+ });
43
+
44
+ export const createDraftCommandInjectionMiddleware = (
45
+ composer: MessageComposer,
46
+ ): MessageDraftCompositionMiddleware => ({
47
+ handlers: {
48
+ compose: ({
49
+ forward,
50
+ next,
51
+ state,
52
+ }: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
53
+ const command = composer.textComposer.command;
54
+ if (!command) {
55
+ return forward();
56
+ }
57
+ const { text } = state.draft;
58
+
59
+ const injection = `/${command?.name}`;
60
+ const enrichedText = `${injection} ${text}`;
61
+
62
+ return next({
63
+ ...state,
64
+ draft: {
65
+ ...state.draft,
66
+ text: enrichedText,
67
+ },
68
+ });
69
+ },
70
+ },
71
+ id: 'stream-io/message-composer-middleware/draft-command-string-injection',
72
+ });
@@ -7,3 +7,4 @@ export * from './MessageComposerMiddlewareExecutor';
7
7
  export * from './messageComposerState';
8
8
  export * from './textComposer';
9
9
  export * from './types';
10
+ export * from './commandInjection';
@@ -0,0 +1,20 @@
1
+ import type { Middleware } from '../../../middleware';
2
+ import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
3
+
4
+ export type PreCommandMiddleware = Middleware<
5
+ TextComposerMiddlewareExecutorState,
6
+ 'onChange' | 'onSuggestionItemSelect'
7
+ >;
8
+
9
+ export const createActiveCommandGuardMiddleware = (): PreCommandMiddleware => ({
10
+ handlers: {
11
+ onChange: ({ complete, forward, state }) => {
12
+ if (state.command) {
13
+ return complete(state);
14
+ }
15
+ return forward();
16
+ },
17
+ onSuggestionItemSelect: ({ forward }) => forward(),
18
+ },
19
+ id: 'stream-io/text-composer/active-command-guard',
20
+ });
@@ -0,0 +1,56 @@
1
+ import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
2
+ import type { CommandSuggestion } from './types';
3
+ import type { Middleware } from '../../../middleware';
4
+ import { escapeRegExp } from './textMiddlewareUtils';
5
+
6
+ export type CommandStringExtractionMiddleware = Middleware<
7
+ TextComposerMiddlewareExecutorState<CommandSuggestion>,
8
+ 'onChange' | 'onSuggestionItemSelect'
9
+ >;
10
+
11
+ const stripCommandFromText = (text: string, commandName: string) =>
12
+ text.replace(new RegExp(`^${escapeRegExp(`/${commandName}`)}\\s*`), '');
13
+
14
+ export const createCommandStringExtractionMiddleware =
15
+ (): CommandStringExtractionMiddleware => ({
16
+ handlers: {
17
+ onChange: ({ complete, forward, state }) => {
18
+ const { command } = state;
19
+
20
+ if (!command?.name) {
21
+ return forward();
22
+ }
23
+
24
+ const newText = stripCommandFromText(state.text, command.name);
25
+
26
+ return complete({
27
+ ...state,
28
+ selection: {
29
+ end: newText.length,
30
+ start: newText.length,
31
+ },
32
+ text: newText,
33
+ });
34
+ },
35
+ onSuggestionItemSelect: ({ next, forward, state }) => {
36
+ const { command } = state;
37
+
38
+ if (!command) {
39
+ return forward();
40
+ }
41
+
42
+ const triggerWithCommand = `/${command?.name} `;
43
+
44
+ const newText = state.text.slice(triggerWithCommand.length);
45
+ return next({
46
+ ...state,
47
+ selection: {
48
+ end: newText.length,
49
+ start: newText.length,
50
+ },
51
+ text: newText,
52
+ });
53
+ },
54
+ },
55
+ id: 'stream-io/text-composer/command-string-extraction',
56
+ });