# Comments

Comments support voting, ranking, threading, images, URL previews, mentions and notifications.

## Adding Comments

```kotlin label="Kotlin"
// Adding a comment to an activity
val comment: Result<CommentData> = feed.addComment(
    request = ActivityAddCommentRequest(
        id = "comment_123", // id is optional, but must be unique. will be auto-generated if not provided
        comment = "So great!",
        activityId = "activity_123",
        custom = mapOf("sentiment" to "positive")
    )
)

// Adding a reply to a comment
val reply: Result<CommentData> = feed.addComment(
    request = ActivityAddCommentRequest(
        comment = "I agree!",
        activityId = "activity_123",
        parentId = "comment_456"
    )
)

// Adding a comment with attachments
val commentWithAttachment: Result<CommentData> = feed.addComment(
    request = ActivityAddCommentRequest(
        comment = "Check out this image!",
        activityId = "activity_123",
        attachments = listOf(
            Attachment(
                imageUrl = "https://example.com/image.jpg",
                type = "image"
            )
        )
    )
)
```

```java label="Java"
AddCommentRequest commentRequest =
    AddCommentRequest.builder()
        .id("comment_123") // id is optional, but must be unique. will be auto-generated if not provided
        .comment("This is a test comment from Java SDK")
        .objectID(activityId)
        .objectType("activity")
        .userID(testUserId)
        .build();

AddCommentResponse response = feeds.addComment(commentRequest).execute().getData();

// Adding a comment with attachments
AddCommentRequest commentWithAttachmentRequest =
    AddCommentRequest.builder()
        .comment("Check out this image!")
        .objectID(activityId)
        .objectType("activity")
        .userID(testUserId)
        .attachments(List.of(
            Attachment.builder()
                .imageUrl("https://example.com/image.jpg")
                .type("image")
                .build()))
        .build();
AddCommentResponse attachmentResponse = feeds.addComment(commentWithAttachmentRequest).execute().getData();
```

