stream-chat 9.6.0 → 9.7.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.
- package/dist/cjs/index.browser.cjs +383 -65
- package/dist/cjs/index.browser.cjs.map +3 -3
- package/dist/cjs/index.node.cjs +387 -68
- package/dist/cjs/index.node.cjs.map +3 -3
- package/dist/esm/index.js +383 -65
- package/dist/esm/index.js.map +3 -3
- package/dist/types/channel.d.ts +36 -4
- package/dist/types/client.d.ts +38 -0
- package/dist/types/messageComposer/messageComposer.d.ts +4 -1
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +5 -5
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +1 -2
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +2 -2
- package/dist/types/offline-support/offline_support_api.d.ts +39 -0
- package/dist/types/offline-support/types.d.ts +36 -2
- package/dist/types/search/BaseSearchSource.d.ts +37 -31
- package/dist/types/search/ChannelSearchSource.d.ts +1 -1
- package/dist/types/search/MessageSearchSource.d.ts +1 -1
- package/dist/types/search/UserSearchSource.d.ts +1 -1
- package/dist/types/search/index.d.ts +1 -0
- package/dist/types/search/types.d.ts +20 -0
- package/dist/types/types.d.ts +5 -1
- package/dist/types/utils/WithSubscriptions.d.ts +7 -2
- package/dist/types/utils.d.ts +11 -2
- package/package.json +1 -1
- package/src/channel.ts +85 -10
- package/src/client.ts +61 -3
- package/src/messageComposer/messageComposer.ts +136 -32
- package/src/messageComposer/middleware/textComposer/commands.ts +6 -7
- package/src/messageComposer/middleware/textComposer/mentions.ts +1 -2
- package/src/messageComposer/middleware/textComposer/types.ts +2 -2
- package/src/offline-support/offline_support_api.ts +79 -0
- package/src/offline-support/types.ts +41 -1
- package/src/search/BaseSearchSource.ts +123 -52
- package/src/search/ChannelSearchSource.ts +1 -1
- package/src/search/MessageSearchSource.ts +1 -1
- package/src/search/UserSearchSource.ts +1 -1
- package/src/search/index.ts +1 -0
- package/src/search/types.ts +20 -0
- package/src/types.ts +8 -1
- package/src/utils/WithSubscriptions.ts +16 -2
- package/src/utils.ts +31 -2
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
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -114,8 +115,6 @@ const initState = (
|
|
|
114
115
|
};
|
|
115
116
|
};
|
|
116
117
|
|
|
117
|
-
const noop = () => undefined;
|
|
118
|
-
|
|
119
118
|
export class MessageComposer extends WithSubscriptions {
|
|
120
119
|
readonly channel: Channel;
|
|
121
120
|
readonly state: StateStore<MessageComposerState>;
|
|
@@ -282,6 +281,10 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
282
281
|
}
|
|
283
282
|
|
|
284
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
|
+
}
|
|
285
288
|
return !!(
|
|
286
289
|
(!this.attachmentManager.uploadsInProgressCount &&
|
|
287
290
|
(!this.textComposer.textIsEmpty ||
|
|
@@ -344,6 +347,25 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
344
347
|
}
|
|
345
348
|
};
|
|
346
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
|
+
|
|
347
369
|
initEditingAuditState = (
|
|
348
370
|
composition?: DraftResponse | MessageResponse | LocalMessage,
|
|
349
371
|
) => initEditingAuditState(composition);
|
|
@@ -362,24 +384,34 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
362
384
|
});
|
|
363
385
|
}
|
|
364
386
|
|
|
387
|
+
public registerDraftEventSubscriptions = () => {
|
|
388
|
+
const unsubscribeDraftUpdated = this.subscribeDraftUpdated();
|
|
389
|
+
const unsubscribeDraftDeleted = this.subscribeDraftDeleted();
|
|
390
|
+
|
|
391
|
+
return () => {
|
|
392
|
+
unsubscribeDraftUpdated();
|
|
393
|
+
unsubscribeDraftDeleted();
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
365
397
|
public registerSubscriptions = (): UnregisterSubscriptions => {
|
|
366
|
-
if (this.hasSubscriptions) {
|
|
367
|
-
|
|
368
|
-
|
|
398
|
+
if (!this.hasSubscriptions) {
|
|
399
|
+
this.addUnsubscribeFunction(this.subscribeMessageComposerSetupStateChange());
|
|
400
|
+
this.addUnsubscribeFunction(this.subscribeMessageUpdated());
|
|
401
|
+
this.addUnsubscribeFunction(this.subscribeMessageDeleted());
|
|
402
|
+
|
|
403
|
+
this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
|
|
404
|
+
this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
|
|
405
|
+
this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
|
|
406
|
+
this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
|
|
407
|
+
this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
|
|
408
|
+
this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
|
|
409
|
+
this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
|
|
369
410
|
}
|
|
370
|
-
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
|
|
375
|
-
this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
|
|
376
|
-
this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
|
|
377
|
-
this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
|
|
378
|
-
this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
|
|
379
|
-
this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
|
|
380
|
-
this.addUnsubscribeFunction(this.subscribeMessageComposerConfigStateChanged());
|
|
381
|
-
|
|
382
|
-
return this.unregisterSubscriptions.bind(this);
|
|
411
|
+
|
|
412
|
+
this.incrementRefCount();
|
|
413
|
+
|
|
414
|
+
return () => this.unregisterSubscriptions();
|
|
383
415
|
};
|
|
384
416
|
|
|
385
417
|
private subscribeMessageUpdated = () => {
|
|
@@ -440,7 +472,7 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
440
472
|
const draft = event.draft as DraftResponse;
|
|
441
473
|
if (
|
|
442
474
|
!draft ||
|
|
443
|
-
|
|
475
|
+
(draft.parent_id ?? null) !== (this.threadId ?? null) ||
|
|
444
476
|
draft.channel_cid !== this.channel.cid
|
|
445
477
|
)
|
|
446
478
|
return;
|
|
@@ -452,7 +484,7 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
452
484
|
const draft = event.draft as DraftResponse;
|
|
453
485
|
if (
|
|
454
486
|
!draft ||
|
|
455
|
-
|
|
487
|
+
(draft.parent_id ?? null) !== (this.threadId ?? null) ||
|
|
456
488
|
draft.channel_cid !== this.channel.cid
|
|
457
489
|
) {
|
|
458
490
|
return;
|
|
@@ -550,7 +582,7 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
550
582
|
});
|
|
551
583
|
|
|
552
584
|
private subscribeMessageComposerConfigStateChanged = () => {
|
|
553
|
-
let
|
|
585
|
+
let draftUnsubscribeFunction: Unsubscribe | null;
|
|
554
586
|
|
|
555
587
|
const unsubscribe = this.configState.subscribeWithSelector(
|
|
556
588
|
(currentValue) => ({
|
|
@@ -565,20 +597,17 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
565
597
|
});
|
|
566
598
|
}
|
|
567
599
|
|
|
568
|
-
if (draftsEnabled && !
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
} else if (!draftsEnabled && draftUnsubscribeFunctions) {
|
|
574
|
-
draftUnsubscribeFunctions.forEach((fn) => fn());
|
|
575
|
-
draftUnsubscribeFunctions = null;
|
|
600
|
+
if (draftsEnabled && !draftUnsubscribeFunction) {
|
|
601
|
+
draftUnsubscribeFunction = this.registerDraftEventSubscriptions();
|
|
602
|
+
} else if (!draftsEnabled && draftUnsubscribeFunction) {
|
|
603
|
+
draftUnsubscribeFunction();
|
|
604
|
+
draftUnsubscribeFunction = null;
|
|
576
605
|
}
|
|
577
606
|
},
|
|
578
607
|
);
|
|
579
608
|
|
|
580
609
|
return () => {
|
|
581
|
-
|
|
610
|
+
draftUnsubscribeFunction?.();
|
|
582
611
|
unsubscribe();
|
|
583
612
|
};
|
|
584
613
|
};
|
|
@@ -660,6 +689,25 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
660
689
|
if (!composition) return;
|
|
661
690
|
const { draft } = composition;
|
|
662
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
|
+
}
|
|
663
711
|
this.logDraftUpdateTimestamp();
|
|
664
712
|
await this.channel.createDraft(draft);
|
|
665
713
|
};
|
|
@@ -667,8 +715,64 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
667
715
|
deleteDraft = async () => {
|
|
668
716
|
if (this.editedMessage || !this.config.drafts.enabled || !this.draftId) return;
|
|
669
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
|
+
}
|
|
670
732
|
this.logDraftUpdateTimestamp();
|
|
671
|
-
await this.channel.deleteDraft({ parent_id:
|
|
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
|
+
}
|
|
672
776
|
};
|
|
673
777
|
|
|
674
778
|
createPoll = async () => {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Channel } from '../../../channel';
|
|
2
2
|
import type { Middleware } from '../../../middleware';
|
|
3
3
|
import type { SearchSourceOptions } from '../../../search';
|
|
4
|
-
import {
|
|
4
|
+
import { BaseSearchSourceSync } from '../../../search';
|
|
5
5
|
import type { CommandResponse } from '../../../types';
|
|
6
6
|
import { mergeWith } from '../../../utils/mergeWith';
|
|
7
7
|
import type { CommandSuggestion, TextComposerMiddlewareOptions } from './types';
|
|
8
8
|
import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils';
|
|
9
9
|
import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
|
|
10
10
|
|
|
11
|
-
export class CommandSearchSource extends
|
|
11
|
+
export class CommandSearchSource extends BaseSearchSourceSync<CommandSuggestion> {
|
|
12
12
|
readonly type = 'commands';
|
|
13
13
|
private channel: Channel;
|
|
14
14
|
|
|
@@ -64,15 +64,14 @@ export class CommandSearchSource extends BaseSearchSource<CommandSuggestion> {
|
|
|
64
64
|
|
|
65
65
|
return 0;
|
|
66
66
|
});
|
|
67
|
-
|
|
67
|
+
|
|
68
|
+
return {
|
|
68
69
|
items: selectedCommands.map((c) => ({ ...c, id: c.name })),
|
|
69
70
|
next: null,
|
|
70
|
-
}
|
|
71
|
+
};
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
protected filterQueryResults(
|
|
74
|
-
items: CommandSuggestion[],
|
|
75
|
-
): CommandSuggestion[] | Promise<CommandSuggestion[]> {
|
|
74
|
+
protected filterQueryResults(items: CommandSuggestion[]) {
|
|
76
75
|
return items;
|
|
77
76
|
}
|
|
78
77
|
}
|
|
@@ -3,8 +3,7 @@ import {
|
|
|
3
3
|
getTriggerCharWithToken,
|
|
4
4
|
insertItemWithTrigger,
|
|
5
5
|
} from './textMiddlewareUtils';
|
|
6
|
-
import type
|
|
7
|
-
import { BaseSearchSource } from '../../../search';
|
|
6
|
+
import { BaseSearchSource, type SearchSourceOptions } from '../../../search';
|
|
8
7
|
import { mergeWith } from '../../../utils/mergeWith';
|
|
9
8
|
import type { TextComposerMiddlewareOptions, UserSuggestion } from './types';
|
|
10
9
|
import type { StreamChat } from '../../../client';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MessageComposer } from '../../messageComposer';
|
|
2
2
|
import type { CommandResponse, UserResponse } from '../../../types';
|
|
3
3
|
import type { TokenizationPayload } from './textMiddlewareUtils';
|
|
4
|
-
import type { SearchSource } from '../../../search';
|
|
4
|
+
import type { SearchSource, SearchSourceSync } from '../../../search';
|
|
5
5
|
import type { CustomTextComposerSuggestion } from '../../types.custom';
|
|
6
6
|
|
|
7
7
|
export type TextComposerSuggestion<T = unknown> = T & {
|
|
@@ -28,7 +28,7 @@ export type TextComposerMiddlewareExecutorOptions = {
|
|
|
28
28
|
|
|
29
29
|
export type Suggestions<T extends Suggestion = Suggestion> = {
|
|
30
30
|
query: string;
|
|
31
|
-
searchSource: SearchSource<T>;
|
|
31
|
+
searchSource: SearchSource<T> | SearchSourceSync<T>;
|
|
32
32
|
trigger: string;
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -194,6 +194,35 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
194
194
|
*/
|
|
195
195
|
abstract updateMessage: OfflineDBApi['updateMessage'];
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @abstract
|
|
199
|
+
* Fetches the provided draft from the DB. Should return as close to
|
|
200
|
+
* the server side DraftResponse as possible.
|
|
201
|
+
* @param {DBGetDraftType} options
|
|
202
|
+
* @returns {Promise<DraftResponse | null>}
|
|
203
|
+
*/
|
|
204
|
+
abstract getDraft: OfflineDBApi['getDraft'];
|
|
205
|
+
/**
|
|
206
|
+
* @abstract
|
|
207
|
+
* Upserts a draft in the DB.
|
|
208
|
+
* Will write to the draft table upserting the draft.
|
|
209
|
+
* Will return the prepared queries for delayed execution (even if they are
|
|
210
|
+
* already executed).
|
|
211
|
+
* @param {DBUpsertDraftType} options
|
|
212
|
+
* @returns {Promise<ExecuteBatchDBQueriesType>}
|
|
213
|
+
*/
|
|
214
|
+
abstract upsertDraft: OfflineDBApi['upsertDraft'];
|
|
215
|
+
/**
|
|
216
|
+
* @abstract
|
|
217
|
+
* Deletes a draft from the DB.
|
|
218
|
+
* Will write to the draft table removing the draft.
|
|
219
|
+
* Will return the prepared queries for delayed execution (even if they are
|
|
220
|
+
* already executed).
|
|
221
|
+
* @param {DBDeleteDraftType} options
|
|
222
|
+
* @returns {Promise<ExecuteBatchDBQueriesType>}
|
|
223
|
+
*/
|
|
224
|
+
abstract deleteDraft: OfflineDBApi['deleteDraft'];
|
|
225
|
+
|
|
197
226
|
/**
|
|
198
227
|
* @abstract
|
|
199
228
|
* Fetches the provided channels from the DB and aggregates all data associated
|
|
@@ -896,6 +925,44 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
896
925
|
);
|
|
897
926
|
};
|
|
898
927
|
|
|
928
|
+
/**
|
|
929
|
+
* A utility handler for all draft events:
|
|
930
|
+
* - draft.updated -> updateDraft
|
|
931
|
+
* - draft.deleted -> deleteDraft
|
|
932
|
+
* @param event - the WS event we are trying to process
|
|
933
|
+
* @param execute - whether to immediately execute the operation.
|
|
934
|
+
*/
|
|
935
|
+
handleDraftEvent = async ({
|
|
936
|
+
event,
|
|
937
|
+
execute = true,
|
|
938
|
+
}: {
|
|
939
|
+
event: Event;
|
|
940
|
+
execute?: boolean;
|
|
941
|
+
}) => {
|
|
942
|
+
const { cid, draft, type } = event;
|
|
943
|
+
|
|
944
|
+
if (!draft) return [];
|
|
945
|
+
|
|
946
|
+
if (type === 'draft.updated') {
|
|
947
|
+
return await this.upsertDraft({
|
|
948
|
+
draft,
|
|
949
|
+
execute,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (type === 'draft.deleted') {
|
|
954
|
+
if (!cid) return [];
|
|
955
|
+
|
|
956
|
+
return await this.deleteDraft({
|
|
957
|
+
cid,
|
|
958
|
+
parent_id: draft.parent_id,
|
|
959
|
+
execute,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return [];
|
|
964
|
+
};
|
|
965
|
+
|
|
899
966
|
/**
|
|
900
967
|
* A generic event handler that decides which DB API to invoke based on
|
|
901
968
|
* event.type for all events we are currently handling. It is used to both
|
|
@@ -944,6 +1011,10 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
944
1011
|
return await this.handleChannelVisibilityEvent({ event, execute });
|
|
945
1012
|
}
|
|
946
1013
|
|
|
1014
|
+
if (type === 'draft.updated' || type === 'draft.deleted') {
|
|
1015
|
+
return await this.handleDraftEvent({ event, execute });
|
|
1016
|
+
}
|
|
1017
|
+
|
|
947
1018
|
// Note: It is a bit counter-intuitive that we do not touch the messages in the
|
|
948
1019
|
// offline DB when receiving notification.message_new, however we do this
|
|
949
1020
|
// because we anyway cannot get the messages for a channel until we run
|
|
@@ -1050,6 +1121,14 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1050
1121
|
return await channel._deleteReaction(...task.payload);
|
|
1051
1122
|
}
|
|
1052
1123
|
|
|
1124
|
+
if (task.type === 'create-draft') {
|
|
1125
|
+
return await channel._createDraft(...task.payload);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (task.type === 'delete-draft') {
|
|
1129
|
+
return await channel._deleteDraft(...task.payload);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1053
1132
|
if (task.type === 'send-message') {
|
|
1054
1133
|
const newMessageResponse = await channel._sendMessage(...task.payload);
|
|
1055
1134
|
const newMessage = newMessageResponse?.message;
|