diff --git a/lib/who_need_help/activities/activity.ex b/lib/who_need_help/activities/activity.ex
index 4eb5693..2adf712 100644
--- a/lib/who_need_help/activities/activity.ex
+++ b/lib/who_need_help/activities/activity.ex
@@ -78,6 +78,15 @@ defmodule WhoNeedHelp.Activities.Activity do
activity
|> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000)
+ |> require_hidden_reason()
+ end
+
+ defp require_hidden_reason(changeset) do
+ if get_field(changeset, :hidden_at) do
+ validate_required(changeset, :hidden_reason)
+ else
+ changeset
+ end
end
def public_coordinates(%__MODULE__{
diff --git a/lib/who_need_help/help/help_request.ex b/lib/who_need_help/help/help_request.ex
index a8508b5..c233ae3 100644
--- a/lib/who_need_help/help/help_request.ex
+++ b/lib/who_need_help/help/help_request.ex
@@ -87,6 +87,15 @@ defmodule WhoNeedHelp.Help.HelpRequest do
request
|> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000)
+ |> require_hidden_reason()
+ end
+
+ defp require_hidden_reason(changeset) do
+ if get_field(changeset, :hidden_at) do
+ validate_required(changeset, :hidden_reason)
+ else
+ changeset
+ end
end
def cancellation_changeset(request, attrs) do
diff --git a/lib/who_need_help/trust.ex b/lib/who_need_help/trust.ex
index 6a1ec00..065ea02 100644
--- a/lib/who_need_help/trust.ex
+++ b/lib/who_need_help/trust.ex
@@ -345,10 +345,10 @@ defmodule WhoNeedHelp.Trust do
:reporter,
:reviewed_by,
request: :requester,
- assignment: :helper,
- message: :sender,
+ assignment: [:helper, :request],
+ message: [:sender, assignment: :request],
activity: :creator,
- activity_message: :sender
+ activity_message: [:sender, :activity]
])
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
diff --git a/lib/who_need_help_web/live/moderation_live.ex b/lib/who_need_help_web/live/moderation_live.ex
index a8fe7ba..ef73cc6 100644
--- a/lib/who_need_help_web/live/moderation_live.ex
+++ b/lib/who_need_help_web/live/moderation_live.ex
@@ -284,6 +284,38 @@ defmodule WhoNeedHelpWeb.ModerationLive do
defp signal_label(:manual), do: gettext("Manual review")
defp signal_label(value), do: to_string(value)
+ defp report_request(%{request: %WhoNeedHelp.Help.HelpRequest{} = request}), do: request
+
+ defp report_request(%{
+ assignment: %WhoNeedHelp.Help.Assignment{
+ request: %WhoNeedHelp.Help.HelpRequest{} = request
+ }
+ }),
+ do: request
+
+ defp report_request(%{
+ message: %WhoNeedHelp.Messaging.Message{
+ assignment: %WhoNeedHelp.Help.Assignment{
+ request: %WhoNeedHelp.Help.HelpRequest{} = request
+ }
+ }
+ }),
+ do: request
+
+ defp report_request(_report), do: nil
+
+ defp report_activity(%{activity: %WhoNeedHelp.Activities.Activity{} = activity}),
+ do: activity
+
+ defp report_activity(%{
+ activity_message: %WhoNeedHelp.Activities.Message{
+ activity: %WhoNeedHelp.Activities.Activity{} = activity
+ }
+ }),
+ do: activity
+
+ defp report_activity(_report), do: nil
+
@impl true
def render(assigns) do
~H"""
@@ -309,6 +341,8 @@ defmodule WhoNeedHelpWeb.ModerationLive do
<% hide_form = scoped_form(%{"note" => ""}, :moderation, "report-hide-#{report.id}") %>
+ <% reported_request = report_request(report) %>
+ <% reported_activity = report_activity(report) %>
<% report_form =
scoped_form(
%{"status" => to_string(report.status), "resolution_note" => ""},
@@ -324,6 +358,12 @@ defmodule WhoNeedHelpWeb.ModerationLive do
{gettext("activity message")}
+
+ {gettext("matched chat message")}
+
+
+ {gettext("matched conversation")}
+
{gettext("by %{name}", name: report.reporter.display_name)}
@@ -338,29 +378,55 @@ defmodule WhoNeedHelpWeb.ModerationLive do
{gettext("View scoped evidence")}
<.form
- :if={report.request_id}
+ :if={reported_request && is_nil(reported_request.hidden_at)}
for={hide_form}
phx-submit="hide-request"
- phx-value-id={report.request_id}
+ phx-value-id={reported_request.id}
class="flex gap-2"
>
- <.input field={hide_form[:note]} placeholder={gettext("Hide reason")} />
+ <.input
+ field={hide_form[:note]}
+ placeholder={gettext("Required reason for hiding")}
+ required
+ />
<.button class="btn btn-sm btn-error self-end">
{gettext("Hide request")}
+
<.form
- :if={report.activity_id}
+ :if={reported_activity && is_nil(reported_activity.hidden_at)}
for={hide_form}
phx-submit="hide-activity"
- phx-value-id={report.activity_id}
+ phx-value-id={reported_activity.id}
class="flex gap-2"
>
- <.input field={hide_form[:note]} placeholder={gettext("Hide reason")} />
+ <.input
+ field={hide_form[:note]}
+ placeholder={gettext("Required reason for hiding")}
+ required
+ />
<.button class="btn btn-sm btn-error self-end">
{gettext("Hide activity")}
+
<.form
for={report_form}
diff --git a/test/who_need_help/trust_safety_test.exs b/test/who_need_help/trust_safety_test.exs
index 60aaae1..c1da46b 100644
--- a/test/who_need_help/trust_safety_test.exs
+++ b/test/who_need_help/trust_safety_test.exs
@@ -153,6 +153,21 @@ defmodule WhoNeedHelp.TrustSafetyTest do
)
end
+ test "hiding a request requires a moderation reason", context do
+ moderator =
+ user_fixture(display_name: "Reason moderator")
+ |> Ecto.Changeset.change(role: :moderator)
+ |> Repo.update!()
+
+ {:ok, request} = Help.create_request(context.requester_scope, context.attrs)
+
+ assert {:error, %Ecto.Changeset{} = changeset} =
+ Trust.hide_request(user_scope_fixture(moderator), request.id, "")
+
+ assert "can't be blank" in errors_on(changeset).hidden_reason
+ refute Repo.get!(WhoNeedHelp.Help.HelpRequest, request.id).hidden_at
+ end
+
test "category moderation cannot cross help and activity taxonomies", context do
moderator =
user_fixture(display_name: "Category moderator")
diff --git a/test/who_need_help_web/live/mutual_aid_live_test.exs b/test/who_need_help_web/live/mutual_aid_live_test.exs
index e1cf205..19c5480 100644
--- a/test/who_need_help_web/live/mutual_aid_live_test.exs
+++ b/test/who_need_help_web/live/mutual_aid_live_test.exs
@@ -1113,7 +1113,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
{:ok, request} = Help.create_request(user_scope_fixture(requester), attrs)
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
- {:ok, _message} =
+ {:ok, message} =
Messaging.send_message(user_scope_fixture(helper), assignment, %{
"body" => "Evidence message"
})
@@ -1122,7 +1122,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
Trust.report(user_scope_fixture(requester), %{
"reason" => "harassment",
"details" => "Please review the matched conversation.",
- "assignment_id" => assignment.id
+ "message_id" => message.id
})
{:ok, _proposal} =
@@ -1140,6 +1140,11 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert html =~ "Please review the matched conversation."
assert html =~ "Bicycle repair"
+ assert has_element?(
+ view,
+ "form[phx-submit='hide-request'][phx-value-id='#{request.id}']"
+ )
+
evidence_html =
view
|> element("button[phx-click='view-evidence']")
@@ -1148,6 +1153,60 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert evidence_html =~ "Evidence message"
end
+ test "message reports expose parent activity moderation and switch to restore", %{
+ conn: conn,
+ user: user,
+ scope: participant_scope
+ } do
+ user |> Ecto.Changeset.change(role: :moderator) |> Repo.update!()
+ organizer = user_fixture(display_name: "Activity organizer")
+ Catalog.seed_defaults()
+ activity_category = Catalog.list_categories(:activity) |> List.first()
+
+ {:ok, activity} =
+ Activities.create_activity(
+ user_scope_fixture(organizer),
+ activity_attrs(activity_category)
+ )
+
+ {:ok, join_request} = Activities.request_to_join(participant_scope, activity.id)
+
+ {:ok, _approved} =
+ Activities.approve_participant(user_scope_fixture(organizer), join_request.id)
+
+ {:ok, message} =
+ Activities.send_message(user_scope_fixture(organizer), activity.id, %{
+ "body" => "Reported activity message"
+ })
+
+ {:ok, _report} =
+ Trust.report(participant_scope, %{
+ "reason" => "harassment",
+ "details" => "Please review this activity message.",
+ "activity_message_id" => message.id
+ })
+
+ {:ok, view, _html} = live(conn, ~p"/moderation")
+
+ assert has_element?(
+ view,
+ "form[phx-submit='hide-activity'][phx-value-id='#{activity.id}']"
+ )
+
+ view
+ |> form("form[phx-submit='hide-activity'][phx-value-id='#{activity.id}']", %{
+ "moderation" => %{"note" => "Unsafe public activity details"}
+ })
+ |> render_submit()
+
+ assert has_element?(
+ view,
+ "button[phx-click='restore-activity'][phx-value-id='#{activity.id}']"
+ )
+
+ assert Repo.get!(WhoNeedHelp.Activities.Activity, activity.id).hidden_at
+ end
+
test "abuse-signal pagination stays available without opening report evidence", %{
conn: conn,
user: user