>
> **Info:** You can use [Stream's CDN](https://getstream.io/activity-feeds/docs/android/file-uploads/) for storing and quickly accessing files attached to comments.
>

### Comment request options

#### AddCommentRequest

| Name | Type | Description | Constraints |
| --- | --- | --- | --- |
| `attachments` | `Attachment[]` | Media attachments for the reply | - |
| `comment` | `string` | Text content of the comment | - |
| `copy_custom_to_notification` | `boolean` | Whether to copy custom data to the notification activity (only applies when create_notification_activity is true) Deprecated: use notification_context.trigger.custom and notification_context.target.custom instead | - |
| `create_notification_activity` | `boolean` | Whether to create a notification activity for this comment | - |
| `custom` | `object` | Custom data for the comment | - |
| `force_moderation` | `boolean` | If true, forces moderation to run for server-side requests. By default, server-side requests skip moderation. Client-side requests always run moderation regardless of this field. | - |
| `id` | `string` | Optional custom ID for the comment (max 255 characters). If not provided, a UUID will be generated. | - |
| `mentioned_user_ids` | `string[]` | List of users mentioned in the reply | - |
| `object_id` | `string` | ID of the object to comment on. Required for root comments | - |
| `object_type` | `string` | Type of the object to comment on. Required for root comments | - |
| `parent_id` | `string` | ID of parent comment for replies. When provided, object_id and object_type are automatically inherited from the parent comment. | - |
| `skip_enrich_url` | `boolean` | Whether to skip URL enrichment for this comment | - |
| `skip_push` | `boolean` | - | - |
| `user` | `UserRequest` | - | - |
| `user_id` | `string` | - | - |

## Updating Comments

`updateComment` endpoint performs a partial update: only the fields you include in the request are changed, and each of those fields is completely overwritten.

```kotlin label="Kotlin"
val result: Result<CommentData> = feed.updateComment(
    commentId = "comment_123",
    request = UpdateCommentRequest(comment = "Not so great")
)
println("Comment edited at: ${result.getOrNull()?.editedAt}")
```

```java label="Java"
UpdateCommentRequest updateRequest =
    UpdateCommentRequest.builder().comment("Updated comment text from Java SDK").build();

UpdateCommentResponse response =
    feeds.updateComment(commentId, updateRequest).execute().getData();
System.out.println("Comment edited at: " + response.getComment().getEditedAt());
```

## Removing Comments

```kotlin label="Kotlin"
feed.deleteComment(commentId = "comment_123")
```

```java label="Java"
DeleteCommentRequest deleteRequest = DeleteCommentRequest.builder().build();
DeleteCommentResponse response =
    feeds.deleteComment(commentId, deleteRequest).execute().getData();
```

## Reading Comments

You'll also want to show/return these comments. The most important is when reading the feed.

```kotlin label="Kotlin"
feed.getOrCreate()
feed.state.activities.collect { it.first().comments }
// or
val activity = client.activity(
    activityId = "activity_123",
    fid = FeedId(group = "user", id = "john")
)
activity.get()
activity.state.comments

// Reading a single comment
val comment = feed.getComment(commentId = "123")
```

```java label="Java"
GetOrCreateFeedResponse feedResponse = feeds.getOrCreateFeed(...).execute().getData();
var comments = feedResponse.getData().getActivities().get(0).getComments();

// Reading a single comment
GetCommentResponse commentResponse = feeds.getComment("123").execute().getData();
```

### Sort options

The following sort options are supported when reading comments:

- `last`: newest comment returned first
- `first`: oldest comment returned first
- `top`: highest score returned first - computed from `upvote` - `downvote` reaction types
- `controversial`: controversial comment returned first - mixed reactions (`upvote` and `downvote`) and lots of comments
- `best`: highest Wilson score returned first - Wilson score balances reaction score (`upvote` - `downvote`) with number of reactions (a comment with lower reaction score but more sum reactions is returned before a comment with higher score, but few sum reactions)

### Comment counts

Two counts describe the comments on an activity:

- `comment_count`: every comment on the activity, including replies at any depth. It's always returned on the activity, and on the get comments response.
- `top_level_comment_count`: only the comments made directly on the activity, excluding replies. It's opt-in: pass `include_top_level_comment_count: true` to get activity (returned on the activity) or get comments (returned on the response).

Use `comment_count` for a total such as "View all 45 comments", and `top_level_comment_count` when paging through top-level comments, for example "20 of 30".

```js label="JavaScript"
// Top-level count on a single activity
const { activity } = await client.getActivity({
  id: "activity_123",
  include_top_level_comment_count: true,
});
console.log(activity.comment_count, activity.top_level_comment_count);

// Both counts on the first page of comments
const response = await client.getComments({
  object_type: "activity",
  object_id: "activity_123",
  limit: 20,
  include_top_level_comment_count: true,
});
console.log(response.comment_count, response.top_level_comment_count);
```

When paging through comments:

- **Keep paging until `next` is empty**, not until the number of loaded comments reaches `top_level_comment_count`. The counts aren't adjusted per user: a user who has blocked someone, or been blocked by them, doesn't see that person's comments, so they page through fewer comments than the count, and a user whose own comments are shadow-blocked still sees those comments, so they page through more. Show `min(loaded, total)` rather than a number above the total.
- **Only request the count on the first page.** It doesn't change between pages, and it doesn't depend on `depth`, `replies_limit` or `id_around`. When you open a thread at a specific comment with `id_around`, that request is the first page.
- `comment_count` minus `top_level_comment_count` is roughly the number of replies. Clamp it at 0 and don't present it as exact: `comment_count` is stored separately and can briefly lag a comment that was just added.

## Search for Comments

You can also query the comments, the following examples show

- how to search across all comments by text
- query all comments of a given user

```kotlin label="Kotlin"
// Search in comment texts
val list1 = client.commentList(
    query = CommentsQuery(
        filter = CommentsFilterField.commentText.query("oat")
    )
)
val comments1: Result<List<CommentData>> = list1.get()

// Comments from a user
val list2 = client.commentList(
    query = CommentsQuery(
        filter = CommentsFilterField.userId.equal("jane")
    )
)
val comments2: Result<List<CommentData>> = list2.get()
```

```java label="Java"
Map<String, Object> filter = new HashMap<>();
filter.put("object_id", activityId);

QueryCommentsRequest queryRequest =
    QueryCommentsRequest.builder().filter(filter).limit(10).build();

QueryCommentsResponse response = feeds.queryComments(queryRequest).execute().getData();
```

### Comment Queryable Built-In Fields

| name                | type                                              | description                                                   | supported operations                                               | example                                               |
| ------------------- | ------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------- |
| `id`                | string or list of strings                         | The ID of the comment                                         | `$in`, `$eq`                                                       | `{ id: { $in: [ 'comment_123', 'comment_456' ] } }`   |
| `user_id`           | string or list of strings                         | The ID of the user who created the comment                    | `$in`, `$eq`                                                       | `{ user_id: { $eq: 'user_123' } }`                    |
| `object_type`       | string or list of strings                         | The type of object being commented on                         | `$eq`, `$ne`, `$in`, `$nin`                                        | `{ object_type: { $in: [ 'activity', 'post' ] } }`    |
| `object_id`         | string or list of strings                         | The ID of the object being commented on                       | `$in`, `$eq`                                                       | `{ object_id: { $eq: 'activity_123' } }`              |
| `parent_id`         | string or list of strings                         | The parent comment ID for replies                             | `$in`, `$eq`                                                       | `{ parent_id: { $eq: 'comment_parent_123' } }`        |
| `comment_text`      | string                                            | The text content of the comment                               | `$q`                                                               | `{ comment_text: { $q: 'search terms' } }`            |
| `reply_count`       | number                                            | The number of replies to this comment                         | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ reply_count: { $gte: 5 } }`                        |
| `upvote_count`      | number                                            | The number of upvotes on the comment                          | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ upvote_count: { $gte: 10 } }`                      |
| `downvote_count`    | number                                            | The number of downvotes on the comment                        | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ downvote_count: { $lt: 5 } }`                      |
| `score`             | number                                            | The overall score of the comment                              | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ score: { $gte: 0 } }`                              |
| `confidence_score`  | number                                            | The confidence score of the comment                           | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ confidence_score: { $gte: 0.5 } }`                 |
| `controversy_score` | number                                            | The controversy score of the comment                          | `$gt`, `$gte`, `$lt`, `$lte`                                       | `{ controversy_score: { $lt: 0.8 } }`                 |
| `created_at`        | string, must be formatted as an RFC3339 timestamp | The time the comment was created                              | `$eq`, `$gt`, `$gte`, `$lt`, `$lte`                                | `{ created_at: { $gte: '2023-12-04T09:30:20.45Z' } }` |
| `custom.<key>`      | string, number, boolean, or JSON (as stored)      | Values from the comment `custom` object. **Enterprise only.** | `$eq`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$exists`, `$contains` | `{ 'custom.priority': { $gte: 10 } }`                 |

