Close terminal help interactions and clarify state

This commit is contained in:
SimpleTest 2026-07-22 06:34:38 +03:00
parent 9acc52ef78
commit f97b00bda8
5 changed files with 281 additions and 29 deletions

View File

@ -352,7 +352,7 @@ defmodule WhoNeedHelp.Help do
is_nil(request) -> is_nil(request) ->
{:error, :not_found} {:error, :not_found}
request.requester_id == user.id and request.status in [:open, :matched] -> request.requester_id == user.id and request.status in [:open, :matched, :in_progress] ->
now = DateTime.utc_now(:second) now = DateTime.utc_now(:second)
assignment = assignment =

View File

@ -60,8 +60,15 @@ defmodule WhoNeedHelp.Messaging do
request = current.request request = current.request
recipient_id = counterpart_id(user.id, current, request) recipient_id = counterpart_id(user.id, current, request)
with true <- Help.participant?(scope, current), cond do
:ok <- Trust.lock_user_pair(user.id, recipient_id), not Help.participant?(scope, current) ->
{:error, :forbidden}
current.status not in [:accepted, :in_progress] ->
{:error, :assignment_inactive}
true ->
with :ok <- Trust.lock_user_pair(user.id, recipient_id),
false <- Trust.blocked_between?(user.id, recipient_id), false <- Trust.blocked_between?(user.id, recipient_id),
{:ok, message} <- {:ok, message} <-
%Message{assignment_id: current.id, sender_id: user.id} %Message{assignment_id: current.id, sender_id: user.id}
@ -77,9 +84,9 @@ defmodule WhoNeedHelp.Messaging do
{:ok, message} {:ok, message}
else else
true -> {:error, :blocked} true -> {:error, :blocked}
false -> {:error, :forbidden}
other -> other other -> other
end end
end
else else
{:error, :not_found} {:error, :not_found}
end end

View File

