stream-chat 9.51.0 → 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 +382 -187
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +391 -188
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +380 -185
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/channel.d.ts +1 -1
- package/dist/types/client.d.ts +10 -0
- 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/types.d.ts +5 -0
- package/dist/types/middleware.d.ts +8 -0
- package/dist/types/uploadManager.d.ts +30 -0
- package/dist/types/utils.d.ts +20 -1
- package/package.json +1 -1
- package/src/channel.ts +16 -8
- package/src/client.ts +17 -1
- 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/types.ts +5 -0
- package/src/middleware.ts +11 -0
- package/src/offline-support/offline_support_api.ts +4 -0
- package/src/uploadManager.ts +45 -0
- package/src/utils.ts +72 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AttachmentManager } from './attachmentManager';
|
|
2
|
+
import { isFinishedUpload, isPendingUpload } from './attachmentIdentity';
|
|
2
3
|
import { CustomDataManager } from './CustomDataManager';
|
|
3
4
|
import { LinkPreviewsManager } from './linkPreviewsManager';
|
|
4
5
|
import { LocationComposer } from './LocationComposer';
|
|
@@ -432,16 +433,50 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
432
433
|
return !currentCommand || this.validateCommandSendability(currentCommand).ready;
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Whether the installed composition middleware composes messages whose uploads are still in
|
|
438
|
+
* flight — see `MessageCompositionMiddleware.allowsPendingUploads`.
|
|
439
|
+
*
|
|
440
|
+
* Derived from the chain rather than from config, so there is exactly one switch: the UI SDK
|
|
441
|
+
* that implements the rest of the flow installs the middleware, and everything that has to
|
|
442
|
+
* follow from that (starting with {@link hasSendableData}) follows on its own.
|
|
443
|
+
*
|
|
444
|
+
* **Temporary API.** In v10 sending with pending uploads becomes part of the composer
|
|
445
|
+
* configuration (`MessageComposerConfig`, set through `updateConfig`), and this getter,
|
|
446
|
+
* together with the middleware declaration it reads, is replaced by that config field.
|
|
447
|
+
*/
|
|
448
|
+
get allowsPendingUploads() {
|
|
449
|
+
return this.compositionMiddlewareExecutor.installedMiddleware.some(
|
|
450
|
+
(middleware) => middleware.allowsPendingUploads,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
435
454
|
get hasSendableData() {
|
|
436
|
-
return
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
455
|
+
if (!this.isCommandSendable) return false;
|
|
456
|
+
|
|
457
|
+
if (this.allowsPendingUploads) {
|
|
458
|
+
// An upload in flight is no longer a blocker, and a pending attachment counts as content
|
|
459
|
+
// in its own right — otherwise an attachment-only message could never be sent before its
|
|
460
|
+
// upload finished, which is the point of the flow. `failed` and `blocked` attachments
|
|
461
|
+
// still do not count: a message whose only attachment was rejected must not look sendable.
|
|
462
|
+
const hasSendableAttachment = this.attachmentManager.attachments.some(
|
|
463
|
+
(attachment) => isFinishedUpload(attachment) || isPendingUpload(attachment),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
return !!(
|
|
467
|
+
!this.textComposer.textIsEmpty ||
|
|
468
|
+
hasSendableAttachment ||
|
|
442
469
|
this.pollId ||
|
|
443
|
-
|
|
444
|
-
)
|
|
470
|
+
this.locationComposer.validLocation
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return !!(
|
|
475
|
+
(!this.attachmentManager.uploadsInProgressCount &&
|
|
476
|
+
(!this.textComposer.textIsEmpty ||
|
|
477
|
+
this.attachmentManager.successfulUploadsCount > 0)) ||
|
|
478
|
+
this.pollId ||
|
|
479
|
+
!!this.locationComposer.validLocation
|
|
445
480
|
);
|
|
446
481
|
}
|
|
447
482
|
|
|
@@ -14,7 +14,7 @@ export class AttachmentPostUploadMiddlewareExecutor extends MiddlewareExecutor<
|
|
|
14
14
|
super();
|
|
15
15
|
this.use([
|
|
16
16
|
createUploadErrorHandlerMiddleware(composer),
|
|
17
|
-
createPostUploadAttachmentEnrichmentMiddleware(),
|
|
17
|
+
createPostUploadAttachmentEnrichmentMiddleware(composer),
|
|
18
18
|
]);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -4,41 +4,66 @@ import type {
|
|
|
4
4
|
AttachmentPostUploadMiddlewareState,
|
|
5
5
|
} from '../types';
|
|
6
6
|
import { isLocalImageAttachment } from '../../../attachmentIdentity';
|
|
7
|
+
import type { MessageComposer } from '../../../messageComposer';
|
|
7
8
|
import type { LocalNotImageAttachment } from '../../../types';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Writes the uploaded URL onto the attachment and releases its local preview.
|
|
12
|
+
*
|
|
13
|
+
* The preview is a blob URL the composer created for the file. Releasing it once the CDN URL is
|
|
14
|
+
* in is what keeps the browser from holding the file in memory.
|
|
15
|
+
*
|
|
16
|
+
* Pass `composer` and the preview is released only while the attachment is still one of the
|
|
17
|
+
* composer's own — still listed in `attachmentManager.attachments`, i.e. still shown above the
|
|
18
|
+
* input, waiting to be sent.
|
|
19
|
+
*
|
|
20
|
+
* This runs when an upload finishes, which can be after the composer was cleared: `uploadFile`
|
|
21
|
+
* executes this chain before the (by then no-op) `updateAttachment`. If the attachment is gone
|
|
22
|
+
* from the composer, a message is rendering from the preview instead, and releasing it would
|
|
23
|
+
* blank that message out. A message can be sent with uploads still running — see
|
|
24
|
+
* `createSendWithPendingUploadsAttachmentsMiddleware`.
|
|
25
|
+
*
|
|
26
|
+
* Omit `composer` and the preview is always released.
|
|
27
|
+
*/
|
|
28
|
+
export const createPostUploadAttachmentEnrichmentMiddleware = (
|
|
29
|
+
composer?: MessageComposer,
|
|
30
|
+
): AttachmentPostUploadMiddleware => ({
|
|
31
|
+
id: 'stream-io/attachment-manager-middleware/post-upload-enrichment',
|
|
32
|
+
handlers: {
|
|
33
|
+
postProcess: ({
|
|
34
|
+
state,
|
|
35
|
+
discard,
|
|
36
|
+
forward,
|
|
37
|
+
next,
|
|
38
|
+
}: MiddlewareHandlerParams<AttachmentPostUploadMiddlewareState>) => {
|
|
39
|
+
const { attachment, error, response } = state;
|
|
40
|
+
if (error) return forward();
|
|
41
|
+
if (!attachment || !response) return discard();
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (isLocalImageAttachment(attachment)) {
|
|
30
|
-
enrichedAttachment.image_url = response.file;
|
|
31
|
-
} else {
|
|
32
|
-
(enrichedAttachment as LocalNotImageAttachment).asset_url = response.file;
|
|
33
|
-
}
|
|
34
|
-
if (response.thumb_url) {
|
|
35
|
-
(enrichedAttachment as LocalNotImageAttachment).thumb_url = response.thumb_url;
|
|
36
|
-
}
|
|
43
|
+
const enrichedAttachment = { ...attachment };
|
|
44
|
+
const { id, previewUri } = attachment.localMetadata;
|
|
45
|
+
// `attachmentsById` is built from the composer's current attachment state, so a hit means
|
|
46
|
+
// the attachment is still in the composer and its preview is not in use elsewhere.
|
|
47
|
+
const attachmentIsStillInComposer =
|
|
48
|
+
!composer || !!composer.attachmentManager.attachmentsById[id];
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
if (previewUri && attachmentIsStillInComposer) {
|
|
51
|
+
if (previewUri.startsWith('blob:')) URL.revokeObjectURL?.(previewUri);
|
|
52
|
+
delete enrichedAttachment.localMetadata.previewUri;
|
|
53
|
+
}
|
|
54
|
+
if (isLocalImageAttachment(attachment)) {
|
|
55
|
+
enrichedAttachment.image_url = response.file;
|
|
56
|
+
} else {
|
|
57
|
+
(enrichedAttachment as LocalNotImageAttachment).asset_url = response.file;
|
|
58
|
+
}
|
|
59
|
+
if (response.thumb_url) {
|
|
60
|
+
(enrichedAttachment as LocalNotImageAttachment).thumb_url = response.thumb_url;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return next({
|
|
64
|
+
...state,
|
|
65
|
+
attachment: enrichedAttachment,
|
|
66
|
+
});
|
|
43
67
|
},
|
|
44
|
-
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AttachmentPostUploadMiddleware,
|
|
5
5
|
AttachmentPostUploadMiddlewareState,
|
|
6
6
|
} from '../types';
|
|
7
|
+
import { isUploadCancellation } from '../../../../uploadManager';
|
|
7
8
|
|
|
8
9
|
export const createUploadErrorHandlerMiddleware = (
|
|
9
10
|
composer: MessageComposer,
|
|
@@ -18,6 +19,9 @@ export const createUploadErrorHandlerMiddleware = (
|
|
|
18
19
|
const { attachment, error } = state;
|
|
19
20
|
if (!error) return forward();
|
|
20
21
|
if (!attachment) return discard();
|
|
22
|
+
// A cancellation is the user getting what they asked for, so it gets no error
|
|
23
|
+
// notification. `StreamChat.doAxiosRequest` draws the same line for the same reason.
|
|
24
|
+
if (isUploadCancellation(error)) return forward();
|
|
21
25
|
|
|
22
26
|
const reason = error instanceof Error ? error.message : 'unknown error';
|
|
23
27
|
composer.client.notifications.addError({
|
|
@@ -23,6 +23,7 @@ import { createCompositionDataCleanupMiddleware } from './cleanData';
|
|
|
23
23
|
import type {
|
|
24
24
|
MessageComposerMiddlewareExecutorOptions,
|
|
25
25
|
MessageComposerMiddlewareState,
|
|
26
|
+
MessageCompositionMiddleware,
|
|
26
27
|
MessageDraftComposerMiddlewareExecutorOptions,
|
|
27
28
|
MessageDraftComposerMiddlewareValueState,
|
|
28
29
|
} from './types';
|
|
@@ -55,6 +56,14 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
|
|
|
55
56
|
createCompositionDataCleanupMiddleware(composer),
|
|
56
57
|
]);
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Narrows the base accessor to the composition middleware type, so callers can read the
|
|
62
|
+
* declarations it carries (`allowsPendingUploads`) without casting.
|
|
63
|
+
*/
|
|
64
|
+
get installedMiddleware(): ReadonlyArray<MessageCompositionMiddleware> {
|
|
65
|
+
return super.installedMiddleware as ReadonlyArray<MessageCompositionMiddleware>;
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
export class MessageDraftComposerMiddlewareExecutor extends MiddlewareExecutor<
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isFinishedUpload, isPendingUpload } from '../../attachmentIdentity';
|
|
1
2
|
import type { MiddlewareHandlerParams } from '../../../middleware';
|
|
2
3
|
import type { Attachment } from '../../../types';
|
|
3
4
|
import type { MessageComposer } from '../../messageComposer';
|
|
@@ -15,6 +16,114 @@ const localAttachmentToAttachment = (localAttachment: LocalAttachment) => {
|
|
|
15
16
|
return attachment as Attachment;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The composition step used by {@link createSendWithPendingUploadsAttachmentsMiddleware}.
|
|
21
|
+
*
|
|
22
|
+
* The two payloads part ways here: `localMessage.attachments` keeps `localMetadata` for
|
|
23
|
+
* anything still uploading - `id` (the `client.uploadManager` key), `file` (the handle needed
|
|
24
|
+
* to await the upload) and `previewUri` (so the message list can render the user's own file
|
|
25
|
+
* meanwhile) - while `message.attachments`, which goes to the API, carries only attachments
|
|
26
|
+
* that already resolved to a URL. Whoever performs the send fills in the rest once the uploads
|
|
27
|
+
* settle.
|
|
28
|
+
*/
|
|
29
|
+
const composeWithPendingUploads = ({
|
|
30
|
+
composer,
|
|
31
|
+
state,
|
|
32
|
+
}: {
|
|
33
|
+
composer: MessageComposer;
|
|
34
|
+
state: MessageComposerMiddlewareState;
|
|
35
|
+
}): MessageComposerMiddlewareState => {
|
|
36
|
+
// `useSubmitHandler` in the UI SDKs deliberately skips `MessageComposer.clear()` when the
|
|
37
|
+
// composition carries a poll - it keeps the composer's contents as a draft. Handing a
|
|
38
|
+
// still-uploading attachment to such a message would leave the same `localMetadata.id` owned
|
|
39
|
+
// by both the sent message and the composer: the user could send it a second time, and the
|
|
40
|
+
// second `UploadManager.upload` call would restart the request under an id whose in-flight
|
|
41
|
+
// entry had already been cleaned up. So a pending upload stays in the composer in that case
|
|
42
|
+
// and rides along with the next message once it finishes.
|
|
43
|
+
const composerIsKeptAsDraft = !!composer.pollId;
|
|
44
|
+
|
|
45
|
+
// Composer order is preserved in both payloads so previews do not reshuffle when an upload
|
|
46
|
+
// settles.
|
|
47
|
+
const relevantAttachments = composer.attachmentManager.attachments.filter(
|
|
48
|
+
(attachment) =>
|
|
49
|
+
isFinishedUpload(attachment) ||
|
|
50
|
+
(!composerIsKeptAsDraft && isPendingUpload(attachment)),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(
|
|
54
|
+
relevantAttachments.map((attachment) =>
|
|
55
|
+
isPendingUpload(attachment) ? attachment : localAttachmentToAttachment(attachment),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
59
|
+
relevantAttachments.filter(isFinishedUpload).map(localAttachmentToAttachment),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!localAttachments.length && !messageAttachments.length) return state;
|
|
63
|
+
|
|
64
|
+
// Each payload gets the `attachments` key only when there is something to put in it, matching
|
|
65
|
+
// the default middleware. An empty array is not "nothing to say": on an edit the API reads it
|
|
66
|
+
// as "remove every attachment", and a message whose uploads are all still in flight produces
|
|
67
|
+
// exactly that - no finished upload to put in `message.attachments` yet.
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
localMessage: localAttachments.length
|
|
71
|
+
? {
|
|
72
|
+
...state.localMessage,
|
|
73
|
+
attachments: localAttachments,
|
|
74
|
+
}
|
|
75
|
+
: state.localMessage,
|
|
76
|
+
message: messageAttachments.length
|
|
77
|
+
? {
|
|
78
|
+
...state.message,
|
|
79
|
+
attachments: messageAttachments,
|
|
80
|
+
}
|
|
81
|
+
: state.message,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Drop-in replacement for {@link createAttachmentsCompositionMiddleware} that lets a message be
|
|
87
|
+
* composed while its attachments are still uploading.
|
|
88
|
+
*
|
|
89
|
+
* Reuses the same middleware id, so installing it with
|
|
90
|
+
* `compositionMiddlewareExecutor.replace([...])` keeps its position in the chain. The default
|
|
91
|
+
* refuses instead: it warns "Wait until all attachments have uploaded" and discards the
|
|
92
|
+
* composition.
|
|
93
|
+
*
|
|
94
|
+
* **Installing this is only half of the flow.** The composition it produces is not ready for the
|
|
95
|
+
* wire — `message.attachments` omits everything that has no URL yet. Whoever performs the send
|
|
96
|
+
* has to await those uploads (`UploadManager.upload` is idempotent by `localMetadata.id`, so
|
|
97
|
+
* calling it again returns the in-flight promise), write the resolved URLs in, and send after
|
|
98
|
+
* that.
|
|
99
|
+
*
|
|
100
|
+
* Sendability follows on its own: the `allowsPendingUploads` declaration below is what
|
|
101
|
+
* {@link MessageComposer.hasSendableData} reads to stop treating an upload in flight as a
|
|
102
|
+
* blocker, so installing this middleware is the only switch there is.
|
|
103
|
+
*
|
|
104
|
+
* This is why there is no config option turning it on: the switch belongs to the UI SDK that
|
|
105
|
+
* implements the other half. stream-chat-react exposes it as a `Chat` prop;
|
|
106
|
+
* stream-chat-react-native as `allowSendBeforeAttachmentsUpload` on the message input.
|
|
107
|
+
*/
|
|
108
|
+
export const createSendWithPendingUploadsAttachmentsMiddleware = (
|
|
109
|
+
composer: MessageComposer,
|
|
110
|
+
): MessageCompositionMiddleware => ({
|
|
111
|
+
allowsPendingUploads: true,
|
|
112
|
+
id: 'stream-io/message-composer-middleware/attachments',
|
|
113
|
+
handlers: {
|
|
114
|
+
compose: ({
|
|
115
|
+
state,
|
|
116
|
+
next,
|
|
117
|
+
forward,
|
|
118
|
+
}: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
|
|
119
|
+
const { attachmentManager } = composer;
|
|
120
|
+
if (!attachmentManager) return forward();
|
|
121
|
+
|
|
122
|
+
return next(composeWithPendingUploads({ composer, state }));
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
18
127
|
export const createAttachmentsCompositionMiddleware = (
|
|
19
128
|
composer: MessageComposer,
|
|
20
129
|
): MessageCompositionMiddleware => ({
|
|
@@ -32,7 +32,28 @@ export type MessageDraftComposerMiddlewareExecutorOptions = {
|
|
|
32
32
|
export type MessageCompositionMiddleware = Middleware<
|
|
33
33
|
MessageComposerMiddlewareState,
|
|
34
34
|
'compose'
|
|
35
|
-
|
|
35
|
+
> & {
|
|
36
|
+
/**
|
|
37
|
+
* Declares that this middleware composes messages whose attachment uploads are still in
|
|
38
|
+
* flight, instead of refusing to compose until they finish.
|
|
39
|
+
*
|
|
40
|
+
* {@link MessageComposer.hasSendableData} reads this: an upload in flight must not block the
|
|
41
|
+
* send while such a middleware is installed, or the send could never be triggered. Any
|
|
42
|
+
* middleware implementing that contract may set it - the composer keys off the declaration,
|
|
43
|
+
* not off a middleware id, so a custom implementation is treated the same as the one shipped
|
|
44
|
+
* here.
|
|
45
|
+
*
|
|
46
|
+
* Composing this way is only half of the flow: whoever performs the send has to await those
|
|
47
|
+
* uploads and write the resolved URLs into the payload. See
|
|
48
|
+
* {@link createSendWithPendingUploadsAttachmentsMiddleware}.
|
|
49
|
+
*
|
|
50
|
+
* **Temporary API.** In v10 sending with pending uploads becomes part of the composer
|
|
51
|
+
* configuration (`MessageComposerConfig`, set through `updateConfig`), and this declaration,
|
|
52
|
+
* together with {@link MessageComposer.allowsPendingUploads}, is replaced by that config
|
|
53
|
+
* field.
|
|
54
|
+
*/
|
|
55
|
+
allowsPendingUploads?: boolean;
|
|
56
|
+
};
|
|
36
57
|
|
|
37
58
|
export type MessageDraftCompositionMiddleware = Middleware<
|
|
38
59
|
MessageDraftComposerMiddlewareValueState,
|
|
@@ -119,6 +119,11 @@ export type LocalAttachmentUploadMetadata = {
|
|
|
119
119
|
*/
|
|
120
120
|
previewUri?: string;
|
|
121
121
|
uploadState: AttachmentLoadingState;
|
|
122
|
+
/**
|
|
123
|
+
* Mirrors {@link UploadRecord.uploadConfirmationPending}: every byte has been sent, but the server has
|
|
124
|
+
* not confirmed yet - not returned response to the upload request. Render an indeterminate indicator rather than a full bar.
|
|
125
|
+
*/
|
|
126
|
+
uploadConfirmationPending?: boolean;
|
|
122
127
|
uploadPermissionCheck?: UploadPermissionCheckResult; // added new
|
|
123
128
|
/** 0–100 while uploading when progress tracking is enabled; undefined otherwise or when indeterminate */
|
|
124
129
|
uploadProgress?: number;
|
package/src/middleware.ts
CHANGED
|
@@ -58,6 +58,17 @@ export class MiddlewareExecutor<TValue, THandlers extends string> {
|
|
|
58
58
|
this.id = generateUUIDv4();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The middleware currently installed, in execution order.
|
|
63
|
+
*
|
|
64
|
+
* Read-only: use `use` / `insert` / `replace` / `setOrder` / `remove` to change the chain. It
|
|
65
|
+
* exists so components can ask what the chain is capable of - a middleware may declare
|
|
66
|
+
* behaviour its host has to account for, and the host has no other way to find out.
|
|
67
|
+
*/
|
|
68
|
+
get installedMiddleware(): ReadonlyArray<Middleware<TValue, THandlers>> {
|
|
69
|
+
return this.middleware;
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
use(middleware: Middleware<TValue, THandlers> | Middleware<TValue, THandlers>[]) {
|
|
62
73
|
this.middleware = this.middleware.concat(middleware);
|
|
63
74
|
return this;
|
|
@@ -1050,6 +1050,10 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
1052
|
if (type === 'message.read' || type === 'notification.mark_read') {
|
|
1053
|
+
// We make sure not to update channel reads (which is what's stored in
|
|
1054
|
+
// the offline DB in any case) whenever we receive a read event for a
|
|
1055
|
+
// a thread specifically.
|
|
1056
|
+
if (event.thread) return [];
|
|
1053
1057
|
return this.handleRead({ event, unreadMessages: 0, execute });
|
|
1054
1058
|
}
|
|
1055
1059
|
|
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 {
|