Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ai-docs/v10-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2645,7 +2645,7 @@ Poll data types and `PollResponse` renaming: [poll types](#poll-types). State ti
**Kind:** removed · **Impact:** medium · **PR:** #1880, #1881

- **v9:** install `createSendWithPendingUploadsAttachmentsMiddleware(composer)` via `compositionMiddlewareExecutor.replace([...])`; it carried `MessageCompositionMiddleware.allowsPendingUploads: true`; `composer.allowsPendingUploads` scanned `compositionMiddlewareExecutor.installedMiddleware` (narrowed override on `MessageComposerMiddlewareExecutor`). The caller had to await uploads and patch URLs into the payload before sending.
- **v10:** the export, the `allowsPendingUploads` middleware field and the `installedMiddleware` narrowing are gone. The switch is `attachments.pendingUploadsEnabled` (default `false`), read per composition by the stock `createAttachmentsCompositionMiddleware`. `composer.allowsPendingUploads` returns `attachmentManager.config.pendingUploadsEnabled`. Settling uploads moved into `channel|thread.messageOperations.send` (`settlePendingAttachmentUploads`). In this mode `failed` uploads count for `hasSendableData` and ride along on `localMessage.attachments` (retried at send); only `blocked` stays behind. The "keep in composer" rule is `config.retainCompositionOnSubmit(composer)` (default `!!composer.pollId`, same as v9's hardcoded check).
- **v10:** the export, the `allowsPendingUploads` middleware field and the `installedMiddleware` narrowing are gone. The switch is `attachments.pendingUploadsEnabled` (default `false`), read per composition by the stock `createAttachmentsCompositionMiddleware`. `composer.allowsPendingUploads` returns `attachmentManager.config.pendingUploadsEnabled`. Settling uploads moved into `channel|thread.messageOperations.send` (`settlePendingAttachmentUploads`). In this mode `failed` uploads count for `hasSendableData` and ride along on `localMessage.attachments` (retried at send); only `blocked` stays behind. The "keep in composer" rule is `config.retainCompositionOnSubmit(composer)` (default `!!composer.pollId` while the poll-only composition middleware is installed, same as v9's hardcoded check). To send a poll together with the message's text and attachments, remove `POLL_ONLY_COMPOSITION_MIDDLEWARE_ID` from `composer.compositionMiddlewareExecutor`; the default then stops retaining.
- **Migrate:** delete the middleware install; `client.config.set({ messageComposer: { attachments: { pendingUploadsEnabled: true } } })` (or `composer.updateConfig(...)`). Drop any own await/patch-URLs step and send through `composer.send()` or `messageOperations.send`, otherwise uploads are handled twice. A custom middleware with `allowsPendingUploads: true` now does nothing.
- **Source:** `src/messageComposer/middleware/messageComposer/attachments.ts:54,62,124`, `src/messageComposer/messageComposer.ts:478,524`, `src/messageComposer/configuration/types.ts:109`, `src/messageOperations/createMessageOperations.ts:43` (v10); `src/messageComposer/middleware/messageComposer/attachments.ts:43,108`, `src/messageComposer/middleware/messageComposer/types.ts:55`, `src/messageComposer/messageComposer.ts:448`, `src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts:64` (v9)

Expand Down Expand Up @@ -2700,7 +2700,7 @@ Poll data types and `PollResponse` renaming: [poll types](#poll-types). State ti
**Kind:** type-shape · **Impact:** low · **PR:** #1831, #1881

- **v9:** `MessageComposerConfig { attachments, commands, drafts, linkPreviews, location, text }`; `AttachmentManagerConfig` without `enabled` / `customCdn` / `pendingUploadsEnabled`; `LocationComposerConfig { enabled, getDeviceId }` with a hardcoded 60 s live-location minimum.
- **v10:** required `MessageComposerConfig.polls: PollComposerConfig` (`{ enabled: true }`), `MessageComposerConfig.retainCompositionOnSubmit: (composer) => boolean` (default `!!composer.pollId`), `AttachmentManagerConfig.enabled` (`true`), `.customCdn` (`false`), `.pendingUploadsEnabled` (`false`), `LocationComposerConfig.minShareDurationMs` (`60000`, replaces the hardcoded constant; a shorter `durationMs` makes `validLocation` null).
- **v10:** required `MessageComposerConfig.polls: PollComposerConfig` (`{ enabled: true }`), `MessageComposerConfig.retainCompositionOnSubmit: (composer) => boolean` (default `!!composer.pollId` while `POLL_ONLY_COMPOSITION_MIDDLEWARE_ID` is installed), `AttachmentManagerConfig.enabled` (`true`), `.customCdn` (`false`), `.pendingUploadsEnabled` (`false`), `LocationComposerConfig.minShareDurationMs` (`60000`, replaces the hardcoded constant; a shorter `durationMs` makes `validLocation` null).
- **Migrate:** partials (`updateConfig`, `client.config.set`, constructor `config`) need nothing. Values annotated as a complete config type must add the six fields.
- **Source:** `src/messageComposer/configuration/types.ts:66,103,109,136,149,152,160,172,178`, `src/messageComposer/configuration/configuration.ts:35-39,55,72` (v10); `src/messageComposer/configuration/types.ts:66,101,111`, `src/messageComposer/LocationComposer.ts:29` (v9)

Expand Down
2 changes: 1 addition & 1 deletion src/configuration/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ const MESSAGE_COMPOSER_FIELDS: Record<keyof MessageComposerConfig, ConfigNode> =
},
retainCompositionOnSubmit: {
description:
"Decides whether submitting keeps the composer's contents instead of clearing them. Retains for a poll message by default.",
"Decides whether submitting keeps the composer's contents instead of clearing them. Retains for a poll message by default, while the composition chain sends polls alone.",
kind: 'value',
type: 'function',
},
Expand Down
7 changes: 6 additions & 1 deletion src/messageComposer/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import { generateUUIDv4 } from '../../utils';
import { deepFreezeConfig } from '../../configuration/utils/deepFreezeConfig';
import { DEFAULT_COMMANDS_CONFIG } from './commands.configuration';
import { POLL_ONLY_COMPOSITION_MIDDLEWARE_ID } from '../middleware/messageComposer/pollOnly';

export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = {
debounceURLEnrichmentMs: 1500,
Expand Down Expand Up @@ -66,6 +67,10 @@ export const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = deepFreezeConfig({
linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
location: DEFAULT_LOCATION_COMPOSER_CONFIG,
polls: DEFAULT_POLL_COMPOSER_CONFIG,
retainCompositionOnSubmit: (composer) => !!composer.pollId,
retainCompositionOnSubmit: (composer) =>
Comment thread
isekovanic marked this conversation as resolved.
Resolved
!!composer.pollId &&
composer.compositionMiddlewareExecutor.installedMiddleware.some(
({ id }) => id === POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
),
text: DEFAULT_TEXT_COMPOSER_CONFIG,
});
5 changes: 4 additions & 1 deletion src/messageComposer/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ export type MessageComposerConfig = {
polls: PollComposerConfig;
/**
* Whether submitting keeps the composer's contents instead of clearing them (also leaving unresolved
* uploads in place). Defaults to retaining for a poll message.
* uploads in place). Defaults to retaining for a poll message while the composition chain sends
* polls alone (`POLL_ONLY_COMPOSITION_MIDDLEWARE_ID` is installed), so the drafted text and
* attachments stay for the next message. Without that step the poll goes out with them, and the
* composer clears.
*/
retainCompositionOnSubmit: (composer: MessageComposer) => boolean;
text: TextComposerConfig;
Expand Down
1 change: 1 addition & 0 deletions src/messageComposer/middleware/messageComposer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './compositionValidation';
export * from './linkPreviews';
export * from './MessageComposerMiddlewareExecutor';
export * from './messageComposerState';
export * from './pollOnly';
export * from './sharedLocation';
export * from './textComposer';
export * from './types';
Expand Down
10 changes: 9 additions & 1 deletion src/messageComposer/middleware/messageComposer/pollOnly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ const pollLocalMessageNullifiedFields: Pick<
text: '',
};

export const POLL_ONLY_COMPOSITION_MIDDLEWARE_ID =
'stream-io/message-composer-middleware/poll-only';

/**
* Sends a composer's poll as a message of its own (outside threads and edits), leaving the text,
* attachments and quoted message in the composer. Remove it from the composer's
* `compositionMiddlewareExecutor` to send the poll together with the rest of the message instead.
*/
export const createPollOnlyCompositionMiddleware = (
composer: MessageComposer,
): MessageCompositionMiddleware => ({
id: 'stream-io/message-composer-middleware/poll-only',
id: POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
handlers: {
compose: ({
state,
Expand Down
42 changes: 41 additions & 1 deletion test/unit/MessageComposer/messageComposer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DEFAULT_COMPOSER_CONFIG,
LocalMessage,
MessageComposerConfig,
POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
SharedLocation,
StreamChat,
Thread,
Expand Down Expand Up @@ -3303,16 +3304,45 @@ describe('MessageComposer submit lifecycle', () => {
const { messageComposer, mockChannel } = setup({ channelConfig: { polls: true } });
withText(messageComposer, 'vote please');
messageComposer.state.partialNext({ pollId: 'poll-1' });
vi.spyOn(mockChannel.messageOperations, 'send').mockResolvedValue(undefined);
const send = vi
.spyOn(mockChannel.messageOperations, 'send')
.mockResolvedValue(undefined);

expect(messageComposer.retainsCompositionOnSubmit).toBe(true);

await messageComposer.send();

expect(send.mock.calls[0][0].message).toEqual({
id: expect.any(String),
poll_id: 'poll-1',
});
expect(messageComposer.pollId).toBeNull();
expect(messageComposer.textComposer.text).toBe('vote please');
});

it('sends the poll with the text once the poll-only middleware is removed, and clears', async () => {
const { messageComposer, mockChannel } = setup({ channelConfig: { polls: true } });
messageComposer.compositionMiddlewareExecutor.remove(
POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
);
withText(messageComposer, 'vote please');
messageComposer.state.partialNext({ pollId: 'poll-1' });
const send = vi
.spyOn(mockChannel.messageOperations, 'send')
.mockResolvedValue(undefined);

expect(messageComposer.retainsCompositionOnSubmit).toBe(false);

await messageComposer.send();

expect(send.mock.calls[0][0].message).toMatchObject({
poll_id: 'poll-1',
text: 'vote please',
});
expect(messageComposer.pollId).toBeNull();
expect(messageComposer.textComposer.text).toBe('');
});

it('routes an update through messageOperations.update', async () => {
const { messageComposer, mockChannel } = setup();
withText(messageComposer);
Expand All @@ -3336,6 +3366,16 @@ describe('MessageComposerConfig.retainCompositionOnSubmit', () => {
expect(messageComposer.retainsCompositionOnSubmit).toBe(true);
});

it('does not retain for a poll while the chain sends polls with the message', () => {
const { messageComposer } = setup({ channelConfig: { polls: true } });
messageComposer.compositionMiddlewareExecutor.remove(
POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
);

messageComposer.state.partialNext({ pollId: 'poll-1' });
expect(messageComposer.retainsCompositionOnSubmit).toBe(false);
});

it('lets an integrator decide, receiving the composer', async () => {
const retainCompositionOnSubmit = vi.fn(
(composer: MessageComposer) => composer.textComposer.text === 'keep me',
Expand Down
Loading