Threads & Replies

Threads allow users to reply to specific messages without cluttering the main channel conversation. A thread is created when a message is sent with a parent_id referencing another message.

Starting a Thread

Send a message with a parent_id to start a thread or add a reply to an existing thread.

// Reply to a message, which starts a thread if it does not have one yet
Channel->SendReply(FMessage{TEXT("This is a reply in a thread")}, ParentMessage);

// Pass true to also show the reply in the main channel message list
Channel->SendReply(FMessage{TEXT("Also shown in the channel")}, ParentMessage, true);

// Or set the fields yourself and send it like any other message
FMessage Reply{TEXT("This is a reply in a thread")};
Reply.ParentId = ParentMessage.Id;
Reply.bShowInChannel = false;
Channel->SendMessage(Reply);

// A reply is recognised by its parent, not by its type: the API reports thread replies
// as regular messages, so check FMessage::IsThreadReply() rather than the message type.

// Also available as a latent Blueprint node: Send Reply.

Thread Parameters

NameTypeDescriptionDefaultOptional
parent_idstringID of the parent message to reply to
show_in_channelbooleanIf true, the reply appears both in the thread and the main channelfalse

Messages in threads support the same features as regular messages: reactions, attachments, and mentions.

Paginating Thread Replies

When querying a channel, thread replies are not included by default. The parent message includes a reply_count field. Use getReplies to fetch thread messages.

// Channel state carries the channel's history and not its threads, so a thread's replies
// are fetched separately, once per thread the user opens.
Channel->QueryReplies(
  ParentMessage,
  20,
  [](const TArray<FMessage>& Replies)
  {
    // Also kept on the channel, at Channel->GetReplies(ParentMessage)
  });

// Page further back as the user scrolls up. Replies are held oldest first, so this asks
// for replies older than the oldest one already held.
Channel->QueryAdditionalReplies(ParentMessage, 20);

// The channel's RepliesUpdated delegate fires with the parent message id whenever a
// thread's replies change, including when a new reply arrives over the WebSocket.

Inline Replies

Reply to a message inline without creating a thread. The referenced message appears within the new message. Use quoted_message_id instead of parent_id.

JavaScript
const message = await channel.sendMessage({
  text: "I agree with this point",
  quoted_message_id: originalMessageId,
});

When querying messages, the quoted_message field is automatically populated:

{
  "id": "new-message-id",
  "text": "I agree with this point",
  "quoted_message_id": "original-message-id",
  "quoted_message": {
    "id": "original-message-id",
    "text": "The original message text"
  }
}

Inline replies are only available one level deep. If Message A replies to Message B, and Message B replies to Message C, you cannot access Message C through Message A. Fetch Message B directly to access its referenced message.

Thread List

Query all threads that the current user participates in. This is useful for building thread list views similar to Slack or Discord.

Querying Threads

Threads are returned with unread replies first, sorted by the latest reply timestamp in descending order.

Client->QueryThreads(
  {},    // Filter
  {},    // Sort options
  10,    // Threads per page
  2,     // Replies to preview per thread
  {},    // Page token from a previous response
  [](const TArray<FChatThread>& Threads, const FString& NextPage)
  {
    for (const FChatThread& Thread : Threads)
    {
      const FMessage& Parent = Thread.ParentMessage;
      const TArray<FMessage>& Preview = Thread.LatestReplies;
    }

    // Pass NextPage back as the fifth argument to fetch the following page
  });

// Only threads with unread replies
Client->QueryThreads(FFilter::Equal(TEXT("has_unread"), true));

// The type is FChatThread, not FThread, which the engine already uses in HAL/Thread.h.
// These threads are not watched, and there is no thread list widget yet.

Query Options

NameTypeDescriptionDefaultOptional
reply_limitnumberNumber of latest replies to fetch per thread2
participant_limitnumberNumber of thread participants to fetch per thread100
limitnumberMaximum number of threads to return10
watchbooleanIf true, watch channels for the returned threadstrue
member_limitnumberNumber of members to fetch per thread channel100

Filtering and Sorting

Filter and sort threads using MongoDB-style query operators.

Supported Filter Fields

