Message threads

Organize conversations into threads for topic-specific discussions.

Benefits:

  • Focus discussions on specific topics
  • Clarify and resolve issues without cross-talk
  • Keep main channel conversations clean

Thread channels have IDs starting with PUBNUB_INTERNAL_THREAD_{channel_id}_{message_id}. Messages with threads have hasThread: true.

ThreadMessage and ThreadChannel extend the base message and channel entities with thread-specific methods.

Interactive demo​

Check what a sample implementation could look like in a React app showcasing how to reply to messages in threads and quote messages sent by others.

Want to implement something similar?

Read how to do that or go straight to the demo's source code.

Create thread​

createThread() creates a thread (channel) for a selected message, sends the first reply, and returns both the thread channel and the updated parent message with hasThread: true.

Method signature​

1message.createThread(
2 text: String,
3 params: SendTextParams = SendTextParams(),
4): PNFuture<CreateThreadResult>

Input parameters​

* required
ParameterDescription
text *
Type: String
Default:
n/a
The text you want to send to the selected channel.
params
Type: SendTextParams
Default:
SendTextParams()
Object with additional send options.
params.meta
Type: Map<String, Any>?
Default:
null
Additional details to publish with the request.
params.shouldStore
Type: Boolean
Default:
true
If true, the messages are stored in Message Persistence if enabled.
params.usePost
Type: Boolean
Default:
false
Use HTTP POST for the request.
params.ttl
Type: Int?
Default:
null
Time (in hours) the message should be stored.
params.usersToMention
Type: Collection<String>?
Default:
null
Collection of user IDs to notify with a mention.
params.customPushData
Type: Map<String, String>?
Default:
null
Additional key-value pairs for push messages.

Output​

TypeDescription
PNFuture<CreateThreadResult>Object containing both the thread channel and the updated parent message.

The returned CreateThreadResult object contains:

PropertyDescription
threadChannel
Type: ThreadChannel
The newly created thread channel for sending and receiving messages.
parentMessage
Type: Message
The updated parent message with hasThread set to true.

Sample code​

Create a thread for the last message on the support channel.

