# `PhoenixKitComments.Comment`
[🔗](https://github.com/BeamLabEU/phoenix_kit_comments/blob/v0.4.5/lib/phoenix_kit_comments/schemas/comment.ex#L1)

Schema for polymorphic comments with unlimited threading depth.

Supports nested comment threads (Reddit-style) with self-referencing parent/child
relationships. Can be attached to any resource type via `resource_type` + `resource_uuid`.

## Comment Status

- `published` - Comment is visible
- `hidden` - Comment is hidden by moderator
- `deleted` - Comment deleted (soft delete)
- `pending` - Awaiting moderation approval

## Fields

- `resource_type` - Type of resource (e.g., "post", "entity", "ticket")
- `resource_uuid` - UUID of the resource
- `user_uuid` - Reference to the commenter
- `parent_uuid` - Reference to parent comment (nil for top-level)
- `content` - Comment text
- `status` - published/hidden/deleted/pending
- `depth` - Nesting level (0=top, 1=reply, 2=reply-to-reply, etc.)
- `like_count` - Denormalized like counter
- `dislike_count` - Denormalized dislike counter
- `metadata` - Arbitrary JSONB data (giphy reactions, custom flags, rich embeds, etc.)

## Media

The `has_many :media` association links to `PhoenixKitComments.CommentMedia`
rows ordered by `position`. Content-or-media validation (`content` is
optional iff Giphy or media is present) checks the loaded association
on updates; the orchestrator passes `has_media: true` on insert when
it's about to attach files in the same transaction.

# `t`

```elixir
@type t() :: %PhoenixKitComments.Comment{
  __meta__: term(),
  attributed_label: term(),
  attributed_project_uuid: term(),
  attribution_mode: term(),
  author_display_name: term(),
  children: [t()] | Ecto.Association.NotLoaded.t(),
  content: String.t(),
  depth: integer(),
  dislike_count: integer(),
  inserted_at: DateTime.t() | nil,
  like_count: integer(),
  media: [PhoenixKitComments.CommentMedia.t()] | Ecto.Association.NotLoaded.t(),
  metadata: map(),
  parent: t() | Ecto.Association.NotLoaded.t() | nil,
  parent_uuid: UUIDv7.t() | nil,
  resource_type: String.t(),
  resource_uuid: Ecto.UUID.t(),
  status: String.t(),
  updated_at: DateTime.t() | nil,
  user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t() | nil,
  user_uuid: UUIDv7.t() | nil,
  uuid: UUIDv7.t() | nil
}
```

# `changeset`

Changeset for creating or updating a comment.

## Required Fields

- `resource_type` - Type of resource being commented on
- `resource_uuid` - UUID of the resource
- `user_uuid` - Reference to commenter
- Either `content`, a Giphy attachment in `metadata["giphy"]`, or media

## Options

- `:has_media` — boolean. When set, overrides the inferred media
  presence (used by `PhoenixKitComments.create_comment/4` because the
  new comment has no `uuid` yet and the `media` association is not
  loaded). Not part of `cast` — callers cannot drive it via attrs.

# `deleted?`

Check if comment is deleted.

# `published?`

Check if comment is published.

# `put_attribution`

```elixir
@spec put_attribution(Ecto.Changeset.t(), map() | nil) :: Ecto.Changeset.t()
```

Freezes who this comment is from. Server-set only — never from `attrs`.

`:personal` pins the display name the reader will see, so a later rename,
a filled-in profile or a departure does not re-sign every comment the
person ever wrote.

`:project` additionally records that they were speaking for the project,
and pins the project's name as it read at the time. **`user_uuid` is left
exactly as it was**: the public sees the project, and internally the
author is still on the row for moderation and audit. A shared voice with
nobody accountable behind it is the failure mode of this feature.

# `reply?`

Check if comment is a reply (has parent).

# `top_level?`

Check if comment is top-level (no parent).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
