### Javascript
```
async function sendReply(replyToMessage, replyText) {
const metadata = {
reply: {
serial: replyToMessage.serial,
timestamp: replyToMessage.timestamp.getTime(),
clientId: replyToMessage.clientId,
previewText: replyToMessage.text.substring(0, 140)
}
};
await room.messages.send({
text: replyText,
metadata: metadata
});
}
```
### React
```
import { useMessages } from '@ably/chat/react';
const ReplyComponent = ({ messageToReplyTo }) => {
const { sendMessage } = useMessages();
const sendReply = async (replyText) => {
const metadata = {
reply: {
serial: messageToReplyTo.serial,
timestamp: messageToReplyTo.timestamp.getTime(),
clientId: messageToReplyTo.clientId,
previewText: messageToReplyTo.text.substring(0, 140)
}
};
await sendMessage({
text: replyText,
metadata: metadata
});
};
return (
);
};
```
### Swift
```
func sendReply(replyToMessage: Message, replyText: String) async throws {
let metadata: MessageMetadata = [
"reply": .object([
"serial": .string(replyToMessage.serial),
"timestamp": .number(Double(replyToMessage.timestamp.timeIntervalSince1970 * 1000)),
"clientId": .string(replyToMessage.clientID),
"previewText": .string(String(replyToMessage.text.prefix(140)))
])
]
try await room.messages.send(withParams: .init(
text: replyText,
metadata: metadata
))
}
```
### Kotlin
```
import com.ably.chat.json.jsonObject
suspend fun sendReply(replyToMessage: Message, replyText: String) {
val metadata = jsonObject {
putObject("reply") {
put("serial", replyToMessage.serial)
put("timestamp", replyToMessage.timestamp)
put("clientId", replyToMessage.clientId)
put("previewText", replyToMessage.text.take(140))
}
}
room.messages.send(
text = replyText,
metadata = metadata
)
}
```
### Jetpack
```
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.ably.chat.Message
import com.ably.chat.Room
import com.ably.chat.json.jsonObject
import kotlinx.coroutines.launch
@Composable
fun SendReplyComponent(room: Room, messageToReplyTo: Message) {
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
val metadata = jsonObject {
putObject("reply") {
put("serial", messageToReplyTo.serial)
put("timestamp", messageToReplyTo.timestamp)
put("clientId", messageToReplyTo.clientId)
put("previewText", messageToReplyTo.text.take(140))
}
}
room.messages.send(
text = "My reply",
metadata = metadata
)
}
}) {
Text("Send Reply")
}
}
```
## Subscribe to message replies
Message replies will be received as normal messages in the room using the [`subscribe()`](https://ably.com/docs/chat/rooms/messages.md#subscribe) method.
You just need to handle storing and displaying the reply:
### Store reply information
When a user replies to a message, extract and store the parent message details:
#### Javascript
```
function prepareReply(parentMessage) {
return {
serial: parentMessage.serial,
timestamp: parentMessage.timestamp.getTime(),
clientId: parentMessage.clientId,
previewText: parentMessage.text.substring(0, 140)
};
}
```
#### React
```
const prepareReply = (parentMessage) => {
return {
serial: parentMessage.serial,
timestamp: parentMessage.timestamp.getTime(),
clientId: parentMessage.clientId,
previewText: parentMessage.text.substring(0, 140)
};
};
```
#### Swift
```
func prepareReply(parentMessage: Message) -> JSONObject {
return [
"serial": .string(parentMessage.serial),
"timestamp": .number(Double(parentMessage.timestamp.timeIntervalSince1970 * 1000)),
"clientId": .string(parentMessage.clientID),
"previewText": .string(String(parentMessage.text.prefix(140)))
]
}
```
#### Kotlin
```
import com.ably.chat.json.jsonObject
fun prepareReply(parentMessage: Message) = jsonObject {
put("serial", parentMessage.serial)
put("timestamp", parentMessage.timestamp)
put("clientId", parentMessage.clientId)
put("previewText", parentMessage.text.take(140))
}
```
#### Jetpack
```
import com.ably.chat.Message
import com.ably.chat.json.jsonObject
fun prepareReply(parentMessage: Message) = jsonObject {
put("serial", parentMessage.serial)
put("timestamp", parentMessage.timestamp)
put("clientId", parentMessage.clientId)
put("previewText", parentMessage.text.take(140))
}
```
If a parent message isn't in local state, fetch it directly using its `serial`:
#### Javascript
```
async function fetchParentMessage(replyData) {
const message = await room.messages.get(replyData.serial);
return message;
}
```
#### React
```
const FetchParentMessage = ({ replyData }) => {
const [parentMessage, setParentMessage] = useState();
useEffect(() => {
const fetchMessage = async () => {
const message = await room.messages.get(replyData.serial);
setParentMessage(message);
};
fetchMessage();
}, [replyData]);
return parentMessage ? (
{parentMessage.text}
) : null;
};
```
#### Swift
```
func fetchParentMessage(replyData: JSONObject) async throws -> Message {
guard let serial = replyData["serial"]?.stringValue else {
throw NSError(domain: "ReplyError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid serial"])
}
return try await room.messages.get(withSerial: serial)
}
```
#### Kotlin
```
import com.ably.chat.json.*
suspend fun fetchParentMessage(replyData: JsonObject): Message {
val serial = (replyData["serial"] as? JsonString)?.value
?: throw IllegalArgumentException("Invalid serial")
return room.messages.get(serial)
}
```
#### Jetpack
```
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.ably.chat.*
import com.ably.chat.json.*
@Composable
fun FetchParentMessageComponent(room: Room, replyData: JsonObject) {
var parentMessage by remember { mutableStateOf(null) }
LaunchedEffect(replyData) {
val serial = (replyData["serial"] as? JsonString)?.value
if (serial != null) {
parentMessage = room.messages.get(serial)
}
}
parentMessage?.let { message ->
Text(text = message.text)
}
}
```
### Display replies
Check incoming messages for reply `metadata` and display accordingly:
#### Javascript
```
room.messages.subscribe((messageEvent) => {
const message = messageEvent.message;
if (message.metadata?.reply) {
const replyData = message.metadata.reply;
const parentMessage = localMessages.find(msg => msg.serial === replyData.serial);
if (parentMessage) {
console.log(`Reply to ${parentMessage.clientId}: ${parentMessage.text}`);
} else {
console.log(`Reply to ${replyData.clientId}: ${replyData.previewText}`);
}
}
console.log(`Message: ${message.text}`);
});
```
#### React
```
import { useMessages } from '@ably/chat/react';
import { ChatMessageEventType } from '@ably/chat';
const MessageList = () => {
const [messages, setMessages] = useState([]);
useMessages({
listener: (event) => {
if (event.type === ChatMessageEventType.Created) {
setMessages(prev => [...prev, event.message]);
}
}
});
const findParentMessage = (replyData) => {
return messages.find(msg => msg.serial === replyData.serial);
};
return (
{messages.map(message => (
{message.metadata?.reply && (
Replying to: {message.metadata.reply.previewText}
)}
{message.text}
))}
);
};
```
#### Swift
```
// Extension to extract reply data from a message
extension Message {
var replySerial: String? {
metadata["reply"]?.objectValue?["serial"]?.stringValue
}
var replyPreview: (clientId: String, text: String)? {
guard let replyData = metadata["reply"]?.objectValue,
let clientId = replyData["clientId"]?.stringValue,
let previewText = replyData["previewText"]?.stringValue else {
return nil
}
return (clientId, previewText)
}
}
// Subscribe to messages and handle replies
var localMessages: [Message] = []
for await event in room.messages.subscribe() {
let message = event.message
if let replySerial = message.replySerial {
if let parentMessage = localMessages.first(where: { $0.serial == replySerial }) {
print("Reply to \(parentMessage.clientID): \(parentMessage.text)")
} else if let preview = message.replyPreview {
print("Reply to \(preview.clientId): \(preview.text)")
}
}
print("Message: \(message.text)")
localMessages.append(message)
}
```
#### Kotlin
```
import com.ably.chat.json.*
// Subscribe to messages and handle replies
val localMessages = mutableListOf()
room.messages.subscribe { event ->
val message = event.message
val replyData = message.metadata["reply"] as? JsonObject
if (replyData != null) {
val replySerial = (replyData["serial"] as? JsonString)?.value
val parentMessage = localMessages.find { it.serial == replySerial }
if (parentMessage != null) {
println("Reply to ${parentMessage.clientId}: ${parentMessage.text}")
} else {
val replyClientId = (replyData["clientId"] as? JsonString)?.value
val previewText = (replyData["previewText"] as? JsonString)?.value
println("Reply to $replyClientId: $previewText")
}
}
println("Message: ${message.text}")
localMessages.add(message)
}
```
#### Jetpack
```
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import com.ably.chat.*
import com.ably.chat.json.*
@Composable
fun MessageListComponent(room: Room) {
val messages = remember { mutableStateListOf() }
DisposableEffect(room) {
val (unsubscribe) = room.messages.subscribe { event ->
messages += event.message
}
onDispose {
unsubscribe()
}
}
Column {
messages.forEach { message ->
Column {
// Display reply information if present
val replyData = message.metadata["reply"] as? JsonObject
if (replyData != null) {
val previewText = (replyData["previewText"] as? JsonString)?.value
Text(text = "Replying to: $previewText")
}
// Display the message text
Text(text = message.text)
}
}
}
}
```
## Considerations
Consider the following when implementing message replies:
- Older messages may not be available depending on message persistence settings.
- Messages can be [updated](https://ably.com/docs/chat/rooms/messages.md#update), potentially removing references to replies.
- The `metadata` field is not server-validated.
- Nested replies can be complex and expensive to implement, so consider limiting reply depth.