1channel.getHistory(count = 1).async { historyResult ->
2 historyResult.onSuccess { history ->
3 val messages = history.messages
4 if (messages.isNotEmpty()) {
5 val lastMessage = messages.last()
6 lastMessage.createThread(
7 text = "Thread starting text",
8 params = SendTextParams(
9 meta = mapOf("key" to "value"),
10 shouldStore = true,
11 ttl = 24
12 )
13 ).async { threadResult ->
14 threadResult.onSuccess { createThreadResult ->
15 val threadChannel = createThreadResult.threadChannel
show all 30 lines

Create thread with result​

Deprecated

This method is deprecated. Use createThread() instead, which now returns CreateThreadResult directly.

createThreadWithResult() creates a thread, sends the first reply, and returns both the thread channel and updated parent message with hasThread: true. Use this when you need immediate confirmation without a separate fetch.

Method signature​

1message.createThreadWithResult(
2 text: String,
3 params: SendTextParams = SendTextParams(),
4): PNFuture<CreateThreadResult>

Input parameters​

* required
ParameterDescription
text *
Type: String
Default:
n/a
Text that you want to send as the first message in the thread.
params
Type: SendTextParams
Default:
SendTextParams()
Object with additional send options.
params.meta
Type: Map<String, Any>?
Default:
null
Additional details to publish with the request.
params.shouldStore
Type: Boolean
Default:
true
If true, the messages are stored in Message Persistence if enabled.
params.usePost
Type: Boolean
Default:
false
Use HTTP POST for the request.
params.ttl
Type: Int?
Default:
null
Time (in hours) the message should be stored in Message Persistence. If shouldStore is true and ttl is 0, the message is stored with no expiry time. If shouldStore is true and ttl is set to a value, the message is stored with that expiry time. If shouldStore is false, this parameter is ignored. If not specified, the expiration defaults to the expiry value for the keyset.
params.usersToMention
Type: Collection<String>?
Default:
null
Collection of user IDs to automatically notify with a mention after this message is sent.
params.customPushData
Type: Map<String, String>?
Default:
null
Additional key-value pairs that will be added to the FCM and/or APNS push messages.

Output​

TypeDescription
PNFuture<CreateThreadResult>Object containing both the threadChannel and the updated parentMessage.

The returned CreateThreadResult object contains:

PropertyDescription
threadChannel
Type: ThreadChannel
The newly created thread channel for sending and receiving messages.
parentMessage
Type: Message
The updated parent message with hasThread set to true.

Sample code​

Create a thread for the last message on the support channel and get the updated parent message.

1channel.getHistory(count = 1).async { historyResult ->
2 historyResult.onSuccess { history ->
3 val messages = history.messages
4 if (messages.isNotEmpty()) {
5 val lastMessage = messages.last()
6 lastMessage.createThreadWithResult(
7 text = "This is the first reply in the thread",
8 params = SendTextParams()
9 ).async { result ->
10 result.onSuccess { createThreadResult ->
11 val threadChannel = createThreadResult.threadChannel
12 val parentMessage = createThreadResult.parentMessage
13
14 // parentMessage now has hasThread = true without needing to re-fetch
15 println("Thread created: ${threadChannel.id}")
show all 27 lines

Send thread message​

Reply to a message in a thread by calling sendText() on the threadChannel from the CreateThreadResult returned by createThread().

Method signature​

Head over to the sendText() method section for details.

Sample code​

Send a message in a thread created for the last message on the support channel.

1channel.getHistory(count = 1).async { historyResult ->
2 historyResult.onSuccess { history ->
3 val messages = history.messages
4 if (messages.isNotEmpty()) {
5 val lastMessage = messages.last()
6 lastMessage.createThread(text = "Good job, guys!").async { threadResult ->
7 threadResult.onSuccess { createThreadResult ->
8 createThreadResult.threadChannel.sendText("Another reply").async { /*...*/ }
9 }.onFailure {
10 // handle failure
11 }
12 }
13 } else {
14 // handle no messages found
15 }
show all 19 lines

Get thread​

Get the thread channel on which the thread message is published.

Method signature​

This method has the following signature:

1message.getThread(): PNFuture<ThreadChannel>

Input​

This method doesn't take any parameters.

Output​

TypeDescription
PNFuture<ThreadChannel>Object returning the thread channel metadata.

Sample code​

Get the thread channel created from the message with the 16200000000000001 timetoken.

1val supportChannel = chat.getChannel("support")
2
3supportChannel.getMessage(16200000000000001).async { messageResult ->
4 messageResult.onSuccess { message ->
5 message?.getThread()?.async { threadResult ->
6 threadResult.onSuccess { threadChannel ->
7 // handle success
8 }.onFailure {
9 // handle failure
10 }
11 } ?: run {
12 // handle message not found
13 }
14 }.onFailure {
15 // handle failure
show all 17 lines

Check if message starts thread​

hasThread indicates if a message starts a thread.

Sample code​

Check if the message with the 16200000000000001 timetoken starts a thread.

1// get the channel
2chat.getChannel("support").async { result ->
3 result.onSuccess { channel ->
4 // fetch the message history
5 channel.getHistory(
6 startTimetoken = 16200000000000000,
7 endTimetoken = 16200000000000001,
8 count = 1
9 ).async { historyResult ->
10 historyResult.onSuccess { history: HistoryResponse<*> ->
11 // reference the message
12 val message = history.messages.firstOrNull()
13
14 // check if the message starts a thread
15 if (message?.hasThread == true) {
show all 29 lines

Get thread updates​

onThreadMessageUpdated() on ThreadMessage receives updates when thread messages or reactions change.

The callback fires whenever messages are added, edited, deleted, or reactions change. Returns an AutoCloseable.

Method signature​

1threadMessage.onThreadMessageUpdated(callback: (message: ThreadMessage) -> Unit): AutoCloseable

Input​

* required
ParameterDescription
callback *
Type: (message: ThreadMessage) -> Unit
Default:
n/a
Callback function executed when the thread message or its reactions change.

Output​

TypeDescription
AutoCloseableInterface that lets you stop receiving thread message updates by invoking the close() method.

Sample code​

Get updates for a thread message published in a thread.

1val messageWithThread: Message
2
3messageWithThread.getThread().async {
4 it.onSuccess { threadChannel ->
5 threadChannel.getHistory().async {
6 it.onSuccess { historyResponse ->
7 val firstThreadMessage = historyResponse.messages.firstOrNull()
8 firstThreadMessage?.let { threadMsg ->
9 val subscription = threadMsg.onThreadMessageUpdated { updatedMessage ->
10 println("Updated thread message: $updatedMessage")
11 }
12 // subscription.close() to stop
13 }
14 }.onFailure { /* ... */ }
15 }
show all 17 lines

Get thread updates​

streamUpdatesOn() on ThreadMessage receives updates when thread messages or reactions change.

Stream update behavior

streamUpdatesOn() returns the complete list of monitored thread messages on each change.

Method signature​

This method takes the following parameters:

1class ThreadMessage {
2 companion object {
3 fun streamUpdatesOn(
4 messages: Collection<ThreadMessage>,
5 callback: (messages: Collection<ThreadMessage>) -> Unit
6 ): AutoCloseable
7 }
8 }

Input​

* required
ParameterDescription
messages *
Type: Collection<ThreadMessage>
Default:
n/a
Collection of ThreadMessage objects for which you want to get updates on changed message threads or related message reactions.
callback *
Type: (messages: Collection<ThreadMessage>) -> Unit
Default:
n/a
Callback function passed to the method as a parameter. It defines the custom behavior to be executed when detecting changes in message threads or related message reactions.

Output​

TypeDescription
AutoCloseableInterface that lets you stop receiving message thread-related updates by invoking the close() method.

Sample code​

Get message threads and message reaction-related updates for the first page of messages published in a thread.

1val messageWithThread: Message
2
3messageWithThread.getThread().async {
4 it.onSuccess { threadChannel ->
5 threadChannel.getHistory().async {
6 it.onSuccess { historyResponse ->
7 // stream updates for the fetched thread messages
8 val autoCloseable =
9 ThreadMessage.streamUpdatesOn(messages = historyResponse.messages) { updatedThreadMessages ->
10 // The callback receives the complete list of all thread messages you're monitoring
11 // (including all reactions) each time any change occurs.
12 updatedThreadMessages.forEach { updatedThreadMessage ->
13 println("-=Updated thread message: $updatedThreadMessage")
14 }
15 }
show all 26 lines

Other examples​

Stop listening to updates for the last ten messages published in a thread.

1val messageWithThread: Message
2
3messageWithThread.getThread().async {
4 it.onSuccess { threadChannel ->
5 threadChannel.getHistory(count = 10).async {
6 it.onSuccess { historyResponse ->
7 // stream updates for the fetched thread messages
8 val autoCloseable =
9 ThreadMessage.streamUpdatesOn(messages = historyResponse.messages) { updatedThreadMessages ->
10 // The callback receives the complete list of all thread messages you're monitoring
11 // (including all reactions) each time any change occurs.
12 updatedThreadMessages.forEach { updatedThreadMessage ->
13 println("-=Updated thread message: $updatedThreadMessage")
14 }
15 }
show all 28 lines

Get historical thread message​

getHistory() on threadChannel fetches historical thread messages.

Method signature​

This method takes the following parameters:

1threadChannel.getHistory(
2 startTimetoken: Long?,
3 endTimetoken: Long?,
4 count: Int
5): PNFuture<HistoryResponse<ThreadMessage>>

Input​

* required
ParameterDescription
startTimetoken
Type: Long
Default:
n/a
Timetoken delimiting the start of a time slice (exclusive) to pull thread messages from. For details, refer to Message Persistence.
endTimetoken
Type: Long
Default:
n/a
Timetoken delimiting the end of a time slice (inclusive) to pull thread messages from. For details, refer to the Message Persistence.
count *
Type: Int
Default:
25
Number of historical thread messages to return for the channel in a single call. Since each call returns all attached message reactions by default, the maximum number of returned thread messages is 25. For more details, refer to the description of the includeMessageActions parameter in the Kotlin SDK docs.

Output​

ParameterDescription
PNFuture<HistoryResponse<ThreadMessage>>
Type: object
PNFuture holding HistoryResponse containing a list of ThreadMessage objects and a Boolean flag indicating if there are more messages available.

By default, each call returns all message reactions and metadata attached to the retrieved thread messages.

Sample code​

From the thread created for the last message in the support channel, fetch 10 historical thread messages that are older than the timetoken 15343325214676133.

1// reference the "support" channel asynchronously
2chat.getChannel("support").async { channelResult ->
3 channelResult.onSuccess { channel ->
4 if (channel != null) {
5 // get the last message on the channel, which is the root message for the thread
6 channel.getHistory(count = 1).async { messageResult ->
7 messageResult.onSuccess { historyResponse ->
8 val message = historyResponse.messages.firstOrNull()
9
10 if (message != null) {
11 // get the thread channel
12 message.getThread().async { threadChannelResult ->
13 threadChannelResult.onSuccess { threadChannel ->
14 if (threadChannel != null) {
15 // fetch the required historical messages
show all 55 lines

Remove thread​

removeThread() removes a thread (channel) for a selected message.

Method signature​

This method has the following signature:

1message.removeThread(): PNFuture<Unit>

Input​

This method doesn’t take any parameters.

Output​

TypeDescription
PNFuture<Unit>Returns when the thread is successfully removed.

Sample code​

Remove a thread for the last message on the support channel.

1// retrieve the "support" channel
2chat.getChannel("support").async { channelResult ->
3 channelResult.onSuccess { channel ->
4 // get the last message from the channel’s history
5 channel.getHistory(count = 1).async { historyResult ->
6 historyResult.onSuccess { historyResponse ->
7 val message = historyResponse.messages.firstOrNull()
8 if (message != null) {
9 // remove the thread for the last message
10 message.removeThread().async { removeThreadResult ->
11 removeThreadResult.onSuccess {
12 println("Thread removed successfully.")
13 }.onFailure { throwable ->
14 println("Failed to remove thread.")
15 throwable.printStackTrace()
show all 30 lines

Pin thread message to thread channel​

pinMessage() on ThreadChannel pins a thread message to the thread channel.

Method signature​

This method takes the following parameters:

1threadChannel.pinMessage(
2 message: Message
3): PNFuture<ThreadChannel>

Input​

* required
ParameterDescription
message *
Type: Message
Default:
n/a
Message object you want to pin to the selected thread channel.

Output​

TypeDescription
PNFuture<ThreadChannel>Object returning the thread channel metadata updated with these custom fields:

pinnedMessageTimetoken to mark the timetoken when the message was pinned

pinnedMessageChannelID to mark the channel on which the message was pinned to the thread channel (unpinning was performed either directly on the parent channel or on a thread channel).

Sample code​

A thread was created for the last message in the support parent channel. Pin the last message from this thread to the thread channel.

1// reference the "support" channel where the root message for the thread is published
2chat.getChannel("support").async { channelResult ->
3 channelResult.onSuccess { channel ->
4 // get the last message on the channel, which is the root message for the thread
5 channel.getHistory(null, null, 1).async { historyResult ->
6 historyResult.onSuccess { history ->
7 val message = history.messages[0]
8
9 // get the thread channel
10 message.getThread().async { threadChannelResult ->
11 threadChannelResult.onSuccess { threadChannel ->
12 // get the last message on the thread channel
13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
14 threadHistoryResult.onSuccess { threadHistory ->
15 val threadMessage = threadHistory.messages[0]
show all 40 lines

Pin thread message to parent channel​

pinMessageToParentChannel() (on ThreadChannel) and pinToParentChannel() (on ThreadMessage) pin a thread message to the parent channel.

Method signature​

These methods take the following parameters:

  • pinMessageToParentChannel()

    1threadChannel.pinMessageToParentChannel(
    2 message: ThreadMessage
    3): PNFuture<Channel>
  • pinToParentChannel()

    1threadMessage.pinToParentChannel(): PNFuture<Channel>

Input​

ParameterRequired in pinMessageToParentChannel()Required in pinToParentChannel()Description
message
Type: ThreadMessage
Default:
n/a
YesNoThreadMessage object you want to pin to the selected parent channel.

Output​

TypeDescription
PNFuture<Channel>Object returning the channel metadata updated with these custom fields:

pinnedMessageTimetoken to mark the timetoken when the message was pinned

pinnedMessageChannelID to mark the channel on which the message was pinned to the parent channel (pinning was performed either directly on the parent channel or on a thread channel).

Sample code​

A thread was created for the last message in the support parent channel. Pin the last message from this thread to the parent channel.

  • pinMessageToParentChannel()

    1// reference the "support" channel where the root message for the thread is published
    2chat.getChannel("support").async { channelResult ->
    3 channelResult.onSuccess { channel ->
    4 // get the last message on the channel, which is the root message for the thread
    5 channel.getHistory(null, null, 1).async { historyResult ->
    6 historyResult.onSuccess { history ->
    7 val message = history.messages[0]
    8
    9 // get the thread channel
    10 message.getThread().async { threadChannelResult ->
    11 threadChannelResult.onSuccess { threadChannel ->
    12 // get the last message on the thread channel
    13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
    14 threadHistoryResult.onSuccess { threadHistory ->
    15 val threadMessage = threadHistory.messages[0]
    show all 40 lines
  • pinToParentChannel()

    1// reference the "support" channel where the root message for the thread is published
    2chat.getChannel("support").async { channelResult ->
    3 channelResult.onSuccess { channel ->
    4 // get the last message on the channel, which is the root message for the thread
    5 channel.getHistory(null, null, 1).async { historyResult ->
    6 historyResult.onSuccess { history ->
    7 val message = history.messages[0]
    8
    9 // get the thread channel
    10 message.getThread().async { threadChannelResult ->
    11 threadChannelResult.onSuccess { threadChannel ->
    12 // get the last message on the thread channel
    13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
    14 threadHistoryResult.onSuccess { threadHistory ->
    15 val threadMessage = threadHistory.messages[0]
    show all 40 lines

Unpin thread message from thread channel​

unpinMessage() on ThreadChannel unpins the pinned thread message.

Method signature​

This method has the following signature:

1threadChannel.unpinMessage(): PNFuture<ThreadChannel>

Input​

This method doesn't take any parameters.

Output​

TypeDescription
PNFuture<ThreadChannel>Object returning the thread channel metadata updated with these custom fields:

pinnedMessageTimetoken to mark the timetoken when the message was unpinned

pinnedMessageChannelID to mark the channel on which the message was unpinned from the thread channel (unpinning was performed either directly on the parent channel or on a thread channel).

Sample code​

Unpin the thread message from the thread (channel) created for the last message on the support channel.

1// reference the "support" channel where the root message for the thread is published
2chat.getChannel("support").async { channelResult ->
3 channelResult.onSuccess { channel ->
4 // get the last message on the channel, which is the root message for the thread
5 channel.getHistory(null, null, 1).async { historyResult ->
6 historyResult.onSuccess { history ->
7 val message = history.messages[0]
8
9 // get the thread channel
10 message.getThread().async { threadChannelResult ->
11 threadChannelResult.onSuccess { threadChannel ->
12 // get the last message on the thread channel
13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
14 threadHistoryResult.onSuccess { threadHistory ->
15 val threadMessage = threadHistory.messages[0]
show all 40 lines

Unpin thread message from parent channel​

unpinMessageFromParentChannel() (on ThreadChannel) and unpinFromParentChannel() (on ThreadMessage) unpin a thread message from the parent channel.

Method signature​

These methods have the following signatures:

  • unpinMessageFromParentChannel()

    1threadChannel.unpinMessageFromParentChannel(): PNFuture<Channel>
  • unpinFromParentChannel()

    1threadMessage.unpinFromParentChannel(): PNFuture<Channel>

Input​

These methods don't take any parameters.

Output​

TypeDescription
PNFuture<Channel>Object returning the channel metadata updated with these custom fields:

pinnedMessageTimetoken to mark the timetoken when the message was unpinned

pinnedMessageChannelID to mark the channel on which the message was unpinned from the parent channel (unpinning was performed either directly on the parent channel or on a thread channel).

Sample code​

Unpin the thread message from the support parent channel.

  • unpinMessageFromParentChannel()

    1// reference the "support" channel where the root message for the thread is published
    2chat.getChannel("support").async { channelResult ->
    3 channelResult.onSuccess { channel ->
    4 // get the last message on the channel, which is the root message for the thread
    5 channel.getHistory(null, null, 1).async { historyResult ->
    6 historyResult.onSuccess { history ->
    7 val message = history.messages[0]
    8
    9 // get the thread channel
    10 message.getThread().async { threadChannelResult ->
    11 threadChannelResult.onSuccess { threadChannel ->
    12 // get the last message on the thread channel
    13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
    14 threadHistoryResult.onSuccess { threadHistory ->
    15 val threadMessage = threadHistory.messages[0]
    show all 40 lines
  • unpinFromParentChannel()

    1// reference the "support" channel where the root message for the thread is published
    2chat.getChannel("support").async { channelResult ->
    3 channelResult.onSuccess { channel ->
    4 // get the last message on the channel, which is the root message for the thread
    5 channel.getHistory(null, null, 1).async { historyResult ->
    6 historyResult.onSuccess { history ->
    7 val message = history.messages[0]
    8
    9 // get the thread channel
    10 message.getThread().async { threadChannelResult ->
    11 threadChannelResult.onSuccess { threadChannel ->
    12 // get the last message on the thread channel
    13 threadChannel.getHistory(null, null, 1).async { threadHistoryResult ->
    14 threadHistoryResult.onSuccess { threadHistory ->
    15 val threadMessage = threadHistory.messages[0]
    show all 40 lines

Create thread message draft​

createThreadMessageDraft() creates a new thread for a message and returns a MessageDraft for composing and sending the first message in that thread, with options like user suggestions and typing indicators.

Method signature​

1fun Message.createThreadMessageDraft(
2 userSuggestionSource: UserSuggestionSource = UserSuggestionSource.CHANNEL,
3 isTypingIndicatorTriggered: Boolean = true,
4 userLimit: Int = 10,
5 channelLimit: Int = 10
6): PNFuture<MessageDraft>

Input parameters​

* required
ParameterDescription
userSuggestionSource
Type: UserSuggestionSource
Default:
UserSuggestionSource.CHANNEL
Scope for searching suggested users.
isTypingIndicatorTriggered
Type: Boolean
Default:
true
Whether modifying the message text triggers a typing indicator.
userLimit
Type: Int
Default:
10
Limit on number of users returned for mentions.
channelLimit
Type: Int
Default:
10
Limit on number of channels returned for references.

Output​

TypeDescription
PNFuture<MessageDraft>Object used for composing and formatting a message draft in a thread channel.

Sample code​

Create a message draft for the last message in the support channel and set up user suggestions and typing indicator.

1channel.getHistory(count = 1).async { historyResult ->
2 historyResult.onSuccess { history ->
3 val messages = history.messages
4 if (messages.isNotEmpty()) {
5 val lastMessage = messages.last()
6 lastMessage.createThreadMessageDraft(
7 userSuggestionSource = UserSuggestionSource.CHANNEL,
8 isTypingIndicatorTriggered = true,
9 userLimit = 10,
10 channelLimit = 10
11 ).async { draftResult ->
12 draftResult.onSuccess { messageDraft ->
13 // Handle success: Compose your message using messageDraft
14 }.onFailure {
15 // Handle failure
show all 24 lines

Create thread (deprecated)​

Deprecated

This method is deprecated. Use Create thread instead.

createThread() creates a thread (channel) for a selected message.

Method signature​

This method has the following signature:

1message.createThread(): PNFuture<ThreadChannel>
Input​

This method doesn't take any parameters.

Output​
TypeDescription
PNFuture<ThreadChannel>Object returning the thread channel metadata for the message updated with the hasThread parameter (and a threadRootId action type underneath).

Sample code​

Create a thread for the last message on the support channel.

1channel.getHistory(count = 1).async { historyResult ->
2 historyResult.onSuccess { history ->
3 val messages = history.messages
4 if (messages.isNotEmpty()) {
5 val lastMessage = messages.last()
6 lastMessage.createThread().async { threadResult ->
7 threadResult.onSuccess { threadChannel ->
8 // handle success
9 }.onFailure {
10 // handle failure
11 }
12 }
13 } else {
14 // handle no messages found
15 }
show all 19 lines

Was this page useful?

Last updated on