defmodule PhoenixKitComments do @moduledoc """ 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 may implement any of these optional callbacks (each guarded by `function_exported?/3`, so implement only what you need): * `on_comment_created(resource_type, resource_uuid, comment)` — new comment (check `comment.parent_uuid` to distinguish a reply). * `on_comment_deleted(resource_type, resource_uuid, comment)` — comment removed. * `on_comment_liked(resource_type, resource_uuid, %{comment: comment, liker_uuid: uuid})` * `on_comment_unliked(resource_type, resource_uuid, %{comment: comment, liker_uuid: uuid})` * `on_comment_disliked(resource_type, resource_uuid, %{comment: comment, liker_uuid: uuid})` * `on_comment_undisliked(resource_type, resource_uuid, %{comment: comment, liker_uuid: uuid})` The reaction callbacks fire only when the reaction state actually changed (`{:ok, :liked}` / `{:ok, :unliked}` …), never on `:already_liked` no-ops. Self-action skipping (e.g. don't notify someone who liked their own comment) is left to the host. The `liker_uuid` is in the payload because the comment row carries the author, not the reacting user. ## Core Functions ### System Management - `enabled?/0` - Check if Comments module is enabled - `enable_system/0` - Enable the Comments module - `disable_system/0` - Disable the Comments module - `get_config/0` - Get module configuration with statistics ### Comment CRUD - `create_comment/4` - Create a comment on a resource - `update_comment/2` - Update a comment - `delete_comment/1` - Delete a comment - `get_comment/2`, `get_comment!/2` - Get by ID - `list_comments/3` - Flat list for a resource - `get_comment_tree/2` - Nested tree for a resource - `count_comments/3` - Count comments for a resource ### Moderation - `approve_comment/1` - Set status to published - `hide_comment/1` - Set status to hidden - `bulk_update_status/2` - Bulk status changes - `list_all_comments/1` - Cross-resource listing with filters - `comment_stats/0` - Aggregate statistics ### Like/Dislike - `like_comment/2`, `unlike_comment/2`, `comment_liked_by?/2` - `dislike_comment/2`, `undislike_comment/2`, `comment_disliked_by?/2` """ use PhoenixKit.Module import Ecto.Query, warn: false require Logger alias PhoenixKit.Dashboard.Tab alias PhoenixKit.PubSubHelper alias PhoenixKit.Settings alias PhoenixKit.Utils.Routes alias PhoenixKit.Utils.UUID, as: UUIDUtils alias PhoenixKitComments.Comment alias PhoenixKitComments.CommentDislike alias PhoenixKitComments.CommentLike alias PhoenixKitComments.CommentMedia # ============================================================================ # Module Status # ============================================================================ @impl PhoenixKit.Module @doc "Checks if the Comments module is enabled." def enabled? do Settings.get_boolean_setting("comments_enabled", false) rescue _ -> false end @impl PhoenixKit.Module @doc "Enables the Comments module." def enable_system do Settings.update_boolean_setting_with_module("comments_enabled", true, "comments") end @impl PhoenixKit.Module @doc "Disables the Comments module." def disable_system do Settings.update_boolean_setting_with_module("comments_enabled", false, "comments") end @impl PhoenixKit.Module @doc "Gets the Comments module configuration with statistics." def get_config do %{ enabled: enabled?(), total_comments: count_all_comments(), published_comments: count_all_comments(status: "published"), pending_comments: count_all_comments(status: "pending"), moderation_enabled: Settings.get_boolean_setting("comments_moderation", false), max_depth: get_max_depth(), max_length: get_max_length() } end @doc "Returns the configured maximum comment depth." def get_max_depth do case Integer.parse(Settings.get_setting("comments_max_depth", "10")) do {n, _} -> n :error -> 10 end end @doc "Returns the configured maximum comment length." def get_max_length do case Integer.parse(Settings.get_setting("comments_max_length", "10000")) do {n, _} -> n :error -> 10_000 end end # ============================================================================ # Composer / editor configuration # ============================================================================ @doc """ 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 `