stream-chat 9.50.3 → 9.52.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.js +601 -275
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +610 -276
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +599 -273
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/channel.d.ts +4 -3
- package/dist/types/client.d.ts +23 -9
- package/dist/types/index.d.ts +7 -0
- package/dist/types/messageComposer/attachmentIdentity.d.ts +4 -0
- package/dist/types/messageComposer/attachmentManager.d.ts +8 -0
- package/dist/types/messageComposer/messageComposer.d.ts +13 -0
- package/dist/types/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.d.ts +20 -1
- package/dist/types/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.d.ts +6 -1
- package/dist/types/messageComposer/middleware/messageComposer/attachments.d.ts +24 -0
- package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +22 -1
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +15 -9
- package/dist/types/messageComposer/types.d.ts +5 -0
- package/dist/types/middleware.d.ts +8 -0
- package/dist/types/offline-support/offline_support_api.d.ts +7 -1
- package/dist/types/offline-support/offline_sync_manager.d.ts +12 -1
- package/dist/types/search/BaseSearchSource.d.ts +42 -24
- package/dist/types/search/ChannelMemberSearchSource.d.ts +9 -4
- package/dist/types/search/ChannelSearchSource.d.ts +9 -3
- package/dist/types/search/MessageSearchSource.d.ts +14 -3
- package/dist/types/search/UserSearchSource.d.ts +9 -3
- package/dist/types/search/types.d.ts +14 -1
- package/dist/types/types.d.ts +12 -1
- package/dist/types/uploadManager.d.ts +30 -0
- package/dist/types/utils.d.ts +21 -2
- package/package.json +7 -7
- package/src/channel.ts +20 -8
- package/src/client.ts +71 -13
- package/src/index.ts +12 -0
- package/src/messageComposer/attachmentIdentity.ts +27 -0
- package/src/messageComposer/attachmentManager.ts +61 -12
- package/src/messageComposer/messageComposer.ts +43 -8
- package/src/messageComposer/middleware/attachmentManager/postUpload/AttachmentPostUploadMiddlewareExecutor.ts +1 -1
- package/src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts +58 -33
- package/src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts +4 -0
- package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +9 -0
- package/src/messageComposer/middleware/messageComposer/attachments.ts +109 -0
- package/src/messageComposer/middleware/messageComposer/types.ts +22 -1
- package/src/messageComposer/middleware/textComposer/mentions.ts +75 -23
- package/src/messageComposer/types.ts +5 -0
- package/src/middleware.ts +11 -0
- package/src/offline-support/offline_support_api.ts +21 -2
- package/src/offline-support/offline_sync_manager.ts +36 -8
- package/src/search/BaseSearchSource.ts +123 -39
- package/src/search/ChannelMemberSearchSource.ts +22 -11
- package/src/search/ChannelSearchSource.ts +16 -5
- package/src/search/MessageSearchSource.ts +36 -4
- package/src/search/UserSearchSource.ts +16 -5
- package/src/search/types.ts +14 -1
- package/src/types.ts +13 -1
- package/src/uploadManager.ts +45 -0
- package/src/utils.ts +78 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import type { FilterBuilderOptions } from '../pagination';
|
|
3
3
|
import { FilterBuilder } from '../pagination';
|
|
4
4
|
import type { Channel } from '../channel';
|
|
@@ -12,6 +12,13 @@ export type ChannelSearchSourceFilterBuilderContext<
|
|
|
12
12
|
C extends CustomContext = CustomContext,
|
|
13
13
|
> = { searchQuery?: string } & C;
|
|
14
14
|
|
|
15
|
+
export type ChannelSearchSourceOptions = SearchSourceOptions & {
|
|
16
|
+
/** Static base filters merged under the dynamically generated ones. */
|
|
17
|
+
filters?: ChannelFilters;
|
|
18
|
+
sort?: ChannelSort;
|
|
19
|
+
searchOptions?: Omit<ChannelOptions, 'limit' | 'offset'>;
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
export class ChannelSearchSource<
|
|
16
23
|
TFilterContext extends CustomContext = CustomContext,
|
|
17
24
|
> extends BaseSearchSource<Channel> {
|
|
@@ -27,14 +34,18 @@ export class ChannelSearchSource<
|
|
|
27
34
|
|
|
28
35
|
constructor(
|
|
29
36
|
client: StreamChat,
|
|
30
|
-
options?:
|
|
37
|
+
options?: ChannelSearchSourceOptions,
|
|
31
38
|
filterBuilderOptions: FilterBuilderOptions<
|
|
32
39
|
ChannelFilters,
|
|
33
40
|
ChannelSearchSourceFilterBuilderContext<TFilterContext>
|
|
34
41
|
> = {},
|
|
35
42
|
) {
|
|
36
|
-
|
|
43
|
+
const { filters, sort, searchOptions, ...restOptions } = options || {};
|
|
44
|
+
super(restOptions);
|
|
37
45
|
this.client = client;
|
|
46
|
+
this.filters = filters;
|
|
47
|
+
this.sort = sort;
|
|
48
|
+
this.searchOptions = searchOptions;
|
|
38
49
|
this.filterBuilder = new FilterBuilder<
|
|
39
50
|
ChannelFilters,
|
|
40
51
|
ChannelSearchSourceFilterBuilderContext<TFilterContext>
|
|
@@ -51,7 +62,7 @@ export class ChannelSearchSource<
|
|
|
51
62
|
});
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
protected async query(searchQuery: string) {
|
|
65
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
55
66
|
const filters = this.filterBuilder.buildFilters({
|
|
56
67
|
baseFilters: {
|
|
57
68
|
...(this.client.userID ? { members: { $in: [this.client.userID] } } : {}),
|
|
@@ -63,7 +74,7 @@ export class ChannelSearchSource<
|
|
|
63
74
|
});
|
|
64
75
|
const sort = this.sort ?? {};
|
|
65
76
|
const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
|
|
66
|
-
const items = await this.client.queryChannels(filters, sort, options);
|
|
77
|
+
const items = await this.client.queryChannels(filters, sort, options, queryOptions);
|
|
67
78
|
return { items };
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import type {
|
|
3
3
|
ChannelFilters,
|
|
4
4
|
ChannelOptions,
|
|
@@ -54,6 +54,18 @@ export type MessageSearchSourceFilterBuilderOptions<
|
|
|
54
54
|
>;
|
|
55
55
|
}>;
|
|
56
56
|
|
|
57
|
+
export type MessageSearchSourceOptions = SearchSourceOptions & {
|
|
58
|
+
/** Static base filters for the channel scope of the message search. */
|
|
59
|
+
messageSearchChannelFilters?: ChannelFilters;
|
|
60
|
+
/** Static base filters for the message search itself. */
|
|
61
|
+
messageSearchFilters?: MessageFilters;
|
|
62
|
+
messageSearchSort?: SearchMessageSort;
|
|
63
|
+
/** Static base filters for the follow-up query that hydrates unknown channels. */
|
|
64
|
+
channelQueryFilters?: ChannelFilters;
|
|
65
|
+
channelQuerySort?: ChannelSort;
|
|
66
|
+
channelQueryOptions?: Omit<ChannelOptions, 'limit' | 'offset'>;
|
|
67
|
+
};
|
|
68
|
+
|
|
57
69
|
export class MessageSearchSource<
|
|
58
70
|
TContexts extends MessageSearchSourceContexts = {},
|
|
59
71
|
> extends BaseSearchSource<MessageResponse> {
|
|
@@ -86,11 +98,26 @@ export class MessageSearchSource<
|
|
|
86
98
|
|
|
87
99
|
constructor(
|
|
88
100
|
client: StreamChat,
|
|
89
|
-
options?:
|
|
101
|
+
options?: MessageSearchSourceOptions,
|
|
90
102
|
filterBuilderOptions?: MessageSearchSourceFilterBuilderOptions<TContexts>,
|
|
91
103
|
) {
|
|
92
|
-
|
|
104
|
+
const {
|
|
105
|
+
messageSearchChannelFilters,
|
|
106
|
+
messageSearchFilters,
|
|
107
|
+
messageSearchSort,
|
|
108
|
+
channelQueryFilters,
|
|
109
|
+
channelQuerySort,
|
|
110
|
+
channelQueryOptions,
|
|
111
|
+
...restOptions
|
|
112
|
+
} = options || {};
|
|
113
|
+
super(restOptions);
|
|
93
114
|
this.client = client;
|
|
115
|
+
this.messageSearchChannelFilters = messageSearchChannelFilters;
|
|
116
|
+
this.messageSearchFilters = messageSearchFilters;
|
|
117
|
+
this.messageSearchSort = messageSearchSort;
|
|
118
|
+
this.channelQueryFilters = channelQueryFilters;
|
|
119
|
+
this.channelQuerySort = channelQuerySort;
|
|
120
|
+
this.channelQueryOptions = channelQueryOptions;
|
|
94
121
|
|
|
95
122
|
this.messageSearchChannelFilterBuilder = new FilterBuilder<
|
|
96
123
|
ChannelFilters,
|
|
@@ -129,7 +156,7 @@ export class MessageSearchSource<
|
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
protected async query(searchQuery: string) {
|
|
159
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
133
160
|
if (!this.client.userID || this.next === null) return { items: [] };
|
|
134
161
|
|
|
135
162
|
const channelFilters = this.messageSearchChannelFilterBuilder.buildFilters({
|
|
@@ -170,9 +197,13 @@ export class MessageSearchSource<
|
|
|
170
197
|
channelFilters,
|
|
171
198
|
messageFilters,
|
|
172
199
|
options,
|
|
200
|
+
queryOptions,
|
|
173
201
|
);
|
|
174
202
|
const items = results.map(({ message }) => message);
|
|
175
203
|
|
|
204
|
+
// a newer query already replaced this one - skip the cid scan and the hydration request
|
|
205
|
+
if (queryOptions.signal?.aborted) return { items, next };
|
|
206
|
+
|
|
176
207
|
const cids = Array.from(
|
|
177
208
|
items.reduce((acc, message) => {
|
|
178
209
|
if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
|
|
@@ -194,6 +225,7 @@ export class MessageSearchSource<
|
|
|
194
225
|
...this.channelQuerySort,
|
|
195
226
|
},
|
|
196
227
|
this.channelQueryOptions,
|
|
228
|
+
queryOptions,
|
|
197
229
|
);
|
|
198
230
|
}
|
|
199
231
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import { FilterBuilder, type FilterBuilderOptions } from '../pagination';
|
|
3
3
|
import type { StreamChat } from '../client';
|
|
4
4
|
import type { UserFilters, UserOptions, UserResponse, UserSort } from '../types';
|
|
@@ -10,6 +10,13 @@ export type UserSearchSourceFilterBuilderContext<
|
|
|
10
10
|
C extends CustomContext = CustomContext,
|
|
11
11
|
> = { searchQuery?: string } & C;
|
|
12
12
|
|
|
13
|
+
export type UserSearchSourceOptions = SearchSourceOptions & {
|
|
14
|
+
/** Static base filters merged under the dynamically generated ones. */
|
|
15
|
+
filters?: UserFilters;
|
|
16
|
+
sort?: UserSort;
|
|
17
|
+
searchOptions?: Omit<UserOptions, 'limit' | 'offset'>;
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
export class UserSearchSource<
|
|
14
21
|
TFilterContext extends CustomContext = CustomContext,
|
|
15
22
|
> extends BaseSearchSource<UserResponse> {
|
|
@@ -25,14 +32,18 @@ export class UserSearchSource<
|
|
|
25
32
|
|
|
26
33
|
constructor(
|
|
27
34
|
client: StreamChat,
|
|
28
|
-
options?:
|
|
35
|
+
options?: UserSearchSourceOptions,
|
|
29
36
|
filterBuilderOptions: FilterBuilderOptions<
|
|
30
37
|
UserFilters,
|
|
31
38
|
UserSearchSourceFilterBuilderContext<TFilterContext>
|
|
32
39
|
> = {},
|
|
33
40
|
) {
|
|
34
|
-
|
|
41
|
+
const { filters, sort, searchOptions, ...restOptions } = options || {};
|
|
42
|
+
super(restOptions);
|
|
35
43
|
this.client = client;
|
|
44
|
+
this.filters = filters;
|
|
45
|
+
this.sort = sort;
|
|
46
|
+
this.searchOptions = searchOptions;
|
|
36
47
|
this.filterBuilder = new FilterBuilder<
|
|
37
48
|
UserFilters,
|
|
38
49
|
UserSearchSourceFilterBuilderContext<TFilterContext>
|
|
@@ -55,7 +66,7 @@ export class UserSearchSource<
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
protected async query(searchQuery: string) {
|
|
69
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
59
70
|
const filters = this.filterBuilder.buildFilters({
|
|
60
71
|
baseFilters: this.filters,
|
|
61
72
|
context: { searchQuery } as UserSearchSourceFilterBuilderContext<TFilterContext>,
|
|
@@ -68,7 +79,7 @@ export class UserSearchSource<
|
|
|
68
79
|
sort = { id: 1, ...this.sort };
|
|
69
80
|
}
|
|
70
81
|
const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
|
|
71
|
-
const { users } = await this.client.queryUsers(filters, sort, options);
|
|
82
|
+
const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
|
|
72
83
|
return { items: users };
|
|
73
84
|
}
|
|
74
85
|
|
package/src/search/types.ts
CHANGED
|
@@ -11,8 +11,21 @@ export type SearchSourceState<T = any> = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export type SearchSourceOptions = {
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Legacy single debounce interval. When set, it applies to both short and long queries,
|
|
16
|
+
* unless overridden by `shortQueryDebounceMs` / `longQueryDebounceMs`.
|
|
17
|
+
*/
|
|
15
18
|
debounceMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Debounce interval for queries no longer than `shortQueryMaxLength`. Such queries have
|
|
21
|
+
* low selectivity and are expensive server-side, so they are debounced harder.
|
|
22
|
+
* Defaults to 500ms.
|
|
23
|
+
*/
|
|
24
|
+
shortQueryDebounceMs?: number;
|
|
25
|
+
/** Debounce interval for queries longer than `shortQueryMaxLength`. Defaults to 300ms. */
|
|
26
|
+
longQueryDebounceMs?: number;
|
|
27
|
+
/** Query length (inclusive) that still counts as short. Defaults to 2. */
|
|
28
|
+
shortQueryMaxLength?: number;
|
|
16
29
|
pageSize?: number;
|
|
17
30
|
/** When true, the source can execute queries with an empty search string. Defaults to false. */
|
|
18
31
|
allowEmptySearchString?: boolean;
|
package/src/types.ts
CHANGED
|
@@ -1189,7 +1189,13 @@ export type ChannelQueryOptions = {
|
|
|
1189
1189
|
watchers?: PaginationOptions;
|
|
1190
1190
|
};
|
|
1191
1191
|
|
|
1192
|
-
|
|
1192
|
+
/**
|
|
1193
|
+
* Composed with RequestOptions because `queryChannels` carries the abort signal in this
|
|
1194
|
+
* bag; neither the state flags nor the signal are serialized into the request. Declaring
|
|
1195
|
+
* the composition keeps passing a RequestOptions here intentional rather than incidental
|
|
1196
|
+
* structural compatibility.
|
|
1197
|
+
*/
|
|
1198
|
+
export type ChannelStateOptions = RequestOptions & {
|
|
1193
1199
|
offlineMode?: boolean;
|
|
1194
1200
|
skipInitialization?: string[];
|
|
1195
1201
|
skipHydration?: boolean;
|
|
@@ -1573,6 +1579,12 @@ export type SearchOptions = {
|
|
|
1573
1579
|
sort?: SearchMessageSort;
|
|
1574
1580
|
};
|
|
1575
1581
|
|
|
1582
|
+
/** Per-request options that are never part of the serialized request payload. */
|
|
1583
|
+
export type RequestOptions = {
|
|
1584
|
+
/** Aborts the request. See AbortController. */
|
|
1585
|
+
signal?: AbortSignal;
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1576
1588
|
export type StreamChatOptions = AxiosRequestConfig & {
|
|
1577
1589
|
/**
|
|
1578
1590
|
* Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
|
package/src/uploadManager.ts
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
1
3
|
import type { StreamChat } from './client';
|
|
2
4
|
import type { UploadRequestOptions } from './messageComposer/configuration/types';
|
|
3
5
|
import { StateStore } from './store';
|
|
4
6
|
import type { AttachmentManager } from '.';
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Whether an upload ended because it was aborted rather than because it failed.
|
|
10
|
+
*
|
|
11
|
+
* {@link UploadManager.deleteUploadRecord} and {@link UploadManager.cancelAllUploads} abort the
|
|
12
|
+
* request through its `AbortController`, which is what happens when an attachment is removed
|
|
13
|
+
* from the composer mid-upload or the client disconnects. Axios reports that as a
|
|
14
|
+
* `CanceledError` (`axios.isCancel`); a custom `doUploadRequest` on another transport
|
|
15
|
+
* conventionally throws a `DOMException` named `AbortError`.
|
|
16
|
+
*
|
|
17
|
+
* Callers use this to keep a deliberate cancellation from being reported as an error.
|
|
18
|
+
*/
|
|
19
|
+
export const isUploadCancellation = (error: unknown) =>
|
|
20
|
+
axios.isCancel(error) || (error instanceof Error && error.name === 'AbortError');
|
|
21
|
+
|
|
6
22
|
export type UploadRecord = {
|
|
7
23
|
id: string;
|
|
24
|
+
/**
|
|
25
|
+
* `true` once every byte has been handed to the transport but the server has not responded
|
|
26
|
+
* yet.
|
|
27
|
+
*
|
|
28
|
+
* Upload progress measures bytes *written to the connection*, not bytes the server
|
|
29
|
+
* acknowledged, so it reaches 100% the moment the request body is flushed — then the
|
|
30
|
+
* connection sits idle while the CDN ingests the file and the response travels back. On a
|
|
31
|
+
* large file over a slow link that window is long, and a UI that renders 100% as "done"
|
|
32
|
+
* claims the upload is confirmed while it is not.
|
|
33
|
+
*
|
|
34
|
+
* Nothing measurable happens during that window, so it is the point at which a determinate
|
|
35
|
+
* progress bar should hand over to an indeterminate one. Confirmation is the record being
|
|
36
|
+
* removed, not progress reaching 100.
|
|
37
|
+
*
|
|
38
|
+
* Requires `AttachmentManagerConfig.trackUploadProgress`; without progress reporting there is
|
|
39
|
+
* no way to tell when the flush happened, and this stays `false`.
|
|
40
|
+
*/
|
|
41
|
+
uploadConfirmationPending?: boolean;
|
|
8
42
|
uploadProgress?: number;
|
|
9
43
|
};
|
|
10
44
|
|
|
@@ -126,6 +160,7 @@ export class UploadManager {
|
|
|
126
160
|
const trackProgress = attachmentManager.config.trackUploadProgress;
|
|
127
161
|
try {
|
|
128
162
|
this.upsertUpload({
|
|
163
|
+
uploadConfirmationPending: false,
|
|
129
164
|
id,
|
|
130
165
|
uploadProgress: trackProgress ? 0 : undefined,
|
|
131
166
|
});
|
|
@@ -135,6 +170,16 @@ export class UploadManager {
|
|
|
135
170
|
this.updateUpload({
|
|
136
171
|
id,
|
|
137
172
|
uploadProgress: progress,
|
|
173
|
+
// Only a number says anything about the flush, so a report without one leaves
|
|
174
|
+
// the flag alone: `undefined` means the transport cannot measure this upload,
|
|
175
|
+
// not that bytes were un-sent. Lowering it there would drop a UI that had
|
|
176
|
+
// already gone indeterminate back to a determinate one with nothing to show.
|
|
177
|
+
//
|
|
178
|
+
// The record is removed in the `finally` below, so a record that still exists
|
|
179
|
+
// while reporting 100% can only mean "flushed, awaiting the response".
|
|
180
|
+
...(typeof progress === 'number'
|
|
181
|
+
? { uploadConfirmationPending: progress >= 100 }
|
|
182
|
+
: {}),
|
|
138
183
|
});
|
|
139
184
|
}
|
|
140
185
|
: undefined;
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import FormData from 'form-data';
|
|
2
2
|
import type {
|
|
3
3
|
AscDesc,
|
|
4
|
+
Attachment,
|
|
4
5
|
ChannelFilters,
|
|
5
6
|
ChannelQueryOptions,
|
|
6
7
|
ChannelSort,
|
|
@@ -382,6 +383,77 @@ export function unformatMessage(message: LocalMessage): MessageResponse {
|
|
|
382
383
|
} as MessageResponse;
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Whether a URL points at something the API and other devices can actually fetch.
|
|
388
|
+
*/
|
|
389
|
+
const isRemoteUrl = (url?: string) => !!url && /^https?:\/\//i.test(url.trim());
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Strips composer-internal state from a message's attachments, and drops any whose upload never
|
|
393
|
+
* resolved.
|
|
394
|
+
*
|
|
395
|
+
* `localMetadata` is how `AttachmentManager` tracks an upload (its id, the `File`, the local
|
|
396
|
+
* preview); it must never reach the API. An attachment that still carries it and has no remote
|
|
397
|
+
* URL was never settled — which is what happens when
|
|
398
|
+
* `createSendWithPendingUploadsAttachmentsMiddleware` is installed by a UI SDK that does not
|
|
399
|
+
* implement the rest of the flow (awaiting those uploads before sending). Sending it would store
|
|
400
|
+
* an attachment pointing at nothing, so it is dropped and logged instead.
|
|
401
|
+
*
|
|
402
|
+
* Returns the same message object when there was nothing to change.
|
|
403
|
+
*/
|
|
404
|
+
export const sanitizeOutgoingAttachments = <T extends { attachments?: Attachment[] }>({
|
|
405
|
+
client,
|
|
406
|
+
message,
|
|
407
|
+
}: {
|
|
408
|
+
client: StreamChat;
|
|
409
|
+
message: T;
|
|
410
|
+
}): T => {
|
|
411
|
+
const attachments = message.attachments;
|
|
412
|
+
if (!attachments?.length) return message;
|
|
413
|
+
|
|
414
|
+
const unresolved: Attachment[] = [];
|
|
415
|
+
let changed = false;
|
|
416
|
+
|
|
417
|
+
const sanitized = attachments.reduce<Attachment[]>((acc, attachment) => {
|
|
418
|
+
const { localMetadata, ...rest } = attachment as Attachment & {
|
|
419
|
+
localMetadata?: Record<string, unknown>;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (!localMetadata) {
|
|
423
|
+
acc.push(attachment);
|
|
424
|
+
return acc;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
changed = true;
|
|
428
|
+
|
|
429
|
+
// Any of these pointing at a remote URL means the attachment resolved to something the API
|
|
430
|
+
// can store and other devices can load.
|
|
431
|
+
const hasRemoteSource =
|
|
432
|
+
isRemoteUrl(rest.asset_url) ||
|
|
433
|
+
isRemoteUrl(rest.image_url) ||
|
|
434
|
+
isRemoteUrl(rest.og_scrape_url) ||
|
|
435
|
+
isRemoteUrl(rest.title_link);
|
|
436
|
+
|
|
437
|
+
if (hasRemoteSource) {
|
|
438
|
+
acc.push(rest as Attachment);
|
|
439
|
+
} else {
|
|
440
|
+
unresolved.push(rest as Attachment);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return acc;
|
|
444
|
+
}, []);
|
|
445
|
+
|
|
446
|
+
if (unresolved.length) {
|
|
447
|
+
const text = `Dropped ${unresolved.length} attachment(s) whose upload never completed. Composing with pending uploads requires awaiting them before sending.`;
|
|
448
|
+
const extraData = { attachments: unresolved, message, tags: ['attachments'] };
|
|
449
|
+
|
|
450
|
+
client.logger('warn', text, extraData);
|
|
451
|
+
console.warn(text, extraData);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return changed ? { ...message, attachments: sanitized } : message;
|
|
455
|
+
};
|
|
456
|
+
|
|
385
457
|
export const localMessageToNewMessagePayload = (localMessage: LocalMessage): Message => {
|
|
386
458
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
387
459
|
const {
|
|
@@ -723,11 +795,13 @@ export interface DebouncedFunc<T extends (...args: any[]) => any> {
|
|
|
723
795
|
flush(): ReturnType<T> | undefined;
|
|
724
796
|
}
|
|
725
797
|
|
|
726
|
-
// works exactly the same as lodash.debounce
|
|
798
|
+
// works exactly the same as lodash.debounce, except that the timeout can also be
|
|
799
|
+
// a function of the call arguments, resolved on every call (e.g. to debounce short,
|
|
800
|
+
// low-selectivity search queries harder than long ones)
|
|
727
801
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
728
802
|
export const debounce = <T extends (...args: any[]) => any>(
|
|
729
803
|
fn: T,
|
|
730
|
-
timeout = 0,
|
|
804
|
+
timeout: number | ((...args: Parameters<T>) => number) = 0,
|
|
731
805
|
{ leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } = {},
|
|
732
806
|
): DebouncedFunc<T> => {
|
|
733
807
|
let runningTimeout: null | NodeJS.Timeout = null;
|
|
@@ -750,7 +824,8 @@ export const debounce = <T extends (...args: any[]) => any>(
|
|
|
750
824
|
runningTimeout = null;
|
|
751
825
|
};
|
|
752
826
|
|
|
753
|
-
|
|
827
|
+
const delay = typeof timeout === 'function' ? timeout(...args) : timeout;
|
|
828
|
+
runningTimeout = setTimeout(timeoutHandler, delay);
|
|
754
829
|
return lastResult;
|
|
755
830
|
};
|
|
756
831
|
|