Skip to content

Add moderator reply to event messages - #241

Open
junkerderprovinz wants to merge 7 commits into
ClaperCo:devfrom
junkerderprovinz:feature/moderator-reply-to-messages
Open

Add moderator reply to event messages#241
junkerderprovinz wants to merge 7 commits into
ClaperCo:devfrom
junkerderprovinz:feature/moderator-reply-to-messages

Conversation

@junkerderprovinz

@junkerderprovinz junkerderprovinz commented Aug 16, 2026

Copy link
Copy Markdown

Summary

#234 asks for a moderator "reply" option on event chat messages.

What this adds

A "Reply" action in the message-moderation panel, next to the existing pin/ban/delete. Clicking it opens an inline composer (client-side Alpine state, no server round-trip just to show or hide the input, the same pattern the actions menu itself already uses). Submitting stores the reply on the same Post row (one reply per message, not a full thread) and broadcasts through the existing :post_updated PubSub event, which the attendee view, presenter view and moderation panel all already handle, so the reply shows up live everywhere without a new event type.

Attendees see it as a small "Moderator reply" block under the original message, and the projected presenter display shows the same block under the message it answers.

New migration adds two nullable columns (reply_body, replied_at) to posts. No change to existing behavior when reply_body is unset.

Defects found and fixed on review

A second pass over my own branch turned up one real gap and several rough edges.

The reply never reached the projected screen. presenter.html.heex builds its own post markup rather than going through PostComponent, and that markup only printed post.body. So a reply reached attendee phones and the moderation panel but never the shared display, which is usually the one screen the whole room is actually looking at. The reply block is now rendered in both the post list and the pinned-post list there, sized off @iframe like the surrounding presenter markup.

Failures were silent. Submitting a blank reply was a no-op with no feedback, and a failing reply_to_post/2 changeset was swallowed just as quietly, so a moderator could not tell a delivered reply from a dropped one. Both now report through put_flash(:error, ...), matching the other failure paths in manage.ex.

Three corrections to the panel's reply block. The composer <form> sat outside the :if={!@readonly} guard, so it was emitted (inert, hidden by x-show) on the read-only stats page too, and is now inside it. The label read "Your reply", which is wrong as soon as a different co-moderator wrote it, so it now reads "Moderator reply", consistent with the attendee and presenter views and reusing a string that already exists. And replied_at was written but never read anywhere, so it now appears next to that label as HH:MM, formatted like the post timestamp directly above it.

Translations

Ran mix gettext.extract --merge, so default.pot and every locale's default.po pick up the five new strings. They land untranslated and fall back to the English source until a translation pass, matching how strings arrived in previous feature PRs such as #214. Note that the same run also refreshes #: source references across the catalogues, which had drifted from the current source tree before this branch, so the diff on those files is larger than the five new strings alone.

Not included

  • one reply per message rather than a thread, which is what FR: allow written answers in event messages聽#234 asks for; a second reply overwrites the first
  • no record of which moderator replied, which is why the label is the neutral "Moderator reply" rather than a name

Verification

mix test gives 342 tests, 0 failures, run just now against this branch at its current head, in a container matching .github/workflows/elixir.yml (Elixir 1.18.4 / OTP 28, postgres:15, same env). mix format --check-formatted and mix credo diff --from-git-merge-base origin/main are both clean. The earlier "336/336" in this description was the count before this round of fixes added tests.

The branch previously had no tests at all for the feature. It now has reply_to_post/2 context tests next to the existing update_post/2 pair, plus LiveView tests for replying from the moderation panel, for the blank reply being reported rather than dropped, and for the reply reaching the presenter display. The last two fail against the unfixed branch, which is how the two bugs above were confirmed rather than assumed.

End to end against a real running instance, two separate browser sessions (one moderator, one anonymous attendee): posted a question as the attendee, replied as the moderator, watched the reply appear on the attendee's screen live with no refresh.

On CI

For transparency, since this PR previously quoted a test number that upstream CI never confirmed: the Elixir CI run on this PR from 2026-08-16 failed at "Check Credo Warnings" and its "Run tests" step was skipped, so the suite never ran upstream at all. 6c5ce05 addressed those Credo findings. The run queued against the current head is sitting in action_required, GitHub's approval gate for workflow runs on pull requests from forks, so it needs a maintainer to approve it before it will execute. Every number above comes from my own container run, not from upstream CI.

Fixes #234

Adds a "Reply" action to the message-moderation panel, next to the
existing pin/ban/delete actions. Clicking it opens an inline composer
(no page reload, no server round-trip just to show/hide the input --
that part is plain Alpine state, matching how the actions menu itself
already toggles); submitting stores the reply on the same Post row
(one reply per message, not a full thread) and broadcasts the update
through the existing :post_updated PubSub event, which the attendee
view, the presenter view and the moderation panel already all handle
-- so the reply shows up live everywhere without adding a new event
type.

Attendees see it as a small "Moderator reply" block under the
original message.

New migration adds two nullable columns (reply_body, replied_at) to
posts. No changes to existing behaviour when reply_body is unset.

Verified end-to-end against a real running instance (compose.dev.yml,
two separate browser sessions -- one as the moderator, one as an
anonymous attendee): posted a question as the attendee, replied as
the moderator, watched the reply appear on the attendee's screen live
with no refresh.

Fixes ClaperCo#234
The "reply" handle_event clause nested three case expressions, which
exceeds Credo's Refactor.Nesting limit (max depth is 2) and failed the
"Check Credo Warnings" step of the Elixir CI workflow.

Move the post lookup and the update call into a private reply/3 helper,
mirroring how the neighbouring "pin" clause delegates to pin/2.

Behaviour is unchanged: the empty-body guard still runs before the
database lookup, and every branch still returns {:noreply, socket}.
presenter.html.heex builds its own post markup instead of going through
PostComponent, and that markup only printed post.body. A reply therefore
reached attendee phones and the moderation panel but never the projected
screen, which is usually the only place the room is looking.

Add the reply block to both the post list and the pinned-post list, sized
off @iFrame like the surrounding presenter markup.
Submitting a blank reply was a silent no-op, and a failing
reply_to_post/2 changeset was swallowed just as quietly, so a moderator
got no feedback either way and could not tell a delivered reply from a
dropped one. Report both with put_flash(:error, ...), the way the other
failure paths in this module already do.
Three small corrections to the reply the panel renders:

- the composer <form> sat outside the :if={!@readonly} guard, so it was
  emitted (inert, x-show hidden) on the read-only stats page too;
- "Your reply" is wrong as soon as a different co-moderator wrote it, so
  it now reads "Moderator reply", matching the attendee and presenter
  views and reusing a string that already exists;
- replied_at was written but never read anywhere, so it now shows next to
  the label as HH:MM, formatted like the post timestamp above it.
Adds reply_to_post/2 context tests next to the update_post/2 pair, and
LiveView tests for replying from the moderation panel, for the blank
reply now being reported rather than dropped, and for the reply reaching
the presenter display.
Ran `mix gettext.extract --merge`, so default.pot and every locale's
default.po pick up the strings this feature adds. They land untranslated
and fall back to the English source until a translation pass, matching
how strings arrived in previous feature PRs.

The run also refreshes `#:` source references across the catalogues,
which had drifted from the current source tree before this branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FR: allow written answers in event messages

1 participant