`$contains` on a custom field follows the shape of the stored value: a scalar value matches an array-valued field containing that element, and an object value matches an object-valued field containing every given key and value. For example, `{ 'custom.extra_context': { $contains: { objectType: 'DONATION' } } }` matches a comment whose `custom.extra_context` object contains that pair. See [`$contains` on custom fields](https://getstream.io/activity-feeds/docs/android/query-activities/#contains-on-custom-fields) for details.

### Comment sort options

All fields support `1` and `-1` directions.

- `created_at`
- `score`: same as [`top` sort option](#sort-options)
- `confidence_score` - same as [`best` sort option](#sort-options)
- `controversy_score` - same as [`controversial` sort option](#sort-options)
- `upvote_count`
- `downvote_count`

## Comment Reactions

When adding reactions and the `enforce_unique` flag is set to `true`, the existing reaction of a user will be overridden with the new reaction. Use this flag if you want to ensure users have a single reaction per each comment/activity. The default value is `false`, in which case users can have multiple reactions. The response tells you whether the reaction was new or replaced an existing one — see [Reaction write outcome](https://getstream.io/activity-feeds/docs/android/reactions/#reaction-write-outcome).

```kotlin label="Kotlin"
// Add a reaction to a comment
feed.addCommentReaction(
    commentId = "comment_123",
    request = AddCommentReactionRequest(
        type = "like",
        // Optionally override existing reaction
        enforceUnique = true
    )
)

// Remove a reaction from a comment
feed.deleteCommentReaction(
    commentId = "comment_123",
    type = "like"
)
```

```java label="Java"
Map<String, Object> filter = new HashMap<>();
filter.put("object_id", activityId);

QueryCommentsRequest queryRequest =
    QueryCommentsRequest.builder().filter(filter).limit(10).build();

QueryCommentsResponse response = feeds.queryComments(queryRequest).execute().getData();
```

### Overview of the reaction model

#### FeedsReactionResponse

| Name | Type | Description | Constraints |
| --- | --- | --- | --- |
| `activity_id` | `string` | - | Required |
| `comment_id` | `string` | - | - |
| `created_at` | `number` | - | Required |
| `custom` | `object` | - | - |
| `type` | `string` | - | Required |
| `updated_at` | `number` | - | Required |
| `user` | `UserResponse` | - | Required |

## Comment Threading

```kotlin label="Kotlin"
val commentList = client.activityCommentList(
    query = ActivityCommentsQuery(
        objectId = "activity_123",
        objectType = "activity",
        depth = 3,
        limit = 20
    )
)
val comments: Result<List<ThreadedCommentData>> = commentList.get()

// Get replies of a specific parent comment
val replyList = client.commentReplyList(
    query = CommentRepliesQuery(
        commentId = "parent_123"
    )
)
val replies: Result<List<ThreadedCommentData>> = replyList.get()
```

```java label="Java"
DeleteCommentRequest deleteRequest = DeleteCommentRequest.builder().build();
DeleteCommentResponse response =
    feeds.deleteComment(commentId, deleteRequest).execute().getData();
```

Reading comment replies supports the [same sort parameters](#sort-options) as reading comments does.

## Restricting comment replies

### Set who can add comments

When creating an activity it's possible to set who can add comments. Possible options:

- Everyone (this is the default setting)
- Only people I follow
- Nobody

>
> **Info:** The activity author is always allowed to add comments regardless of the reply settings.
>

```java label="Java"
AddActivityRequest activity =
    AddActivityRequest.builder()
        .type("post")
        .feeds(List.of(testFeedId))
        .text("apple stock will go up")
        .userID(testUserId)
        .restrictReplies(RestrictReplies.PEOPLE_I_FOLLOW) // Options: EVERYONE, PEOPLE_I_FOLLOW, NOBODY
        .build();

AddActivityResponse response = feeds.addActivity(activity).execute().getData();
```

### Show/hide comment input

If a user isn't allowed to add comments, it's a good idea to hide the comment input.

When everyone/nobody can comment this is straightforward. However if only people the activity author follows can comment, we have to check if there are any feeds owned by the current user that is followed by the activity author, the code snippet below shows how to check this:

```js label="JavaScript"
const activity: ActivityResponse = /* fetch a single activity or read a feed */;

// Lists follow relationships where
// - target feed is owned by current user
// - source feed is owned by activity author
const feed = client.feed(
  activity.current_feed.group_id,
  activity.current_feed.id,
);
console.log(feed.currentState.own_followings);
```

>
> **Info:** `own_followings` is only set when reading a feed/activity from client-side, for server-side requests it will be empty.
>

- When fetching a single activity, `own_followings` is set by default
- When reading a feed, you have to explicitly enable fetching `own_followings`:

```js label="JavaScript"
feed.getOrCreate({ enrichment_options: { enrich_own_followings: true } });
```

## Overview of the comment model

Comment text can be translated on demand and projected when reading comments. See [Translation](https://getstream.io/activity-feeds/docs/android/translation/) for translate endpoints, read-time `language` projection, and the `i18n` object format.

### CommentResponse

| Name | Type | Description | Constraints |
| --- | --- | --- | --- |
| `attachments` | `Attachment[]` | Attachments associated with the comment | - |
| `bookmark_count` | `integer` | - | Required |
| `confidence_score` | `number` | Confidence score of the comment | Required |
| `controversy_score` | `number` | Controversy score of the comment | - |
| `created_at` | `number` | When the comment was created | Required |
| `custom` | `object` | Custom data for the comment | - |
| `deleted_at` | `number` | When the comment was deleted | - |
| `downvote_count` | `integer` | Number of downvotes for this comment | Required |
| `edited_at` | `number` | When the comment was last edited | - |
| `hidden` | `boolean` | Only true in QueryComments results filtered with hidden: true | Required |
| `i18n` | `object` | - | - |
| `id` | `string` | Unique identifier for the comment | Required |
| `latest_reactions` | `FeedsReactionResponse[]` | Recent reactions to the comment | - |
| `mentioned_users` | `UserResponse[]` | Users mentioned in the comment | Required |
| `moderation` | `ModerationV2Response` | Moderation details for the comment | - |
| `object_id` | `string` | ID of the object this comment is associated with | Required |
| `object_type` | `string` | Type of the object this comment is associated with | Required |
| `own_reactions` | `FeedsReactionResponse[]` | Current user's reactions to this activity | Required |
| `parent_id` | `string` | ID of parent comment for nested replies | - |
| `reaction_count` | `integer` | Number of reactions to this comment | Required |
| `reaction_groups` | `object` | Grouped reactions by type | - |
| `reply_count` | `integer` | Number of replies to this comment | Required |
| `score` | `integer` | Score of the comment based on reactions | Required |
| `status` | `string (active, deleted, removed, hidden, shadow_blocked)` | Status of the comment. One of: active, deleted, removed, hidden | Required |
| `text` | `string` | Text content of the comment | - |
| `updated_at` | `number` | When the comment was last updated | Required |
| `upvote_count` | `integer` | Number of upvotes for this comment | Required |
| `user` | `UserResponse` | User who created the comment | Required |

## Add comments in batch

```kotlin label="Kotlin"
val activity = client.activity(
    activityId = "activity_123",
    fid = FeedId(group = "user", id = "john")
)
val result = activity.addCommentsBatch(
    requests = listOf(
        ActivityAddCommentRequest(
            id = "comment_123", // id is optional, but must be unique. will be auto-generated if not provided
            comment = "So great!",
            activityId = "activity_123",
            custom = mapOf("sentiment" to "positive")
        ),
    )
)
```

```java label="Java"
AddCommentsBatchRequest batchRequest = AddCommentsBatchRequest.builder()
    .comments(List.of(
        AddCommentRequest.builder()
            .id("comment_123") // id is optional, but must be unique. will be auto-generated if not provided
            .comment("So great!")
            .objectID(activityId)
            .objectType("activity")
            .userID(testUserId)
            .custom(Map.of("sentiment", "positive"))
            .build()
    ))
    .build();

AddCommentsBatchResponse response = feeds.addCommentsBatch(batchRequest).execute().getData();
```

---

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/android/comments/](https://getstream.io/activity-feeds/docs/android/comments/).