fix: align realtime feedback and map privacy
This commit is contained in:
parent
f0512fab84
commit
7ce052d0a1
|
|
@ -393,15 +393,22 @@ const createAidMap = element => {
|
||||||
|
|
||||||
exactPoints.forEach(point => {
|
exactPoints.forEach(point => {
|
||||||
const popup = new maplibregl.Popup({offset: 18}).setDOMContent(state.popupContent(point))
|
const popup = new maplibregl.Popup({offset: 18}).setDOMContent(state.popupContent(point))
|
||||||
const marker = new maplibregl.Marker({color: point.exact ? "#d6573b" : "#278467"})
|
const markerColor = point.type === "live" ? "#2563eb" : point.exact ? "#d6573b" : "#278467"
|
||||||
|
const marker = new maplibregl.Marker({color: markerColor})
|
||||||
.setLngLat([point.longitude, point.latitude])
|
.setLngLat([point.longitude, point.latitude])
|
||||||
.setPopup(popup)
|
.setPopup(popup)
|
||||||
.addTo(state.map)
|
.addTo(state.map)
|
||||||
|
|
||||||
const markerElement = marker.getElement()
|
const markerElement = marker.getElement()
|
||||||
markerElement.dataset.requestId = point.id
|
markerElement.dataset.markerType = point.type || "request"
|
||||||
markerElement.addEventListener("click", () => state.selectRequest(point.id))
|
markerElement.setAttribute("aria-label", point.title || "Map marker")
|
||||||
state.markerElements.set(point.id, markerElement)
|
|
||||||
|
if (point.id) {
|
||||||
|
markerElement.dataset.requestId = point.id
|
||||||
|
markerElement.addEventListener("click", () => state.selectRequest(point.id))
|
||||||
|
state.markerElements.set(point.id, markerElement)
|
||||||
|
}
|
||||||
|
|
||||||
state.markers.push(marker)
|
state.markers.push(marker)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -591,6 +598,7 @@ export const Hooks = {
|
||||||
PushNotifications: {
|
PushNotifications: {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.button = this.el.querySelector("[data-enable-push]")
|
this.button = this.el.querySelector("[data-enable-push]")
|
||||||
|
this.buttonLabel = this.el.querySelector("[data-enable-push-label]")
|
||||||
this.status = this.el.querySelector("[data-push-status]")
|
this.status = this.el.querySelector("[data-push-status]")
|
||||||
this.native = typeof window.WhoNeedHelpAndroid?.postMessage === "function"
|
this.native = typeof window.WhoNeedHelpAndroid?.postMessage === "function"
|
||||||
|
|
||||||
|
|
@ -600,6 +608,13 @@ export const Hooks = {
|
||||||
if (this.status) this.status.textContent = message
|
if (this.status) this.status.textContent = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setButtonState = enabled => {
|
||||||
|
if (!this.buttonLabel) return
|
||||||
|
this.buttonLabel.textContent = enabled
|
||||||
|
? this.buttonLabel.dataset.enabledLabel
|
||||||
|
: this.buttonLabel.dataset.enableLabel
|
||||||
|
}
|
||||||
|
|
||||||
this.decodeApplicationServerKey = value => {
|
this.decodeApplicationServerKey = value => {
|
||||||
const padding = "=".repeat((4 - value.length % 4) % 4)
|
const padding = "=".repeat((4 - value.length % 4) % 4)
|
||||||
const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/")
|
const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/")
|
||||||
|
|
@ -701,6 +716,9 @@ export const Hooks = {
|
||||||
await subscription?.unsubscribe()
|
await subscription?.unsubscribe()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Browser push unsubscribe failed", error)
|
console.warn("Browser push unsubscribe failed", error)
|
||||||
|
} finally {
|
||||||
|
this.setButtonState(false)
|
||||||
|
this.setStatus("Push notifications are disabled on this device.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -746,6 +764,7 @@ export const Hooks = {
|
||||||
device_label: `${navigator.platform || "Browser"} · Web Push`.slice(0, 120),
|
device_label: `${navigator.platform || "Browser"} · Web Push`.slice(0, 120),
|
||||||
user_agent: navigator.userAgent.slice(0, 500)
|
user_agent: navigator.userAgent.slice(0, 500)
|
||||||
})
|
})
|
||||||
|
this.setButtonState(true)
|
||||||
this.setStatus("Push notifications are enabled on this device.")
|
this.setStatus("Push notifications are enabled on this device.")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Push notification registration failed", error)
|
console.warn("Push notification registration failed", error)
|
||||||
|
|
@ -755,11 +774,48 @@ export const Hooks = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.restoreWebState = async () => {
|
||||||
|
if (this.native) return
|
||||||
|
|
||||||
|
const publicKey = String(this.el.dataset.vapidPublicKey || "")
|
||||||
|
if (!publicKey || !("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||||
|
this.setStatus("Push notifications are unavailable in this browser.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission === "denied") {
|
||||||
|
this.setStatus("Notification permission was not granted.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready
|
||||||
|
const subscription = await registration.pushManager.getSubscription()
|
||||||
|
const deviceId = window.localStorage.getItem("wnh.push.server-device-id")
|
||||||
|
const activeServerDevice = deviceId &&
|
||||||
|
[...this.el.querySelectorAll("[data-disable-device]")]
|
||||||
|
.some(element => element.dataset.disableDevice === deviceId)
|
||||||
|
|
||||||
|
if (subscription && activeServerDevice) {
|
||||||
|
this.setButtonState(true)
|
||||||
|
this.setStatus("Push notifications are enabled on this device.")
|
||||||
|
} else if (subscription || activeServerDevice) {
|
||||||
|
this.setStatus("This browser has a push subscription. Enable it to reconnect this device.")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Push notification state could not be restored", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("wnh:native-push-token", this.nativeToken)
|
window.addEventListener("wnh:native-push-token", this.nativeToken)
|
||||||
window.addEventListener("wnh:native-push-error", this.nativeError)
|
window.addEventListener("wnh:native-push-error", this.nativeError)
|
||||||
this.el.addEventListener("click", this.disableCurrentDevice)
|
this.el.addEventListener("click", this.disableCurrentDevice)
|
||||||
this.button?.addEventListener("click", this.enable)
|
this.button?.addEventListener("click", this.enable)
|
||||||
if (this.native) this.postNative({action: "push_token_request"})
|
if (this.native) {
|
||||||
|
this.postNative({action: "push_token_request"})
|
||||||
|
} else {
|
||||||
|
this.restoreWebState()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener("wnh:native-push-token", this.nativeToken)
|
window.removeEventListener("wnh:native-push-token", this.nativeToken)
|
||||||
|
|
@ -867,15 +923,22 @@ export const Hooks = {
|
||||||
|
|
||||||
LocalTime: {
|
LocalTime: {
|
||||||
mounted() {
|
mounted() {
|
||||||
const value = this.el.dateTime || this.el.dataset.datetime
|
this.format = () => {
|
||||||
const date = new Date(value)
|
const value = this.el.dateTime || this.el.dataset.datetime
|
||||||
if (Number.isNaN(date.getTime())) return
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return
|
||||||
|
|
||||||
this.el.textContent = new Intl.DateTimeFormat(undefined, {
|
this.el.textContent = new Intl.DateTimeFormat(undefined, {
|
||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "short"
|
timeStyle: "short"
|
||||||
}).format(date)
|
}).format(date)
|
||||||
this.el.title = date.toISOString()
|
this.el.title = date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.format()
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
this.format()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -620,11 +620,18 @@ defmodule WhoNeedHelp.Help do
|
||||||
request.assignment.status in [:accepted, :in_progress] and
|
request.assignment.status in [:accepted, :in_progress] and
|
||||||
not Trust.blocked_between?(scope.user.id, request.requester_id)
|
not Trust.blocked_between?(scope.user.id, request.requester_id)
|
||||||
|
|
||||||
if owner or
|
cond do
|
||||||
(matched_participant and request.location_visibility == :exact_for_active_match) do
|
owner and request.location_visibility == :approximate_public ->
|
||||||
exact_request_coordinates(request)
|
HelpRequest.public_coordinates(request)
|
||||||
else
|
|
||||||
HelpRequest.public_coordinates(request)
|
owner ->
|
||||||
|
exact_request_coordinates(request)
|
||||||
|
|
||||||
|
matched_participant and request.location_visibility == :exact_for_active_match ->
|
||||||
|
exact_request_coordinates(request)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
HelpRequest.public_coordinates(request)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,13 @@ defmodule WhoNeedHelpWeb.NotificationLive do
|
||||||
class="btn btn-success w-full"
|
class="btn btn-success w-full"
|
||||||
>
|
>
|
||||||
<.icon name="hero-bell-alert" class="size-5" />
|
<.icon name="hero-bell-alert" class="size-5" />
|
||||||
{gettext("Enable push on this device")}
|
<span
|
||||||
|
data-enable-push-label
|
||||||
|
data-enable-label={gettext("Enable push on this device")}
|
||||||
|
data-enabled-label={gettext("Push enabled on this device")}
|
||||||
|
>
|
||||||
|
{gettext("Enable push on this device")}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<p data-push-status role="status" aria-live="polite" class="mt-2 text-xs">
|
<p data-push-status role="status" aria-live="polite" class="mt-2 text-xs">
|
||||||
<%= if @web_push_public_key do %>
|
<%= if @web_push_public_key do %>
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> maybe_put_status_transition_flash(
|
|> maybe_put_status_transition_flash(
|
||||||
previous_status,
|
previous_status,
|
||||||
request.status,
|
request.status,
|
||||||
previous_assignment
|
previous_assignment,
|
||||||
|
request.assignment
|
||||||
)
|
)
|
||||||
|
|
||||||
{:noreply, sync_tracking_after_request_update(socket, request)}
|
{:noreply, sync_tracking_after_request_update(socket, request)}
|
||||||
|
|
@ -142,10 +143,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
case Help.accept_request(socket.assigns.current_scope, socket.assigns.request.id) do
|
case Help.accept_request(socket.assigns.current_scope, socket.assigns.request.id) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
put_flash(socket, :info, gettext("You are matched. Use chat to coordinate safely."))}
|
put_action_flash(
|
||||||
|
socket,
|
||||||
|
:info,
|
||||||
|
gettext("You accepted this request. Use private chat to coordinate safely.")
|
||||||
|
)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -154,10 +159,10 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|
|
||||||
case Help.cancel_request(socket.assigns.current_scope, socket.assigns.request.id, attrs) do
|
case Help.cancel_request(socket.assigns.current_scope, socket.assigns.request.id, attrs) do
|
||||||
{:ok, _request} ->
|
{:ok, _request} ->
|
||||||
{:noreply, put_flash(socket, :info, gettext("Request cancelled."))}
|
{:noreply, put_action_flash(socket, :info, gettext("Request cancelled."))}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -185,24 +190,24 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
) do
|
) do
|
||||||
{:ok, _assignment} ->
|
{:ok, _assignment} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
put_flash(
|
put_action_flash(
|
||||||
socket,
|
socket,
|
||||||
:info,
|
:info,
|
||||||
gettext("You left this match. The request is open for another helper if time remains.")
|
gettext("You left this match. The request is open for another helper if time remains.")
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("verify-code", %{"handover" => %{"code" => code}}, socket) do
|
def handle_event("verify-code", %{"handover" => %{"code" => code}}, socket) do
|
||||||
case Help.verify_handover(socket.assigns.current_scope, socket.assigns.assignment.id, code) do
|
case Help.verify_handover(socket.assigns.current_scope, socket.assigns.assignment.id, code) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
{:noreply, put_flash(socket, :info, gettext("Handover code verified."))}
|
{:noreply, put_action_flash(socket, :info, gettext("Handover code verified."))}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -215,7 +220,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> push_event("reset-message-form", %{id: "message-form"})}
|
|> push_event("reset-message-form", %{id: "message-form"})}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -276,7 +281,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> maybe_start_native_tracking()}
|
|> maybe_start_native_tracking()}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -294,7 +299,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
{:error, _reason} -> socket
|
{:error, _reason} -> socket
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, put_flash(socket, :error, gettext("The browser could not share your location."))}
|
{:noreply,
|
||||||
|
put_action_flash(socket, :error, gettext("The browser could not share your location."))}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("stop-tracking", _, socket) do
|
def handle_event("stop-tracking", _, socket) do
|
||||||
|
|
@ -303,7 +309,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
{:noreply, stopped_socket}
|
{:noreply, stopped_socket}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -311,14 +317,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
case Trust.submit_review(socket.assigns.current_scope, socket.assigns.assignment, params) do
|
case Trust.submit_review(socket.assigns.current_scope, socket.assigns.assignment, params) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
put_flash(
|
put_action_flash(
|
||||||
socket,
|
socket,
|
||||||
:info,
|
:info,
|
||||||
gettext("Review saved. It appears after both participants review.")
|
gettext("Review saved. It appears after both participants review.")
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -342,11 +348,11 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
socket
|
socket
|
||||||
|> assign(:report_form, report_form())
|
|> assign(:report_form, report_form())
|
||||||
|> assign(:report_message_id, nil)
|
|> assign(:report_message_id, nil)
|
||||||
|> put_flash(:info, gettext("Report sent to moderators."))
|
|> put_action_flash(:info, gettext("Report sent to moderators."))
|
||||||
|> push_event("reset-message-form", %{id: "request-report-form"})}
|
|> push_event("reset-message-form", %{id: "request-report-form"})}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -360,7 +366,11 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
{:noreply, assign(socket, :report_message_id, message.id)}
|
{:noreply, assign(socket, :report_message_id, message.id)}
|
||||||
else
|
else
|
||||||
{:noreply,
|
{:noreply,
|
||||||
put_flash(socket, :error, gettext("That message cannot be reported from this view."))}
|
put_action_flash(
|
||||||
|
socket,
|
||||||
|
:error,
|
||||||
|
gettext("That message cannot be reported from this view.")
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -384,17 +394,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> assign(:positions, %{})
|
|> assign(:positions, %{})
|
||||||
|> assign(:tracking_active, false)
|
|> assign(:tracking_active, false)
|
||||||
|> assign(:tracking_session_id, nil)
|
|> assign(:tracking_session_id, nil)
|
||||||
|> assign(
|
|> refresh_request_coordinates()
|
||||||
:markers,
|
|> put_action_flash(
|
||||||
Jason.encode!(request_markers(socket.assigns.current_scope, socket.assigns.request))
|
|
||||||
)
|
|
||||||
|> put_flash(
|
|
||||||
:info,
|
:info,
|
||||||
gettext("User blocked. Their requests and new messages are hidden.")
|
gettext("User blocked. Their requests and new messages are hidden.")
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -412,13 +419,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
socket
|
socket
|
||||||
|> assign(:blocked_by_current, false)
|
|> assign(:blocked_by_current, false)
|
||||||
|> assign(:interaction_blocked, interaction_blocked)
|
|> assign(:interaction_blocked, interaction_blocked)
|
||||||
|
|> refresh_request_coordinates()
|
||||||
|> restore_private_interaction(interaction_blocked)
|
|> restore_private_interaction(interaction_blocked)
|
||||||
|> put_flash(:info, gettext("User unblocked."))
|
|> put_action_flash(:info, gettext("User unblocked."))
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:noreply, put_flash(socket, :error, message(reason))}
|
{:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -488,57 +496,121 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|
|
||||||
defp transition(socket, fun, success) do
|
defp transition(socket, fun, success) do
|
||||||
case fun.(socket.assigns.current_scope, socket.assigns.assignment.id) do
|
case fun.(socket.assigns.current_scope, socket.assigns.assignment.id) do
|
||||||
{:ok, _} -> {:noreply, put_flash(socket, :info, success)}
|
{:ok, _} -> {:noreply, put_action_flash(socket, :info, success)}
|
||||||
{:error, reason} -> {:noreply, put_flash(socket, :error, message(reason))}
|
{:error, reason} -> {:noreply, put_action_flash(socket, :error, message(reason))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, status, status, _previous_assignment), do: socket
|
defp put_action_flash(socket, kind, message) do
|
||||||
|
socket
|
||||||
|
|> clear_flash()
|
||||||
|
|> put_flash(kind, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_status_transition_flash(
|
||||||
|
socket,
|
||||||
|
status,
|
||||||
|
status,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
),
|
||||||
|
do: socket
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(
|
defp maybe_put_status_transition_flash(
|
||||||
%{assigns: %{current_scope: %{user: %{id: user_id}}}} = socket,
|
%{assigns: %{current_scope: %{user: %{id: user_id}}}} = socket,
|
||||||
previous,
|
previous,
|
||||||
:open,
|
:open,
|
||||||
%{helper_id: user_id}
|
%{helper_id: user_id},
|
||||||
|
_assignment
|
||||||
)
|
)
|
||||||
when previous in [:matched, :in_progress] do
|
when previous in [:matched, :in_progress] do
|
||||||
put_flash(
|
put_action_flash(
|
||||||
socket,
|
socket,
|
||||||
:info,
|
:info,
|
||||||
gettext("You left this match. The request is open for another helper if time remains.")
|
gettext("You left this match. The request is open for another helper if time remains.")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, previous, :open, _previous_assignment)
|
defp maybe_put_status_transition_flash(
|
||||||
|
socket,
|
||||||
|
previous,
|
||||||
|
:open,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
)
|
||||||
when previous in [:matched, :in_progress] do
|
when previous in [:matched, :in_progress] do
|
||||||
put_flash(
|
put_action_flash(
|
||||||
socket,
|
socket,
|
||||||
:info,
|
:info,
|
||||||
gettext("The previous helper left. This request is open for a new helper again.")
|
gettext("The previous helper left. This request is open for a new helper again.")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, _previous, :matched, _previous_assignment) do
|
defp maybe_put_status_transition_flash(
|
||||||
put_flash(
|
%{assigns: %{current_scope: %{user: %{id: user_id}}}} = socket,
|
||||||
|
_previous,
|
||||||
|
:matched,
|
||||||
|
_previous_assignment,
|
||||||
|
%{helper_id: user_id}
|
||||||
|
) do
|
||||||
|
put_action_flash(
|
||||||
|
socket,
|
||||||
|
:info,
|
||||||
|
gettext("You accepted this request. Use private chat to coordinate safely.")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_status_transition_flash(
|
||||||
|
socket,
|
||||||
|
_previous,
|
||||||
|
:matched,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
) do
|
||||||
|
put_action_flash(
|
||||||
socket,
|
socket,
|
||||||
:info,
|
:info,
|
||||||
gettext("A volunteer accepted this request. Use private chat to coordinate safely.")
|
gettext("A volunteer accepted this request. Use private chat to coordinate safely.")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, _previous, :in_progress, _previous_assignment) do
|
defp maybe_put_status_transition_flash(
|
||||||
put_flash(socket, :info, gettext("Help is now in progress."))
|
socket,
|
||||||
|
_previous,
|
||||||
|
:in_progress,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
) do
|
||||||
|
put_action_flash(socket, :info, gettext("Help is now in progress."))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, _previous, :completed, _previous_assignment) do
|
defp maybe_put_status_transition_flash(
|
||||||
put_flash(socket, :info, gettext("Help is complete."))
|
socket,
|
||||||
|
_previous,
|
||||||
|
:completed,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
) do
|
||||||
|
put_action_flash(socket, :info, gettext("Help is complete."))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, _previous, :cancelled, _previous_assignment) do
|
defp maybe_put_status_transition_flash(
|
||||||
put_flash(socket, :info, gettext("This request was cancelled."))
|
socket,
|
||||||
|
_previous,
|
||||||
|
:cancelled,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
) do
|
||||||
|
put_action_flash(socket, :info, gettext("This request was cancelled."))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put_status_transition_flash(socket, _previous, _status, _previous_assignment),
|
defp maybe_put_status_transition_flash(
|
||||||
|
socket,
|
||||||
|
_previous,
|
||||||
|
_status,
|
||||||
|
_previous_assignment,
|
||||||
|
_assignment
|
||||||
|
),
|
||||||
do: socket
|
do: socket
|
||||||
|
|
||||||
defp restore_private_interaction(socket, true), do: socket
|
defp restore_private_interaction(socket, true), do: socket
|
||||||
|
|
@ -593,6 +665,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|> Trust.reputations()
|
|> Trust.reputations()
|
||||||
|
|
||||||
|
request_coordinates = Help.request_coordinates(socket.assigns.current_scope, request)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, request.title)
|
|> assign(:page_title, request.title)
|
||||||
|> assign(:request, request)
|
|> assign(:request, request)
|
||||||
|
|
@ -625,6 +699,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> assign(:message_window_limit, max(length(messages_page.entries), @initial_message_window))
|
|> assign(:message_window_limit, max(length(messages_page.entries), @initial_message_window))
|
||||||
|> assign(:report_message_id, nil)
|
|> assign(:report_message_id, nil)
|
||||||
|> assign(:positions, positions)
|
|> assign(:positions, positions)
|
||||||
|
|> assign(:request_coordinates, request_coordinates)
|
||||||
|> assign(:tracking_active, tracking_active)
|
|> assign(:tracking_active, tracking_active)
|
||||||
|> assign(:message_form, to_form(%{"body" => ""}, as: :message))
|
|> assign(:message_form, to_form(%{"body" => ""}, as: :message))
|
||||||
|> assign(:handover_form, to_form(%{"code" => ""}, as: :handover))
|
|> assign(:handover_form, to_form(%{"code" => ""}, as: :handover))
|
||||||
|
|
@ -647,7 +722,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|> assign(
|
|> assign(
|
||||||
:markers,
|
:markers,
|
||||||
Jason.encode!(
|
Jason.encode!(
|
||||||
request_markers(socket.assigns.current_scope, request) ++ position_markers(positions)
|
request_markers(request, request_coordinates) ++
|
||||||
|
position_markers(positions, request, assignment)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -711,14 +787,23 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|
|
||||||
defp assign_positions(socket, positions) do
|
defp assign_positions(socket, positions) do
|
||||||
markers =
|
markers =
|
||||||
request_markers(socket.assigns.current_scope, socket.assigns.request) ++
|
request_markers(socket.assigns.request, socket.assigns.request_coordinates) ++
|
||||||
position_markers(positions)
|
position_markers(positions, socket.assigns.request, socket.assigns.assignment)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(:positions, positions)
|
|> assign(:positions, positions)
|
||||||
|> assign(:markers, Jason.encode!(markers))
|
|> assign(:markers, Jason.encode!(markers))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp refresh_request_coordinates(socket) do
|
||||||
|
request_coordinates =
|
||||||
|
Help.request_coordinates(socket.assigns.current_scope, socket.assigns.request)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:request_coordinates, request_coordinates)
|
||||||
|
|> assign_positions(socket.assigns.positions)
|
||||||
|
end
|
||||||
|
|
||||||
defp assign_assignment_evidence(%{assigns: %{assignment: nil}} = socket, _evidence),
|
defp assign_assignment_evidence(%{assigns: %{assignment: nil}} = socket, _evidence),
|
||||||
do: socket
|
do: socket
|
||||||
|
|
||||||
|
|
@ -775,8 +860,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
|
|
||||||
defp localized(_labels, _locale), do: nil
|
defp localized(_labels, _locale), do: nil
|
||||||
|
|
||||||
defp request_markers(scope, request) do
|
defp request_markers(request, request_coordinates) do
|
||||||
case Help.request_coordinates(scope, request) do
|
case request_coordinates do
|
||||||
nil ->
|
nil ->
|
||||||
[]
|
[]
|
||||||
|
|
||||||
|
|
@ -790,13 +875,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp position_markers(positions) do
|
defp position_markers(positions, request, assignment) do
|
||||||
Enum.map(positions, fn {_user_id, point} ->
|
Enum.map(positions, fn {user_id, point} ->
|
||||||
%{
|
%{
|
||||||
latitude: point.latitude,
|
latitude: point.latitude,
|
||||||
longitude: point.longitude,
|
longitude: point.longitude,
|
||||||
exact: true,
|
exact: true,
|
||||||
title: gettext("Shared location"),
|
type: "live",
|
||||||
|
title: live_location_title(user_id, request, assignment),
|
||||||
location:
|
location:
|
||||||
gettext(
|
gettext(
|
||||||
"Last update: %{time}",
|
"Last update: %{time}",
|
||||||
|
|
@ -806,6 +892,14 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp live_location_title(user_id, request, assignment) do
|
||||||
|
cond do
|
||||||
|
user_id == request.requester_id -> gettext("Requester's live location")
|
||||||
|
assignment && user_id == assignment.helper_id -> gettext("Helper's live location")
|
||||||
|
true -> gettext("Shared live location")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp message(:own_request), do: gettext("You cannot accept your own request.")
|
defp message(:own_request), do: gettext("You cannot accept your own request.")
|
||||||
defp message(:not_open), do: gettext("Another helper already accepted this request.")
|
defp message(:not_open), do: gettext("Another helper already accepted this request.")
|
||||||
defp message(:invalid_code), do: gettext("That handover code is not correct.")
|
defp message(:invalid_code), do: gettext("That handover code is not correct.")
|
||||||
|
|
@ -1154,17 +1248,50 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="space-y-5">
|
<aside class="space-y-5">
|
||||||
<div
|
<div class="space-y-2">
|
||||||
id="show-map"
|
<div
|
||||||
phx-hook="AidMap"
|
id="show-map"
|
||||||
phx-update="ignore"
|
phx-hook="AidMap"
|
||||||
data-markers={@markers}
|
phx-update="ignore"
|
||||||
data-map-unavailable-label={
|
data-markers={@markers}
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
data-map-unavailable-label={
|
||||||
}
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
data-map-retry-label={gettext("Try map again")}
|
}
|
||||||
class="aid-map"
|
data-map-retry-label={gettext("Try map again")}
|
||||||
/>
|
class="aid-map"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:if={
|
||||||
|
@request_coordinates || map_size(@positions) > 0
|
||||||
|
}
|
||||||
|
class="flex flex-wrap gap-x-4 gap-y-1 px-1 text-xs text-base-content/65"
|
||||||
|
aria-label={gettext("Map legend")}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:if={@request_coordinates}
|
||||||
|
class="inline-flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={[
|
||||||
|
"size-2.5 rounded-full",
|
||||||
|
@request.location_visibility == :approximate_public && "bg-success",
|
||||||
|
@request.location_visibility != :approximate_public && "bg-error"
|
||||||
|
]}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{if @request.location_visibility == :approximate_public,
|
||||||
|
do: gettext("Request area"),
|
||||||
|
else: gettext("Request point")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span :if={map_size(@positions) > 0} class="inline-flex items-center gap-1.5">
|
||||||
|
<span class="size-2.5 rounded-full bg-blue-600" aria-hidden="true"></span>
|
||||||
|
<span>{gettext("Live shared location")}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-3xl bg-neutral p-6 text-neutral-content">
|
<div class="rounded-3xl bg-neutral p-6 text-neutral-content">
|
||||||
<h2 class="text-lg font-bold">{gettext("Action center")}</h2>
|
<h2 class="text-lg font-bold">{gettext("Action center")}</h2>
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,13 @@ defmodule WhoNeedHelp.TrustSafetyTest do
|
||||||
radius_meters: 500
|
radius_meters: 500
|
||||||
} = WhoNeedHelp.Help.HelpRequest.public_coordinates(request)
|
} = WhoNeedHelp.Help.HelpRequest.public_coordinates(request)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
latitude: 50.4501,
|
||||||
|
longitude: 30.5234,
|
||||||
|
exact: false,
|
||||||
|
radius_meters: 500
|
||||||
|
} = Help.request_coordinates(context.requester_scope, request)
|
||||||
|
|
||||||
invalid_attrs = Map.put(context.attrs, "location_radius_meters", "5000")
|
invalid_attrs = Map.put(context.attrs, "location_radius_meters", "5000")
|
||||||
assert {:error, changeset} = Help.create_request(context.requester_scope, invalid_attrs)
|
assert {:error, changeset} = Help.create_request(context.requester_scope, invalid_attrs)
|
||||||
assert "is invalid" in errors_on(changeset).location_radius_meters
|
assert "is invalid" in errors_on(changeset).location_radius_meters
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,24 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
idempotency_key: "live-notification:#{Ecto.UUID.generate()}"
|
idempotency_key: "live-notification:#{Ecto.UUID.generate()}"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
{:ok, push_device} =
|
||||||
|
Notifications.register_device(scope, %{
|
||||||
|
"platform" => "web",
|
||||||
|
"provider" => "web_push",
|
||||||
|
"token" => "https://push.example/subscriptions/#{Ecto.UUID.generate()}",
|
||||||
|
"installation_id" => Ecto.UUID.generate(),
|
||||||
|
"p256dh" => "test-public-key",
|
||||||
|
"auth_secret" => "test-auth-secret",
|
||||||
|
"device_label" => "Test browser"
|
||||||
|
})
|
||||||
|
|
||||||
{:ok, view, html} = live(conn, ~p"/notifications")
|
{:ok, view, html} = live(conn, ~p"/notifications")
|
||||||
assert html =~ "Notifications"
|
assert html =~ "Notifications"
|
||||||
assert html =~ "Support request updated"
|
assert html =~ "Support request updated"
|
||||||
assert html =~ "1 unread"
|
assert html =~ "1 unread"
|
||||||
|
assert html =~ ~s(data-enable-label="Enable push on this device")
|
||||||
|
assert html =~ ~s(data-enabled-label="Push enabled on this device")
|
||||||
|
assert has_element?(view, "[data-disable-device='#{push_device.id}']")
|
||||||
|
|
||||||
view |> element("button[phx-click='mark-all-read']") |> render_click()
|
view |> element("button[phx-click='mark-all-read']") |> render_click()
|
||||||
assert render(view) =~ "0 unread"
|
assert render(view) =~ "0 unread"
|
||||||
|
|
@ -704,6 +718,12 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
|> element("button[phx-click='accept']")
|
|> element("button[phx-click='accept']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
assert render(helper_view) =~
|
||||||
|
"You accepted this request. Use private chat to coordinate safely."
|
||||||
|
|
||||||
|
assert render(requester_view) =~
|
||||||
|
"A volunteer accepted this request. Use private chat to coordinate safely."
|
||||||
|
|
||||||
assert render(requester_view) =~ "Private match chat"
|
assert render(requester_view) =~ "Private match chat"
|
||||||
assert render(helper_view) =~ "Private match chat"
|
assert render(helper_view) =~ "Private match chat"
|
||||||
assert has_element?(helper_view, "#message-form")
|
assert has_element?(helper_view, "#message-form")
|
||||||
|
|
@ -765,10 +785,15 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert render(requester_view) =~ "50.452"
|
requester_html = render(requester_view)
|
||||||
assert render(requester_view) =~ "30.526"
|
assert requester_html =~ "50.452"
|
||||||
assert render(requester_view) =~ "helper movement supported"
|
assert requester_html =~ "30.526"
|
||||||
refute render(requester_view) =~ "no movement evidence"
|
assert requester_html =~ "helper movement supported"
|
||||||
|
assert requester_html =~ "Live shared location"
|
||||||
|
refute requester_html =~ "no movement evidence"
|
||||||
|
|
||||||
|
assert has_element?(requester_view, "#show-map[data-markers*='live']")
|
||||||
|
assert has_element?(requester_view, "#show-map[data-markers*='Helper']")
|
||||||
|
|
||||||
send(requester_view.pid, {:tracking_stopped, helper.id})
|
send(requester_view.pid, {:tracking_stopped, helper.id})
|
||||||
|
|
||||||
|
|
@ -1069,11 +1094,18 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
code = Help.handover_code(request.id)
|
code = Help.handover_code(request.id)
|
||||||
pasted_code = String.slice(code, 0, 3) <> " " <> String.slice(code, 3, 3)
|
pasted_code = String.slice(code, 0, 3) <> " " <> String.slice(code, 3, 3)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> form("#handover-form", handover: %{code: "000 000"})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert render(view) =~ "That handover code is not correct."
|
||||||
|
|
||||||
view
|
view
|
||||||
|> form("#handover-form", handover: %{code: pasted_code})
|
|> form("#handover-form", handover: %{code: pasted_code})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert render(view) =~ "Handover code verified."
|
assert render(view) =~ "Handover code verified."
|
||||||
|
refute render(view) =~ "That handover code is not correct."
|
||||||
refute has_element?(view, "#handover-form")
|
refute has_element?(view, "#handover-form")
|
||||||
assert Help.get_request!(request.id).assignment.handover_verified_at
|
assert Help.get_request!(request.id).assignment.handover_verified_at
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user