Standalone, resource-agnostic comments module.
Provides polymorphic commenting for any resource type (posts, entities, tickets, etc.) with unlimited threading, likes/dislikes, and moderation support.
Architecture
Comments are linked to resources via resource_type (string) + resource_uuid (UUID).
No foreign key constraints on the resource side — any module can use comments.
Resource Handler Callbacks
Modules that consume comments can register handlers to receive notifications when comments are created or deleted. Configure in your app:
config :phoenix_kit, :comment_resource_handlers, %{
"post" => PhoenixKitPosts
}Handler modules should implement on_comment_created/3 and on_comment_deleted/3.
Core Functions
System Management
enabled?/0- Check if Comments module is enabledenable_system/0- Enable the Comments moduledisable_system/0- Disable the Comments moduleget_config/0- Get module configuration with statistics
Comment CRUD
create_comment/4- Create a comment on a resourceupdate_comment/2- Update a commentdelete_comment/1- Delete a commentget_comment/2,get_comment!/2- Get by IDlist_comments/3- Flat list for a resourceget_comment_tree/2- Nested tree for a resourcecount_comments/3- Count comments for a resource
Moderation
approve_comment/1- Set status to publishedhide_comment/1- Set status to hiddenbulk_update_status/2- Bulk status changeslist_all_comments/1- Cross-resource listing with filterscomment_stats/0- Aggregate statistics
Like/Dislike
Summary
Functions
Sets a comment's status to published.
Attaches an uploaded file to a comment.
Returns true when comment attachments are enabled in settings.
Bulk-updates status for multiple comment UUIDs.
Checks if a user has disliked a comment.
Checks if a user has liked a comment.
Returns aggregate statistics for all comments.
Counts comments for a resource, or a batch of resources.
Returns comment counts grouped by resource type.
Creates a comment on a resource.
Soft-deletes a comment by setting its status to "deleted".
Detaches a media row by (comment_uuid, file_uuid).
Detaches a media row by its own uuid.
Disables the Comments module.
User dislikes a comment. Removes any existing like first.
Enables the Comments module.
Checks if the Comments module is enabled.
Gets a single comment by ID with optional preloads.
Gets a single comment by ID with optional preloads.
Gets nested comment tree for a resource.
Gets the Comments module configuration with statistics.
Returns the configured Giphy API key (empty string when unset).
Returns the configured Giphy content rating (g/pg/pg-13/r).
Returns the per-attachment size cap in MB.
Returns the per-comment attachment count cap (default 4).
Returns the configured maximum comment depth.
Returns the configured maximum comment length.
Gets configured resource templates (path + optional display title).
Returns true when the Giphy picker should be shown in the comment form.
Sets a comment's status to hidden.
User likes a comment. Removes any existing dislike first.
Lists all comments across all resource types with filters.
Lists all dislikes for a comment.
Lists all likes for a comment.
Lists media for a comment, ordered by position.
Lists comments for a resource (flat list).
Returns distinct metadata keys grouped by resource type.
Returns distinct resource types that have comments.
Lists comment UUIDs from comment_uuids disliked by user_uuid.
Lists comment UUIDs from comment_uuids liked by user_uuid.
Validates a prospective comment before any uploads are consumed.
Resolves resource context (title and admin path) for a list of comments.
Returns true when the rich-text (Leaf) editor should be used in the
comment composer.
Searches Giphy for GIFs matching the query, using the configured API key and rating.
Subscribes the calling process to a resource's comment activity.
Returns the PubSub topic for a resource's comment activity.
User removes dislike from a comment. Deletes the dislike row and
decrements the counter atomically. Returns {:ok, :undisliked} or
{:error, :not_found}.
User unlikes a comment. Deletes the like row and decrements the counter
atomically. Returns {:ok, :unliked} or {:error, :not_found}.
Unsubscribes the calling process from a resource's comment activity.
Updates a comment.
Updates resource templates for resource types.
Types
Functions
Sets a comment's status to published.
@spec attach_media(UUIDv7.t(), UUIDv7.t(), keyword()) :: {:ok, PhoenixKitComments.CommentMedia.t()} | {:error, Ecto.Changeset.t()}
Attaches an uploaded file to a comment.
position defaults to 1; the caller is responsible for assigning
non-colliding positions (the DB has a unique constraint on
(comment_uuid, position)).
@spec attachments_enabled?() :: boolean()
Returns true when comment attachments are enabled in settings.
Bulk-updates status for multiple comment UUIDs.
Routes through update_comment/2 (and delete_comment/1 for the
"deleted" case) so resource-handler callbacks fire per row. Returns
{ok_count, error_count}.
Checks if a user has disliked a comment.
Checks if a user has liked a comment.
Returns aggregate statistics for all comments.
@spec count_comments(String.t(), Ecto.UUID.t() | [Ecto.UUID.t()], keyword()) :: non_neg_integer() | %{optional(Ecto.UUID.t()) => non_neg_integer()}
Counts comments for a resource, or a batch of resources.
When resource_uuid is a single UUID, returns the integer count for that
resource. When a list of UUIDs is given, returns a uuid => count map
in a single grouped query — including a 0 entry for every requested UUID
with no comments, so callers can render every row uniformly without an
N+1 (see count_comments/3 with a list below).
Mirrors list_comments/3: deleted rows are excluded unless :status is
set explicitly or include_deleted: true is passed.
Examples
iex> count_comments("order", order_uuid)
3
iex> count_comments("order", [uuid_a, uuid_b, uuid_c])
%{uuid_a => 3, uuid_b => 0, uuid_c => 7}
Returns comment counts grouped by resource type.
Creates a comment on a resource.
Automatically calculates depth from parent. Invokes resource handler callback if configured.
Parameters
resource_type- Type of resource (e.g., "post")resource_uuid- UUID of the resourceuser_uuid- UUID of commenterattrs- Comment attributes (content, parent_uuid, metadata, etc.). May include:attachment_file_uuids— a list ofPhoenixKit.Modules.Storage.FileUUIDs to attach to the new comment in display order. Comment insert + attachments run in one transaction; any attach failure rolls back the comment too.
Soft-deletes a comment by setting its status to "deleted".
Invokes resource handler callback if configured.
Detaches a media row by (comment_uuid, file_uuid).
Detaches a media row by its own uuid.
Disables the Comments module.
User dislikes a comment. Removes any existing like first.
Returns {:ok, :disliked} when a new dislike row was created, or
{:ok, :already_disliked} when the user had already disliked the comment.
Enables the Comments module.
Checks if the Comments module is enabled.
Gets a single comment by ID with optional preloads.
Returns nil if not found.
Gets a single comment by ID with optional preloads.
Raises Ecto.NoResultsError if not found.
Gets nested comment tree for a resource.
Returns all published comments organized in a tree structure. Deleted
comments with published descendants are preserved as [removed]
placeholders so reply chains stay attached; deleted leaves are pruned.
Gets the Comments module configuration with statistics.
@spec get_giphy_api_key() :: String.t()
Returns the configured Giphy API key (empty string when unset).
@spec get_giphy_rating() :: String.t()
Returns the configured Giphy content rating (g/pg/pg-13/r).
@spec get_max_attachment_size_mb() :: pos_integer()
Returns the per-attachment size cap in MB.
Clamped against the global storage_max_upload_size_mb so an admin
can't accidentally let comment uploads exceed the platform cap.
@spec get_max_attachments() :: pos_integer()
Returns the per-comment attachment count cap (default 4).
Returns the configured maximum comment depth.
Returns the configured maximum comment length.
Gets configured resource templates (path + optional display title).
Returns a map of resource_type => config, where config is either:
- A plain string (legacy path-only format)
- A map with
"path"and optional"title"keys
Examples
%{"shoes" => "/order/shoes/:uuid"}
%{"shoes" => %{"path" => "/order/shoes/:uuid", "title" => ":metadata.name"}}
@spec giphy_enabled?() :: boolean()
Returns true when the Giphy picker should be shown in the comment form.
Requires both the comments_giphy_enabled toggle and a non-empty API key.
Sets a comment's status to hidden.
User likes a comment. Removes any existing dislike first.
Returns {:ok, :liked} when a new like row was created, or
{:ok, :already_liked} when the user had already liked the comment.
Lists all comments across all resource types with filters.
Options
:resource_type- Filter by resource type:status- Filter by status:user_uuid- Filter by user:search- Search in content:page- Page number (default: 1):per_page- Items per page (default: 20)
Lists all dislikes for a comment.
Lists all likes for a comment.
Lists media for a comment, ordered by position.
Lists comments for a resource (flat list).
Soft-deleted comments are excluded by default. Pass include_deleted: true
(or an explicit status:) for admin callers that need them.
Options
:preload- Associations to preload:status- Filter by status:include_deleted- Includestatus == "deleted"rows (default: false)
Returns distinct metadata keys grouped by resource type.
Queries the JSONB metadata column for all keys in use, e.g.:
%{"manga_annotation" => ["chapter", "page", "slug", "source"],
"post" => ["category"]}
Returns distinct resource types that have comments.
Lists comment UUIDs from comment_uuids disliked by user_uuid.
Lists comment UUIDs from comment_uuids liked by user_uuid.
@spec precheck_create(String.t(), term(), String.t(), map(), non_neg_integer()) :: :ok | {:error, atom()}
Validates a prospective comment before any uploads are consumed.
Use this in form handlers ahead of Phoenix.LiveView.consume_uploaded_entries/3
so that depth / length / attachment-cap failures don't leak files into
the storage backend. Accepts the same attrs as create_comment/4
except :attachment_file_uuids — pass entry_count instead, which is
how many uploads are currently staged on the LiveView.
Returns :ok or {:error, reason} with the same reasons
create_comment/4 would surface (:invalid_user_uuid,
:max_depth_exceeded, :content_too_long, :attachments_disabled,
:too_many_attachments, :empty_comment).
Resolves resource context (title and admin path) for a list of comments.
Returns a map of {resource_type, resource_uuid} => %{title: ..., path: ...}
by delegating to registered comment_resource_handlers that implement
resolve_comment_resources/1.
@spec rich_text_enabled?() :: boolean()
Returns true when the rich-text (Leaf) editor should be used in the
comment composer.
The Leaf editor requires the host application to register Leaf's JS hook in
its LiveSocket. When the hook is missing the editor hangs on its loading
text with no server error — so hosts that haven't wired the JS (or simply
don't want rich text) can fall back to the always-working plain <textarea>
by setting comments_rich_text to false, or by passing
rich_text={false} to CommentsComponent.
Defaults to true (Leaf is a required dependency).
Searches Giphy for GIFs matching the query, using the configured API key and rating.
Returns {:ok, [gif_map]} on success or {:error, reason} on failure. Each gif_map
has string keys: "id", "url" (original image), "preview_url" (thumbnail),
"width", "height".
Subscribes the calling process to a resource's comment activity.
Call this from a LiveView's mount/3 (in the connected branch) so the
view receives cross-session updates when any user comments on, deletes
from, or reacts to the resource:
def mount(_params, _session, socket) do
if connected?(socket), do: PhoenixKitComments.subscribe("order", order_uuid)
{:ok, socket}
end
def handle_info({:comments_updated, %{action: action}}, socket) do
# action is :created | :deleted | :reaction
{:noreply, refresh_comment_badges(socket)}
endThe broadcast payload mirrors the {:comments_updated, …} message the
CommentsComponent already sends to its own host on create/delete, so a
host has one message contract for both local and remote updates.
The PubSub server is resolved via PhoenixKit.PubSubHelper (configurable
with config :phoenix_kit, pubsub: MyApp.PubSub).
Returns the PubSub topic for a resource's comment activity.
Hosts rarely need this directly — use subscribe/2 — but it's exposed so
callers can match or build topics themselves.
User removes dislike from a comment. Deletes the dislike row and
decrements the counter atomically. Returns {:ok, :undisliked} or
{:error, :not_found}.
User unlikes a comment. Deletes the like row and decrements the counter
atomically. Returns {:ok, :unliked} or {:error, :not_found}.
Unsubscribes the calling process from a resource's comment activity.
Updates a comment.
Parameters
comment- Comment to updateattrs- Attributes to update (content, status)
Updates resource templates for resource types.
Accepts both legacy string values and new map values with "path" and "title" keys.