Current section
Files
Jump to
Current section
Files
phoenix_kit_comments
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to PhoenixKitComments will be documented in this file.
## 0.4.1 - 2026-08-17
Comment backdating for server-created anchor comments, and a comment-card UI
rework (#36).
### Added
- **`create_comment/4` accepts `:inserted_at`** — a server-created anchor/topic
comment (e.g. the thread a shape's discussion hangs under) can now carry the
timestamp of the thing it anchors instead of the moment the thread was
lazily instantiated by the first reply. Deliberately not part of the
changeset `cast`, so no client form payload can drive it: only a real
`DateTime` struct triggers the override, anything else (including a
client-shaped JSON string) is silently ignored, and `updated_at` still
records when the row was actually written. Added integration coverage —
the PR that introduced this shipped with none.
### Changed
- **Comment card actions reworked.** A card at rest now shows author / body /
date / reactions only. Edit and Delete move into a "…" dropdown at the top
right, revealed on hover (`focus-within` keeps it visible for keyboard
users). Reply moves to the bottom-right action row, hover-revealed ahead of
the always-visible like/dislike buttons. The dropdown blurs itself on
Edit/Delete click so a LiveView patch (which never moves focus) can't leave
the menu hanging open over an edited or deleted card.
## 0.4.0 - 2026-08-11
Metadata-keyed reads and writes, a declared resource-handler contract, and the
translated sidebar labels.
### Added
- **Metadata-keyed reads** (#34). `list_comments/3` takes a `:metadata` map of
keys that must match, and `:any` in place of a resource uuid to list across a
whole resource type. `resource_uuid` is a UUID column and plenty of hosts key
their comments on something that isn't one — a `(source, slug, chapter)`
triple — so they mint a throwaway uuid and put the real key in `metadata`. The
listing they actually want was not expressible, and they dropped to schemaless
SQL against this package's own table to get it.
- **`update_metadata/2` and `merge_metadata/3`** (#34). Single atomic
`metadata || patch` writes, so they cannot lose a concurrent writer's keys the
way read-modify-write does. `merge_metadata/3` is the rename case — a host
renames a slug and every comment carrying the old one has to follow. An empty
match is refused rather than treated as "everything".
- **`count_replies/2`** (#34) — `parent_uuid => count` for a batch of parents in
one grouped query, with a `0` entry for every uuid asked about, so a thread
list renders uniformly without an N+1.
- **`PhoenixKitComments.ResourceHandler.callbacks/0` and `event_callbacks/0`**
(#35), and the rationale for adopting the behaviour at all: dispatch is by
`function_exported?/3`, so a misnamed or wrong-arity callback is
indistinguishable from one you chose not to write — nothing fires and there is
no error to find it by.
### Changed
- **Sidebar tab labels are translated** (#33). The admin and settings tabs
declare `gettext_backend` and `gettext_domain`, which they need in order to
resolve at all.
### Fixed
- **Estonian and Russian entries that carried another string's translation**
(#33), and entries whose `fuzzy` flag meant Gettext ignored them at runtime
and showed the English msgid instead. Two plural forms had also dropped their
`%{action}` interpolation, so a bulk-action result read "5 comments" instead
of "5 comments deleted".
- **`count_replies/2` reported zeros silently on a failed query.** "0 replies"
on a thread that has replies is a plausible-looking wrong answer rather than an
obviously broken one, and nothing anywhere distinguished it from a quiet
thread. It now logs before degrading.
## 0.3.0 - 2026-08-10
### Changed
- **⚠️ Requires `phoenix_kit ~> 2.0`.** The core pin moved to `~> 2.0`, so this
release no longer resolves against core 1.7.
Core 2.0.0 squashes the migration chain into a single `V135` baseline and makes
V135 the chain's floor: `mix ecto.migrate` now *refuses* on a database below it
rather than migrating. Check `mix phoenix_kit.status` **before** upgrading. A
host below V135 must install `phoenix_kit 1.7.236` — the migration bridge, the
last release carrying the full pre-squash chain — migrate until the reported
version is at least V135, and only then move to 2.0.
This package does not call migration internals, so the change is the pin
itself.
### Security
- **Stored XSS in every comment body (PR #31).** Bodies render with MDEx
`unsafe: true`, which disables MDEx's escaping and makes whatever runs next
the security boundary — that was six regexes over the rendered string, a
blocklist over HTML. Two payloads went through untouched: `<script>alert(1)`
with no closing tag (the pattern required a matching `</script>`) and
`<a href=javascript:alert(1)>` (the pattern required the value to be quoted).
Either executed for every reader of a thread **and again in the admin
moderation list**, which renders the same component — so an unprivileged
commenter ran script in an owner's authenticated session.
Rendering now uses MDEx's `:sanitize` allow-list (ammonia), which drops
unknown tags and every attribute outside the allowed set instead of matching
against a pattern. MDEx's default list permits `style` on `div`, enough to
lay an overlay over the moderation UI's own buttons, so that is stripped too.
Ten payloads are pinned by tests.
- **`save_decoration` had no authorization check at all** — a logged-out
visitor could rename any host record backed by a visible comment, and the
`send_update` the host received was byte-identical to a legitimate one.
- **Admin reads were unguarded while every write was checked.** An admin
*without* the `comments` permission could read every comment on the platform,
commenter emails, and the Giphy API key as a form value.
- **`@enabled` gated only the template**, so writes landed on a disabled thread.
- **`save_edit` forwarded the decoration before checking permission**, so a
refused request still committed half an edit.
### Fixed
- **Reaction counters were inflatable.** Dedup was a SELECT-then-INSERT with no
unique index behind it — the index was dropped during the uuid-FK migration
and never recreated, which also left the schemas' `unique_constraint` as dead
code. Reactions now serialise on the parent comment row.
- **The counter drifted permanently once duplicates existed** — `delete_all`
removes N rows while the decrement was hardcoded to 1.
- **A repeat click committed a write and reported nothing** — both paths remove
the opposing reaction before deciding, and `after_reaction/3` skipped exactly
those two atoms.
### Changed
- **⚠️ Breaking (small): `render_markdown/2` is now `render_markdown/1`,** and
the `sanitize` attr is gone from `<.comment_markdown>`. There is no caller for
whom skipping sanitisation of a user-authored body would be correct, and an
opt-out is one `sanitize={false}` away from stored XSS on every reader. No
sibling module in the umbrella calls this function.
## 0.2.15 — 2026-07-27
### Added
- **Comment editors open in the admin-configured default mode (PR #30).** Both
the composer and the inline edit form pass the site-wide editor mode (set
under Settings → Content Editor in core) to Leaf, instead of always opening in
Leaf's `:hybrid` default.
- **Named-schema (`--prefix`) support at runtime (PR #29).** Every table-backed
schema (`Comment`, `CommentLike`, `CommentDislike`, `CommentMedia`) compiles in
`PhoenixKit.SchemaPrefix`, so on a prefixed install this module's queries
target the named schema directly rather than relying on the DB role's
`search_path`. No behaviour change for unprefixed installs. A conformance test
scans `lib/` so future table-backed schemas can't silently skip the attribute.
### Changed
- **Moderation dashboard bulk-select moved client-side (PR #29).** Per-checkbox
`phx-click` round-trips and the `selected_uuids` assign are replaced by core's
`BulkSelect` components and the `BulkSelectScope` JS hook — selection lives in
the browser and the server only learns the UUIDs when a toolbar button is
clicked. Adds a header "select all" checkbox; `bulk_action` splits into
`bulk_approve` / `bulk_hide` / `bulk_delete_comments`, all keeping the same
authorization gate and the delete confirmation.
### Fixed
- **Comments component no longer crashes on the editor-mode lookup (post-merge
review of PR #30).** The merged code called
`PhoenixKit.Settings.get_editor_mode/0`, which does not exist in any
phoenix_kit release the `~> 1.7.189` pin can resolve (latest is 1.7.213) —
raising `UndefinedFunctionError` in `update/2` on **every** render of every
comments embed. The lookup is now capability-probed and falls back to Leaf's
`:hybrid` default on older cores, and starts honouring the setting
automatically once core ships it.
- **Editor mode is normalized before it reaches Leaf.** Leaf's `:mode` clauses
have no catch-all, so a string setting value (the shape settings round-trip
as) or an unrecognised mode would raise `FunctionClauseError` inside Leaf.
Values are now coerced to one of `:visual` / `:hybrid` / `:markdown` / `:html`,
with test coverage pinning the contract.
- **Editor mode is read once per component lifetime** instead of on every
`update/2` — Leaf only honours `:mode` on its first render, so the repeated
settings lookup on each parent re-render / `send_update` was pure cost.
### Dependencies
- Bump the `phoenix_kit` floor to `~> 1.7.189` (provides `PhoenixKit.SchemaPrefix`).
- Drop the orphaned `:beamlab_ex_aws_sqs` entry from `mix.lock`, which made
`mix deps.unlock --check-unused` (and therefore `mix precommit`) fail. No
change to the resolved dependency set.
## 0.2.14 — 2026-07-10
### Changed
- **Settings page modernized (`Web.Settings`).** The five stacked cards collapse
into a single card with lightweight in-card section headers (new
`settings_section_header/1` component — a local copy of core's
`FormSection.section_header/1` so the package renders identically without
waiting on a core release). Toggles and inputs move to daisyUI label patterns,
and the **Reset to Defaults** button gains a `data-confirm` prompt since it
wipes moderation, limits, Giphy, and attachment settings.
- **Resource resolution delegated to core `PhoenixKit.ResourceLinks`.**
`PhoenixKitComments` drops its own ~170-line handler registry and
path-template engine and `defdelegate`s `get_resource_path_templates/0`,
`update_resource_path_templates/1`, and `resolve_resource_context/1` (plus the
notification-callback dispatch) to core. The comments moderation admin and the
Activity feed now resolve deep-links through one shared resolver, off one set
of handlers and the shared `comment_resource_paths` templates. Behaviour is
preserved — same return shape, same default handlers (`post`/`file`/`user`),
same template fallback — with `integration` and module-declared handlers added
for free.
### Added
- **Full gettext coverage for the settings page.** Every user-facing string on
`Web.Settings` (template + flash messages) is now wrapped in
`gettext`/`ngettext` against the module's own `PhoenixKitComments.Gettext`
backend, and the `default.pot` + `en`/`et`/`ru` catalogs are extended and
fully translated to Russian and Estonian.
### Dependencies
- Bump `phoenix_kit` to `1.7.181` (provides `PhoenixKit.ResourceLinks`).
## 0.2.13 — 2026-06-25
### Added
- **Russian and Estonian translations for the comments UI.** The comment
moderation dashboard (`Web.Index`) and the embeddable comment LiveComponent
(`Web.CommentsComponent`) now ship a dedicated `PhoenixKitComments.Gettext`
backend with its own `priv/gettext` catalogs, fully translated to `ru` and
`et` (English remains the source). The strings follow the host's global locale
automatically — no extra wiring — so a Russian- or Estonian-locale session
sees those screens in that language; any other locale falls back to English.
The catalogs now ship with the package (`priv` added to the Hex `files` list).
### Fixed
- **`mix dialyzer` no longer fails on the Russian plural catalog.** The new
gettext backend's generated code passes an opaque `%Expo.PluralForms{}` (the
`ru` `nplurals=3` plural form) to `Gettext.Plural.plural/2`, which Dialyzer
reported as an opaqueness violation and broke the `quality.ci` / `precommit`
gate. Added a targeted `.dialyzer_ignore.exs` filter (the value is correct at
runtime; the suite exercises it).
## 0.2.12 — 2026-06-24
### Added
- **User comments link to the user's admin page.** Registers
`PhoenixKit.Users.CommentResources` as the `"user"` resource handler (gated on
the module being present in `phoenix_kit` core, alongside the existing
`"post"`/`"file"` handlers). A comment attached to a user
(`resource_type: "user"`) now resolves to that user's display name and
`/admin/users/view/:uuid` detail page, with their avatar as the chip
thumbnail, instead of rendering a bare uuid in the moderation dashboard.
Gracefully no-ops on older core versions that don't ship the module.
### Changed
- **Friendlier no-handler fallback resource chip.** When a comment's
`resource_type` has neither a registered handler nor a host-configured path
template, the moderation-dashboard chip now renders a tag icon, a humanized
type label (`"test_page"` → `"Test page"`), and a short uuid, with a
`"<type>: <uuid>"` tooltip — instead of a raw type badge plus a bare uuid.
## 0.2.11 — 2026-06-17
### Changed
- **Dropped the direct `mdex` dependency.** `phoenix_kit` core now declares
`mdex` directly, so comments no longer needs its own dep —
`lib/phoenix_kit_comments/web/markdown.ex` calls `MDEx` and resolves it
transitively through `phoenix_kit`, keeping a single shared version across all
modules and eliminating version-mismatch risk. Mirrors the existing `leaf`
arrangement.
## 0.2.10 — 2026-06-17
### Added
- **Comments admin moderation overhaul.** Title/subtitle moved into the top
navbar (matching the media admin), so they collapse cleanly on mobile and
free up vertical space; an Actions tile (Settings shortcut) joined the stats
row; the toolbar gained a filtered count and a clear-X on the search field.
- **Resource chips with thumbnails.** Each comment shows a clickable chip for
the resource it belongs to (with an image thumbnail where available), in both
the table and the grid/card view. File comments link straight to their media,
deep-linking to the corresponding Etcher shape when one exists.
- **Reply navigation.** A reply can jump to the original comment it answers by
filtering the list to that comment's uuid (entered into the search box) — no
modal.
- **Status-aware row actions** in a `…` dropdown that matches the users table:
deleted comments offer Restore only; Status now sits next to Actions.
- **Long-comment handling.** Admin previews truncate to one line with a
"Read more ›" cue and open the full, markdown-rendered comment in a modal. In
the public `CommentsComponent`, long comments collapse behind a YouTube-style
"Read more" / "Show less" toggle.
### Changed
- **Comment markdown now renders with MDEx (comrak) instead of Earmark** on
display, in both the public component and the admin grid/card view. Comments
are authored as markdown in the Leaf composer, which already renders with
MDEx; using the same engine and `render` options (`hardbreaks`, `unsafe`) on
display makes the rendered comment match what was typed. Output still passes
through core's `HtmlSanitizer`, so XSS protection is unchanged; block spacing
is restored via an explicit `.pk-comment-md` stylesheet (no typography plugin
required). Adds `{:mdex, "~> 0.13"}` as a direct dependency (dropped again in
0.2.11 once `phoenix_kit` core began providing it transitively).
- **i18n sweep** of the comments admin — all user-facing strings now go through
`gettext`.
### Fixed
- **Reply indicator no longer crashes on media-only parents.** The
`/admin/comments` "— Re: …" reply label sliced `parent.content` directly, but a
comment can have blank content when it carries only a GIF or attachment — so a
reply to such a comment raised `String.slice(nil, _)` and crashed the
moderation page. Blank parents now show a `[no text]` placeholder.
- **Unified the comment-markdown styles.** The public `CommentsComponent` shipped
its own inline `.pk-comment-md` `<style>` copy that had drifted from the shared
`comment_markdown_styles/1` helper the admin uses (different paragraph/heading
spacing). The component now renders the shared helper, so public and admin
render identical markdown from a single source of truth.
- **Media-only comments are no longer invisible in the admin.** A GIF- or
attachment-only comment (blank text) used to render an empty preview row and an
empty full-comment modal. The list now shows a "GIF" / "Attachment" placeholder,
and the modal renders the GIF and an attachment count.
- **Resource-chip thumbnails dropped the inline `onerror` handler** in favour of
a CSS background image, so a missing thumbnail degrades gracefully without inline
JS (consistent with the `window.PhoenixKitHooks` convention and strict CSPs).
- **Resource chips link correctly to host/external pages.** Non-prefixed resource
paths (host controller pages or external URLs) now use a plain `href` instead of
LiveView `navigate`, which only works between LiveViews in the session.
- **Keyboard access for the admin comment preview.** The clickable one-line
preview is now a focusable `role="button"` that opens the full-comment modal on
Enter.
- **Pagination links** no longer carry empty `search=`/`resource_type=`/`status=`
query params; they reuse `build_url_params/2`, which strips blanks.
- **Editing a comment to empty text now works when it has a GIF or attachment.**
The composer's `save_edit` rejected blank content unconditionally; it now allows
it for GIF/attachment-only comments (which the changeset already considers valid),
and only blocks a truly empty edit.
- **Admin grid/card view shows the status as a badge** (matching the table column)
instead of a bare string.
## 0.2.9 — 2026-06-16
### Added
- **Reaction resource-handler callbacks** so hosts can surface "X liked your
comment" notifications, symmetric with the existing
`on_comment_created/3` / `on_comment_deleted/3` pair. `like_comment/2`,
`unlike_comment/2`, `dislike_comment/2`, and `undislike_comment/2` now
dispatch (best-effort, after the existing PubSub broadcast) to the registered
resource handler's optional callback:
- `on_comment_liked/3`, `on_comment_unliked/3`, `on_comment_disliked/3`,
`on_comment_undisliked/3`
- Third arg is a `%{comment: %Comment{}, liker_uuid: binary}` payload (the
comment row carries the author, not the reacting user). The map shape keeps
the existing 3-arity handler contract and stays extensible.
- Each callback is optional (`function_exported?/3` guard) and fires only when
the reaction state actually changed — never on `:already_liked` no-ops or
`{:error, _}` results. Self-action skipping is left to the host. Purely
additive: existing handlers are unaffected.
### Changed
- Reaction broadcasts and resource-handler callbacks now share a single comment
lookup instead of two, restoring the original one-query cost per reaction
toggle. No-op results (`:already_liked`, `:already_disliked`, errors) issue
zero extra reads, and the post-commit side-effect is fully rescue-wrapped so a
transient read failure cannot fail an already-committed reaction.
- Removed `leaf` as a direct dependency. PhoenixKit already requires `leaf`, so
the package remains available transitively; the comments module still detects
its presence at runtime and falls back to a plain `<textarea>` when it is
absent or not wired by the host.
## 0.2.8 — 2026-06-09
### Added
- **Cross-session live updates over `Phoenix.PubSub`** (#20). Comment create,
delete, and reaction changes now broadcast `{:comments_updated, %{resource_type,
resource_uuid, action}}` (`action` is `:created | :deleted | :reaction`) on a
per-resource topic. Hosts subscribe with `PhoenixKitComments.subscribe/2` (and
`unsubscribe/2`) so other connected users — count badges, open threads — update
without a reload. The payload mirrors the existing self-message the component
sends its own host, so there's one message contract for local and remote
updates. Broadcasts are best-effort and never break the write path; the PubSub
server is resolved via `PhoenixKit.PubSubHelper`.
- **Batch comment counting** (#19). `count_comments/3` now accepts a **list** of
`resource_uuid`s and returns a `uuid => count` map from a single grouped query
(with a `0` entry for every requested uuid), avoiding the N+1 a host hit when
rendering one count badge per row. Honors the same `:status` / `:include_deleted`
options as the scalar form.
### Changed
- **Rich-text editor is now opt-out** (#18). The Leaf editor requires the host to
register Leaf's JS hook; when it's missing the composer silently hangs on its
loading text. Hosts can now fall back to the always-working plain `<textarea>`
via the new `comments_rich_text` setting (default `true`) or a `rich_text`
attr on `CommentsComponent`. Added an admin settings toggle and a "JavaScript
wiring" + troubleshooting section to the README documenting which hooks the
host must register.
## 0.2.7 — 2026-06-08
### Added
- Comment bodies now render their markdown (authored in the Leaf editor) to
sanitized HTML on display via core's `<.markdown>` component (Earmark +
`HtmlSanitizer`), so bold / italics / lists / quotes / links / code show
formatted instead of as raw markdown. Previously the raw content string was
shown verbatim.
- Scoped CSS (`.pk-comment-md`) restores list / blockquote / heading / code
styling that Tailwind's preflight resets, without depending on the
`@tailwindcss/typography` (`prose`) plugin being present in the host — so
lists render with bullets/numbers even where prose isn't configured. Inline
`` `code` `` gets a chip background, fenced `<pre>` blocks get a backdrop, and
links render in the theme primary color so they're distinguishable from
emphasized text.
### Changed
- `leaf` is now a required dependency at `~> 0.2.22` (was `optional: true`,
`~> 0.2`). The comment composer is built on the Leaf editor, and phoenix_kit
core already hard-depends on leaf, so it's always present wherever comments
runs — the optional declaration described an unreachable leaf-free build. The
`0.2.22` minimum pulls in Leaf's markdown-link round-trip fix (editing a
comment with a link no longer doubles it into `[[label](url)](url)`). The
`leaf_available?/0` textarea fallback stays as defensive code.
### Fixed
- `CommentsComponent` no longer flips to "Sign in to post a comment" for a
logged-in user on a partial `send_update`. `can_post?` was derived from
the incoming `assigns[:current_user]` (nil on any update that omits it,
e.g. a parent poking `loaded?: false` to refresh the thread — as
PhoenixKit's MediaCanvasViewer does when an annotation is drawn). It now
reads the resolved socket value (kept across updates by `assign_new`),
so the composer stays available.
## 0.2.6 — 2026-06-07
### Features
- New `PhoenixKitComments.Embed` macro. A host LiveView embedding
`CommentsComponent` must forward the composer's rich-text (Leaf)
`{:leaf_changed, …}` process message into
`CommentsComponent.forward_leaf_event/2`, or the editor's content never
reaches the component and "Post comment" silently no-ops. `use
PhoenixKitComments.Embed` wires that forward as an `on_mount`
`attach_hook(:handle_info)` lifecycle hook, so it composes with a host that
already defines its own `handle_info` (no clause-grouping clash, no
clobbering). For hosts that hard-depend on `phoenix_kit_comments`;
soft-dependency hosts resolve `forward_leaf_event/2` at runtime instead (see
the moduledoc).
### Changed
- Bumped dependencies (`mix.lock`).
## 0.2.5 — 2026-05-29
First Hex release since 0.2.1; bundles the unreleased 0.2.2–0.2.4 work
below plus the fixes and cleanup in this entry.
### Fixed
- Likes/dislikes no longer crash. `insert_reaction/2` had been switched to
`insert_all` with `on_conflict: :nothing,
conflict_target: [:comment_uuid, :user_uuid]`, but no composite unique
index exists on those columns (the original `UNIQUE(comment_id,
user_id)` was dropped with the integer `user_id` column during the
uuid-FK migration and never recreated on `user_uuid`). That made every
like/dislike raise a Postgrex "no unique or exclusion constraint
matching the ON CONFLICT specification" error. Restored the
`exists?`-precheck + changeset insert, which doesn't depend on the
missing index.
### Changed
- Reaction highlight state (`liked_comment_uuids` /
`disliked_comment_uuids`) is now reloaded only when comments reload or
the viewer / `show_likes` change, instead of on every `update/2` —
removing two redundant queries per parent re-render.
- The inline reply composer is no longer a feature-poor duplicate of the
top/bottom composer. Both now share one `composer_form/1`, so replies
gain the GIF picker, audio recorder, and full attach menu, and the form
markup / translations live in a single place.
- Wrapped the remaining user-facing strings in the edit and reply forms
in `gettext` (they had been missed in the gettext sweep).
## 0.2.4 — 2026-05-29
### Features
- Header is now configurable via three optional, backward-compatible
assigns (defaults reproduce current behavior exactly):
- `show_title` (default `true`) — when `false`, the
"{title} ({count})" header line is not rendered.
- `collapsible` (default `false`) — turns the header into a
disclosure toggle (chevron + `aria-expanded`/`aria-controls`) that
collapses/expands the whole body. Collapse state is ephemeral
(in-memory, resets on remount).
- `initial_collapsed` (default `false`) — starting collapse state when
`collapsible`; host-customizable.
Note: the collapse chevron lives in the header, so `collapsible` has
no visible toggle when `show_title: false` (the thread stays per
`initial_collapsed`).
- `composer_position` (default `:top`) — render the "Write comment"
composer at `:top`, `:bottom`, or `:both`. Bottom is off by default.
Internally the composer's open state is now position-aware
(`composer_open_at`), so `:both` never mounts two Leaf editors or two
upload inputs — only the opened position shows the form; the other
stays a button.
## 0.2.3 — 2026-05-29
### Changed
- Comment card layout restructured to a strict vertical stack
(avatar + email → decoration label → body → date → actions) so the
card reads correctly in narrow embed containers (media sidebar,
MediaDetail panel) instead of truncating the email under the action
buttons.
- Action buttons (like / dislike / reply / edit / delete) are now
right-aligned in their footer row.
- Decoration label (annotation title) now renders at the comment body's
size, bold (`text-base font-bold`) — and gets top spacing so it reads
as a peer title rather than a cramped sub-heading.
- Image attachments fill the comment width (`w-full max-h-96
object-contain`) instead of being capped at `max-w-xs`.
- Bump leaf 0.2.13 → 0.2.21.
### Merged
- Integrated upstream `main` (gettext sweep + `precheck_create` upload
refactor) with the local Leaf-editor, decoration-registry, inline-reply,
reaction, and composer-toggle work.
## 0.2.2 — 2026-05-14
### Fixed
- Complete gettext/i18n coverage for `CommentsComponent` flash messages,
error helpers, upload labels, video/audio fallback text, and accessibility
attributes (`alt`, `aria-label`).
- Version sync between `mix.exs`, `version/0`, and test assertion.
- Precommit cleanliness: removed stale self-referential `phoenix_kit_comments`
from `mix.lock`, formatted `mix.exs`.
## 0.2.1 — 2026-05-12
### Features
- Rendered comments now carry `data-comment-uuid` and (when present)
`data-annotation-uuid` on the outer wrapper, letting sibling components
on the host page correlate DOM nodes with comment + linked-resource
uuids without reaching into render internals.
`data-annotation-uuid` is sourced from `metadata["annotation_uuid"]`
and omitted when nil.
## 0.2.0 — 2026-05-11
### Features
- **Comment attachments**: comments can now carry images, video, audio, and
miscellaneous file uploads alongside text or a Giphy GIF. Attachments
flow through the parent `PhoenixKit.Modules.Storage` stack (multi-bucket
redundancy, variant generation) and link to comments via a new
`phoenix_kit_comment_media` junction table.
- **In-browser voice recording**: a microphone button on the comment form
records via `MediaRecorder` (webm/opus) and submits the result through
the same upload queue as drag-and-drop attachments. Recordings are
audio attachments — no separate code path.
- Three new admin settings under `/admin/settings/comments` →
"Attachments":
- `comments_attachments_enabled` (master toggle, default off)
- `comments_max_attachments` (per-comment cap, 1–10, default 4)
- `comments_attachment_max_size_mb` (per-file size cap, 1–500,
clamped against the global `storage_max_upload_size_mb`)
- `create_comment/4` accepts a new `:attachment_file_uuids` key — a list
of `PhoenixKit.Modules.Storage.File` UUIDs to attach in display order.
Insert + attaches run in one transaction.
- New public context fns: `attach_media/3`, `detach_media/2`,
`detach_media_by_uuid/1`, `list_comment_media/2`,
`attachments_enabled?/0`, `get_max_attachments/0`,
`get_max_attachment_size_mb/0`.
- Comment validity rule generalized to "content **OR** Giphy **OR**
attachments". The Comment schema gains a virtual `:has_attachments?`
field set by the orchestrator at insert time.
### Migration
- Requires the new `phoenix_kit_comment_media` table introduced in
PhoenixKit migration V113. Run `mix ecto.migrate` after bumping the
parent `phoenix_kit` dep.
## 0.1.5 — 2026-04-17
### Features
- Optional Giphy integration for the comment form. Users can post text-only,
GIF-only, or text + GIF comments via a floating Giphy picker; the selected GIF
is stored on `comment.metadata["giphy"]` and rendered inline with the comment.
- New admin settings under `/admin/settings/comments` → "Giphy Integration":
enable toggle, API key (stored in DB), and content rating filter (G/PG/PG-13/R).
- `:form_extras` slot on `CommentsComponent` — parent projects can inject their
own inputs into the new-comment form and any `name="metadata[<key>]"` values
are merged into `comment.metadata` on submit (the `"giphy"` key is reserved).
- Character counter and Cancel button added to the top-level comment form.
- Responsive mobile overhaul of the entire comments component (down to ~320px):
card `overflow-hidden`, wrapping header, `break-words` content,
container-scaled GIFs, and a mobile bottom-sheet variant of the picker.
### Breaking
- `Comment.changeset/2` no longer requires `:content`; either `content` or a GIF
attachment in `metadata["giphy"]` is accepted. Downstream consumers that built
their own changeset relying on the `content can't be blank` error should check
the new validation path.
### Dependencies
- Add `giphy_api ~> 0.1.1`. The API key is passed per-call via the library's
`:api_key` option, so no global `Application.put_env/3` write happens on each
search. Empty keys short-circuit before any HTTP call.
## 0.1.4 — 2026-04-12
### Fixed
- Add routing anti-pattern warning to AGENTS.md.
## 0.1.3 — 2026-04-02
### Improvements
- Migrate select elements to daisyUI 5 label wrapper pattern and remove deprecated
`select-bordered` class.
- Mobile-responsive admin filter bar with separate mobile/desktop layouts.
## 0.1.2 — 2026-03-31
### Features
- Truncate long metadata values in display titles — individual values capped at 15 characters
with `...` suffix to keep titles compact.
- Mobile-responsive settings page — table switches to stacked card view on small screens, toggles
and buttons stack vertically, overflow prevention throughout.
- Client-side badge coloring — metadata field badges now highlight based on the currently focused
input via the JS hook instead of server-side rendering.
- Display title shown in mobile card view on comments admin page when a display template is
configured for the resource type.
## 0.1.1 — 2026-03-31
### Features
- Add `:prefix` placeholder for resource path templates — paths no longer auto-prefix with
`Routes.path()`; include `:prefix` in your template to get the site URL prefix.
- Add configurable display title templates for resource types — show meaningful names instead of
truncated UUIDs in the admin comment list.
- Add inline editing for resource link patterns in settings (edit button, save/cancel).
- Add inline comment content editing in CommentsComponent (edit button, save/cancel).
- Add clickable metadata field badges with live color updates — green when used in the template,
gray when unused. Clicking inserts the placeholder at cursor position.
- Add `list_metadata_keys_by_type/0` — queries distinct JSONB metadata keys per resource type
for display in settings.
### Bug Fixes
- Fix placeholder collision — `:metadata.prefix` and `:metadata.uuid` were corrupted by naive
substring replacement. Metadata placeholders are now resolved first.
- Fix event listener accumulation in InsertAtCursor JS hook — listeners were re-added on every
LiveView patch without cleanup. Now uses `AbortController` for proper teardown.
- Fix XSS vector in InsertAtCursor hook — replaced `querySelector` built from input name string
with direct element reference.
- Fix missing server-side content validation on comment edits — now enforces empty check and
configurable `comments_max_length` setting (previously only enforced on creation).
- Fix edit/reply state collision in CommentsComponent — entering edit mode now clears reply state
and vice versa.
- Fix `editing_path_value` not cleared after saving resource path edit.
- Fix draft state (`draft_paths`/`draft_titles`) not cleaned up after adding unconfigured type.
- Add `Logger.warning` to `list_metadata_keys_by_type/0` rescue block instead of silently
swallowing errors.
- Fix nested forms in settings page — Resource Link Patterns card was rendered inside the main
settings `<form>`, producing invalid HTML. Moved outside the form.
- Fix stale assigns after adding/removing resource path templates — `unconfigured_types` now
stays in sync without requiring a page refresh.
- Add path template input validation — templates must start with `/` or `:prefix` and cannot
contain `://`, preventing XSS via `javascript:` URIs and open redirects.
- Add `Logger.warning` to rescue blocks in `count_comments_by_type/0` and
`get_resource_path_templates/0` instead of silently swallowing errors.
### Improvements
- Deduplicate resource path add/save logic into shared `save_resource_config/5`.
- Make `extract_path/1` and `extract_title/1` private (only used within settings module).
- Resource path table uses `table-fixed` with `break-all` to handle long templates.
## 0.1.0 — 2026-03-27
### Features
- Initial release — polymorphic comments module extracted from PhoenixKit.
- Resource-agnostic design via `(resource_type, resource_uuid)` tuples with no FK constraints.
- Unlimited self-referencing comment threading with configurable max depth.
- Like/dislike system with denormalized counters and transactional safety.
- Moderation workflow: pending, published, hidden, deleted statuses with bulk operations.
- Admin UI: paginated comment list with search, status filters, and resource type grouping.
- Settings UI: toggles for enable/moderation, configurable max depth and max length.
- Resource handler callbacks for `on_comment_created/3` and `on_comment_deleted/3`.
- Resource resolution system with handler-based and path-template-based fallbacks.
- ILIKE search with proper wildcard escaping.
- Soft delete preserving comment tree structure.