FieldTypeOperatorsDescription
channel_cidstring or list of strings$eq, $inChannel CID
channel.disabledboolean$eqChannel disabled status
channel.teamstring or list of strings$eq, $inChannel team
parent_message_idstring or list of strings$eq, $inParent message ID
created_by_user_idstring or list of strings$eq, $inThread creator's user ID
created_atstring (RFC3339)$eq, $gt, $lt, $gte, $lteThread creation timestamp
updated_atstring (RFC3339)$eq, $gt, $lt, $gte, $lteThread update timestamp
last_message_atstring (RFC3339)$eq, $gt, $lt, $gte, $lteLast message timestamp

Supported Sort Fields

  • active_participant_count
  • created_at
  • last_message_at
  • parent_message_id
  • participant_count
  • reply_count
  • updated_at

Use 1 for ascending order and -1 for descending order.

JavaScript
// Get threads created by a specific user, sorted by creation date
const { threads, next } = await client.queryThreads({
  filter: {
    created_by_user_id: { $eq: "user-1" },
    updated_at: { $gte: "2024-01-01T00:00:00Z" },
  },
  sort: [{ created_at: -1 }],
  limit: 10,
});

// Get next page
const { threads: page2 } = await client.queryThreads({
  filter: {
    created_by_user_id: { $eq: "user-1" },
    updated_at: { $gte: "2024-01-01T00:00:00Z" },
  },
  sort: [{ created_at: -1 }],
  limit: 10,
  next,
});

Getting a Thread by ID

Retrieve a specific thread using the parent message ID.

// Pass a participant limit to get participants back. Left unset, this endpoint returns
// none at all rather than defaulting to some number of them.
Client->GetThread(
  ParentMessageId,
  10,    // Reply limit
  25,    // Participant limit
  [](const FChatThread& Thread)
  {
    const int32 ReplyCount = Thread.ReplyCount;
    const TArray<FUserRef>& Participants = Thread.ThreadParticipants;
  });

Updating Thread Title and Custom Data

Assign a title and custom data to a thread.

JavaScript
// Set properties
const { thread } = await client.partialUpdateThread(threadId, {
  set: {
    title: "Project Discussion",
    priority: "high",
  },
});

// Remove properties
await client.partialUpdateThread(threadId, {
  unset: ["priority"],
});

Thread Unread Counts

Total Unread Threads

The total number of unread threads is available after connecting.

JavaScript
const { me } = await client.connectUser({ id: "user-id" }, token);
console.log(me.unread_threads);

// Or access via thread manager
client.threads.registerSubscriptions();
const { unreadThreadCount } = client.threads.state.getLatestValue();

Marking Threads as Read or Unread

JavaScript
// Mark thread as read
await channel.markRead({ thread_id: parentMessageId });

// Mark thread as unread
await channel.markUnread({ thread_id: parentMessageId });

Unread Count Per Thread

JavaScript
const response = await client.getUnreadCount();

console.log(response.total_unread_threads_count);

for (const thread of response.threads) {
  console.log(thread.parent_message_id);
  console.log(thread.unread_count);
  console.log(thread.last_read);
}

Thread Manager

The ThreadManager class provides built-in pagination and state management for threads.

JavaScript
// Access the client's thread manager
const threadManager = client.threads;

// Subscribe to state updates
const unsubscribe = threadManager.state.subscribe((state) => {
  console.log(state.threads);
  console.log(state.unreadThreadCount);
});

// Load threads
await threadManager.loadNextPage();

// Access current state
const { threads } = threadManager.state.getLatestValue();

Event Handling

Register subscriptions to receive real-time updates for threads.

const { threads } = await client.queryThreads({ watch: true, limit: 10 });
const [thread] = threads;

// Register event handlers for a single thread
thread.registerSubscriptions();

const unsubscribe = thread.state.subscribe((state) => {
  console.log(state.replies);
});

The watch parameter is required when querying threads to receive real-time updates.

For ThreadManager, call registerSubscriptions once to automatically manage subscriptions for all loaded threads:

const threadManager = client.threads;
threadManager.registerSubscriptions();

await threadManager.loadNextPage();

// All threads are now listening to channel events
const { threads } = threadManager.state.getLatestValue();