@ -65,7 +65,13 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
def handle_info({event, request}, socket) when event in [:request_updated, :request_created] do def handle_info({event, request}, socket) when event in [:request_updated, :request_created] do
case Help.get_request(socket.assigns.current_scope, request.id) do case Help.get_request(socket.assigns.current_scope, request.id) do
{:ok, request} -> {:ok, request} ->
socket = maybe_subscribe_assignment(socket, request) previous_status = socket.assigns.request.status
socket =
socket
|> maybe_subscribe_assignment(request)
|> maybe_put_status_transition_flash(previous_status, request.status)
{:noreply, sync_tracking_after_request_update(socket, request)} {:noreply, sync_tracking_after_request_update(socket, request)}
{:error, :not_found} -> {:error, :not_found} ->
@ -369,11 +375,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
socket.assigns.other_user_id socket.assigns.other_user_id
) )
{:noreply, socket =
socket socket
|> assign(:blocked_by_current, false) |> assign(:blocked_by_current, false)
|> assign(:interaction_blocked, interaction_blocked) |> assign(:interaction_blocked, interaction_blocked)
|> put_flash(:info, gettext("User unblocked."))} |> restore_private_interaction(interaction_blocked)
|> put_flash(:info, gettext("User unblocked."))
{:noreply, socket}
{:error, reason} -> {:error, reason} ->
{:noreply, put_flash(socket, :error, message(reason))} {:noreply, put_flash(socket, :error, message(reason))}
@ -436,6 +445,55 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
end end
end end
defp maybe_put_status_transition_flash(socket, status, status), do: socket
defp maybe_put_status_transition_flash(socket, _previous, :matched) do
put_flash(
socket,
:info,
gettext("A volunteer accepted this request. Use private chat to coordinate safely.")
)
end
defp maybe_put_status_transition_flash(socket, _previous, :in_progress) do
put_flash(socket, :info, gettext("Help is now in progress."))
end
defp maybe_put_status_transition_flash(socket, _previous, :completed) do
put_flash(socket, :info, gettext("Help is complete."))
end
defp maybe_put_status_transition_flash(socket, _previous, :cancelled) do
put_flash(socket, :info, gettext("This request was cancelled."))
end
defp maybe_put_status_transition_flash(socket, _previous, _status), do: socket
defp restore_private_interaction(socket, true), do: socket
defp restore_private_interaction(%{assigns: %{assignment: nil}} = socket, false), do: socket
defp restore_private_interaction(socket, false) do
assignment = socket.assigns.assignment
messages_page =
Messaging.paginate_messages(socket.assigns.current_scope, assignment)
positions =
if assignment.status in [:accepted, :in_progress],
do: Tracking.list_current_positions(socket.assigns.current_scope, assignment),
else: %{}
socket
|> assign(:messages, messages_page.entries)
|> assign(:messages_cursor, messages_page.next_cursor)
|> assign(
:message_window_limit,
max(length(messages_page.entries), @initial_message_window)
)
|> assign_positions(positions)
end
defp load(socket, request, tracking_active) do defp load(socket, request, tracking_active) do
assignment = request.assignment assignment = request.assignment
participant = assignment && Help.participant?(socket.assigns.current_scope, assignment) participant = assignment && Help.participant?(socket.assigns.current_scope, assignment)
@ -699,6 +757,12 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
defp status_label(:cancelled), do: gettext("Cancelled") defp status_label(:cancelled), do: gettext("Cancelled")
defp status_label(value), do: value defp status_label(value), do: value
defp current_user_confirmed?(scope, request, assignment) do
if Help.requester?(scope, request),
do: not is_nil(assignment.requester_confirmed_at),
else: not is_nil(assignment.helper_confirmed_at)
end
defp visibility_label(:approximate_public), do: gettext("Approximate publicly") defp visibility_label(:approximate_public), do: gettext("Approximate publicly")
defp visibility_label(:hidden), do: gettext("Hidden") defp visibility_label(:hidden), do: gettext("Hidden")
defp visibility_label(:exact_for_active_match), do: gettext("Exact only for active match") defp visibility_label(:exact_for_active_match), do: gettext("Exact only for active match")
@ -892,7 +956,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
</div> </div>
</div> </div>
<.form <.form
:if={!@interaction_blocked} :if={!@interaction_blocked && @assignment.status in [:accepted, :in_progress]}
id="message-form" id="message-form"
for={@message_form} for={@message_form}
phx-submit="send-message" phx-submit="send-message"
@ -918,6 +982,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
{gettext("Send")} {gettext("Send")}
</.button> </.button>
</.form> </.form>
<p
:if={!@interaction_blocked && @assignment.status in [:completed, :cancelled]}
class="mt-4 rounded-2xl bg-base-200 p-4 text-sm text-base-content/65"
>
{if @assignment.status == :completed,
do: gettext("This help request is complete. Chat history is read-only."),
else: gettext("This help request was cancelled. Chat history is read-only.")}
</p>
</section> </section>
<section <section
@ -1036,6 +1108,44 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
)} )}
</p> </p>
</div> </div>
<div class="space-y-2 rounded-xl bg-white/10 p-3 text-xs">
<div class="flex items-center justify-between gap-3">
<span>{gettext("Requester confirmed")}</span>
<span class="badge badge-sm">
{if @assignment.requester_confirmed_at,
do: gettext("Yes"),
else: gettext("Pending")}
</span>
</div>
<div class="flex items-center justify-between gap-3">
<span>{gettext("Helper confirmed")}</span>
<span class="badge badge-sm">
{if @assignment.helper_confirmed_at,
do: gettext("Yes"),
else: gettext("Pending")}
</span>
</div>
<div class="flex items-center justify-between gap-3">
<span>{gettext("Handover code")}</span>
<span class="badge badge-sm">
{if @assignment.handover_verified_at,
do: gettext("Verified"),
else: gettext("Pending")}
</span>
</div>
</div>
<div
:if={@assignment.status == :completed}
class="alert alert-success text-sm text-base-content"
>
{gettext("Help completed and verified by both participants.")}
</div>
<div
:if={@assignment.status == :cancelled}
class="alert alert-warning text-sm text-base-content"
>
{gettext("This match was cancelled. No further handover action is required.")}
</div>
<div <div
:if={@assignment.helper.social_identities != []} :if={@assignment.helper.social_identities != []}
class="flex flex-wrap gap-2 rounded-xl bg-white/10 p-3" class="flex flex-wrap gap-2 rounded-xl bg-white/10 p-3"
@ -1060,12 +1170,24 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
{gettext("Start helping")} {gettext("Start helping")}
</button> </button>
<button <button
:if={@assignment.status in [:accepted, :in_progress]} :if={
@assignment.status in [:accepted, :in_progress] &&
!current_user_confirmed?(@current_scope, @request, @assignment)
}
phx-click="confirm" phx-click="confirm"
class="btn btn-outline w-full" class="btn btn-outline w-full"
> >
{gettext("Confirm my part is complete")} {gettext("Confirm my part is complete")}
</button> </button>
<p
:if={
@assignment.status in [:accepted, :in_progress] &&
current_user_confirmed?(@current_scope, @request, @assignment)
}
class="rounded-xl bg-success/20 p-3 text-sm"
>
{gettext("Your completion confirmation is saved.")}
</p>
<button <button
:if={ :if={
Help.helper?(@current_scope, @assignment) && Help.helper?(@current_scope, @assignment) &&
@ -1088,7 +1210,11 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
</button> </button>
<div <div
:if={Help.requester?(@current_scope, @request)} :if={
Help.requester?(@current_scope, @request) &&
@assignment.status in [:accepted, :in_progress] &&
is_nil(@assignment.handover_verified_at)
}
class="rounded-xl bg-white/10 p-4 text-center" class="rounded-xl bg-white/10 p-4 text-center"
> >
<div class="text-xs opacity-65"> <div class="text-xs opacity-65">
@ -1115,6 +1241,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
<.form <.form
:if={ :if={
Help.helper?(@current_scope, @assignment) && Help.helper?(@current_scope, @assignment) &&
@assignment.status in [:accepted, :in_progress] &&
is_nil(@assignment.handover_verified_at) is_nil(@assignment.handover_verified_at)
} }
id="handover-form" id="handover-form"

View File

@ -167,6 +167,41 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
Messaging.send_message(outsider_scope, assignment, %{"body" => "not allowed"}) Messaging.send_message(outsider_scope, assignment, %{"body" => "not allowed"})
end end
test "cancelling in-progress help closes interaction and deletes live positions", context do
{:ok, request} = Help.create_request(context.requester_scope, context.request_attrs)
{:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
{:ok, assignment} = Help.start_assignment(context.helper_scope, assignment.id)
{:ok, message} =
Messaging.send_message(context.helper_scope, assignment, %{"body" => "I have started."})
{:ok, tracking_session} = Tracking.start_session(context.helper_scope, assignment)
{:ok, _position} =
Tracking.update_position(context.helper_scope, assignment, %{
"latitude" => 50.45,
"longitude" => 30.52,
"accuracy_meters" => 8.0
})
assert {:ok, cancelled_request} =
Help.cancel_request(context.requester_scope, request.id)
assert cancelled_request.status == :cancelled
assert cancelled_request.assignment.status == :cancelled
refute Repo.get_by(Position, tracking_session_id: tracking_session.id)
assert {:error, :assignment_inactive} =
Messaging.send_message(context.helper_scope, assignment, %{
"body" => "This must not be stored."
})
assert [%{id: message_id, body: "I have started."}] =
Messaging.list_messages(context.requester_scope, assignment)
assert message_id == message.id
end
test "participants can mark only counterpart messages as read", context do test "participants can mark only counterpart messages as read", context do
outsider_scope = user_fixture(display_name: "Outsider") |> user_scope_fixture() outsider_scope = user_fixture(display_name: "Outsider") |> user_scope_fixture()
{:ok, request} = Help.create_request(context.requester_scope, context.request_attrs) {:ok, request} = Help.create_request(context.requester_scope, context.request_attrs)

View File

@ -625,6 +625,89 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
refute has_element?(view, "#message-form") refute has_element?(view, "#message-form")
end end
test "unblocking restores active match history without a page reload", %{
conn: requester_conn,
scope: requester_scope
} do
category = Catalog.seed_defaults()
helper = user_fixture(display_name: "Restored helper")
{:ok, request} = Help.create_request(requester_scope, request_attrs(category))
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
{:ok, _message} =
Messaging.send_message(user_scope_fixture(helper), assignment, %{
"body" => "Restore this message after unblocking."
})
{:ok, view, html} = live(requester_conn, ~p"/requests/#{request.id}")
assert html =~ "Restore this message after unblocking."
assert has_element?(view, "#message-form")
view
|> element("button[phx-click='block-user']")
|> render_click()
blocked_html = render(view)
assert blocked_html =~ "This interaction is blocked."
refute blocked_html =~ "Restore this message after unblocking."
refute has_element?(view, "#message-form")
view
|> element("button[phx-click='unblock-user']")
|> render_click()
restored_html = render(view)
assert restored_html =~ "Restore this message after unblocking."
assert has_element?(view, "#message-form")
end
test "cancelled in-progress help keeps history read-only and hides handover actions" do
category = Catalog.seed_defaults()
requester = user_fixture(display_name: "Cancelling requester")
helper = user_fixture(display_name: "Cancelled helper")
requester_scope = user_scope_fixture(requester)
helper_scope = user_scope_fixture(helper)
{:ok, request} = Help.create_request(requester_scope, request_attrs(category))
{:ok, assignment} = Help.accept_request(helper_scope, request.id)
{:ok, _message} =
Messaging.send_message(helper_scope, assignment, %{"body" => "Visible history."})
{:ok, requester_view, _html} =
build_conn() |> log_in_user(requester) |> live(~p"/requests/#{request.id}")
{:ok, helper_view, _html} =
build_conn() |> log_in_user(helper) |> live(~p"/requests/#{request.id}")
helper_view
|> element("button[phx-click='start']")
|> render_click()
assert render(helper_view) =~ "Help is now in progress."
requester_view
|> element("button[phx-click='cancel-request']")
|> render_click()
for view <- [requester_view, helper_view] do
html = render(view)
assert html =~ "This match was cancelled"
assert html =~ "This request was cancelled."
refute html =~ "Help is now in progress."
assert html =~ "Chat history is read-only"
assert html =~ "Visible history."
refute has_element?(view, "#message-form")
refute has_element?(view, "#handover-form")
refute has_element?(view, "#copy-handover-code")
refute has_element?(view, "button[phx-click='confirm']")
end
stop_live_view(requester_view)
stop_live_view(helper_view)
end
test "terminal request updates stop Android foreground tracking", _context do test "terminal request updates stop Android foreground tracking", _context do
category = Catalog.seed_defaults() category = Catalog.seed_defaults()
requester = user_fixture(display_name: "Tracking requester") requester = user_fixture(display_name: "Tracking requester")