graph TD
MC[MessageComposer] --> TC[TextComposer]
MC --> PC[PollComposer]
MC --> AM[AttachmentManager]
MC --> LPM[LinkPreviewsManager]
MC --> CDM[CustomDataManager]
MessageComposer API
The following sections will be dedicated to the description of the API that MessageComposer
and its sub-managers expose and how it can be used.
The MessageComposer
class provides a comprehensive API for managing message composition, including text, attachments, polls, and custom data. It is a coordinator that manages and orchestrates various specialized submanagers, each responsible for a specific aspect of message composition.
MessageComposer Class Architecture
MessageComposer follows a coordinator pattern where it delegates specific responsibilities to specialized submanagers:
Submanager Responsibilities
TextComposer
- Handles text input and editing
- Manages text state and validation
- Processes text-related events
PollComposer
- Manages poll creation and editing
- Handles poll state and validation
AttachmentManager
- Manages attachments other than link previews
- Handles uploads and downloads
LinkPreviewsManager
- Manages link previews
- Handles URL detection and preview generation
CustomDataManager
- Manages custom message data
- Manages custom composer data (bucket to store any temporary data that will not be submitted with the message)
Message Composition Scenarios
MessageComposer can operate in different composition scenarios, each with specific initialization and behavior:
1. Message Editing
When editing an existing message, MessageComposer initializes with the message’s current state:
const composer = new MessageComposer({
composition: existingMessage, // LocalMessage
compositionContext: channel,
client,
});
Key characteristics:
- Initializes with message’s current state (text, attachments, etc.)
- Tracks editing timestamps via
editingAuditState
- Prevents draft creation (drafts are disabled)
- Maintains original message ID
- Updates are applied to the existing message
2. Drafts
Drafts can be enabled via the MessageComposer
configuration:
const composer = new MessageComposer({
composition: draftResponse, // DraftResponse
compositionContext: channel,
client,
config: { drafts: { enabled: true } },
});
composer.updateConfig({ drafts: { enabled: false } });
The composer exposes the draft management API:
await composer.createDraft();
await composer.deleteDraft();
No parameters are needed as the composer extracts all the necessary data from its state nad draft composition middleware.
The composer takes care of local state updates based on draft.updated
and draft.deleted
events automatically.
3. New Message Context
When creating a new message, MessageComposer starts with an empty state:
const composer = new MessageComposer({
compositionContext: channel,
client,
});
Key characteristics:
- Starts with empty state
- Generates new message ID
- No draft tracking by default
- Clean slate for all submanagers
MessageComposer API
MessageComposer Constructor
constructor({
composition,
config,
compositionContext,
client,
}: MessageComposerOptions)
Where MessageComposerOptions
is:
type MessageComposerOptions = {
client: StreamChat;
compositionContext: CompositionContext; // Channel | Thread | LocalMessageWithLegacyThreadId
composition?: DraftResponse | MessageResponse | LocalMessage;
config?: DeepPartial<MessageComposerConfig>;
};
MessageComposer State Management
// Initialize or reset composer state
initState({ composition }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}): void
// Clear all composer state
clear(): void
// Restore state from edited message or clear
restore(): void
// Update composer configuration
updateConfig(config: DeepPartial<MessageComposerConfig>): void
// Set quoted message
setQuotedMessage(quotedMessage: LocalMessage | null): void
Message Composition
// Compose a message
compose(): Promise<MessageComposerMiddlewareValue['state'] | undefined>
// Compose a draft message
composeDraft(): Promise<MessageComposerMiddlewareValue['state'] | undefined>
// Create a draft message
createDraft(): Promise<void>
// Delete a draft message
deleteDraft(): Promise<void>
// Create a poll
createPoll(): Promise<void>
Context Information Getters
// Get context type (channel, thread, legacy_thread, or message)
get contextType(): string
// Get composer tag
get tag(): string
// Get thread ID
get threadId(): string | null
MessageComposer State Getters
// Get message ID
get id(): string
// Get draft ID
get draftId(): string | null
// Get last change timestamp
get lastChange(): LastComposerChange
// Get quoted message
get quotedMessage(): LocalMessage | null
// Get poll ID
get pollId(): string | null
Composition State Getters
// Check if composer has sendable data
get hasSendableData(): boolean
// Check if composition is empty
get compositionIsEmpty(): boolean
// Check if last change was local
get lastChangeOriginIsLocal(): boolean
Static Methods
// Generate a unique ID
static generateId(): string
Text Composition With Suggestions
Attachment Management
The AttachmentManager
class handles file attachments in message composition. It provides methods for managing attachments and their upload states.
Basic Attachment Management
// Get current attachments
const attachments = attachmentManager.attachments;
// Add or update attachments
attachmentManager.upsertAttachments([newAttachment]);
// Remove attachments
attachmentManager.removeAttachments([attachmentId]);
Uploading Attachments
When using uploadAttachment
, ensure correct attachment object structure:
// Convert file to proper attachment object
const localAttachment =
await attachmentManager.fileToLocalUploadAttachment(file);
// Upload the attachment
const uploadedAttachment =
await attachmentManager.uploadAttachment(localAttachment);
Uploaded attachment objects carry localMetadata
, that are discarded before a message is sent to the server. The localMetadata
of an uploaded attachment contain:
file
- the file referenceuploadState
- the values are'uploading'
,'finished'
,'failed'
,'blocked'
,'pending'
(to be uploaded)uploadPermissionCheck
- contains the upload permission check result to the CDN and explains the reason if blocked
When editing a draft or an existing message, the already uploaded attachments are automatically marked with uploadState: "finished"
and have no file reference neither uploadPermissionCheck
.
Custom Upload Configuration
File Filtering
Use fileUploadFilter
to add custom filtering logic beyond acceptedFiles
:
attachmentManager.fileUploadFilter = (file) => {
// Custom filter logic
return file.size < 10 * 1024 * 1024; // Example: 10MB limit
};
Custom Upload Destination
Use doUploadRequest
to customize where files are uploaded:
attachmentManager.setCustomUploadFn(async (file) => {
// Upload to custom CDN
const result = await customCDN.upload(file);
return { file: result.url };
});
For custom CDN uploads, you may need to override getUploadConfigCheck
:
class CustomAttachmentManager extends AttachmentManager {
getUploadConfigCheck = async (file) => {
// Skip default upload checks for custom CDN
return { uploadBlocked: false };
};
}
Attachment Identity Functions
The AttachmentManager
uses internally attachment identity functions to determine operation appropriate to the attachment type. We recommend using attachment identity functions for type safety:
import { isLocalImageAttachment } from "stream-chat";
if (isLocalImageAttachment(attachment)) {
// Type-safe access to image-specific properties
console.log(attachment.original_height);
}
Note, the attachments to be considered “local” they need to have localMetadata
key.
Function | Description |
---|---|
isLocalAttachment | Checks if attachment has local metadata |
isLocalUploadAttachment | Checks if attachment has upload state metadata |
isFileAttachment | Checks if attachment is a file attachment |
isLocalFileAttachment | Checks if attachment is a local file attachment |
isImageAttachment | Checks if attachment is an image attachment |
isLocalImageAttachment | Checks if attachment is a local image attachment |
isAudioAttachment | Checks if attachment is an audio attachment |
isLocalAudioAttachment | Checks if attachment is a local audio attachment |
isVoiceRecordingAttachment | Checks if attachment is a voice recording attachment |
isLocalVoiceRecordingAttachment | Checks if attachment is a local voice recording attachment |
isVideoAttachment | Checks if attachment is a video attachment |
isLocalVideoAttachment | Checks if attachment is a local video attachment |
isUploadedAttachment | Checks if attachment is an uploaded attachment (audio, file, image, video, or voice recording) |
isScrapedContent | Checks if attachment is scraped content (has og_scrape_url or title_link) |
Link Preview Management
The LinkPreviewsManager
class handles link preview generation and management in message composition. It provides methods for finding, enriching, and managing link previews. The link previews are converted to attachments before sending the message to the server.
State
The manager maintains a state with the following properties:
type LinkPreviewsManagerState = {
previews: Map<string, LinkPreview>; // URL -> Preview mapping
};
Each preview has a status:
LOADING
: Preview is being fetchedLOADED
: Preview successfully loadedFAILED
: Preview loading failedDISMISSED
: Preview was dismissed by userPENDING
: Preview is waiting to be processed
LinkPreviewsManager API
Finding and Enriching URLs
The enrichment request execution is debounced by 1.5 seconds by default.
// Find and enrich URLs in text (debounced)
linkPreviewsManager.findAndEnrichUrls(text);
// Cancel ongoing enrichment
linkPreviewsManager.cancelURLEnrichment();
Managing Previews
// Clear non-dismissed previews
linkPreviewsManager.clearPreviews();
// Update a specific preview
linkPreviewsManager.updatePreview(url, {
title: "New Title",
description: "New Description",
status: LinkPreviewStatus.LOADED,
});
// Dismiss a preview
linkPreviewsManager.dismissPreview(url);
Dismissed previews are not re-enriched when the same URL appears again in the text. The clearPreviews
method preserves dismissed previews while removing others.
Static Helpers
LinkPreviewManager
exposes static API to determine the preview state:
// Check preview status
LinkPreviewsManager.previewIsLoading(preview);
LinkPreviewsManager.previewIsLoaded(preview);
LinkPreviewsManager.previewIsDismissed(preview);
LinkPreviewsManager.previewIsFailed(preview);
LinkPreviewsManager.previewIsPending(preview);
The getPreviewData
static method extracts the preview data that will be converted to a scraped attachment when the message is sent to the server.
// Get preview data without status
LinkPreviewsManager.getPreviewData(preview);
Poll Composition
The PollComposer
class handles poll composition and creation. It provides methods for composing poll data that will be attached to a message.
PollComposer State
The poll state consists of poll data and validation errors:
type PollComposerState = {
data: {
id: string;
name: string;
description: string;
options: Array<{ id: string; text: string }>;
max_votes_allowed: string;
enforce_unique_vote: boolean;
allow_answers: boolean;
allow_user_suggested_options: boolean;
voting_visibility: VotingVisibility;
user_id: string;
};
errors: Record<string, string>;
};
Validation is important as it prevents failed creation requests on the server side.
PollComposer State Management
The state is initiated or can be reset:
// Reset to initial state
pollComposer.initState();
As poll composition consists of multiple fields, these can be updated. Navigation between fields leads to them being blurred. It is possible to react to both types of events. The updateFields
method accepts partial objects:
// Update poll fields
pollComposer.updateFields({
name: "Favorite Color?",
description: "Choose your favorite color",
options: [
{ id: "1", text: "Red" },
{ id: "2", text: "Blue" },
],
max_votes_allowed: "1",
enforce_unique_vote: true,
});
// Handle field blur validation
pollComposer.handleFieldBlur("name");
On each update or blur event the data validation and processing take place. Both can be customized via PollComposer middleware (see the PollComposer middleware guide for detailed information).
Poll Creation
messageComposer.createPoll();
Behind the scenes, the poll is composed and created on the server. Watch for failed creation request by suscribing to StreamChat.notifications.state
(see the client notifications service guide).
State Access
// Check if poll can be created
const canCreate = pollComposer.canCreatePoll;
// Access poll fields
const name = pollComposer.name;
const description = pollComposer.description;
const options = pollComposer.options;
const maxVotes = pollComposer.max_votes_allowed;
const enforceUniqueVote = pollComposer.enforce_unique_vote;
const allowAnswers = pollComposer.allow_answers;
const allowUserOptions = pollComposer.allow_user_suggested_options;
const votingVisibility = pollComposer.voting_visibility;
Validation
The canCreatePoll
property checks the minimum requiremets for a poll to be created:
- At least one non-empty option exists
- Poll name is not empty
max_votes_allowed
is either empty or a valid number between 2 and 10- No validation errors are present
Custom Data Management
The CustomDataManager
class handles custom data for both messages and the composer itself. It provides methods for managing two types of custom data:
- Message custom data - data that will be sent with the message
- Composer custom data - data that stays in the composer and is not sent with the message
State
The custom data state consists of two separate objects:
type CustomDataManagerState = {
message: CustomMessageData; // Data sent with the message
custom: CustomMessageComposerData; // Data for custom integration needs, not sent with the message
};
The custom
property holds any data that an integrator may need for their custom integration that should not be sent with the message. This is useful for storing UI state, temporary data, or any other integration-specific information that needs to persist during message composition.
State Management
// Get current state values
const messageData = customDataManager.customMessageData;
const composerData = customDataManager.customComposerData;
// Update message custom data
customDataManager.setMessageData({
customField: "value",
});
// Update composer custom data
customDataManager.setCustomData({
composerField: "value",
});
// Initialize state
customDataManager.initState({ message: existingMessage });
Custom Data Comparison
The isMessageDataEqual
method determines if the message custom data has changed. By default, it performs a JSON string comparison of the message data:
isMessageDataEqual = (
nextState: CustomDataManagerState,
previousState?: CustomDataManagerState,
) =>
JSON.stringify(nextState.message) === JSON.stringify(previousState?.message);
Integrators can override this method to implement custom comparison logic based on their specific needs. For example:
class CustomDataManager {
isMessageDataEqual = (
nextState: CustomDataManagerState,
previousState?: CustomDataManagerState,
) => {
// Custom comparison logic
return nextState.message.customField === previousState?.message.customField;
};
}
This is particularly useful when:
- Only specific fields need to be compared
- Complex data structures require special comparison logic
- Certain fields should be ignored in the comparison