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.
- package/dist/cjs/index.browser.cjs +532 -57
- package/dist/cjs/index.browser.cjs.map +4 -4
- package/dist/cjs/index.node.cjs +538 -57
- package/dist/cjs/index.node.cjs.map +4 -4
- package/dist/esm/index.js +532 -57
- package/dist/esm/index.js.map +4 -4
- 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/messageComposer/commandInjection.d.ts +4 -0
- package/dist/types/messageComposer/middleware/messageComposer/index.d.ts +1 -0
- package/dist/types/messageComposer/middleware/textComposer/activeCommandGuard.d.ts +4 -0
- package/dist/types/messageComposer/middleware/textComposer/commandStringExtraction.d.ts +5 -0
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +5 -5
- package/dist/types/messageComposer/middleware/textComposer/index.d.ts +2 -0
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +1 -2
- package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +6 -0
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +3 -2
- package/dist/types/messageComposer/textComposer.d.ts +6 -3
- 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/pagination/BasePaginator.d.ts +1 -1
- package/dist/types/pagination/ReminderPaginator.d.ts +6 -2
- package/dist/types/reminders/ReminderManager.d.ts +3 -3
- 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 +6 -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 +120 -14
- package/src/messageComposer/middleware/messageComposer/commandInjection.ts +72 -0
- package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
- package/src/messageComposer/middleware/textComposer/activeCommandGuard.ts +20 -0
- package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +56 -0
- package/src/messageComposer/middleware/textComposer/commands.ts +28 -11
- package/src/messageComposer/middleware/textComposer/index.ts +2 -0
- package/src/messageComposer/middleware/textComposer/mentions.ts +1 -2
- package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +14 -0
- package/src/messageComposer/middleware/textComposer/types.ts +3 -2
- package/src/messageComposer/textComposer.ts +23 -3
- package/src/offline-support/offline_support_api.ts +79 -0
- package/src/offline-support/types.ts +41 -1
- package/src/pagination/BasePaginator.ts +1 -1
- package/src/pagination/ReminderPaginator.ts +20 -2
- package/src/reminders/ReminderManager.ts +16 -2
- 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 +9 -2
- 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
|
+
};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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
|
|
50
|
+
* and sets the status to `received` if missing; returns a new LocalMessage object.
|
|
51
51
|
*
|
|
52
|
-
* @param {
|
|
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
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 && !
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
});
|
|
@@ -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
|
+
});
|