PhoenixKitComments (PhoenixKitComments v0.2.1)

Copy Markdown View Source

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

Comment CRUD

Moderation

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.

Returns comment counts grouped by resource type.

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.

Validates a prospective comment before any uploads are consumed.

Resolves resource context (title and admin path) for a list of comments.

Searches Giphy for GIFs matching the query, using the configured API key and rating.

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}.

Updates a comment.

Updates resource templates for resource types.

Types

gif_map()

@type gif_map() :: %{required(String.t()) => String.t() | integer() | nil}

Functions

approve_comment(comment)

Sets a comment's status to published.

attach_media(comment_uuid, file_uuid, opts \\ [])

@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)).

attachments_enabled?()

@spec attachments_enabled?() :: boolean()

Returns true when comment attachments are enabled in settings.

bulk_update_status(comment_uuids, status)

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}.

comment_disliked_by?(comment_uuid, user_uuid)

Checks if a user has disliked a comment.

comment_liked_by?(comment_uuid, user_uuid)

Checks if a user has liked a comment.

comment_stats()

Returns aggregate statistics for all comments.

count_comments(resource_type, resource_uuid, opts \\ [])

Counts comments for a resource.

Mirrors list_comments/3: deleted rows are excluded unless :status is set explicitly or include_deleted: true is passed.

count_comments_by_type()

Returns comment counts grouped by resource type.

create_comment(resource_type, resource_uuid, user_uuid, attrs)

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 resource
  • user_uuid - UUID of commenter
  • attrs - Comment attributes (content, parent_uuid, metadata, etc.). May include :attachment_file_uuids — a list of PhoenixKit.Modules.Storage.File UUIDs to attach to the new comment in display order. Comment insert + attachments run in one transaction; any attach failure rolls back the comment too.

delete_comment(comment)

Soft-deletes a comment by setting its status to "deleted".

Invokes resource handler callback if configured.

detach_media(comment_uuid, file_uuid)

Detaches a media row by (comment_uuid, file_uuid).

detach_media_by_uuid(media_uuid)

Detaches a media row by its own uuid.

disable_system()

Disables the Comments module.

dislike_comment(comment_uuid, user_uuid)

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.

enable_system()

Enables the Comments module.

enabled?()

Checks if the Comments module is enabled.

get_comment(id, opts \\ [])

Gets a single comment by ID with optional preloads.

Returns nil if not found.

get_comment!(id, opts \\ [])

Gets a single comment by ID with optional preloads.

Raises Ecto.NoResultsError if not found.

get_comment_tree(resource_type, resource_uuid)

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.

get_config()

Gets the Comments module configuration with statistics.

get_giphy_api_key()

@spec get_giphy_api_key() :: String.t()

Returns the configured Giphy API key (empty string when unset).

get_giphy_rating()

@spec get_giphy_rating() :: String.t()

Returns the configured Giphy content rating (g/pg/pg-13/r).

get_max_attachment_size_mb()

@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.

get_max_attachments()

@spec get_max_attachments() :: pos_integer()

Returns the per-comment attachment count cap (default 4).

get_max_depth()

Returns the configured maximum comment depth.

get_max_length()

Returns the configured maximum comment length.

get_resource_path_templates()

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"}}

giphy_enabled?()

@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.

hide_comment(comment)

Sets a comment's status to hidden.

like_comment(comment_uuid, user_uuid)

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.

list_all_comments(opts \\ [])

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)

list_comment_dislikes(comment_uuid, opts \\ [])

Lists all dislikes for a comment.

list_comment_likes(comment_uuid, opts \\ [])

Lists all likes for a comment.

list_comment_media(comment_uuid, opts \\ [])

Lists media for a comment, ordered by position.

list_comments(resource_type, resource_uuid, opts \\ [])

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 - Include status == "deleted" rows (default: false)

list_metadata_keys_by_type()

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"]}

list_resource_types()

Returns distinct resource types that have comments.

precheck_create(resource_type, resource_uuid, user_uuid, attrs, entry_count \\ 0)

@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).

resolve_resource_context(comments)

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.

search_giphy(query, opts \\ [])

@spec search_giphy(
  String.t(),
  keyword()
) :: {:ok, [gif_map()]} | {:error, atom()}

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".

undislike_comment(comment_uuid, user_uuid)

User removes dislike from a comment. Deletes the dislike row and decrements the counter atomically. Returns {:ok, :undisliked} or {:error, :not_found}.

unlike_comment(comment_uuid, user_uuid)

User unlikes a comment. Deletes the like row and decrements the counter atomically. Returns {:ok, :unliked} or {:error, :not_found}.

update_comment(comment, attrs)

Updates a comment.

Parameters

  • comment - Comment to update
  • attrs - Attributes to update (content, status)

update_resource_path_templates(templates)

Updates resource templates for resource types.

Accepts both legacy string values and new map values with "path" and "title" keys.