Send and list messages with XMTP
The message payload can be a plain string, but you can configure custom content types. To learn more, see Content types.
Send messages
To send a message, the recipient must have already started their client at least once and consequently advertised their key bundle on the network.
Messages are limited to just short of 1MB (1048214 bytes) . Use remote attachments to support larger messages.
- JavaScript
- React
- Kotlin
- Swift
- Dart
- React Native
const conversation = await xmtp.conversations.newConversation(
"0x3F11b27F323b62B159D2642964fa27C46C841897",
);
await conversation.send("Hello world");
You might want to consider optimistically sending messages. This way, the app will not have to wait for the network to process the message first. Optimistic sending is especially useful for mobile apps where the user might have a spotty connection, making the app continue to run with multiple threads.
// standard (string) message
const preparedTextMessage = await conversation.prepareMessage(messageText);
//After preparing an optimistic message, use its `send` method to send it.
try {
preparedMessage.send();
} catch (e) {
// handle error, enable canceling and retries (see below)
}
import { useSendMessage } from "@xmtp/react-sdk";
import type { Conversation } from "@xmtp/react-sdk";
import { useCallback, useState } from "react";
export const SendMessage: React.FC<{ conversation: CachedConversation }> = ({
conversation,
}) => {
const [peerAddress, setPeerAddress] = useState("");
const [message, setMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const { sendMessage } = useSendMessage();
const handleAddressChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setPeerAddress(e.target.value);
},
[],
);
const handleMessageChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setMessage(e.target.value);
},
[],
);
const handleSendMessage = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (peerAddress && isValidAddress(peerAddress) && message) {
setIsLoading(true);
await sendMessage(conversation, message);
setIsLoading(false);
}
},
[message, peerAddress, sendMessage],
);
return (
<form onSubmit={handleSendMessage}>
<input
name="addressInput"
type="text"
onChange={handleAddressChange}
disabled={isSending}
/>
<input
name="messageInput"
type="text"
onChange={handleMessageChange}
disabled={isSending}
/>
</form>
);
};
Optimistic sending with React
When a user sends a message with XMTP, they might experience a slight delay between sending the message and seeing their sent message display in their app UI.
Typically, the slight delay is caused by the app needing to wait for the XMTP network to finish processing the message before the app can display the message in its UI.
The local-first architecture of the React SDK automatically includes optimistic sending, which immediately displays the sent message in the sender’s UI while processing the message in the background. Optimistic sending provides the sender with immediate feedback and enables them to continue messaging without waiting for their previous message to finish processing.
Messages in the sending state have their isSending
property set to true
.
Handle messages that fail to send with React
If a message fails to complete the sending process, you must provide an error state that alerts the user and enables them to either resend the message or cancel sending the message.
While in this unsent state, the message remains in its original location in the user’s conversation flow, with any newer sent and received messages displaying after it.
If the user resends the message, the message moves into the most recently sent message position in the conversation. Once it successfully sends, it remains in that position.
If the user cancels sending the message, the message is removed from the conversation flow.
Messages that fail to send have their hasSendError
property set to true
.
Resend a failed message
Use the resendMessage
function from the useResendMessage
hook to resend a failed message.
const { resendMessage } = useResendMessage();
// resend the message
resendMessage(failedMessage);
val conversation =
client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897")
conversation.send(text = "Hello world")
let conversation = try await client.conversations.newConversation(
with: "0x3F11b27F323b62B159D2642964fa27C46C841897")
try await conversation.send(content: "Hello world")
var convo = await client.newConversation("0x...");
await client.sendMessage(convo, 'gm');
const conversation = await xmtp.conversations.newConversation(
"0x3F11b27F323b62B159D2642964fa27C46C841897",
);
await conversation.send("Hello world");
List messages in a conversation
You can receive the complete message history in a conversation.
- JavaScript
- React
- Kotlin
- Swift
- Dart
- React Native
for (const conversation of await xmtp.conversations.list()) {
// All parameters are optional and can be omitted
const opts = {
// Only show messages from last 24 hours
startTime: new Date(new Date().setDate(new Date().getDate() - 1)),
endTime: new Date(),
};
const messagesInConversation = await conversation.messages(opts);
}
import { useCallback } from "react";
import { useMessages } from "@xmtp/react-sdk";
import type { CachedConversation } from "@xmtp/react-sdk";
export const Messages: React.FC<{
conversation: CachedConversation;
}> = ({ conversation }) => {
// error callback
const onError = useCallback((err: Error) => {
// handle error
}, []);
// messages callback
const onMessages = useCallback((msgs: DecodedMessage[]) => {
// do something with messages
}, []);
const { error, messages, isLoading } = useMessages(conversation, {
onError,
onMessages,
});
if (error) {
return "An error occurred while loading messages";
}
if (isLoading) {
return "Loading messages...";
}
return (
...
);
};
for (conversation in client.conversations.list()) {
val messagesInConversation = conversation.messages()
}
for conversation in client.conversations.list() {
let messagesInConversation = try await conversation.messages()
}
// Only show messages from the last 24 hours.
var messages = await alice.listMessages(convo,
start: DateTime.now().subtract(const Duration(hours: 24)));
for (const conversation of await xmtp.conversations.list()) {
const messagesInConversation = await conversation.messages(before: new Date(
new Date().setDate(new Date().getDate() - 1)), after: new Date())
}
List messages in a conversation with pagination
If a conversation has a lot of messages, it's more performant to retrieve and process the messages page by page instead of handling all of the messages at once.
- JavaScript
- React
- Kotlin
- Swift
- Dart
- React Native
Automatically handled by the SDK
Automatically handled by the SDK
Call conversation.messages(limit: Int, before: Date)
to return the specified number of messages sent before that time.
val conversation =
client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897")
val messages = conversation.messages(limit = 25)
val nextPage = conversation.messages(limit = 25, before = messages[0].sent)
Call conversation.messages(limit: Int, before: Date)
, which will return the specified number of messages sent before that time.
let conversation = try await client.conversations.newConversation(
with: "0x3F11b27F323b62B159D2642964fa27C46C841897")
let messages = try await conversation.messages(limit: 25)
let nextPage = try await conversation.messages(limit: 25, before: messages[0].sent)
Specify limit
and end
, which will return the specified number
of messages sent before that time.
var messages = await alice.listMessages(convo, limit: 10);
var nextPage = await alice.listMessages(
convo, limit: 10, end: messages.last.sentAt);
const conversation = await xmtp.conversations.newConversation(
'0x3F11b27F323b62B159D2642964fa27C46C841897'
)
for await (const page of conversation.messages(limit: 25)) {
for (const msg of page) {
// Breaking from the outer loop will stop the client from requesting any further pages
if (msg.content === 'gm') {
return
}
console.log(msg.content)
}
}
The methods for sending and listing messages, as well as handling unsupported content types, are applicable to both one-on-one and group conversations within XMTP. This means you can use the same approach to manage messages in group chats as you would in individual conversations.
For additional information on group chat-specific features, such as managing group members or listening for new messages in a group chat, please see the Group Chat documentation.
Handle unsupported content types
As more custom and standards-track content types are introduced into the XMTP ecosystem, your app may encounter content types it does not support. This situation, if not handled properly, could lead to app crashes.
Each message is accompanied by a contentFallback
property, which offers a descriptive string representing the content type's expected value. It's important to note that content fallbacks are immutable and are predefined in the content type specification. In instances where contentFallback
is undefined
, such as read receipts, it indicates that the content is not intended to be rendered. If you're venturing into creating custom content types, you're provided with the flexibility to specify a custom fallback string. For a deeper dive into this, consider exploring the Custom Content Type Tutorial.
- JavaScript
- React
- Kotlin
- Swift
- Dart
- React Native
const codec = client.codecFor(content.contentType);
if (!codec) {
/*Not supported content type*/
if (message.contentFallback !== undefined) {
return message.contentFallback;
}
// Handle other types like ReadReceipts which are not mean to be displayed
}
const codec = client.codecFor(content.contentType);
if (!codec) {
/*Not supported content type*/
if (message.contentFallback !== undefined) {
return message.contentFallback;
}
// Handle other types like ReadReceipts which are not mean to be displayed
}
val codec = client.codecRegistry.find(options?.contentType)
if (!codec) {
/*Not supported content type*/
if (message.contentFallback != null) {
return message.contentFallback
}
// Handle other types like ReadReceipts which are not meant to be displayed
}
let codec = client.codecRegistry.find(for: contentType)
if (!codec) {
/*Not supported content type*/
if (message.contentFallback != null) {
return message.contentFallback
}
// Handle other types like ReadReceipts which are not meant to be displayed
}
Dart doesnt have a public method for checking if the content type is registered. Instead the decode function uses this method internally and returns the encoded.fallback
if exists.
import 'package:xmtp_proto/xmtp_proto.dart' as xmtp;
import 'package:xmtp/src/content/codec_registry.dart';
import 'package:xmtp/src/content/text_codec.dart';
// Create a CodecRegistry and register a TextCodec
var registry = CodecRegistry();
registry.registerCodec(TextCodec());
// Use the registry to decode the content
var decoded = await registry.decode(encoded);
//if the content type doesn't exists. will return `encoded.fallback`
if (decoded == null) {
// Handle other types like ReadReceipts which are not meant to be displayed
}
const contentTypeID = `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}`;
const isRegistered = contentTypeID in client.codecRegistry;
if (!isRegistered) {
// Not supported content type
if (message?.fallback != null) {
return message?.fallback;
}
// Handle other types like ReadReceipts which are not meant to be displayed
}
Note: ReadReceipts
have an undefined
or nil
fallback, indicating the message is not expected to be displayed.