diff --git a/lib/who_need_help/support.ex b/lib/who_need_help/support.ex index 9159271..ad74c86 100644 --- a/lib/who_need_help/support.ex +++ b/lib/who_need_help/support.ex @@ -13,6 +13,13 @@ defmodule WhoNeedHelp.Support do alias WhoNeedHelp.Trust.RateLimiter @access_salt "support-request-access" + @staff_topic "support:staff" + + def subscribe_request(id), + do: Phoenix.PubSub.subscribe(WhoNeedHelp.PubSub, request_topic(id)) + + def subscribe_staff, + do: Phoenix.PubSub.subscribe(WhoNeedHelp.PubSub, @staff_topic) def change_request(%SupportRequest{} = request, attrs \\ %{}) do SupportRequest.submission_changeset(request, attrs) @@ -56,6 +63,7 @@ defmodule WhoNeedHelp.Support do end end) |> notify_received() + |> broadcast_request_update() end end @@ -201,6 +209,7 @@ defmodule WhoNeedHelp.Support do end end) |> notify_decision() + |> broadcast_request_update() else false -> {:error, :forbidden} _ -> {:error, :not_found} @@ -269,6 +278,7 @@ defmodule WhoNeedHelp.Support do {:ok, {updated, _message}} -> {:ok, preload_conversation(updated)} error -> error end) + |> broadcast_request_update() else nil -> {:error, :invalid_message} {:error, _reason} = error -> error @@ -313,6 +323,7 @@ defmodule WhoNeedHelp.Support do {:ok, updated} -> {:ok, preload_conversation(updated)} error -> error end) + |> broadcast_request_update() else false -> {:error, :invalid_status} {:error, _reason} = error -> error @@ -371,6 +382,19 @@ defmodule WhoNeedHelp.Support do defp notify_operator_update(result), do: result + defp broadcast_request_update({:ok, %SupportRequest{} = request} = result) do + event = {:support_request_updated, request.id} + + Phoenix.PubSub.broadcast(WhoNeedHelp.PubSub, request_topic(request.id), event) + Phoenix.PubSub.broadcast(WhoNeedHelp.PubSub, @staff_topic, event) + + result + end + + defp broadcast_request_update(result), do: result + + defp request_topic(id), do: "support:request:#{id}" + defp verify_contact(%SupportRequest{contact_verified_at: nil} = request) do request |> Ecto.Changeset.change(contact_verified_at: DateTime.utc_now(:second)) diff --git a/lib/who_need_help_web/controllers/support_controller.ex b/lib/who_need_help_web/controllers/support_controller.ex index f2b6b75..9f62dc5 100644 --- a/lib/who_need_help_web/controllers/support_controller.ex +++ b/lib/who_need_help_web/controllers/support_controller.ex @@ -101,15 +101,13 @@ defmodule WhoNeedHelpWeb.SupportController do |> put_flash(:info, gettext("Your message was sent to support.")) |> redirect(to: case_path(request.id, token)) - {:error, %Ecto.Changeset{} = changeset} -> - with {:ok, request} <- - Support.get_for_viewer(conn.assigns.current_scope, id, token) do - conn - |> put_status(:unprocessable_entity) - |> render_case(request, token, changeset) - else - _ -> send_resp(conn, :not_found, "Not found") - end + {:error, %Ecto.Changeset{}} -> + conn + |> put_flash( + :error, + gettext("Could not send your message. Please check it and try again.") + ) + |> redirect(to: case_path(id, token)) {:error, :rate_limited} -> conn @@ -156,18 +154,12 @@ defmodule WhoNeedHelpWeb.SupportController do def received(conn, _params), do: redirect(conn, to: ~p"/support") - defp render_case(conn, request, token, changeset \\ nil) do - message_form = - case changeset do - %Ecto.Changeset{} -> Phoenix.Component.to_form(changeset, as: :message) - nil -> Phoenix.Component.to_form(%{"body" => ""}, as: :message) - end - - render(conn, :show, - request: request, - access_token: token, - message_form: message_form, - reopen_form: Phoenix.Component.to_form(%{}, as: :reopen) + defp render_case(conn, request, token) do + Phoenix.LiveView.Controller.live_render(conn, WhoNeedHelpWeb.SupportCaseLive, + session: %{ + "support_request_id" => request.id, + "support_access_token" => token + } ) end diff --git a/lib/who_need_help_web/controllers/support_html/received.html.heex b/lib/who_need_help_web/controllers/support_html/received.html.heex index b95756c..fda4c36 100644 --- a/lib/who_need_help_web/controllers/support_html/received.html.heex +++ b/lib/who_need_help_web/controllers/support_html/received.html.heex @@ -6,8 +6,18 @@ >
+
+ {gettext( + "We created support request %{reference} and added it to your account. You can open it now and return to it from My support requests.", + reference: @reference + )} +
+{gettext( "We created support request %{reference}. A private status link has been sent to the contact email when delivery is configured. Opening that link verifies the address for public requests.", reference: @reference diff --git a/lib/who_need_help_web/controllers/support_html/show.html.heex b/lib/who_need_help_web/controllers/support_html/show.html.heex index ca7cbf2..3887460 100644 --- a/lib/who_need_help_web/controllers/support_html/show.html.heex +++ b/lib/who_need_help_web/controllers/support_html/show.html.heex @@ -83,13 +83,16 @@ <.form for={@message_form} + id="support-message-form" action={~p"/support/cases/#{@request.id}/messages"} method="post" + phx-submit="send-message" class="mt-5" > <.input field={@message_form[:body]} + id={"support-message-body-#{@message_form_version}"} type="textarea" label={gettext("Message to support")} placeholder={gettext("Add details or answer the support team")} @@ -100,8 +103,10 @@ <.form :if={@request.status in [:resolved, :closed]} for={@reopen_form} + id="support-reopen-form" action={~p"/support/cases/#{@request.id}/reopen"} method="post" + phx-submit="reopen" class="mt-3" > diff --git a/lib/who_need_help_web/live/support_case_live.ex b/lib/who_need_help_web/live/support_case_live.ex new file mode 100644 index 0000000..1afd510 --- /dev/null +++ b/lib/who_need_help_web/live/support_case_live.ex @@ -0,0 +1,127 @@ +defmodule WhoNeedHelpWeb.SupportCaseLive do + use WhoNeedHelpWeb, :live_view + + alias WhoNeedHelp.Support + alias WhoNeedHelpWeb.SupportHTML + + on_mount {WhoNeedHelpWeb.UserAuth, :mount_current_scope} + + @impl true + def mount( + _params, + %{ + "support_request_id" => request_id, + "support_access_token" => access_token + }, + socket + ) do + case Support.get_for_viewer(socket.assigns.current_scope, request_id, access_token) do + {:ok, request} -> + if connected?(socket), do: Support.subscribe_request(request.id) + + {:ok, + socket + |> assign(:page_title, request.subject) + |> assign(:request, request) + |> assign(:access_token, access_token) + |> assign(:message_form_version, 0) + |> assign_message_form() + |> assign(:reopen_form, to_form(%{}, as: :reopen))} + + {:error, :not_found} -> + {:ok, + socket + |> put_flash(:error, gettext("This support request is no longer available.")) + |> redirect(to: ~p"/support")} + end + end + + @impl true + def handle_event("send-message", %{"message" => params}, socket) do + case Support.add_requester_message( + socket.assigns.current_scope, + socket.assigns.request.id, + socket.assigns.access_token, + params + ) do + {:ok, request} -> + {:noreply, + socket + |> assign(:request, request) + |> update(:message_form_version, &(&1 + 1)) + |> assign_message_form() + |> put_flash(:info, gettext("Your message was sent to support."))} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :message_form, to_form(changeset, as: :message))} + + {:error, :rate_limited} -> + {:noreply, + put_flash(socket, :error, gettext("Too many messages. Please try again later."))} + + {:error, :not_found} -> + {:noreply, + socket + |> put_flash(:error, gettext("This support request is no longer available.")) + |> redirect(to: ~p"/support")} + + {:error, _reason} -> + {:noreply, + put_flash( + socket, + :error, + gettext("Could not send your message. Please check it and try again.") + )} + end + end + + def handle_event("reopen", _params, socket) do + case Support.reopen_request( + socket.assigns.current_scope, + socket.assigns.request.id, + socket.assigns.access_token + ) do + {:ok, request} -> + {:noreply, + socket + |> assign(:request, request) + |> put_flash(:info, gettext("The support request is open again."))} + + {:error, :invalid_status} -> + {:noreply, + put_flash( + socket, + :error, + gettext("Only a resolved or closed request can be reopened.") + )} + + {:error, :not_found} -> + {:noreply, + socket + |> put_flash(:error, gettext("This support request is no longer available.")) + |> redirect(to: ~p"/support")} + end + end + + @impl true + def handle_info( + {:support_request_updated, request_id}, + %{assigns: %{request: %{id: request_id}}} = socket + ) do + case Support.get_for_viewer( + socket.assigns.current_scope, + request_id, + socket.assigns.access_token + ) do + {:ok, request} -> {:noreply, assign(socket, :request, request)} + {:error, :not_found} -> {:noreply, redirect(socket, to: ~p"/support")} + end + end + + defp assign_message_form(socket) do + assign(socket, :message_form, to_form(%{"body" => ""}, as: :message)) + end + + @impl true + def render(assigns), do: SupportHTML.show(assigns) +end diff --git a/lib/who_need_help_web/live/support_operations_live.ex b/lib/who_need_help_web/live/support_operations_live.ex index 5ae0544..66a0e3a 100644 --- a/lib/who_need_help_web/live/support_operations_live.ex +++ b/lib/who_need_help_web/live/support_operations_live.ex @@ -5,6 +5,8 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do @impl true def mount(_params, _session, socket) do + if connected?(socket), do: Support.subscribe_staff() + {:ok, socket |> assign(:page_title, gettext("Support operations")) @@ -12,6 +14,11 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do |> load()} end + @impl true + def handle_info({:support_request_updated, _request_id}, socket) do + {:noreply, load(socket)} + end + @impl true def handle_event("moderate-support", %{"id" => id, "moderation" => params}, socket) do respond( @@ -196,29 +203,44 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
<.form for={form} + id={"removal-moderation-#{notice.id}"} phx-submit="moderate-removal" phx-value-id={notice.id} - class="mt-4 grid gap-2 md:grid-cols-[13rem_1fr_auto]" + class="mt-4 rounded-2xl border border-error/20 bg-error/5 p-4" > - <.input - field={form[:status]} - type="select" - options={[ - {gettext("Open"), "open"}, - {gettext("Urgent review"), "urgent_review"}, - {gettext("Reviewing"), "reviewing"}, - {gettext("More information needed"), "needs_information"}, - {gettext("Action taken"), "actioned"}, - {gettext("Rejected"), "rejected"}, - {gettext("Closed"), "closed"} - ]} - /> - <.input - field={form[:resolution_note]} - type="textarea" - placeholder={gettext("Decision or information request")} - /> - <.button class="btn btn-error self-end">{gettext("Save and notify")} ++ {gettext("Record the decision and notify the submitter when contact is available.")} +
+@@ -333,27 +355,42 @@ defmodule WhoNeedHelpWeb.SupportOperationsLive do
+ {gettext("Update the case status and add a private reply when needed.")} +
+diff --git a/test/who_need_help_web/controllers/support_controller_test.exs b/test/who_need_help_web/controllers/support_controller_test.exs index 0aa3d17..7250f8a 100644 --- a/test/who_need_help_web/controllers/support_controller_test.exs +++ b/test/who_need_help_web/controllers/support_controller_test.exs @@ -1,6 +1,7 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do use WhoNeedHelpWeb.ConnCase, async: false + import Phoenix.LiveViewTest import WhoNeedHelp.AccountsFixtures import Swoosh.TestAssertions @@ -80,6 +81,18 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do describe "authenticated support" do setup :register_and_log_in_user + test "receipt gives an authenticated requester immediate account access", %{conn: conn} do + page = + conn + |> get(~p"/support/received?reference=SUP-ACCOUNT") + |> html_response(200) + + assert page =~ "Support request created" + assert page =~ "added it to your account" + assert page =~ "View my requests" + refute page =~ "