From ebaaeaf517ae8729cb6bd26fb378c4026c3f6071 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Tue, 28 Jul 2026 18:11:12 +0300 Subject: [PATCH] Add persistent million-row discovery scale profile --- assets/js/hooks.js | 5 +- compose.cpu-replay.yaml | 26 +- compose.scale.yaml | 12 + docs/performance.md | 146 ++++- lib/mix/tasks/wnh.db_scale_benchmark.ex | 93 ++-- lib/mix/tasks/wnh.scale_fixtures.ex | 519 ++++++++++++++++++ lib/who_need_help/activities.ex | 31 +- lib/who_need_help/application.ex | 4 + lib/who_need_help/discovery_query_cache.ex | 123 +++++ lib/who_need_help/help.ex | 98 +++- lib/who_need_help/help/discovery_viewport.ex | 41 ++ .../live/activity_live/index.ex | 463 ++++++++++------ .../live/request_live/index.ex | 160 ++++-- load/k6/scale-readonly.js | 125 +++++ priv/gettext/default.pot | 434 ++++++++------- priv/gettext/en/LC_MESSAGES/default.po | 434 ++++++++------- priv/gettext/ru/LC_MESSAGES/default.po | 434 ++++++++------- priv/gettext/uk/LC_MESSAGES/default.po | 434 ++++++++------- scripts/load-cycle.sh | 23 + scripts/load-run.sh | 11 + scripts/load-stack-up.sh | 51 +- scripts/quality.sh | 1 + scripts/scale-common.sh | 62 +++ scripts/scale-readonly-load.sh | 161 ++++++ scripts/scale-seed.sh | 58 ++ scripts/scale-stack-stop.sh | 10 + scripts/scale-stack-up.sh | 50 ++ scripts/scale-verify.sh | 59 ++ scripts/verify-beam-runtime.sh | 13 +- test/who_need_help/activities_test.exs | 5 +- .../discovery_query_cache_test.exs | 36 ++ .../help/discovery_viewport_test.exs | 40 ++ test/who_need_help/mutual_aid_flow_test.exs | 8 +- .../live/mutual_aid_live_test.exs | 90 ++- 34 files changed, 3149 insertions(+), 1111 deletions(-) create mode 100644 compose.scale.yaml create mode 100644 lib/mix/tasks/wnh.scale_fixtures.ex create mode 100644 lib/who_need_help/discovery_query_cache.ex create mode 100644 load/k6/scale-readonly.js create mode 100755 scripts/scale-common.sh create mode 100755 scripts/scale-readonly-load.sh create mode 100755 scripts/scale-seed.sh create mode 100755 scripts/scale-stack-stop.sh create mode 100755 scripts/scale-stack-up.sh create mode 100755 scripts/scale-verify.sh create mode 100644 test/who_need_help/discovery_query_cache_test.exs diff --git a/assets/js/hooks.js b/assets/js/hooks.js index 384cfbc..35e3a46 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -1695,11 +1695,12 @@ export const Hooks = { RequestDiscovery: { mounted() { - this.storageKey = "wnh:request-discovery-view" + this.storageKey = this.el.dataset.storageKey || "wnh:request-discovery-view" + this.mapTarget = this.el.dataset.mapTarget || "#request-map" this.allowedViews = ["list", "split", "map"] this.resizeMap = () => { - document.querySelector("#request-map")?._aidMap?.resize() + document.querySelector(this.mapTarget)?._aidMap?.resize() } this.persistView = () => { diff --git a/compose.cpu-replay.yaml b/compose.cpu-replay.yaml index 6c60c2c..47744ac 100644 --- a/compose.cpu-replay.yaml +++ b/compose.cpu-replay.yaml @@ -1,14 +1,32 @@ -# Optional measurement-only override. It constrains CPU visibility for each -# long-running BEAM container so scheduler-count and memory behavior can be -# reproduced on a many-core development host. Values are experiment inputs, -# not production recommendations or minimum requirements. +# Optional measurement-only override. Every service is pinned to the same +# explicit host CPU set so the isolated stack can reproduce contention on a +# small CPU budget without constraining the external load generator. The BEAM +# services additionally use explicit scheduler counts. Values are experiment +# inputs, not production recommendations or minimum requirements. services: + docker-api-proxy: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} + + proxy: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} + + db: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} + + mailpit: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} + + migrate: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} + web: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} cpus: ${CPU_REPLAY_WEB_CPUS:?Set CPU_REPLAY_WEB_CPUS for this experiment} environment: ERL_ZFLAGS: "+Q ${ERLANG_PORT_LIMIT:-65536} +S ${CPU_REPLAY_WEB_SCHEDULERS:?Set CPU_REPLAY_WEB_SCHEDULERS}:${CPU_REPLAY_WEB_SCHEDULERS:?Set CPU_REPLAY_WEB_SCHEDULERS}" worker: + cpuset: ${CPU_REPLAY_CPUSET:?Set CPU_REPLAY_CPUSET for this experiment} cpus: ${CPU_REPLAY_WORKER_CPUS:?Set CPU_REPLAY_WORKER_CPUS for this experiment} environment: ERL_ZFLAGS: "+Q ${ERLANG_PORT_LIMIT:-65536} +S ${CPU_REPLAY_WORKER_SCHEDULERS:?Set CPU_REPLAY_WORKER_SCHEDULERS}:${CPU_REPLAY_WORKER_SCHEDULERS:?Set CPU_REPLAY_WORKER_SCHEDULERS}" diff --git a/compose.scale.yaml b/compose.scale.yaml new file mode 100644 index 0000000..6321ec0 --- /dev/null +++ b/compose.scale.yaml @@ -0,0 +1,12 @@ +services: + proxy: + ports: + - "${SCALE_HTTPS_BIND_ADDRESS:-127.0.0.1}:${SCALE_HTTPS_PORT:-4121}:443" + + web: + labels: + - traefik.http.routers.${TRAEFIK_APP_NAME}-tls.rule=${TRAEFIK_ROUTER_RULE} + - traefik.http.routers.${TRAEFIK_APP_NAME}-tls.entrypoints=websecure + - traefik.http.routers.${TRAEFIK_APP_NAME}-tls.service=${TRAEFIK_APP_NAME} + - traefik.http.routers.${TRAEFIK_APP_NAME}-tls.middlewares=${TRAEFIK_APP_NAME}-forwarded,${TRAEFIK_APP_NAME}-retry + - traefik.http.routers.${TRAEFIK_APP_NAME}-tls.tls=true diff --git a/docs/performance.md b/docs/performance.md index dbe48a6..aa1dba8 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -193,21 +193,43 @@ startup and rolling-verification scripts check the effective value in every running web and worker VM. On a host with more CPUs than the intended server, the optional -`compose.cpu-replay.yaml` override can constrain the CPUs visible to each -long-running BEAM container: +`compose.cpu-replay.yaml` override can pin the application, worker, database, +proxy, and supporting services to the same explicit host CPU set. The external +load generator remains outside that set: ```sh +CPU_REPLAY_CPUSET=0 \ CPU_REPLAY_WEB_CPUS=1 CPU_REPLAY_WORKER_CPUS=1 \ CPU_REPLAY_WEB_SCHEDULERS=1 CPU_REPLAY_WORKER_SCHEDULERS=1 \ docker compose -f compose.yaml -f compose.cpu-replay.yaml up -d --wait ``` -All four values are explicit experiment inputs. The override does not set a memory -limit or represent a whole-server CPU budget. The scheduler inputs make total -and online BEAM scheduler counts match the intended replay instead of leaving -offline scheduler threads sized from the development host. None of these -inputs is a production sizing recommendation. Running ordinary -`docker compose up` without the override removes the replay constraints. +All five values are explicit experiment inputs. Pinning every stack service to +the same one-element CPU set reproduces CPU contention, but does not reproduce +an unknown server's memory, storage, network, kernel, or neighbouring +workloads. The scheduler inputs make total and online BEAM scheduler counts +match the intended replay instead of leaving offline scheduler threads sized +from the development host. None of these inputs is a production sizing +recommendation. Running ordinary `docker compose up` without the override +removes the replay constraints. + +The isolated lifecycle accepts the same override without touching the ordinary +Compose project. The explicit VU and replica values below are experiment +inputs: + +```sh +WNH_LOAD_CPU_REPLAY=true \ +CPU_REPLAY_CPUSET=0 \ +CPU_REPLAY_WEB_CPUS=1 CPU_REPLAY_WORKER_CPUS=1 \ +CPU_REPLAY_WEB_SCHEDULERS=1 CPU_REPLAY_WORKER_SCHEDULERS=1 \ +WNH_LOAD_WEB_REPLICAS_OVERRIDE=1 \ +WNH_LOAD_WORKER_REPLICAS_OVERRIDE=1 \ +WNH_LOAD_HTTP_VUS_OVERRIDE=5 \ +WNH_LOAD_WS_VUS_OVERRIDE=5 \ +WNH_LOAD_AUTH_VUS_OVERRIDE=1 \ +LOAD_DURATION_OVERRIDE=30s \ + ./scripts/load-cycle.sh one-cpu-baseline load +``` After the Helm rollout, the unchanged kind control-plane container measured 1.796 GiB, a reduction of 7.952 GiB (81.6%). Its four application pod cgroups @@ -726,6 +748,114 @@ The stop script reads the actual `LOAD_PROJECT`, refuses the staging project name, and does not delete the volume. Volume deletion is intentionally not automated. +## Persistent million-row discovery profile + +The write-capable load lifecycle above intentionally cleans its fixtures. For +repeatable map, clustering, pagination, and responsive-UI work at high +cardinality, a second profile keeps its synthetic database: + +```sh +./scripts/scale-stack-up.sh +./scripts/scale-seed.sh +./scripts/scale-verify.sh +``` + +The profile is pinned to Compose project `who_need_help_scale`, database +`who_need_help_scale`, and named volume +`who_need_help_scale_postgres_data`. Its generated credentials live only in +ignored `output/runtime/scale.env`. Fixture mutation additionally requires the +exact database name and `WNH_SCALE_FIXTURE_CONFIRM=persistent-local-scale`. +The seed task refuses a partially populated database instead of mixing data +sets. `scale-stack-stop.sh` stops only this Compose project and deliberately +preserves the named volume. + +The canonical local fixture contains 1,000,000 requests, 1,000,000 activities, +1,000 synthetic request owners, and one scale viewer. Records are distributed +across 20 cities and include varied categories, states, urgency, hidden +locations, exact public points, and privacy radii. The recorded database size +was 1,237,898,943 bytes: 709,197,824 bytes for requests and 505,962,496 bytes +for activities including their indexes and TOAST data. + +Every scale service is pinned to CPU `0` in this experiment. Three warm +application-level samples recorded the following medians: + +| Viewport | Request list / map | Activity list / map | Map rows / represented records | +| --- | ---: | ---: | ---: | +| Kyiv | 171.139 / 297.860 ms | 265.442 / 700.397 ms | 1 / 49,485 | +| Europe | 23.072 / 2,302.602 ms | 1,544.453 / 4,130.415 ms | 8 / 395,878 | +| World | 56.182 / 3,975.605 ms | 3,910.191 / 7,416.714 ms | 10 / 791,753 | + +Server clustering kept each measured map payload between 135 and 1,463 bytes +instead of returning hundreds of thousands of points. The broad low-zoom map +queries remain the measured optimization target: clustering bounds the +network/browser payload, but PostgreSQL still reads and groups the matching +records. + +A headed Chrome check used the same persistent profile. It verified 24-card +initial pages, duplicate-free 48-card pagination, List/Map/Together layouts, +390-by-844 mobile rendering, cluster zoom, and request/activity switching. +That check exposed a functional defect: grid-cell coordinates could pan to an +empty area after selecting a cluster. Cluster coordinates now use the average +of the public points in that group while the snapped cell remains only the +grouping and stable-ID key. The full suite then exposed a second privacy +regression: an activity close to a narrow viewport could pass the expanded +index candidate envelope even when its rounded public point was outside that +viewport. The query now retains the indexed candidate filter and also requires +an exact intersection against the rounded public point. The final headed check +confirmed cluster zoom, an explicit `Search this area` refresh, 390-by-844 +mobile rendering without horizontal overflow, and no application or map +console errors. The only console warning was the expected inability to +register a service worker under the local self-signed TLS certificate. + +The first authenticated HTML response now renders an honest loading shell and +starts discovery only after LiveView connects. The discovery runs +asynchronously, so a disconnected client no longer leaves its LiveView blocked +inside a broad spatial query. During the headed verification, reconnects +initially caused three identical logical world queries to overlap (nine active +PostgreSQL worker processes). The application now coalesces an identical +in-flight discovery key within each BEAM node and reuses a completed result for +10 seconds, which covers the observed 4.5- and 6.5-second local reconnect +intervals. Each key includes the viewer, filters, view mode, and viewport; +PubSub mutations invalidate the corresponding key. This is a node-local +single-flight optimization, not a cluster-wide cache or a capacity guarantee. + +The final headed request and activity map checks each represented 791,753 +records as 10 clusters. No fresh application `500`/`503`, database pool timeout, +discovery error, or container restart was observed. Screenshots are retained +in ignored local evidence: + +- `output/playwright/scale-million-request-map-desktop-stable.png` +- `output/playwright/scale-million-activity-map-desktop-stable.png` + +Authenticated HTTP load can reuse a restricted browser state without a login +or fixture write: + +```sh +WNH_SCALE_READONLY_VUS= \ +WNH_SCALE_READONLY_DURATION= \ +WNH_SCALE_READONLY_THINK_SECONDS= \ +WNH_SCALE_READONLY_LABEL= \ + ./scripts/scale-readonly-load.sh +``` + +The k6 file has a compiled four-route GET allowlist for the request/activity +static LiveView shells in Europe and the world. It intentionally does not claim +to measure connected LiveView discovery: `scripts/scale-verify.sh` measures the +actual list and clustering queries separately. The runner verifies the exact +Compose project and database, uses the stack's internal network, and compares +counts for users, tokens, requests, activities, assignments, participants, +messages, and rate-limit buckets before and after. + +The final 2026-07-28 one-VU, 30-second, one-CPU static-shell observation +completed 60 authenticated GETs with 0 HTTP failures and 120/120 checks. +Overall HTTP duration was 2.566 ms median, 3.196 ms average, and 8.709 ms +maximum. Every tracked table count was identical before and after. The +discovery-query medians are the independently measured values in the table +above. These are measurements of one local CPU-contention replay, not a +production SLO, capacity claim, or minimum resource requirement. Ignored +evidence is retained under `output/scale/verification.json` and +`output/performance/scale-million-onecpu-20260728-async-shell/`. + ## Isolated database scale and cursor plans Run the database-only before/after measurement with: diff --git a/lib/mix/tasks/wnh.db_scale_benchmark.ex b/lib/mix/tasks/wnh.db_scale_benchmark.ex index 9b8ab11..c54f8f9 100644 --- a/lib/mix/tasks/wnh.db_scale_benchmark.ex +++ b/lib/mix/tasks/wnh.db_scale_benchmark.ex @@ -376,15 +376,14 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do SELECT id FROM help_requests WHERE status = 'open' AND hidden_at IS NULL AND expires_at > now() AND location IS NOT NULL - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - 2000 - ) - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - COALESCE(location_radius_meters, 0) + AND location && ST_MakeEnvelope(30.4755, 50.3720, 30.5325, 50.4280, 4326) + AND ( + ST_Intersects(location, ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)) + OR ST_DWithin( + location::geography, + ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, + COALESCE(location_radius_meters, 0) + ) ) AND requester_id NOT IN ( SELECT blocked_id FROM blocks WHERE blocker_id = #{viewer} @@ -397,20 +396,15 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do "help_discovery_clusters" => """ SELECT count(id), - ST_X(ST_Centroid(ST_Collect(ST_Transform(location, 3857)))), - ST_Y(ST_Centroid(ST_Collect(ST_Transform(location, 3857)))) + avg(ST_X(location)), + avg(ST_Y(location)) FROM help_requests WHERE status = 'open' AND hidden_at IS NULL AND expires_at > now() AND location IS NOT NULL - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - 2000 - ) - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - COALESCE(location_radius_meters, 0) + AND location && ST_MakeEnvelope(30.4985, 50.3850, 30.5095, 50.4150, 4326) + AND ST_Intersects( + location, + ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326) ) GROUP BY ST_SnapToGrid(ST_Transform(location, 3857), 200.0) """, @@ -425,6 +419,37 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do AND starts_at > now() AND join_deadline > now() ORDER BY starts_at ASC, id ASC LIMIT 25 """, + "activity_discovery_viewport" => """ + SELECT id FROM activities + WHERE status = 'open' AND hidden_at IS NULL + AND starts_at > now() AND join_deadline > now() + AND location_visibility = 'approximate_public' + AND location && ST_MakeEnvelope(30.4985, 50.3850, 30.5095, 50.4150, 4326) + ORDER BY starts_at ASC, id ASC LIMIT 25 + """, + "activity_discovery_clusters" => """ + SELECT + count(id), + avg(ST_X(public_location)), + avg(ST_Y(public_location)) + FROM ( + SELECT + id, + ST_SetSRID( + ST_MakePoint( + round(ST_X(location)::numeric, 2)::double precision, + round(ST_Y(location)::numeric, 2)::double precision + ), + 4326 + ) AS public_location + FROM activities + WHERE status = 'open' AND hidden_at IS NULL + AND starts_at > now() AND join_deadline > now() + AND location_visibility = 'approximate_public' + AND location && ST_MakeEnvelope(30.4985, 50.3850, 30.5095, 50.4150, 4326) + ) AS visible_activities + GROUP BY ST_SnapToGrid(ST_Transform(public_location, 3857), 200.0) + """, "activity_participant" => """ SELECT activities.id FROM activities @@ -561,6 +586,8 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do "help_discovery_clusters" => "help_requests_location_geography_index", "help_requester" => "help_requests_requester_cursor_index", "activity_discovery" => "activities_discovery_cursor_index", + "activity_discovery_viewport" => "activities_location_index", + "activity_discovery_clusters" => "activities_location_index", "activity_participant" => "activity_participants_user_id_status_index", "messages" => "messages_assignment_cursor_index", "activity_messages" => "activity_messages_activity_cursor_index", @@ -637,15 +664,10 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do SELECT count(*) FROM help_requests WHERE status = 'open' AND hidden_at IS NULL AND expires_at > now() AND location IS NOT NULL - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - 2000 - ) - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - COALESCE(location_radius_meters, 0) + AND location && ST_MakeEnvelope(30.4985, 50.3850, 30.5095, 50.4150, 4326) + AND ST_Intersects( + location, + ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326) ) """) @@ -655,15 +677,10 @@ defmodule Mix.Tasks.Wnh.DbScaleBenchmark do SELECT 1 FROM help_requests WHERE status = 'open' AND hidden_at IS NULL AND expires_at > now() AND location IS NOT NULL - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - 2000 - ) - AND ST_DWithin( - location::geography, - ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326)::geography, - COALESCE(location_radius_meters, 0) + AND location && ST_MakeEnvelope(30.4985, 50.3850, 30.5095, 50.4150, 4326) + AND ST_Intersects( + location, + ST_MakeEnvelope(30.5035, 50.3900, 30.5045, 50.4100, 4326) ) GROUP BY ST_SnapToGrid(ST_Transform(location, 3857), 200.0) ) AS viewport_clusters diff --git a/lib/mix/tasks/wnh.scale_fixtures.ex b/lib/mix/tasks/wnh.scale_fixtures.ex new file mode 100644 index 0000000..a7a31c3 --- /dev/null +++ b/lib/mix/tasks/wnh.scale_fixtures.ex @@ -0,0 +1,519 @@ +defmodule Mix.Tasks.Wnh.ScaleFixtures do + use Mix.Task + + alias WhoNeedHelp.Accounts.{Scope, User} + alias WhoNeedHelp.{Activities, Catalog, Help, Repo} + alias WhoNeedHelp.Help.DiscoveryViewport + + @shortdoc "Prepares or verifies a persistent, isolated local discovery-scale data set" + @confirmation "persistent-local-scale" + @default_rows 1_000_000 + @synthetic_users 1_000 + @viewer_email "scale-viewer@example.invalid" + + @impl Mix.Task + def run(arguments) do + Mix.Task.run("app.start") + + {options, rest, invalid} = + OptionParser.parse(arguments, + strict: [requests: :integer, activities: :integer, samples: :integer, output: :string] + ) + + if invalid != [], do: Mix.raise("invalid options: #{inspect(invalid)}") + + action = + case rest do + [value] when value in ["prepare", "verify"] -> value + _other -> Mix.raise(usage()) + end + + request_count = positive_count!(options, :requests) + activity_count = positive_count!(options, :activities) + samples = positive_count!(options, :samples, 3) + output = Keyword.get(options, :output) || Mix.raise("--output PATH is required") + context = verified_context!(output) + + case action do + "prepare" -> prepare!(context, request_count, activity_count) + "verify" -> verify!(context, request_count, activity_count, samples) + end + end + + defp usage do + "usage: mix wnh.scale_fixtures prepare|verify " <> + "[--requests COUNT] [--activities COUNT] [--samples COUNT] --output /output/FILE.json" + end + + defp positive_count!(options, key, default \\ @default_rows) do + value = Keyword.get(options, key, default) + if is_integer(value) and value > 0, do: value, else: Mix.raise("--#{key} must be positive") + end + + defp verified_context!(output) do + expected_database = required_env!("WNH_SCALE_EXPECTED_DATABASE") + output = Path.expand(output) + + unless System.get_env("WNH_SCALE_FIXTURE_CONFIRM") == @confirmation do + Mix.raise("WNH_SCALE_FIXTURE_CONFIRM must equal #{@confirmation}") + end + + unless String.starts_with?(output, "/output/") do + Mix.raise("--output must resolve below /output") + end + + %Postgrex.Result{rows: [[actual_database]]} = + Repo.query!("SELECT current_database()", [], log: false) + + unless actual_database == expected_database do + Mix.raise( + "refusing scale mutation: expected database #{inspect(expected_database)}, " <> + "observed #{inspect(actual_database)}" + ) + end + + %{database: actual_database, output: output} + end + + defp prepare!(context, request_count, activity_count) do + counts = discovery_counts() + + cond do + counts.help_requests == request_count and counts.activities == activity_count -> + ensure_viewer!() + write_summary!(context, "prepared", request_count, activity_count, %{}) + + counts.help_requests != 0 or counts.activities != 0 -> + Mix.raise( + "the isolated scale database already contains #{counts.help_requests} help requests " <> + "and #{counts.activities} activities; refusing to mix data sets" + ) + + true -> + categories = seed_categories!() + ensure_viewer!() + + Repo.transaction( + fn -> + seed_users!() + seed_requests!(request_count, categories.help) + seed_activities!(activity_count, categories.activity) + end, + timeout: :infinity + ) + + Repo.query!( + "ANALYZE users, categories, help_requests, activities", + [], + timeout: :infinity, + log: false + ) + + assert_counts!(request_count, activity_count) + write_summary!(context, "prepared", request_count, activity_count, %{}) + end + + Mix.shell().info( + "persistent scale data is ready in #{context.database}: " <> + "#{request_count} requests and #{activity_count} activities" + ) + end + + defp verify!(context, request_count, activity_count, samples) do + assert_counts!(request_count, activity_count) + viewer = Repo.get_by!(User, email: @viewer_email) + scope = Scope.for_user(viewer) + + viewports = %{ + kyiv: viewport!(30.30, 50.25, 30.80, 50.65, 10), + europe: viewport!(-12.0, 34.0, 35.0, 61.0, 4), + world: viewport!(-179.99, -80.0, 179.99, 80.0, 1) + } + + measurements = + Map.new(viewports, fn {name, viewport} -> + request_page = + timed_samples( + fn -> Help.paginate_open_requests(scope, %{}, viewport: viewport) end, + samples + ) + + request_map = + timed_samples(fn -> Help.map_discovery_items(scope, %{}, viewport) end, samples) + + activity_page = + timed_samples( + fn -> Activities.paginate_open_activities(scope, %{}, viewport: viewport) end, + samples + ) + + activity_map = + timed_samples(fn -> Activities.map_discovery_items(scope, %{}, viewport) end, samples) + + {name, + %{ + request_page: page_measurement(request_page), + request_map: map_measurement(request_map), + activity_page: page_measurement(activity_page), + activity_map: map_measurement(activity_map) + }} + end) + + write_summary!(context, "verified", request_count, activity_count, %{ + measurement_samples: samples, + viewport_measurements: measurements + }) + + Mix.shell().info("scale verification written to #{context.output}") + end + + defp seed_categories! do + Catalog.seed_defaults() + + categories = Catalog.list_all_categories() + + %{ + help: category_ids!(categories, :help), + activity: category_ids!(categories, :activity) + } + end + + defp category_ids!(categories, mode) do + leaves = + Enum.filter(categories, fn category -> + category.mode == mode and not is_nil(category.parent_id) + end) + + ids = + case leaves do + [] -> categories |> Enum.filter(&(&1.mode == mode)) |> Enum.map(& &1.id) + values -> Enum.map(values, & &1.id) + end + + if ids == [], do: Mix.raise("no #{mode} categories were seeded"), else: ids + end + + defp ensure_viewer! do + password = required_env!("SCALE_VIEWER_PASSWORD") + + unless byte_size(password) in 12..72 do + Mix.raise("SCALE_VIEWER_PASSWORD must contain between 12 and 72 bytes") + end + + case Repo.get_by(User, email: @viewer_email) do + nil -> + now = DateTime.utc_now(:second) + + %User{ + email: @viewer_email, + display_name: "Scale Viewer", + hashed_password: Bcrypt.hash_pwd_salt(password), + confirmed_at: now, + accepted_terms_at: now, + locale: "en" + } + |> Repo.insert!() + + %User{} -> + :ok + end + end + + defp seed_users! do + Repo.query!( + """ + INSERT INTO users ( + id, email, display_name, confirmed_at, accepted_terms_at, locale, + inserted_at, updated_at + ) + SELECT + md5('wnh-scale-user-' || value)::uuid, + 'scale-user-' || value || '@example.invalid', + 'Scale user ' || value, + date_trunc('second', now()), + date_trunc('second', now()), + CASE value % 3 WHEN 0 THEN 'uk' WHEN 1 THEN 'en' ELSE 'ru' END, + date_trunc('second', now()) - value * interval '1 second', + date_trunc('second', now()) - value * interval '1 second' + FROM generate_series(1, $1) AS value + ON CONFLICT DO NOTHING + """, + [@synthetic_users], + timeout: :infinity, + log: false + ) + end + + defp seed_requests!(rows, category_ids) do + Repo.query!( + """ + INSERT INTO help_requests ( + id, title, description, pickup_instructions, structured_data, + location_label, location, status, urgency, location_visibility, + location_radius_meters, expires_at, requester_id, category_id, + inserted_at, updated_at + ) + SELECT + md5('wnh-scale-request-v1-' || value)::uuid, + 'Scale help request ' || value, + 'Persistent synthetic local scale request used to test viewport discovery and UI.', + 'Synthetic data only; no real-world action is requested.', + '{"scale_fixture":true}'::jsonb, + city.name || ' scale area', + CASE WHEN value % 97 = 0 THEN NULL ELSE ST_SetSRID( + ST_MakePoint( + city.longitude + (((value * 97) % 1001) - 500) / 100000.0, + city.latitude + (((value * 193) % 1001) - 500) / 100000.0 + ), + 4326 + ) END, + CASE WHEN value % 10 < 8 THEN 'open' + WHEN value % 10 = 8 THEN 'completed' + ELSE 'cancelled' END, + CASE value % 3 WHEN 0 THEN 'now' WHEN 1 THEN 'today' ELSE 'scheduled' END, + CASE WHEN value % 97 = 0 THEN 'hidden' + WHEN value % 4 = 0 THEN 'exact_public' + WHEN value % 4 = 1 THEN 'exact_for_active_match' + ELSE 'approximate_public' END, + CASE WHEN value % 97 = 0 OR value % 4 = 0 THEN NULL + WHEN value % 3 = 0 THEN 500 + WHEN value % 3 = 1 THEN 1000 + ELSE 2000 END, + date_trunc('second', now()) + ((value % 30) + 1) * interval '1 day', + md5('wnh-scale-user-' || ((value % $2) + 1))::uuid, + (($3::text[])[((value - 1) % cardinality($3::text[])) + 1])::uuid, + date_trunc('second', now()) - value * interval '1 second', + date_trunc('second', now()) - value * interval '1 second' + FROM generate_series(1, $1) AS value + CROSS JOIN LATERAL ( + SELECT * + FROM ( + VALUES + (0, 'Kyiv', 30.5234::double precision, 50.4501::double precision), + (1, 'Frankfurt', 8.6821, 50.1109), + (2, 'Berlin', 13.4050, 52.5200), + (3, 'Warsaw', 21.0122, 52.2297), + (4, 'Prague', 14.4378, 50.0755), + (5, 'London', -0.1276, 51.5072), + (6, 'Paris', 2.3522, 48.8566), + (7, 'Madrid', -3.7038, 40.4168), + (8, 'Rome', 12.4964, 41.9028), + (9, 'New York', -74.0060, 40.7128), + (10, 'Toronto', -79.3832, 43.6532), + (11, 'Mexico City', -99.1332, 19.4326), + (12, 'São Paulo', -46.6333, -23.5505), + (13, 'Cape Town', 18.4241, -33.9249), + (14, 'Nairobi', 36.8219, -1.2921), + (15, 'Delhi', 77.1025, 28.7041), + (16, 'Bangkok', 100.5018, 13.7563), + (17, 'Tokyo', 139.6917, 35.6895), + (18, 'Sydney', 151.2093, -33.8688), + (19, 'Auckland', 174.7633, -36.8485) + ) AS cities(index, name, longitude, latitude) + WHERE cities.index = value % 20 + ) AS city + """, + [rows, @synthetic_users, category_ids], + timeout: :infinity, + log: false + ) + end + + defp seed_activities!(rows, category_ids) do + Repo.query!( + """ + INSERT INTO activities ( + id, title, description, structured_data, location_label, location, + location_visibility, status, starts_at, join_deadline, capacity, + creator_id, category_id, inserted_at, updated_at + ) + SELECT + md5('wnh-scale-activity-v1-' || value)::uuid, + 'Scale community activity ' || value, + 'Persistent synthetic local scale activity used to test viewport discovery and UI.', + '{"scale_fixture":true}'::jsonb, + city.name || ' public meeting area', + ST_SetSRID( + ST_MakePoint( + city.longitude + (((value * 89) % 1001) - 500) / 100000.0, + city.latitude + (((value * 181) % 1001) - 500) / 100000.0 + ), + 4326 + ), + CASE WHEN value % 97 = 0 THEN 'hidden' ELSE 'approximate_public' END, + CASE WHEN value % 10 < 8 THEN 'open' + WHEN value % 10 = 8 THEN 'completed' + ELSE 'cancelled' END, + date_trunc('second', now()) + ((value % 60) + 2) * interval '1 day', + date_trunc('second', now()) + ((value % 60) + 1) * interval '1 day', + 2 + (value % 19), + md5('wnh-scale-user-' || ((value % $2) + 1))::uuid, + (($3::text[])[((value - 1) % cardinality($3::text[])) + 1])::uuid, + date_trunc('second', now()) - value * interval '1 second', + date_trunc('second', now()) - value * interval '1 second' + FROM generate_series(1, $1) AS value + CROSS JOIN LATERAL ( + SELECT * + FROM ( + VALUES + (0, 'Kyiv', 30.5234::double precision, 50.4501::double precision), + (1, 'Frankfurt', 8.6821, 50.1109), + (2, 'Berlin', 13.4050, 52.5200), + (3, 'Warsaw', 21.0122, 52.2297), + (4, 'Prague', 14.4378, 50.0755), + (5, 'London', -0.1276, 51.5072), + (6, 'Paris', 2.3522, 48.8566), + (7, 'Madrid', -3.7038, 40.4168), + (8, 'Rome', 12.4964, 41.9028), + (9, 'New York', -74.0060, 40.7128), + (10, 'Toronto', -79.3832, 43.6532), + (11, 'Mexico City', -99.1332, 19.4326), + (12, 'São Paulo', -46.6333, -23.5505), + (13, 'Cape Town', 18.4241, -33.9249), + (14, 'Nairobi', 36.8219, -1.2921), + (15, 'Delhi', 77.1025, 28.7041), + (16, 'Bangkok', 100.5018, 13.7563), + (17, 'Tokyo', 139.6917, 35.6895), + (18, 'Sydney', 151.2093, -33.8688), + (19, 'Auckland', 174.7633, -36.8485) + ) AS cities(index, name, longitude, latitude) + WHERE cities.index = value % 20 + ) AS city + """, + [rows, @synthetic_users, category_ids], + timeout: :infinity, + log: false + ) + end + + defp viewport!(west, south, east, north, zoom) do + {:ok, viewport} = + DiscoveryViewport.cast(%{ + "west" => west, + "south" => south, + "east" => east, + "north" => north, + "zoom" => zoom, + "width" => 1_280, + "height" => 720 + }) + + viewport + end + + defp timed_samples(callback, samples) do + _warm_result = callback.() + + measurements = + Enum.map(1..samples, fn _sample -> + {microseconds, result} = :timer.tc(callback) + %{elapsed_ms: Float.round(microseconds / 1_000, 3), result: result} + end) + + elapsed = measurements |> Enum.map(& &1.elapsed_ms) |> Enum.sort() + + %{ + elapsed_ms: median(elapsed), + min_elapsed_ms: hd(elapsed), + max_elapsed_ms: List.last(elapsed), + samples_ms: elapsed, + result: measurements |> List.last() |> Map.fetch!(:result) + } + end + + defp median(values) do + middle = div(length(values), 2) + + if rem(length(values), 2) == 1 do + Enum.at(values, middle) + else + Float.round((Enum.at(values, middle - 1) + Enum.at(values, middle)) / 2, 3) + end + end + + defp page_measurement(%{result: page} = measurement) do + measurement + |> timing_measurement() + |> Map.merge(%{ + returned_rows: length(page.entries), + has_next_page: not is_nil(page.next_cursor) + }) + end + + defp map_measurement(%{result: items} = measurement) do + measurement + |> timing_measurement() + |> Map.merge(%{ + returned_rows: length(items), + represented_records: Enum.reduce(items, 0, &(Map.get(&1, :count, 1) + &2)), + json_bytes: items |> Jason.encode!() |> byte_size() + }) + end + + defp timing_measurement(measurement) do + Map.take(measurement, [:elapsed_ms, :min_elapsed_ms, :max_elapsed_ms, :samples_ms]) + end + + defp assert_counts!(request_count, activity_count) do + actual = discovery_counts() + + unless actual.help_requests == request_count and actual.activities == activity_count do + Mix.raise( + "expected #{request_count} requests and #{activity_count} activities, " <> + "found #{actual.help_requests} and #{actual.activities}" + ) + end + end + + defp discovery_counts do + %{ + help_requests: scalar!("SELECT count(*) FROM help_requests"), + activities: scalar!("SELECT count(*) FROM activities") + } + end + + defp write_summary!(context, status, request_count, activity_count, extra) do + File.mkdir_p!(Path.dirname(context.output)) + + summary = + Map.merge( + %{ + schema_version: 1, + status: status, + database: context.database, + expected: %{help_requests: request_count, activities: activity_count}, + counts: discovery_counts(), + database_bytes: scalar!("SELECT pg_database_size(current_database())"), + relation_bytes: relation_sizes(), + captured_at: DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() + }, + extra + ) + + File.write!(context.output, Jason.encode_to_iodata!(summary, pretty: true)) + end + + defp relation_sizes do + Repo.query!( + """ + SELECT relation, pg_total_relation_size(relation::regclass) + FROM unnest(ARRAY['users', 'categories', 'help_requests', 'activities']) AS relation + ORDER BY relation + """, + [], + log: false + ).rows + |> Map.new(fn [name, bytes] -> {name, bytes} end) + end + + defp scalar!(sql) do + Repo.query!(sql, [], timeout: :infinity, log: false).rows |> hd() |> hd() + end + + defp required_env!(name) do + case System.get_env(name) do + value when is_binary(value) and value != "" -> value + _missing -> Mix.raise("#{name} is required") + end + end +end diff --git a/lib/who_need_help/activities.ex b/lib/who_need_help/activities.ex index 518b574..351b61e 100644 --- a/lib/who_need_help/activities.ex +++ b/lib/who_need_help/activities.ex @@ -88,12 +88,14 @@ defmodule WhoNeedHelp.Activities do location_label: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.location_label), longitude: fragment( - "ST_X(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(?, 3857))), 4326))", + "CASE WHEN count(*) = 1 THEN min(ST_X(?)) ELSE avg(ST_X(?)) END", + point.public_location, point.public_location ), latitude: fragment( - "ST_Y(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(?, 3857))), 4326))", + "CASE WHEN count(*) = 1 THEN min(ST_Y(?)) ELSE avg(ST_Y(?)) END", + point.public_location, point.public_location ), cell_x: @@ -817,7 +819,28 @@ defmodule WhoNeedHelp.Activities do defp filter_discovery_viewport(query, nil, _filters), do: query defp filter_discovery_viewport(query, %DiscoveryViewport{} = viewport, filters) do - spatial_condition = + candidate_condition = + Enum.reduce(DiscoveryViewport.envelopes(viewport), dynamic(false), fn + {west, south, east, north}, condition -> + dynamic( + [activity], + ^condition or + fragment( + """ + ( + ? && ST_MakeEnvelope(?, ?, ?, ?, 4326) + ) + """, + activity.location, + ^(west - 0.005), + ^(south - 0.005), + ^(east + 0.005), + ^(north + 0.005) + ) + ) + end) + + exact_condition = Enum.reduce(DiscoveryViewport.envelopes(viewport), dynamic(false), fn {west, south, east, north}, condition -> dynamic( @@ -846,6 +869,8 @@ defmodule WhoNeedHelp.Activities do ) end) + spatial_condition = dynamic([activity], ^candidate_condition and ^exact_condition) + area = filters["area"] || filters[:area] condition = diff --git a/lib/who_need_help/application.ex b/lib/who_need_help/application.ex index 8ddc03f..2d48ef3 100644 --- a/lib/who_need_help/application.ex +++ b/lib/who_need_help/application.ex @@ -32,6 +32,8 @@ defmodule WhoNeedHelp.Application do :web -> [ {Oban, oban_client_config}, + {Task.Supervisor, name: WhoNeedHelp.DiscoveryQueryTaskSupervisor}, + WhoNeedHelp.DiscoveryQueryCache, WhoNeedHelpWeb.Presence, WhoNeedHelp.TrackingPresenceCleanup, WhoNeedHelpWeb.Endpoint @@ -43,6 +45,8 @@ defmodule WhoNeedHelp.Application do :combined -> [ {Oban, oban_config}, + {Task.Supervisor, name: WhoNeedHelp.DiscoveryQueryTaskSupervisor}, + WhoNeedHelp.DiscoveryQueryCache, WhoNeedHelpWeb.Presence, WhoNeedHelp.TrackingPresenceCleanup, WhoNeedHelpWeb.Endpoint diff --git a/lib/who_need_help/discovery_query_cache.ex b/lib/who_need_help/discovery_query_cache.ex new file mode 100644 index 0000000..43c586f --- /dev/null +++ b/lib/who_need_help/discovery_query_cache.ex @@ -0,0 +1,123 @@ +defmodule WhoNeedHelp.DiscoveryQueryCache do + @moduledoc """ + Coalesces identical discovery reads on one web node. + + LiveView reconnects may briefly overlap. Without coalescing, every mount can + run the same broad spatial aggregation against PostgreSQL. Completed results + are retained only long enough to bridge the reconnect intervals measured by + the persistent local scale profile. PubSub-driven refreshes invalidate the + exact key before starting a replacement read. + """ + + use GenServer + + @cache_ttl_ms 10_000 + @task_supervisor WhoNeedHelp.DiscoveryQueryTaskSupervisor + + def start_link(options) do + GenServer.start_link(__MODULE__, options, name: __MODULE__) + end + + def fetch(key, function) when is_function(function, 0) do + case GenServer.call(__MODULE__, {:fetch, key, function}, :infinity) do + {:ok, value} -> + value + + {:error, {kind, reason, stacktrace}} -> + :erlang.raise(kind, reason, stacktrace) + end + end + + def invalidate(key), do: GenServer.call(__MODULE__, {:invalidate, key}) + + @impl true + def init(_options) do + {:ok, %{entries: %{}, pending: %{}, refs: %{}}} + end + + @impl true + def handle_call({:fetch, key, function}, from, state) do + now = System.monotonic_time(:millisecond) + + case state.entries do + %{^key => {expires_at, value}} when expires_at > now -> + {:reply, {:ok, value}, state} + + _expired_or_missing -> + state = update_in(state.entries, &Map.delete(&1, key)) + + case state.pending do + %{^key => pending} -> + pending = update_in(pending.waiters, &[from | &1]) + {:noreply, put_in(state.pending[key], pending)} + + _missing -> + task = + Task.Supervisor.async_nolink(@task_supervisor, fn -> + try do + {:ok, function.()} + catch + kind, reason -> {:error, {kind, reason, __STACKTRACE__}} + end + end) + + pending = %{ref: task.ref, waiters: [from]} + + {:noreply, + state + |> put_in([:pending, key], pending) + |> put_in([:refs, task.ref], key)} + end + end + end + + @impl true + def handle_call({:invalidate, key}, _from, state) do + {:reply, :ok, update_in(state.entries, &Map.delete(&1, key))} + end + + @impl true + def handle_info({ref, result}, state) when is_reference(ref) do + case Map.fetch(state.refs, ref) do + {:ok, key} -> + Process.demonitor(ref, [:flush]) + %{waiters: waiters} = state.pending[key] + Enum.each(waiters, &GenServer.reply(&1, result)) + + state = + state + |> update_in([:pending], &Map.delete(&1, key)) + |> update_in([:refs], &Map.delete(&1, ref)) + |> maybe_store(key, result) + + {:noreply, state} + + :error -> + {:noreply, state} + end + end + + def handle_info({:DOWN, ref, :process, _pid, reason}, state) do + case Map.fetch(state.refs, ref) do + {:ok, key} -> + %{waiters: waiters} = state.pending[key] + result = {:error, {:exit, reason, []}} + Enum.each(waiters, &GenServer.reply(&1, result)) + + {:noreply, + state + |> update_in([:pending], &Map.delete(&1, key)) + |> update_in([:refs], &Map.delete(&1, ref))} + + :error -> + {:noreply, state} + end + end + + defp maybe_store(state, key, {:ok, value}) do + expires_at = System.monotonic_time(:millisecond) + @cache_ttl_ms + put_in(state.entries[key], {expires_at, value}) + end + + defp maybe_store(state, _key, {:error, _reason}), do: state +end diff --git a/lib/who_need_help/help.ex b/lib/who_need_help/help.ex index 32392f4..85cf533 100644 --- a/lib/who_need_help/help.ex +++ b/lib/who_need_help/help.ex @@ -68,8 +68,9 @@ defmodule WhoNeedHelp.Help do points = user - |> open_requests_query(filters, viewport) + |> open_requests_query(filters, nil) |> where([request], not is_nil(request.location)) + |> filter_request_map_viewport(viewport) |> select([request], %{ id: request.id, title: request.title, @@ -111,12 +112,14 @@ defmodule WhoNeedHelp.Help do radius_meters: fragment("CASE WHEN count(*) = 1 THEN max(?) END", point.radius_meters), longitude: fragment( - "ST_X(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(?, 3857))), 4326))", + "CASE WHEN count(*) = 1 THEN min(ST_X(?)) ELSE avg(ST_X(?)) END", + point.public_location, point.public_location ), latitude: fragment( - "ST_Y(ST_Transform(ST_Centroid(ST_Collect(ST_Transform(?, 3857))), 4326))", + "CASE WHEN count(*) = 1 THEN min(ST_Y(?)) ELSE avg(ST_Y(?)) END", + point.public_location, point.public_location ), cell_x: @@ -151,6 +154,58 @@ defmodule WhoNeedHelp.Help do |> Enum.map(&map_discovery_item(&1, viewport.zoom)) end + # Discovery lists include a privacy area's intersection with the viewport. + # Map markers, however, are loaded by their displayed center. This both + # matches normal map panning behaviour and keeps the map query indexable + # without evaluating a geography distance against every privacy circle. + defp filter_request_map_viewport(query, %DiscoveryViewport{} = viewport) do + spatial_condition = + Enum.reduce(DiscoveryViewport.envelopes(viewport), dynamic(false), fn + {west, south, east, north}, condition -> + dynamic( + [request], + ^condition or + fragment( + """ + ( + ? && ST_MakeEnvelope(?, ?, ?, ?, 4326) + AND + ST_Intersects( + CASE + WHEN ? = 'exact_for_active_match' THEN + ST_SetSRID( + ST_MakePoint( + round(ST_X(?)::numeric, 2)::double precision, + round(ST_Y(?)::numeric, 2)::double precision + ), + 4326 + ) + ELSE ? + END, + ST_MakeEnvelope(?, ?, ?, ?, 4326) + ) + ) + """, + request.location, + ^(west - 0.005), + ^(south - 0.005), + ^(east + 0.005), + ^(north + 0.005), + request.location_visibility, + request.location, + request.location, + request.location, + ^west, + ^south, + ^east, + ^north + ) + ) + end) + + where(query, ^spatial_condition) + end + def list_open_requests, do: raise(ArgumentError, "an authenticated scope is required") def visible_open_request?(%Scope{user: user}, %HelpRequest{} = request, filters \\ %{}) do @@ -1127,20 +1182,49 @@ defmodule WhoNeedHelp.Help do defp filter_discovery_viewport(query, %DiscoveryViewport{} = viewport, filters) do maximum_public_radius = Enum.max(HelpRequest.allowed_location_radii()) - spatial_condition = + candidate_condition = + Enum.reduce( + DiscoveryViewport.padded_envelopes(viewport, maximum_public_radius), + dynamic(false), + fn {west, south, east, north}, condition -> + dynamic( + [request], + ^condition or + fragment( + "? && ST_MakeEnvelope(?, ?, ?, ?, 4326)", + request.location, + ^west, + ^south, + ^east, + ^north + ) + ) + end + ) + + exact_condition = Enum.reduce(DiscoveryViewport.envelopes(viewport), dynamic(false), fn {west, south, east, north}, condition -> dynamic( [request], ^condition or fragment( - "ST_DWithin((?)::geography, ST_MakeEnvelope(?, ?, ?, ?, 4326)::geography, ?) AND ST_DWithin((?)::geography, ST_MakeEnvelope(?, ?, ?, ?, 4326)::geography, COALESCE(?, 0))", + """ + ( + ST_Intersects(?, ST_MakeEnvelope(?, ?, ?, ?, 4326)) + OR + ST_DWithin( + (?)::geography, + ST_MakeEnvelope(?, ?, ?, ?, 4326)::geography, + COALESCE(?, 0) + ) + ) + """, request.location, ^west, ^south, ^east, ^north, - ^maximum_public_radius, request.location, ^west, ^south, @@ -1151,6 +1235,8 @@ defmodule WhoNeedHelp.Help do ) end) + spatial_condition = dynamic([request], ^candidate_condition and ^exact_condition) + area = filters["area"] || filters[:area] condition = diff --git a/lib/who_need_help/help/discovery_viewport.ex b/lib/who_need_help/help/discovery_viewport.ex index 45e4ea3..58db4cb 100644 --- a/lib/who_need_help/help/discovery_viewport.ex +++ b/lib/who_need_help/help/discovery_viewport.ex @@ -65,6 +65,47 @@ defmodule WhoNeedHelp.Help.DiscoveryViewport do [{west, south, 180.0, north}, {-180.0, south, east, north}] end + @doc """ + Returns viewport envelopes expanded by a geodesic point radius. + + The expansion is conservative in longitude at the viewport's highest + absolute latitude and preserves antimeridian wrapping. It is intended as an + indexable bounding-box prefilter before an exact geography predicate. + """ + def padded_envelopes(%__MODULE__{} = viewport, radius_meters) + when is_number(radius_meters) and radius_meters >= 0 do + latitude_delta = radius_meters / 111_320.0 + + maximum_latitude = + viewport + |> then(&max(abs(&1.south), abs(&1.north))) + |> min(@max_mercator_latitude) + + longitude_scale = max(:math.cos(degrees_to_radians(maximum_latitude)), 0.01) + longitude_delta = radius_meters / (111_320.0 * longitude_scale) + + south = + clamp(viewport.south - latitude_delta, -@max_mercator_latitude, @max_mercator_latitude) + + north = + clamp(viewport.north + latitude_delta, -@max_mercator_latitude, @max_mercator_latitude) + + viewport + |> envelopes() + |> Enum.flat_map(fn {west, _south, east, _north} -> + longitude_intervals(west - longitude_delta, east + longitude_delta) + |> Enum.map(fn {padded_west, padded_east} -> + { + clamp(padded_west, -180.0, 180.0), + south, + clamp(padded_east, -180.0, 180.0), + north + } + end) + end) + |> Enum.uniq() + end + def cluster_cell_meters(%__MODULE__{} = viewport) do horizontal_cells = max(viewport.width / @cluster_cell_pixels, 1.0) vertical_cells = max(viewport.height / @cluster_cell_pixels, 1.0) diff --git a/lib/who_need_help_web/live/activity_live/index.ex b/lib/who_need_help_web/live/activity_live/index.ex index 29e8b62..e29bf87 100644 --- a/lib/who_need_help_web/live/activity_live/index.ex +++ b/lib/who_need_help_web/live/activity_live/index.ex @@ -1,10 +1,13 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do use WhoNeedHelpWeb, :live_view - alias WhoNeedHelp.{Activities, Catalog, Pagination} + require Logger + + alias WhoNeedHelp.{Activities, Catalog, DiscoveryQueryCache, Pagination} alias WhoNeedHelp.Help.DiscoveryViewport @discovery_modes ~w(nearby mine) + @view_modes ~w(list split map) @filter_keys ~w(category_id area) @impl true @@ -18,12 +21,16 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do socket |> assign(:filters, empty_filters()) |> assign(:discovery_mode, "nearby") + |> assign(:view_mode, "split") + |> assign(:view_explicit, false) |> assign(:viewport, nil) |> assign(:pending_viewport, nil) |> assign(:activities, []) |> assign(:activities_cursor, nil) |> assign(:map_items, Jason.encode!([])) |> assign(:map_result_count, 0) + |> assign(:discovery_loading, true) + |> assign(:refresh_timer, nil) |> assign(:my_activities, []) |> assign(:my_activities_cursor, nil) |> assign(:page_title, gettext("Activities")) @@ -35,20 +42,24 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do filters = normalize_filters(params) viewport = parse_viewport(params) discovery_mode = normalize_discovery_mode(params["scope"]) + view_mode = normalize_view_mode(params["view"]) {:noreply, socket |> assign(:filters, filters) |> assign(:filter_form, to_form(filters, as: :filters)) |> assign(:discovery_mode, discovery_mode) + |> assign(:view_mode, view_mode) + |> assign(:view_explicit, params["view"] in @view_modes) |> assign(:viewport, viewport) |> assign(:pending_viewport, nil) - |> load()} + |> load_if_connected()} end @impl true def handle_info({event, activity}, socket) when event in [:activity_created, :activity_updated] do + socket = invalidate_discovery_cache(socket) user_id = socket.assigns.current_scope.user.id already_mine? = Enum.any?(socket.assigns.my_activities, &(&1.id == activity.id)) @@ -76,13 +87,19 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do socket.assigns.my_activities_cursor ) - {:noreply, - socket - |> assign(:activities, activities) - |> assign(:activities_cursor, activities_cursor) - |> assign(:my_activities, my_activities) - |> assign(:my_activities_cursor, my_activities_cursor) - |> refresh_map_items()} + socket = + socket + |> assign(:activities, activities) + |> assign(:activities_cursor, activities_cursor) + |> assign(:my_activities, my_activities) + |> assign(:my_activities_cursor, my_activities_cursor) + + if socket.assigns.refresh_timer || socket.assigns.view_mode == "list" do + {:noreply, socket} + else + timer = Process.send_after(self(), :refresh_discovery, 100) + {:noreply, assign(socket, :refresh_timer, timer)} + end end def handle_info({:my_activity_updated, activity, member?}, socket) do @@ -101,6 +118,10 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do |> assign(:my_activities_cursor, my_activities_cursor)} end + def handle_info(:refresh_discovery, socket) do + {:noreply, socket |> assign(:refresh_timer, nil) |> load_if_connected()} + end + @impl true def handle_event("filter", %{"filters" => filters}, socket) do {:noreply, push_patch(socket, to: activity_path(socket, filters: normalize_filters(filters)))} @@ -111,6 +132,20 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do {:noreply, push_patch(socket, to: activity_path(socket, discovery_mode: discovery_mode))} end + def handle_event("set-view", %{"view" => view_mode}, socket) + when view_mode in @view_modes do + {:noreply, push_patch(socket, to: activity_path(socket, view_mode: view_mode))} + end + + def handle_event("restore-view-mode", %{"view" => view_mode}, socket) + when view_mode in @view_modes do + if socket.assigns.view_explicit do + {:noreply, socket} + else + {:noreply, push_patch(socket, to: activity_path(socket, view_mode: view_mode))} + end + end + def handle_event("map-viewport-change", params, socket) do case DiscoveryViewport.cast(params) do {:ok, viewport} -> @@ -160,8 +195,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do {:noreply, socket |> assign(:activities, activities) - |> assign(:activities_cursor, page.next_cursor) - |> refresh_map_items()} + |> assign(:activities_cursor, page.next_cursor)} end def handle_event("load-more-my-activities", _params, socket) do @@ -176,23 +210,36 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do |> assign(:my_activities_cursor, page.next_cursor)} end - defp load(socket) do + @impl true + def handle_async(:load_discovery, {:ok, result}, socket) do + {:noreply, assign(socket, result)} + end + + def handle_async(:load_discovery, {:exit, {:shutdown, :cancel}}, socket), + do: {:noreply, socket} + + def handle_async(:load_discovery, {:exit, reason}, socket) do + Logger.warning("activity discovery load failed: #{Exception.format_exit(reason)}") + {:noreply, assign(socket, :discovery_loading, false)} + end + + defp load_data(scope, filters, discovery_mode, view_mode, viewport) do {activities_page, map_items} = - case {socket.assigns.discovery_mode, socket.assigns.viewport} do + case {discovery_mode, viewport} do {"nearby", %DiscoveryViewport{} = viewport} -> page = - Activities.paginate_open_activities( - socket.assigns.current_scope, - socket.assigns.filters, - viewport: viewport - ) + if view_mode == "map" do + %Pagination.Page{} + else + Activities.paginate_open_activities(scope, filters, viewport: viewport) + end items = - Activities.map_discovery_items( - socket.assigns.current_scope, - socket.assigns.filters, - viewport - ) + if view_mode == "list" do + [] + else + Activities.map_discovery_items(scope, filters, viewport) + end {page, items} @@ -201,22 +248,45 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do end my_activities_page = - if socket.assigns.discovery_mode == "mine" do - Activities.paginate_my_activities(socket.assigns.current_scope) + if discovery_mode == "mine" do + Activities.paginate_my_activities(scope) else %Pagination.Page{} end activities = activities_page.entries - socket - |> assign(:activities, activities) - |> assign(:activities_cursor, activities_page.next_cursor) - |> assign(:my_activities, my_activities_page.entries) - |> assign(:my_activities_cursor, my_activities_page.next_cursor) - |> assign(:filter_form, to_form(socket.assigns.filters, as: :filters)) - |> assign(:map_items, Jason.encode!(map_items)) - |> assign(:map_result_count, Enum.reduce(map_items, 0, &(map_item_count(&1) + &2))) + %{ + discovery_loading: false, + activities: activities, + activities_cursor: activities_page.next_cursor, + my_activities: my_activities_page.entries, + my_activities_cursor: my_activities_page.next_cursor, + map_items: Jason.encode!(map_items), + map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2)) + } + end + + defp load_if_connected(socket) do + if connected?(socket) do + scope = socket.assigns.current_scope + filters = socket.assigns.filters + discovery_mode = socket.assigns.discovery_mode + view_mode = socket.assigns.view_mode + viewport = socket.assigns.viewport + + socket + |> cancel_async(:load_discovery) + |> assign(:discovery_loading, true) + |> start_async(:load_discovery, fn -> + DiscoveryQueryCache.fetch( + discovery_cache_key(scope, filters, discovery_mode, view_mode, viewport), + fn -> load_data(scope, filters, discovery_mode, view_mode, viewport) end + ) + end) + else + assign(socket, :discovery_loading, true) + end end defp append_unique(existing, incoming) do @@ -243,24 +313,27 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do {entries, cursor} end - defp refresh_map_items(%{assigns: %{viewport: %DiscoveryViewport{} = viewport}} = socket) do - map_items = - Activities.map_discovery_items( - socket.assigns.current_scope, - socket.assigns.filters, - viewport + defp map_item_count(%{type: "cluster", count: count}), do: count + defp map_item_count(_activity), do: 1 + + defp invalidate_discovery_cache(socket) do + :ok = + DiscoveryQueryCache.invalidate( + discovery_cache_key( + socket.assigns.current_scope, + socket.assigns.filters, + socket.assigns.discovery_mode, + socket.assigns.view_mode, + socket.assigns.viewport + ) ) socket - |> assign(:map_items, Jason.encode!(map_items)) - |> assign(:map_result_count, Enum.reduce(map_items, 0, &(map_item_count(&1) + &2))) end - defp refresh_map_items(socket), - do: socket |> assign(:map_items, Jason.encode!([])) |> assign(:map_result_count, 0) - - defp map_item_count(%{type: "cluster", count: count}), do: count - defp map_item_count(_activity), do: 1 + defp discovery_cache_key(scope, filters, discovery_mode, view_mode, viewport) do + {:activities, scope.user.id, filters, discovery_mode, view_mode, viewport} + end defp empty_filters, do: %{"category_id" => "", "area" => ""} @@ -282,14 +355,19 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do defp normalize_discovery_mode(_discovery_mode), do: "nearby" + defp normalize_view_mode(view_mode) when view_mode in @view_modes, do: view_mode + defp normalize_view_mode(_view_mode), do: "split" + defp activity_path(socket, overrides) do filters = Keyword.get(overrides, :filters, socket.assigns.filters) viewport = Keyword.get(overrides, :viewport, socket.assigns.viewport) discovery_mode = Keyword.get(overrides, :discovery_mode, socket.assigns.discovery_mode) + view_mode = Keyword.get(overrides, :view_mode, socket.assigns.view_mode) params = filters |> Map.reject(fn {_key, value} -> value == "" end) + |> Map.put("view", view_mode) |> then(fn params -> if discovery_mode == "mine", do: Map.put(params, "scope", "mine"), else: params end) @@ -399,129 +477,202 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do /> -
-
-
- <.icon name="hero-map" class="mx-auto size-8 text-info" /> -

{gettext("Choose an area on the map")}

-

- {gettext("Only activities from the selected map area will be loaded.")} +

+
+ {gettext("Loading results…")} +
+ +
+ + {ngettext( + "%{count} activity in this map area", + "%{count} activities in this map area", + @map_result_count, + count: @map_result_count + )} + +

+ {gettext("Move the map, then search the newly visible area.")}

-
-

{gettext("No open activities in this area")}

-

- {gettext("Move or zoom the map to search somewhere else.")} -

-
- - <.link - :for={activity <- @activities} - id={"open-activity-#{activity.id}"} - navigate={~p"/activities/#{activity.id}"} - class="block rounded-3xl border border-base-300 bg-base-100 p-6 transition hover:border-info/50" - > -
-
-
- - {Catalog.category_path( - activity.category, - Gettext.get_locale(WhoNeedHelpWeb.Gettext) - )} - - - {activity.approved_participant_count}/{activity.capacity} - -
-

{activity.title}

-

{activity.description}

-
- <.icon name="hero-chevron-right" class="mt-2 size-5 shrink-0" /> -
-
- 📍 {activity.location_label} - - <.icon name="hero-clock" class="size-4" /> - <.local_time - id={"activity-start-#{activity.id}"} - value={activity.starts_at} - date_style="medium" - time_style="short" - /> - - - {gettext("by %{name}", - name: activity.creator.display_name || gettext("Community member") - )} - -
- - -
- - -
+
+ +
+
+
+
+ +
+ <.icon name="hero-map" class="mx-auto size-8 text-info" /> +

{gettext("Choose an area on the map")}

+

+ {gettext("Only activities from the selected map area will be loaded.")} +

+
+ +
+

{gettext("No open activities in this area")}

+

+ {gettext("Move or zoom the map to search somewhere else.")} +

+
+ + <.link + :for={activity <- @activities} + id={"open-activity-#{activity.id}"} + navigate={~p"/activities/#{activity.id}"} + class="block rounded-3xl border border-base-300 bg-base-100 p-6 transition hover:border-info/50" + > +
+
+
+ + {Catalog.category_path( + activity.category, + Gettext.get_locale(WhoNeedHelpWeb.Gettext) + )} + + + {activity.approved_participant_count}/{activity.capacity} + +
+

{activity.title}

+

{activity.description}

+
+ <.icon name="hero-chevron-right" class="mt-2 size-5 shrink-0" /> +
+
+ 📍 {activity.location_label} + + <.icon name="hero-clock" class="size-4" /> + <.local_time + id={"activity-start-#{activity.id}"} + value={activity.starts_at} + date_style="medium" + time_style="short" + /> + + + {gettext("by %{name}", + name: activity.creator.display_name || gettext("Community member") + )} + +
+ + +
+ + +
+
+
+ +
<.icon name="hero-user-group" class="mx-auto size-8 text-info" /> diff --git a/lib/who_need_help_web/live/request_live/index.ex b/lib/who_need_help_web/live/request_live/index.ex index acc106c..b688aeb 100644 --- a/lib/who_need_help_web/live/request_live/index.ex +++ b/lib/who_need_help_web/live/request_live/index.ex @@ -1,12 +1,21 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do use WhoNeedHelpWeb, :live_view - alias WhoNeedHelp.{Help, Pagination, Trust} + require Logger + + alias WhoNeedHelp.{DiscoveryQueryCache, Help, Pagination, Trust} alias WhoNeedHelp.Help.DiscoveryViewport @discovery_modes ~w(nearby mine) @view_modes ~w(list split map) @filter_keys ~w(category_id urgency area) + @empty_reputation %{ + completed: 0, + unique_people: 0, + verified_handovers: 0, + location_supported: 0, + rating: nil + } @impl true def mount(_params, _session, socket) do @@ -27,8 +36,12 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do |> assign(:query_params, %{}) |> assign(:requests, []) |> assign(:requests_cursor, nil) + |> assign(:my_requests, []) + |> assign(:my_requests_cursor, nil) + |> assign(:reputation, @empty_reputation) |> assign(:map_items, Jason.encode!([])) |> assign(:map_result_count, 0) + |> assign(:discovery_loading, true) |> assign(:refresh_timer, nil) |> assign(:page_title, gettext("Nearby help")) |> assign(:categories, WhoNeedHelp.Catalog.list_categories())} @@ -55,7 +68,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do normalize_query_params(filters, viewport, view_mode, discovery_mode) ) |> refresh_discovery_subscription(viewport, filters, discovery_mode) - |> load() + |> load_if_connected() {:noreply, socket} end @@ -150,7 +163,10 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do @impl true def handle_info({event, request}, socket) when event in [:request_created, :request_updated] do - socket = maybe_refresh_reputation(socket, request, socket.assigns.current_scope.user.id) + socket = + socket + |> invalidate_discovery_cache() + |> maybe_refresh_reputation(request, socket.assigns.current_scope.user.id) if socket.assigns.refresh_timer do {:noreply, socket} @@ -161,27 +177,64 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do end def handle_info(:refresh_discovery, socket) do - {:noreply, socket |> assign(:refresh_timer, nil) |> load()} + {:noreply, socket |> assign(:refresh_timer, nil) |> load_if_connected()} end - defp load(socket) do + @impl true + def handle_async(:load_discovery, {:ok, result}, socket) do + {:noreply, assign(socket, result)} + end + + def handle_async(:load_discovery, {:exit, {:shutdown, :cancel}}, socket), + do: {:noreply, socket} + + def handle_async(:load_discovery, {:exit, reason}, socket) do + Logger.warning("request discovery load failed: #{Exception.format_exit(reason)}") + {:noreply, assign(socket, :discovery_loading, false)} + end + + defp load_if_connected(socket) do + if connected?(socket) do + scope = socket.assigns.current_scope + filters = socket.assigns.filters + discovery_mode = socket.assigns.discovery_mode + view_mode = socket.assigns.view_mode + viewport = socket.assigns.viewport + + socket + |> cancel_async(:load_discovery) + |> assign(:discovery_loading, true) + |> start_async(:load_discovery, fn -> + DiscoveryQueryCache.fetch( + discovery_cache_key(scope, filters, discovery_mode, view_mode, viewport), + fn -> load_data(scope, filters, discovery_mode, view_mode, viewport) end + ) + end) + else + assign(socket, :discovery_loading, true) + end + end + + defp load_data(scope, filters, discovery_mode, view_mode, viewport) do {requests_page, map_items} = - case {socket.assigns.discovery_mode, socket.assigns.viewport} do + case {discovery_mode, viewport} do {"mine", _viewport} -> {%Pagination.Page{}, []} {"nearby", %DiscoveryViewport{} = viewport} -> page = - Help.paginate_open_requests(socket.assigns.current_scope, socket.assigns.filters, - viewport: viewport - ) + if view_mode == "map" do + %Pagination.Page{} + else + Help.paginate_open_requests(scope, filters, viewport: viewport) + end items = - Help.map_discovery_items( - socket.assigns.current_scope, - socket.assigns.filters, - viewport - ) + if view_mode == "list" do + [] + else + Help.map_discovery_items(scope, filters, viewport) + end {page, items} @@ -190,22 +243,22 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do end my_requests_page = - if socket.assigns.discovery_mode == "mine" do - Help.paginate_my_requests(socket.assigns.current_scope) + if discovery_mode == "mine" do + Help.paginate_my_requests(scope) else %Pagination.Page{} end - user = socket.assigns.current_scope.user - - socket - |> assign(:requests, requests_page.entries) - |> assign(:requests_cursor, requests_page.next_cursor) - |> assign(:my_requests, my_requests_page.entries) - |> assign(:my_requests_cursor, my_requests_page.next_cursor) - |> assign(:reputation, Trust.reputation(user.id)) - |> assign(:map_items, Jason.encode!(map_items)) - |> assign(:map_result_count, Enum.reduce(map_items, 0, &(map_item_count(&1) + &2))) + %{ + discovery_loading: false, + requests: requests_page.entries, + requests_cursor: requests_page.next_cursor, + my_requests: my_requests_page.entries, + my_requests_cursor: my_requests_page.next_cursor, + reputation: Trust.reputation(scope.user.id), + map_items: Jason.encode!(map_items), + map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2)) + } end defp refresh_discovery_subscription(socket, viewport, filters, discovery_mode) do @@ -246,6 +299,25 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do defp map_item_count(%{type: "cluster", count: count}), do: count defp map_item_count(_request), do: 1 + defp invalidate_discovery_cache(socket) do + :ok = + DiscoveryQueryCache.invalidate( + discovery_cache_key( + socket.assigns.current_scope, + socket.assigns.filters, + socket.assigns.discovery_mode, + socket.assigns.view_mode, + socket.assigns.viewport + ) + ) + + socket + end + + defp discovery_cache_key(scope, filters, discovery_mode, view_mode, viewport) do + {:requests, scope.user.id, filters, discovery_mode, view_mode, viewport} + end + defp empty_filters, do: %{"category_id" => "", "urgency" => "", "area" => ""} defp normalize_filters(params) do @@ -426,10 +498,15 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do data-view-mode={@view_mode} data-view-explicit={to_string(@view_explicit)} data-has-viewport={to_string(not is_nil(@viewport))} + aria-busy={to_string(@discovery_loading)} class="request-discovery mt-6" >
-
+
+ {gettext("Loading results…")} +
+ +
{ngettext( "%{count} request in this map area", @@ -480,7 +557,16 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
+
+ +
<.icon name="hero-map" class="mx-auto size-8 text-success" /> @@ -491,7 +577,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do

{gettext("No open requests in this area")}

@@ -633,7 +719,16 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
+
+ +
<.icon name="hero-clipboard-document-list" class="mx-auto size-9 text-success" /> @@ -646,7 +741,10 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
-
+
<.link :for={request <- @my_requests} id={"my-request-#{request.id}"} diff --git a/load/k6/scale-readonly.js b/load/k6/scale-readonly.js new file mode 100644 index 0000000..fa46201 --- /dev/null +++ b/load/k6/scale-readonly.js @@ -0,0 +1,125 @@ +import { check, sleep } from "k6"; +import http from "k6/http"; +import exec from "k6/execution"; +import { Trend } from "k6/metrics"; + +function required(name) { + const value = __ENV[name]; + + if (value === undefined || value === "") { + throw new Error(`${name} is required`); + } + + return value; +} + +function positiveInteger(name) { + const value = Number(required(name)); + + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${name} must be a positive integer`); + } + + return value; +} + +function nonNegativeNumber(name) { + const value = Number(required(name)); + + if (!Number.isFinite(value) || value < 0) { + throw new Error(`${name} must be a non-negative number`); + } + + return value; +} + +const baseUrl = required("BASE_URL").replace(/\/+$/, ""); + +if (baseUrl !== "http://web:4000") { + throw new Error("The scale read-only profile is pinned to http://web:4000"); +} + +const sessionCookie = required("SESSION_COOKIE"); +const vus = positiveInteger("VUS"); +const duration = required("DURATION"); +const thinkSeconds = nonNegativeNumber("THINK_SECONDS"); + +const viewports = [ + { + path: + "/requests?west=22.096886&south=45.034261&east=38.949914&north=55.310025&zoom=5&width=767&height=734&view=split", + endpoint: "requests_europe", + heading: "Who needs help?", + }, + { + path: + "/activities?west=22.096886&south=45.034261&east=38.949914&north=55.310025&zoom=5&width=767&height=734&view=split", + endpoint: "activities_europe", + heading: "Nearby activities", + }, + { + path: + "/requests?west=-179.9&south=-80&east=179.9&north=80&zoom=1&width=1280&height=720&view=map", + endpoint: "requests_world", + heading: "Who needs help?", + }, + { + path: + "/activities?west=-179.9&south=-80&east=179.9&north=80&zoom=1&width=1280&height=720&view=map", + endpoint: "activities_world", + heading: "Nearby activities", + }, +]; + +const endpointDurations = { + requests_europe: new Trend("wnh_requests_europe_duration", true), + activities_europe: new Trend("wnh_activities_europe_duration", true), + requests_world: new Trend("wnh_requests_world_duration", true), + activities_world: new Trend("wnh_activities_world_duration", true), +}; + +export const options = { + scenarios: { + scale_readonly_http: { + executor: "constant-vus", + exec: "scaleReadonlyHttp", + vus, + duration, + }, + }, + thresholds: { + checks: [{ threshold: "rate==1", abortOnFail: true }], + http_req_failed: [{ threshold: "rate==0", abortOnFail: true }], + }, +}; + +export function scaleReadonlyHttp() { + const viewport = viewports[exec.scenario.iterationInTest % viewports.length]; + const response = http.get(`${baseUrl}${viewport.path}`, { + redirects: 0, + tags: { + endpoint: viewport.endpoint, + safety_scope: "persistent_local_scale_readonly", + }, + headers: { + Cookie: sessionCookie, + Host: "scale.localhost", + "X-Forwarded-Host": "scale.localhost", + "X-Forwarded-Port": "4121", + "X-Forwarded-Proto": "https", + "User-Agent": "WhoNeedHelp-persistent-scale-readonly/1", + }, + }); + + endpointDurations[viewport.endpoint].add(response.timings.duration); + + check(response, { + [`GET ${viewport.endpoint} returned 200`]: (candidate) => + candidate.status === 200, + [`GET ${viewport.endpoint} rendered its loading shell`]: (candidate) => + candidate.body.includes(viewport.heading) && + candidate.body.includes("Loading results"), + }); + + sleep(thinkSeconds); +} diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index afa9789..1f3ad17 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -13,7 +13,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:142 #: lib/who_need_help_web/components/layouts.ex:387 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:115 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:119 #, elixir-autogen, elixir-format msgid "Account settings" msgstr "" @@ -45,7 +45,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:95 #: lib/who_need_help_web/components/layouts.ex:171 #: lib/who_need_help_web/controllers/user_registration_html/new.html.heex:14 -#: lib/who_need_help_web/controllers/user_session_controller.ex:211 +#: lib/who_need_help_web/controllers/user_session_controller.ex:215 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:10 #, elixir-autogen, elixir-format msgid "Log in" @@ -66,7 +66,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:92 #: lib/who_need_help_web/components/layouts.ex:168 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:141 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:142 #, elixir-autogen, elixir-format msgid "Register" msgstr "" @@ -131,7 +131,7 @@ msgstr "" msgid "5 — excellent" msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:130 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:134 #, elixir-autogen, elixir-format msgid "A link to confirm your email change has been sent to the new address." msgstr "" @@ -179,7 +179,7 @@ msgid "Active" msgstr "" #: lib/who_need_help_web/components/layouts.ex:231 -#: lib/who_need_help_web/live/activity_live/index.ex:29 +#: lib/who_need_help_web/live/activity_live/index.ex:33 #, elixir-autogen, elixir-format msgid "Activities" msgstr "" @@ -215,7 +215,7 @@ msgstr "" msgid "Activity restored." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:385 +#: lib/who_need_help_web/live/activity_live/index.ex:427 #: lib/who_need_help_web/live/activity_live/new.ex:204 #, elixir-autogen, elixir-format msgid "Activity type" @@ -232,12 +232,12 @@ msgstr "" msgid "Administrator" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:387 +#: lib/who_need_help_web/live/activity_live/index.ex:429 #, elixir-autogen, elixir-format msgid "All activities" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:393 +#: lib/who_need_help_web/live/request_live/index.ex:407 #, elixir-autogen, elixir-format msgid "All categories" msgstr "" @@ -252,17 +252,17 @@ msgstr "" msgid "Another helper already accepted this request." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:408 +#: lib/who_need_help_web/live/request_live/index.ex:422 #, elixir-autogen, elixir-format msgid "Any urgency" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:573 +#: lib/who_need_help_web/live/activity_live/show.ex:591 #, elixir-autogen, elixir-format msgid "Approval pending" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:650 +#: lib/who_need_help_web/live/activity_live/show.ex:668 #, elixir-autogen, elixir-format msgid "Approve" msgstr "" @@ -272,7 +272,7 @@ msgstr "" msgid "Approved group chat" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:676 +#: lib/who_need_help_web/live/activity_live/show.ex:694 #, elixir-autogen, elixir-format msgid "Approved participants" msgstr "" @@ -315,7 +315,7 @@ msgstr "" msgid "Block and report" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:719 +#: lib/who_need_help_web/live/activity_live/show.ex:737 #, elixir-autogen, elixir-format msgid "Block organizer" msgstr "" @@ -356,7 +356,7 @@ msgstr "" msgid "COMMUNITY-DRIVEN TAXONOMY" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:670 +#: lib/who_need_help_web/live/activity_live/show.ex:688 #, elixir-autogen, elixir-format msgid "Cancel activity" msgstr "" @@ -367,16 +367,16 @@ msgstr "" msgid "Cancel request" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:316 +#: lib/who_need_help_web/live/activity_live/index.ex:358 #: lib/who_need_help_web/live/activity_live/show.ex:344 -#: lib/who_need_help_web/live/request_live/index.ex:313 -#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:327 +#: lib/who_need_help_web/live/request_live/index.ex:328 #: lib/who_need_help_web/live/request_live/show.ex:956 #, elixir-autogen, elixir-format msgid "Cancelled" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:391 +#: lib/who_need_help_web/live/request_live/index.ex:405 #: lib/who_need_help_web/live/request_live/new.ex:302 #, elixir-autogen, elixir-format msgid "Category" @@ -443,7 +443,7 @@ msgstr "" msgid "City centre, near the main entrance" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:737 +#: lib/who_need_help_web/live/activity_live/show.ex:755 #: lib/who_need_help_web/live/request_live/show.ex:1759 #, elixir-autogen, elixir-format msgid "Clear" @@ -459,7 +459,7 @@ msgstr "" msgid "Coffee and conversation" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:330 +#: lib/who_need_help_web/live/activity_live/index.ex:372 #, elixir-autogen, elixir-format msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings." msgstr "" @@ -474,19 +474,19 @@ msgstr "" msgid "Community helpers" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:578 #: lib/who_need_help_web/live/activity_live/show.ex:403 -#: lib/who_need_help_web/live/activity_live/show.ex:621 -#: lib/who_need_help_web/live/activity_live/show.ex:680 -#: lib/who_need_help_web/live/request_live/index.ex:537 +#: lib/who_need_help_web/live/activity_live/show.ex:639 +#: lib/who_need_help_web/live/activity_live/show.ex:698 +#: lib/who_need_help_web/live/request_live/index.ex:565 #, elixir-autogen, elixir-format msgid "Community member" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:315 +#: lib/who_need_help_web/live/activity_live/index.ex:357 #: lib/who_need_help_web/live/activity_live/show.ex:343 -#: lib/who_need_help_web/live/request_live/index.ex:311 -#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:325 +#: lib/who_need_help_web/live/request_live/index.ex:326 #: lib/who_need_help_web/live/request_live/show.ex:955 #: lib/who_need_help_web/live/request_live/show.ex:974 #, elixir-autogen, elixir-format @@ -537,13 +537,13 @@ msgstr "" msgid "Confirming..." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:336 +#: lib/who_need_help_web/live/activity_live/index.ex:378 #: lib/who_need_help_web/live/activity_live/new.ex:12 #, elixir-autogen, elixir-format msgid "Create activity" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:535 +#: lib/who_need_help_web/live/activity_live/index.ex:650 #: lib/who_need_help_web/live/activity_live/new.ex:180 #, elixir-autogen, elixir-format msgid "Create an activity" @@ -554,12 +554,12 @@ msgstr "" msgid "Create category" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:342 +#: lib/who_need_help_web/live/request_live/index.ex:356 #, elixir-autogen, elixir-format msgid "Create urgent request" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:746 +#: lib/who_need_help_web/live/activity_live/show.ex:764 #, elixir-autogen, elixir-format msgid "Dangerous plan" msgstr "" @@ -586,7 +586,7 @@ msgstr "" msgid "Decisions and access to reported chat evidence are written to the audit log." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:657 +#: lib/who_need_help_web/live/activity_live/show.ex:675 #, elixir-autogen, elixir-format msgid "Decline" msgstr "" @@ -642,13 +642,13 @@ msgstr "" msgid "Email" msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:90 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:97 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:94 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:101 #, elixir-autogen, elixir-format msgid "Email change link is invalid or it has expired." msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:85 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:89 #, elixir-autogen, elixir-format msgid "Email changed successfully." msgstr "" @@ -704,12 +704,12 @@ msgstr "" msgid "Explain what is already arranged and what the volunteer needs to do." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:327 +#: lib/who_need_help_web/live/activity_live/index.ex:369 #, elixir-autogen, elixir-format msgid "Find people to join" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:747 +#: lib/who_need_help_web/live/activity_live/show.ex:765 #: lib/who_need_help_web/live/moderation_live.ex:251 #: lib/who_need_help_web/live/report_live.ex:112 #: lib/who_need_help_web/live/request_live/show.ex:1769 @@ -747,7 +747,7 @@ msgstr "" msgid "Handover code verified." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:745 +#: lib/who_need_help_web/live/activity_live/show.ex:763 #: lib/who_need_help_web/live/moderation_live.ex:250 #: lib/who_need_help_web/live/report_live.ex:111 #: lib/who_need_help_web/live/request_live/show.ex:1768 @@ -834,7 +834,7 @@ msgstr "" msgid "I confirm this is not an emergency, there is no immediate danger, and the requested task is lawful." msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:526 +#: lib/who_need_help_web/live/activity_live/new.ex:530 #, elixir-autogen, elixir-format msgid "I will approve participants individually, keep sensitive details in the approved group, and use an appropriate public first meeting point." msgstr "" @@ -844,14 +844,14 @@ msgstr "" msgid "Identity and reputation" msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:104 +#: lib/who_need_help_web/controllers/user_session_controller.ex:108 #, elixir-autogen, elixir-format msgid "If your email is in our system, you will receive instructions for logging in shortly." msgstr "" #: lib/who_need_help_web/controllers/content_removal_html.ex:9 #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:47 -#: lib/who_need_help_web/live/activity_live/show.ex:749 +#: lib/who_need_help_web/live/activity_live/show.ex:767 #: lib/who_need_help_web/live/moderation_live.ex:254 #: lib/who_need_help_web/live/report_live.ex:115 #: lib/who_need_help_web/live/request_live/show.ex:1772 @@ -865,8 +865,8 @@ msgstr "" msgid "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:309 -#: lib/who_need_help_web/live/request_live/index.ex:310 +#: lib/who_need_help_web/live/request_live/index.ex:323 +#: lib/who_need_help_web/live/request_live/index.ex:324 #: lib/who_need_help_web/live/request_live/show.ex:954 #, elixir-autogen, elixir-format msgid "In progress" @@ -883,7 +883,7 @@ msgstr "" msgid "Internal note" msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:153 +#: lib/who_need_help_web/controllers/user_session_controller.ex:157 #, elixir-autogen, elixir-format msgid "Invalid email or password" msgstr "" @@ -899,14 +899,14 @@ msgstr "" msgid "Join requests close" msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:482 +#: lib/who_need_help_web/live/activity_live/new.ex:486 #: lib/who_need_help_web/live/notification_live.ex:686 #: lib/who_need_help_web/live/request_live/new.ex:668 #, elixir-autogen, elixir-format msgid "Latitude" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:608 +#: lib/who_need_help_web/live/activity_live/show.ex:626 #, elixir-autogen, elixir-format msgid "Leave activity" msgstr "" @@ -917,7 +917,7 @@ msgid "Live location" msgstr "" #: lib/who_need_help_web/controllers/page_html/safety.html.heex:51 -#: lib/who_need_help_web/live/activity_live/show.ex:550 +#: lib/who_need_help_web/live/activity_live/show.ex:568 #, elixir-autogen, elixir-format msgid "Location privacy" msgstr "" @@ -952,12 +952,12 @@ msgstr "" msgid "Log in only this time" msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:133 +#: lib/who_need_help_web/controllers/user_session_controller.ex:137 #, elixir-autogen, elixir-format msgid "Logged out successfully." msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:496 +#: lib/who_need_help_web/live/activity_live/new.ex:500 #: lib/who_need_help_web/live/notification_live.ex:700 #: lib/who_need_help_web/live/request_live/new.ex:682 #, elixir-autogen, elixir-format @@ -979,7 +979,7 @@ msgstr "" msgid "Manually added links are public and marked unverified. A verified badge is only granted after completing a provider authorization flow." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:667 +#: lib/who_need_help_web/live/activity_live/show.ex:685 #, elixir-autogen, elixir-format msgid "Mark completed" msgstr "" @@ -989,8 +989,8 @@ msgstr "" msgid "Match and coordinate" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:307 -#: lib/who_need_help_web/live/request_live/index.ex:308 +#: lib/who_need_help_web/live/request_live/index.ex:321 +#: lib/who_need_help_web/live/request_live/index.ex:322 #: lib/who_need_help_web/live/request_live/show.ex:952 #, elixir-autogen, elixir-format msgid "Matched" @@ -1016,7 +1016,7 @@ msgstr "" msgid "Medicine requests" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:343 +#: lib/who_need_help_web/live/activity_live/index.ex:385 #, elixir-autogen, elixir-format msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe." msgstr "" @@ -1067,12 +1067,12 @@ msgstr "" msgid "Moderator access is required." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:333 +#: lib/who_need_help_web/live/request_live/index.ex:347 #, elixir-autogen, elixir-format msgid "NEARBY MUTUAL AID" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:33 +#: lib/who_need_help_web/live/request_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Nearby help" msgstr "" @@ -1116,7 +1116,7 @@ msgstr "" msgid "No open signals." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:662 +#: lib/who_need_help_web/live/activity_live/show.ex:680 #, elixir-autogen, elixir-format msgid "No pending join requests." msgstr "" @@ -1153,9 +1153,9 @@ msgid "Not for emergencies" msgstr "" #: lib/who_need_help_web/live/notification_live.ex:748 -#: lib/who_need_help_web/live/request_live/index.ex:297 -#: lib/who_need_help_web/live/request_live/index.ex:298 -#: lib/who_need_help_web/live/request_live/index.ex:409 +#: lib/who_need_help_web/live/request_live/index.ex:311 +#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:423 #: lib/who_need_help_web/live/request_live/new.ex:449 #: lib/who_need_help_web/live/request_live/show.ex:943 #: lib/who_need_help_web/live/request_live/show.ex:944 @@ -1163,7 +1163,7 @@ msgstr "" msgid "Now" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:325 +#: lib/who_need_help_web/live/activity_live/index.ex:367 #, elixir-autogen, elixir-format msgid "OPTIONAL SOCIAL PLANS" msgstr "" @@ -1181,12 +1181,12 @@ msgstr "" #: lib/who_need_help/notifications/text.ex:140 #: lib/who_need_help_web/controllers/content_removal_html.ex:21 #: lib/who_need_help_web/controllers/support_html.ex:16 -#: lib/who_need_help_web/live/activity_live/index.ex:314 +#: lib/who_need_help_web/live/activity_live/index.ex:356 #: lib/who_need_help_web/live/activity_live/show.ex:342 #: lib/who_need_help_web/live/moderation_live.ex:258 #: lib/who_need_help_web/live/report_live.ex:104 -#: lib/who_need_help_web/live/request_live/index.ex:305 -#: lib/who_need_help_web/live/request_live/index.ex:306 +#: lib/who_need_help_web/live/request_live/index.ex:319 +#: lib/who_need_help_web/live/request_live/index.ex:320 #: lib/who_need_help_web/live/request_live/show.ex:951 #: lib/who_need_help_web/live/support_operations_live.ex:237 #: lib/who_need_help_web/live/support_operations_live.ex:438 @@ -1224,7 +1224,7 @@ msgstr "" msgid "Organizer blocked. Their activities and messages are hidden." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:613 +#: lib/who_need_help_web/live/activity_live/show.ex:631 #, elixir-autogen, elixir-format msgid "Organizer controls" msgstr "" @@ -1239,7 +1239,7 @@ msgstr "" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:74 #: lib/who_need_help_web/controllers/support_html.ex:13 #: lib/who_need_help_web/controllers/support_html/new.html.heex:38 -#: lib/who_need_help_web/live/activity_live/show.ex:750 +#: lib/who_need_help_web/live/activity_live/show.ex:768 #: lib/who_need_help_web/live/moderation_live.ex:255 #: lib/who_need_help_web/live/profile_live.ex:320 #: lib/who_need_help_web/live/public_profile_live.ex:53 @@ -1263,7 +1263,7 @@ msgstr "" msgid "Password" msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:61 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:65 #, elixir-autogen, elixir-format msgid "Password updated successfully." msgstr "" @@ -1383,7 +1383,7 @@ msgstr "" msgid "Publish a clear public plan. Join requests stay pending until you approve them." msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:538 +#: lib/who_need_help_web/live/activity_live/new.ex:542 #, elixir-autogen, elixir-format msgid "Publish activity" msgstr "" @@ -1398,7 +1398,7 @@ msgstr "" msgid "Publish request" msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:536 +#: lib/who_need_help_web/live/activity_live/new.ex:540 #: lib/who_need_help_web/live/request_live/new.ex:742 #, elixir-autogen, elixir-format msgid "Publishing…" @@ -1434,7 +1434,7 @@ msgid "Read the" msgstr "" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:42 -#: lib/who_need_help_web/live/activity_live/show.ex:743 +#: lib/who_need_help_web/live/activity_live/show.ex:761 #: lib/who_need_help_web/live/request_live/show.ex:1342 #: lib/who_need_help_web/live/request_live/show.ex:1536 #: lib/who_need_help_web/live/request_live/show.ex:1579 @@ -1518,7 +1518,7 @@ msgstr "" msgid "Request restored." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:570 +#: lib/who_need_help_web/live/activity_live/show.ex:588 #, elixir-autogen, elixir-format msgid "Request to join" msgstr "" @@ -1603,7 +1603,7 @@ msgstr "" msgid "Russian name (optional)" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:713 +#: lib/who_need_help_web/live/activity_live/show.ex:731 #: lib/who_need_help_web/live/request_live/show.ex:1727 #, elixir-autogen, elixir-format msgid "Safety controls" @@ -1639,9 +1639,9 @@ msgid "Save profile" msgstr "" #: lib/who_need_help_web/live/notification_live.ex:750 -#: lib/who_need_help_web/live/request_live/index.ex:301 -#: lib/who_need_help_web/live/request_live/index.ex:302 -#: lib/who_need_help_web/live/request_live/index.ex:411 +#: lib/who_need_help_web/live/request_live/index.ex:315 +#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:425 #: lib/who_need_help_web/live/request_live/new.ex:451 #: lib/who_need_help_web/live/request_live/show.ex:947 #: lib/who_need_help_web/live/request_live/show.ex:948 @@ -1655,7 +1655,7 @@ msgstr "" msgid "Send" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:759 +#: lib/who_need_help_web/live/activity_live/show.ex:777 #: lib/who_need_help_web/live/request_live/show.ex:1782 #, elixir-autogen, elixir-format msgid "Send report" @@ -1753,7 +1753,7 @@ msgstr "" msgid "Social verification session is invalid." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:748 +#: lib/who_need_help_web/live/activity_live/show.ex:766 #: lib/who_need_help_web/live/moderation_live.ex:253 #: lib/who_need_help_web/live/report_live.ex:114 #: lib/who_need_help_web/live/request_live/show.ex:1771 @@ -1848,12 +1848,12 @@ msgstr "" msgid "The last administrator cannot demote themselves." msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:51 +#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #, elixir-autogen, elixir-format msgid "The link is invalid or it has expired." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:575 +#: lib/who_need_help_web/live/activity_live/show.ex:593 #, elixir-autogen, elixir-format msgid "The organizer has not approved your request yet." msgstr "" @@ -1868,12 +1868,12 @@ msgstr "" msgid "The platform never handles, requires, splits, or guarantees a payment." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:555 +#: lib/who_need_help_web/live/activity_live/show.ex:573 #, elixir-autogen, elixir-format msgid "The public map is approximate or hidden. Exact coordinates appear only after approval." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:731 +#: lib/who_need_help_web/live/activity_live/show.ex:749 #, elixir-autogen, elixir-format msgid "The report is scoped to the selected group message." msgstr "" @@ -1904,7 +1904,7 @@ msgstr "" msgid "This interaction is blocked." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:349 +#: lib/who_need_help_web/live/request_live/index.ex:363 #: lib/who_need_help_web/live/request_live/show.ex:1022 #, elixir-autogen, elixir-format msgid "This is not an emergency service. Contact local emergency services for immediate danger." @@ -1952,9 +1952,9 @@ msgid "To see sent emails, visit" msgstr "" #: lib/who_need_help_web/live/notification_live.ex:749 -#: lib/who_need_help_web/live/request_live/index.ex:299 -#: lib/who_need_help_web/live/request_live/index.ex:300 -#: lib/who_need_help_web/live/request_live/index.ex:410 +#: lib/who_need_help_web/live/request_live/index.ex:313 +#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:424 #: lib/who_need_help_web/live/request_live/new.ex:450 #: lib/who_need_help_web/live/request_live/show.ex:945 #: lib/who_need_help_web/live/request_live/show.ex:946 @@ -1971,8 +1971,8 @@ msgstr "" msgid "Too many actions in the configured time window." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:125 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:61 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:130 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:62 #, elixir-autogen, elixir-format msgid "Too many registration attempts in the configured time window." msgstr "" @@ -1982,8 +1982,8 @@ msgstr "" msgid "Too many requests in the configured time window." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:175 -#: lib/who_need_help_web/controllers/user_session_controller.ex:117 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:181 +#: lib/who_need_help_web/controllers/user_session_controller.ex:121 #, elixir-autogen, elixir-format msgid "Too many sign-in emails in the configured time window." msgstr "" @@ -2029,7 +2029,7 @@ msgid "Update email instructions" msgstr "" #: lib/who_need_help_web/live/notification_live.ex:743 -#: lib/who_need_help_web/live/request_live/index.ex:406 +#: lib/who_need_help_web/live/request_live/index.ex:420 #: lib/who_need_help_web/live/request_live/new.ex:447 #, elixir-autogen, elixir-format msgid "Urgency" @@ -2078,7 +2078,7 @@ msgstr "" msgid "User blocked. Their requests and new messages are hidden." msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:42 +#: lib/who_need_help_web/controllers/user_session_controller.ex:41 #, elixir-autogen, elixir-format msgid "User confirmed successfully." msgstr "" @@ -2129,10 +2129,10 @@ msgstr "" msgid "Waiting for a nearby volunteer." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:349 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:435 -#: lib/who_need_help_web/controllers/user_session_controller.ex:41 -#: lib/who_need_help_web/controllers/user_session_controller.ex:65 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:355 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:441 +#: lib/who_need_help_web/controllers/user_session_controller.ex:40 +#: lib/who_need_help_web/controllers/user_session_controller.ex:69 #, elixir-autogen, elixir-format msgid "Welcome back!" msgstr "" @@ -2142,7 +2142,7 @@ msgstr "" msgid "What Codex contributed" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:756 +#: lib/who_need_help_web/live/activity_live/show.ex:774 #: lib/who_need_help_web/live/request_live/show.ex:1779 #, elixir-autogen, elixir-format msgid "What happened?" @@ -2174,7 +2174,7 @@ msgstr "" msgid "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:334 +#: lib/who_need_help_web/live/request_live/index.ex:348 #, elixir-autogen, elixir-format msgid "Who needs help?" msgstr "" @@ -2214,7 +2214,7 @@ msgstr "" msgid "You already voted for this proposal." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:553 +#: lib/who_need_help_web/live/activity_live/show.ex:571 #, elixir-autogen, elixir-format msgid "You are approved, so the map can show the exact meeting coordinates." msgstr "" @@ -2292,7 +2292,7 @@ msgstr "" msgid "Your review is revealed only after both participants submit." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:602 +#: lib/who_need_help_web/live/request_live/index.ex:630 #, elixir-autogen, elixir-format msgid "Your trust summary" msgstr "" @@ -2313,17 +2313,17 @@ msgstr "" msgid "before confirming." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:464 +#: lib/who_need_help_web/live/activity_live/index.ex:577 #: lib/who_need_help_web/live/category_proposal_live.ex:252 #: lib/who_need_help_web/live/moderation_live.ex:372 -#: lib/who_need_help_web/live/request_live/index.ex:536 +#: lib/who_need_help_web/live/request_live/index.ex:564 #, elixir-autogen, elixir-format msgid "by %{name}" msgstr "" #: lib/who_need_help_web/live/profile_live.ex:190 #: lib/who_need_help_web/live/public_profile_live.ex:133 -#: lib/who_need_help_web/live/request_live/index.ex:606 +#: lib/who_need_help_web/live/request_live/index.ex:634 #: lib/who_need_help_web/live/request_live/show.ex:1374 #, elixir-autogen, elixir-format msgid "completed" @@ -2349,7 +2349,7 @@ msgstr "" msgid "no proximity evidence" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:683 +#: lib/who_need_help_web/live/activity_live/show.ex:701 #, elixir-autogen, elixir-format msgid "organizer" msgstr "" @@ -2359,7 +2359,7 @@ msgstr "" msgid "participants only" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:610 +#: lib/who_need_help_web/live/request_live/index.ex:638 #: lib/who_need_help_web/live/request_live/show.ex:1378 #, elixir-autogen, elixir-format msgid "people" @@ -2399,8 +2399,8 @@ msgid "unique people" msgstr "" #: lib/who_need_help_web/live/activity_live/show.ex:426 -#: lib/who_need_help_web/live/activity_live/show.ex:641 -#: lib/who_need_help_web/live/activity_live/show.ex:702 +#: lib/who_need_help_web/live/activity_live/show.ex:659 +#: lib/who_need_help_web/live/activity_live/show.ex:720 #: lib/who_need_help_web/live/profile_live.ex:286 #: lib/who_need_help_web/live/public_profile_live.ex:98 #: lib/who_need_help_web/live/request_live/show.ex:1094 @@ -2415,11 +2415,11 @@ msgid "urgent" msgstr "" #: lib/who_need_help_web/live/activity_live/show.ex:425 -#: lib/who_need_help_web/live/activity_live/show.ex:640 -#: lib/who_need_help_web/live/activity_live/show.ex:701 +#: lib/who_need_help_web/live/activity_live/show.ex:658 +#: lib/who_need_help_web/live/activity_live/show.ex:719 #: lib/who_need_help_web/live/profile_live.ex:285 #: lib/who_need_help_web/live/public_profile_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:614 +#: lib/who_need_help_web/live/request_live/index.ex:642 #: lib/who_need_help_web/live/request_live/show.ex:1093 #: lib/who_need_help_web/live/request_live/show.ex:1476 #, elixir-autogen, elixir-format @@ -2452,8 +2452,8 @@ msgstr "" msgid "Approved" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:315 -#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:329 +#: lib/who_need_help_web/live/request_live/index.ex:330 #, elixir-autogen, elixir-format msgid "Expired" msgstr "" @@ -2506,8 +2506,8 @@ msgstr "" msgid "Load earlier messages" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:476 -#: lib/who_need_help_web/live/activity_live/index.ex:564 +#: lib/who_need_help_web/live/activity_live/index.ex:589 +#: lib/who_need_help_web/live/activity_live/index.ex:679 #: lib/who_need_help_web/live/category_proposal_live.ex:284 #: lib/who_need_help_web/live/leaderboard_live.ex:91 #: lib/who_need_help_web/live/moderation_live.ex:465 @@ -2519,21 +2519,21 @@ msgstr "" #: lib/who_need_help_web/live/profile_live.ex:363 #: lib/who_need_help_web/live/public_profile_live.ex:178 #: lib/who_need_help_web/live/report_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:671 +#: lib/who_need_help_web/live/request_live/index.ex:699 #: lib/who_need_help_web/live/support_operations_live.ex:306 #: lib/who_need_help_web/live/support_operations_live.ex:505 #, elixir-autogen, elixir-format msgid "Load more" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:543 -#: lib/who_need_help_web/live/request_live/index.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:544 +#: lib/who_need_help_web/live/request_live/index.ex:610 #: lib/who_need_help_web/live/request_live/show.ex:1275 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Request details remain usable." msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:114 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:118 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:9 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:21 #, elixir-autogen, elixir-format @@ -2592,7 +2592,7 @@ msgstr "" msgid "The organizer is already attending." msgstr "" -#: lib/who_need_help_web/controllers/user_registration_controller.ex:70 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:71 #, elixir-autogen, elixir-format msgid "The registration form is invalid." msgstr "" @@ -2603,12 +2603,12 @@ msgstr "" msgid "The selected record is no longer available." msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:73 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:77 #, elixir-autogen, elixir-format msgid "The settings form is invalid." msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:126 +#: lib/who_need_help_web/controllers/user_session_controller.ex:130 #, elixir-autogen, elixir-format msgid "The sign-in form is invalid." msgstr "" @@ -2634,7 +2634,7 @@ msgstr "" msgid "This request has expired." msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:75 +#: lib/who_need_help_web/controllers/user_session_controller.ex:79 #, elixir-autogen, elixir-format msgid "Too many sign-in attempts in the configured time window." msgstr "" @@ -2644,12 +2644,12 @@ msgstr "" msgid "Your secure email link is ready. Continue to sign in." msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:140 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:144 #, elixir-autogen, elixir-format msgid "The confirmation email could not be sent. Please try again." msgstr "" -#: lib/who_need_help_web/controllers/user_registration_controller.ex:119 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:120 #, elixir-autogen, elixir-format msgid "If this address can be registered or signed in, login instructions will arrive shortly." msgstr "" @@ -2659,7 +2659,7 @@ msgstr "" msgid "This account cannot perform that action." msgstr "" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:41 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:45 #, elixir-autogen, elixir-format msgid "Too many email-change requests in the configured time window." msgstr "" @@ -2669,7 +2669,7 @@ msgstr "" msgid "A Google account is connected. You can use it to sign in." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:382 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:388 #, elixir-autogen, elixir-format msgid "A different Google account is already connected." msgstr "" @@ -2699,13 +2699,13 @@ msgstr "" msgid "Email me a sign-in link" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:390 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:396 #, elixir-autogen, elixir-format msgid "Google account connection session is invalid." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:218 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:288 #, elixir-autogen, elixir-format msgid "Google did not provide a verified email address." msgstr "" @@ -2715,38 +2715,38 @@ msgstr "" msgid "Google sign-in" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:372 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:378 #, elixir-autogen, elixir-format msgid "Google sign-in connected." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:315 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:321 #, elixir-autogen, elixir-format msgid "Google sign-in could not be started." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:217 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:287 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:294 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:223 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:293 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:300 #, elixir-autogen, elixir-format msgid "Google sign-in failed. Please try again." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:320 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:326 #, elixir-autogen, elixir-format msgid "Google sign-in is not configured yet." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:87 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:202 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:272 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:529 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:86 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:208 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:278 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 #, elixir-autogen, elixir-format msgid "Google sign-in session expired. Please start again." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:206 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:276 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 #, elixir-autogen, elixir-format msgid "Google sign-in session is invalid." msgstr "" @@ -2766,18 +2766,18 @@ msgstr "" msgid "Sign up with Google" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:377 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:383 #, elixir-autogen, elixir-format msgid "That Google account belongs to another local account." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:36 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:150 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:35 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:155 #, elixir-autogen, elixir-format msgid "The Google registration form is invalid." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:541 #, elixir-autogen, elixir-format msgid "This Google account cannot be used right now." msgstr "" @@ -2797,7 +2797,7 @@ msgstr "" msgid "We will send a one-time sign-in link. No password is required." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:105 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:110 #, elixir-autogen, elixir-format msgid "Your account was created with Google." msgstr "" @@ -2822,12 +2822,12 @@ msgstr "" msgid "Disconnect Google sign-in from this account?" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:51 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:50 #, elixir-autogen, elixir-format msgid "Google sign-in disconnected." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:56 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:55 #, elixir-autogen, elixir-format msgid "No Google account is connected." msgstr "" @@ -3262,7 +3262,7 @@ msgstr "" msgid "Subject" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:767 +#: lib/who_need_help_web/live/activity_live/show.ex:785 #: lib/who_need_help_web/live/request_live/show.ex:1790 #, elixir-autogen, elixir-format msgid "Submit a legal content-removal notice" @@ -3819,7 +3819,7 @@ msgstr "" msgid "Confirm access once so nobody can attach Google to your account using only a matching email address." msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:491 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:497 #: lib/who_need_help_web/controllers/google_auth_html/complete.html.heex:10 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:82 #, elixir-autogen, elixir-format @@ -3846,7 +3846,7 @@ msgstr "" msgid "Email me a verification link" msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:179 +#: lib/who_need_help_web/controllers/user_session_controller.ex:183 #, elixir-autogen, elixir-format msgid "Google sign-in connected. You can use it next time." msgstr "" @@ -3876,8 +3876,8 @@ msgstr "" msgid "This email already has an account" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:117 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:139 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:122 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:144 #, elixir-autogen, elixir-format msgid "This email already has an account. Confirm it once to connect Google." msgstr "" @@ -3893,12 +3893,12 @@ msgstr "" msgid "Verified" msgstr "" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:164 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:170 #, elixir-autogen, elixir-format msgid "We sent a verification link to %{email}. Open it to finish connecting Google." msgstr "" -#: lib/who_need_help_web/controllers/user_session_controller.ex:192 +#: lib/who_need_help_web/controllers/user_session_controller.ex:196 #, elixir-autogen, elixir-format msgid "You are signed in, but Google could not be connected. Try again in account settings." msgstr "" @@ -4017,7 +4017,7 @@ msgstr "" msgid "Date" msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:474 +#: lib/who_need_help_web/live/activity_live/new.ex:478 #: lib/who_need_help_web/live/notification_live.ex:683 #: lib/who_need_help_web/live/request_live/new.ex:660 #, elixir-autogen, elixir-format @@ -4216,7 +4216,7 @@ msgstr "" msgid "Request expires" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:569 +#: lib/who_need_help_web/live/activity_live/show.ex:587 #, elixir-autogen, elixir-format msgid "Request to join again" msgstr "" @@ -4431,12 +4431,12 @@ msgstr "" msgid "Where is help needed?" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:584 +#: lib/who_need_help_web/live/activity_live/show.ex:602 #, elixir-autogen, elixir-format msgid "You are no longer a participant and cannot access the group chat or exact meeting point. You can request to join again while registration is open." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:595 +#: lib/who_need_help_web/live/activity_live/show.ex:613 #, elixir-autogen, elixir-format msgid "You are not a participant. You can send a new request while registration is open." msgstr "" @@ -4451,7 +4451,7 @@ msgstr "" msgid "You have not submitted any content-removal notices while signed in." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:600 #, elixir-autogen, elixir-format msgid "You left this activity" msgstr "" @@ -4466,7 +4466,7 @@ msgstr "" msgid "Your exact position is not shown" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:593 +#: lib/who_need_help_web/live/activity_live/show.ex:611 #, elixir-autogen, elixir-format msgid "Your join request was declined" msgstr "" @@ -4516,42 +4516,44 @@ msgstr "" msgid "Exact point selected. Drag the red pin or click the map to move it." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:434 +#: lib/who_need_help_web/live/request_live/index.ex:453 #, elixir-autogen, elixir-format msgid "%{count} request in this map area" msgid_plural "%{count} requests in this map area" msgstr[0] "" msgstr[1] "" -#: lib/who_need_help_web/live/activity_live/index.ex:396 -#: lib/who_need_help_web/live/request_live/index.ex:416 +#: lib/who_need_help_web/live/activity_live/index.ex:438 +#: lib/who_need_help_web/live/request_live/index.ex:430 #, elixir-autogen, elixir-format msgid "Area or city" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:412 -#: lib/who_need_help_web/live/request_live/index.ex:487 +#: lib/who_need_help_web/live/activity_live/index.ex:525 +#: lib/who_need_help_web/live/request_live/index.ex:515 #, elixir-autogen, elixir-format msgid "Choose an area on the map" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:336 +#: lib/who_need_help_web/live/request_live/index.ex:350 #, elixir-autogen, elixir-format msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:455 +#: lib/who_need_help_web/live/activity_live/index.ex:484 +#: lib/who_need_help_web/live/request_live/index.ex:474 #, elixir-autogen, elixir-format msgid "List" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:558 +#: lib/who_need_help_web/live/request_live/index.ex:586 #, elixir-autogen, elixir-format msgid "Load more in this area" msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:5 -#: lib/who_need_help_web/live/request_live/index.ex:475 +#: lib/who_need_help_web/live/activity_live/index.ex:504 +#: lib/who_need_help_web/live/request_live/index.ex:494 #, elixir-autogen, elixir-format msgid "Map" msgstr "" @@ -4561,51 +4563,53 @@ msgstr "" msgid "Map preview" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:424 -#: lib/who_need_help_web/live/request_live/index.ex:499 +#: lib/who_need_help_web/live/activity_live/index.ex:537 +#: lib/who_need_help_web/live/request_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Move or zoom the map to search somewhere else." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:442 +#: lib/who_need_help_web/live/activity_live/index.ex:471 +#: lib/who_need_help_web/live/request_live/index.ex:461 #, elixir-autogen, elixir-format msgid "Move the map, then search the newly visible area." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:497 +#: lib/who_need_help_web/live/request_live/index.ex:525 #, elixir-autogen, elixir-format msgid "No open requests in this area" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:489 +#: lib/who_need_help_web/live/request_live/index.ex:517 #, elixir-autogen, elixir-format msgid "Only requests from the selected map area will be loaded." msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:8 -#: lib/who_need_help_web/live/request_live/index.ex:585 +#: lib/who_need_help_web/live/request_live/index.ex:613 #, elixir-autogen, elixir-format msgid "Open request" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:397 -#: lib/who_need_help_web/live/request_live/index.ex:417 +#: lib/who_need_help_web/live/activity_live/index.ex:439 +#: lib/who_need_help_web/live/request_live/index.ex:431 #, elixir-autogen, elixir-format msgid "Optional text search" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:564 +#: lib/who_need_help_web/live/request_live/index.ex:592 #, elixir-autogen, elixir-format msgid "Requests without map coordinates appear only when their public area description matches the text search." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:446 +#: lib/who_need_help_web/live/activity_live/index.ex:475 +#: lib/who_need_help_web/live/request_live/index.ex:465 #, elixir-autogen, elixir-format msgid "Results layout" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:508 -#: lib/who_need_help_web/live/request_live/index.ex:597 +#: lib/who_need_help_web/live/activity_live/index.ex:621 +#: lib/who_need_help_web/live/request_live/index.ex:625 #, elixir-autogen, elixir-format msgid "Search this area" msgstr "" @@ -4615,20 +4619,21 @@ msgstr "" msgid "Static preview" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/request_live/index.ex:484 #, elixir-autogen, elixir-format msgid "Together" msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:6 #: lib/who_need_help_web/controllers/page_html/home.html.heex:160 -#: lib/who_need_help_web/live/activity_live/index.ex:496 +#: lib/who_need_help_web/live/activity_live/index.ex:609 #: lib/who_need_help_web/live/activity_live/new.ex:355 #: lib/who_need_help_web/live/activity_live/new.ex:414 -#: lib/who_need_help_web/live/activity_live/show.ex:545 +#: lib/who_need_help_web/live/activity_live/show.ex:546 #: lib/who_need_help_web/live/notification_live.ex:602 #: lib/who_need_help_web/live/notification_live.ex:621 -#: lib/who_need_help_web/live/request_live/index.ex:584 +#: lib/who_need_help_web/live/request_live/index.ex:612 #: lib/who_need_help_web/live/request_live/new.ex:485 #: lib/who_need_help_web/live/request_live/new.ex:563 #: lib/who_need_help_web/live/request_live/show.ex:1277 @@ -5017,7 +5022,7 @@ msgid "Nearby alert deleted." msgstr "" #: lib/who_need_help_web/live/notification_live.ex:456 -#: lib/who_need_help_web/live/request_live/index.ex:362 +#: lib/who_need_help_web/live/request_live/index.ex:376 #, elixir-autogen, elixir-format msgid "Nearby requests" msgstr "" @@ -5357,8 +5362,8 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:7 -#: lib/who_need_help_web/live/activity_live/index.ex:497 -#: lib/who_need_help_web/live/activity_live/show.ex:546 +#: lib/who_need_help_web/live/activity_live/index.ex:610 +#: lib/who_need_help_web/live/activity_live/show.ex:547 #: lib/who_need_help_web/live/request_live/show.ex:1278 #, elixir-autogen, elixir-format msgid "Show location on map" @@ -5922,7 +5927,7 @@ msgstr "" msgid "Delivery settings" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:541 +#: lib/who_need_help_web/live/request_live/index.ex:569 #, elixir-autogen, elixir-format msgid "Expires" msgstr "" @@ -5942,29 +5947,29 @@ msgstr "" msgid "Notification sections" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:492 +#: lib/who_need_help_web/live/activity_live/index.ex:605 #, elixir-autogen, elixir-format msgid "%{count} activities" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:512 +#: lib/who_need_help_web/live/activity_live/index.ex:463 #, elixir-autogen, elixir-format msgid "%{count} activity in this map area" msgid_plural "%{count} activities in this map area" msgstr[0] "" msgstr[1] "" -#: lib/who_need_help_web/live/request_live/index.ex:586 +#: lib/who_need_help_web/live/request_live/index.ex:614 #, elixir-autogen, elixir-format msgid "%{count} requests" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:530 +#: lib/who_need_help_web/live/activity_live/index.ex:645 #, elixir-autogen, elixir-format msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:351 +#: lib/who_need_help_web/live/activity_live/index.ex:393 #, elixir-autogen, elixir-format msgid "Activity views" msgstr "" @@ -5979,12 +5984,12 @@ msgstr "" msgid "Confirm the safety guidance before asking nearby people for help." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:631 +#: lib/who_need_help_web/live/request_live/index.ex:659 #, elixir-autogen, elixir-format msgid "Create request" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:645 +#: lib/who_need_help_web/live/request_live/index.ex:673 #, elixir-autogen, elixir-format msgid "Create your first request" msgstr "" @@ -5999,43 +6004,43 @@ msgstr "" msgid "Location" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:357 +#: lib/who_need_help_web/live/activity_live/index.ex:399 #, elixir-autogen, elixir-format msgid "My activities" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:363 -#: lib/who_need_help_web/live/request_live/index.ex:625 +#: lib/who_need_help_web/live/request_live/index.ex:377 +#: lib/who_need_help_web/live/request_live/index.ex:653 #, elixir-autogen, elixir-format msgid "My requests" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:356 +#: lib/who_need_help_web/live/activity_live/index.ex:398 #, elixir-autogen, elixir-format msgid "Nearby activities" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:528 +#: lib/who_need_help_web/live/activity_live/index.ex:643 #, elixir-autogen, elixir-format msgid "No activities yet" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:422 +#: lib/who_need_help_web/live/activity_live/index.ex:535 #, elixir-autogen, elixir-format msgid "No open activities in this area" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:640 +#: lib/who_need_help_web/live/request_live/index.ex:668 #, elixir-autogen, elixir-format msgid "No requests yet" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:414 +#: lib/who_need_help_web/live/activity_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Only activities from the selected map area will be loaded." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:491 +#: lib/who_need_help_web/live/activity_live/index.ex:604 #, elixir-autogen, elixir-format msgid "Open activity" msgstr "" @@ -6045,12 +6050,12 @@ msgstr "" msgid "Request form steps" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:357 +#: lib/who_need_help_web/live/request_live/index.ex:371 #, elixir-autogen, elixir-format msgid "Request views" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:642 +#: lib/who_need_help_web/live/request_live/index.ex:670 #, elixir-autogen, elixir-format msgid "Requests you publish or accept will appear here." msgstr "" @@ -6075,7 +6080,7 @@ msgstr "" msgid "Share only as much location detail as helpers need." msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/activity_live/index.ex:607 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Activity details remain usable." msgstr "" @@ -6085,7 +6090,7 @@ msgstr "" msgid "Timing" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:627 +#: lib/who_need_help_web/live/request_live/index.ex:655 #, elixir-autogen, elixir-format msgid "Track requests you created and requests you accepted as a helper." msgstr "" @@ -6690,6 +6695,7 @@ msgid "People can find it by the area description" msgstr "" #: lib/who_need_help_web/live/activity_live/new.ex:455 +#: lib/who_need_help_web/live/activity_live/show.ex:560 #, elixir-autogen, elixir-format msgid "People can search by the public area description. Approved participants receive the exact point." msgstr "" @@ -6725,11 +6731,12 @@ msgid "The public map stays approximate" msgstr "" #: lib/who_need_help_web/live/activity_live/new.ex:453 +#: lib/who_need_help_web/live/activity_live/show.ex:558 #, elixir-autogen, elixir-format msgid "The public map stays hidden" msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:476 +#: lib/who_need_help_web/live/activity_live/new.ex:480 #, elixir-autogen, elixir-format msgid "These coordinates are the exact meeting point. They are revealed only after approval." msgstr "" @@ -6743,3 +6750,12 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Choose the meeting point on the map or use your current location." msgstr "" + +#: lib/who_need_help_web/live/activity_live/index.ex:458 +#: lib/who_need_help_web/live/activity_live/index.ex:517 +#: lib/who_need_help_web/live/activity_live/index.ex:635 +#: lib/who_need_help_web/live/request_live/index.ex:448 +#: lib/who_need_help_web/live/request_live/index.ex:507 +#, elixir-autogen, elixir-format +msgid "Loading results…" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 91bfc93..9cac1d9 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -13,7 +13,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:142 #: lib/who_need_help_web/components/layouts.ex:387 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:115 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:119 #, elixir-autogen, elixir-format msgid "Account settings" msgstr "Account settings" @@ -45,7 +45,7 @@ msgstr "Categories" #: lib/who_need_help_web/components/layouts.ex:95 #: lib/who_need_help_web/components/layouts.ex:171 #: lib/who_need_help_web/controllers/user_registration_html/new.html.heex:14 -#: lib/who_need_help_web/controllers/user_session_controller.ex:211 +#: lib/who_need_help_web/controllers/user_session_controller.ex:215 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:10 #, elixir-autogen, elixir-format msgid "Log in" @@ -66,7 +66,7 @@ msgstr "Profile" #: lib/who_need_help_web/components/layouts.ex:92 #: lib/who_need_help_web/components/layouts.ex:168 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:141 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:142 #, elixir-autogen, elixir-format msgid "Register" msgstr "Register" @@ -131,7 +131,7 @@ msgstr "4 — good" msgid "5 — excellent" msgstr "5 — excellent" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:130 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:134 #, elixir-autogen, elixir-format msgid "A link to confirm your email change has been sent to the new address." msgstr "A link to confirm your email change has been sent to the new address." @@ -179,7 +179,7 @@ msgid "Active" msgstr "Active" #: lib/who_need_help_web/components/layouts.ex:231 -#: lib/who_need_help_web/live/activity_live/index.ex:29 +#: lib/who_need_help_web/live/activity_live/index.ex:33 #, elixir-autogen, elixir-format msgid "Activities" msgstr "Activities" @@ -215,7 +215,7 @@ msgstr "Activity published. You approve every participant." msgid "Activity restored." msgstr "Activity restored." -#: lib/who_need_help_web/live/activity_live/index.ex:385 +#: lib/who_need_help_web/live/activity_live/index.ex:427 #: lib/who_need_help_web/live/activity_live/new.ex:204 #, elixir-autogen, elixir-format msgid "Activity type" @@ -232,12 +232,12 @@ msgstr "Add unverified link" msgid "Administrator" msgstr "Administrator" -#: lib/who_need_help_web/live/activity_live/index.ex:387 +#: lib/who_need_help_web/live/activity_live/index.ex:429 #, elixir-autogen, elixir-format msgid "All activities" msgstr "All activities" -#: lib/who_need_help_web/live/request_live/index.ex:393 +#: lib/who_need_help_web/live/request_live/index.ex:407 #, elixir-autogen, elixir-format msgid "All categories" msgstr "All categories" @@ -252,17 +252,17 @@ msgstr "Already registered?" msgid "Another helper already accepted this request." msgstr "Another helper already accepted this request." -#: lib/who_need_help_web/live/request_live/index.ex:408 +#: lib/who_need_help_web/live/request_live/index.ex:422 #, elixir-autogen, elixir-format msgid "Any urgency" msgstr "Any urgency" -#: lib/who_need_help_web/live/activity_live/show.ex:573 +#: lib/who_need_help_web/live/activity_live/show.ex:591 #, elixir-autogen, elixir-format msgid "Approval pending" msgstr "Approval pending" -#: lib/who_need_help_web/live/activity_live/show.ex:650 +#: lib/who_need_help_web/live/activity_live/show.ex:668 #, elixir-autogen, elixir-format msgid "Approve" msgstr "Approve" @@ -272,7 +272,7 @@ msgstr "Approve" msgid "Approved group chat" msgstr "Approved group chat" -#: lib/who_need_help_web/live/activity_live/show.ex:676 +#: lib/who_need_help_web/live/activity_live/show.ex:694 #, elixir-autogen, elixir-format msgid "Approved participants" msgstr "Approved participants" @@ -315,7 +315,7 @@ msgstr "Block a person to hide discovery in both directions and stop new chat. R msgid "Block and report" msgstr "Block and report" -#: lib/who_need_help_web/live/activity_live/show.ex:719 +#: lib/who_need_help_web/live/activity_live/show.ex:737 #, elixir-autogen, elixir-format msgid "Block organizer" msgstr "Block organizer" @@ -356,7 +356,7 @@ msgstr "COMMUNITY TRUST" msgid "COMMUNITY-DRIVEN TAXONOMY" msgstr "COMMUNITY-DRIVEN TAXONOMY" -#: lib/who_need_help_web/live/activity_live/show.ex:670 +#: lib/who_need_help_web/live/activity_live/show.ex:688 #, elixir-autogen, elixir-format msgid "Cancel activity" msgstr "Cancel activity" @@ -367,16 +367,16 @@ msgstr "Cancel activity" msgid "Cancel request" msgstr "Cancel request" -#: lib/who_need_help_web/live/activity_live/index.ex:316 +#: lib/who_need_help_web/live/activity_live/index.ex:358 #: lib/who_need_help_web/live/activity_live/show.ex:344 -#: lib/who_need_help_web/live/request_live/index.ex:313 -#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:327 +#: lib/who_need_help_web/live/request_live/index.ex:328 #: lib/who_need_help_web/live/request_live/show.ex:956 #, elixir-autogen, elixir-format msgid "Cancelled" msgstr "Cancelled" -#: lib/who_need_help_web/live/request_live/index.ex:391 +#: lib/who_need_help_web/live/request_live/index.ex:405 #: lib/who_need_help_web/live/request_live/new.ex:302 #, elixir-autogen, elixir-format msgid "Category" @@ -443,7 +443,7 @@ msgstr "Choose…" msgid "City centre, near the main entrance" msgstr "City centre, near the main entrance" -#: lib/who_need_help_web/live/activity_live/show.ex:737 +#: lib/who_need_help_web/live/activity_live/show.ex:755 #: lib/who_need_help_web/live/request_live/show.ex:1759 #, elixir-autogen, elixir-format msgid "Clear" @@ -459,7 +459,7 @@ msgstr "Close" msgid "Coffee and conversation" msgstr "Coffee and conversation" -#: lib/who_need_help_web/live/activity_live/index.ex:330 +#: lib/who_need_help_web/live/activity_live/index.ex:372 #, elixir-autogen, elixir-format msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings." msgstr "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings." @@ -474,19 +474,19 @@ msgstr "Comment (optional)" msgid "Community helpers" msgstr "Community helpers" -#: lib/who_need_help_web/live/activity_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:578 #: lib/who_need_help_web/live/activity_live/show.ex:403 -#: lib/who_need_help_web/live/activity_live/show.ex:621 -#: lib/who_need_help_web/live/activity_live/show.ex:680 -#: lib/who_need_help_web/live/request_live/index.ex:537 +#: lib/who_need_help_web/live/activity_live/show.ex:639 +#: lib/who_need_help_web/live/activity_live/show.ex:698 +#: lib/who_need_help_web/live/request_live/index.ex:565 #, elixir-autogen, elixir-format msgid "Community member" msgstr "Community member" -#: lib/who_need_help_web/live/activity_live/index.ex:315 +#: lib/who_need_help_web/live/activity_live/index.ex:357 #: lib/who_need_help_web/live/activity_live/show.ex:343 -#: lib/who_need_help_web/live/request_live/index.ex:311 -#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:325 +#: lib/who_need_help_web/live/request_live/index.ex:326 #: lib/who_need_help_web/live/request_live/show.ex:955 #: lib/who_need_help_web/live/request_live/show.ex:974 #, elixir-autogen, elixir-format @@ -537,13 +537,13 @@ msgstr "Confirmation instructions" msgid "Confirming..." msgstr "Confirming..." -#: lib/who_need_help_web/live/activity_live/index.ex:336 +#: lib/who_need_help_web/live/activity_live/index.ex:378 #: lib/who_need_help_web/live/activity_live/new.ex:12 #, elixir-autogen, elixir-format msgid "Create activity" msgstr "Create activity" -#: lib/who_need_help_web/live/activity_live/index.ex:535 +#: lib/who_need_help_web/live/activity_live/index.ex:650 #: lib/who_need_help_web/live/activity_live/new.ex:180 #, elixir-autogen, elixir-format msgid "Create an activity" @@ -554,12 +554,12 @@ msgstr "Create an activity" msgid "Create category" msgstr "Create category" -#: lib/who_need_help_web/live/request_live/index.ex:342 +#: lib/who_need_help_web/live/request_live/index.ex:356 #, elixir-autogen, elixir-format msgid "Create urgent request" msgstr "Create urgent request" -#: lib/who_need_help_web/live/activity_live/show.ex:746 +#: lib/who_need_help_web/live/activity_live/show.ex:764 #, elixir-autogen, elixir-format msgid "Dangerous plan" msgstr "Dangerous plan" @@ -586,7 +586,7 @@ msgstr "Decision note" msgid "Decisions and access to reported chat evidence are written to the audit log." msgstr "Decisions and access to reported chat evidence are written to the audit log." -#: lib/who_need_help_web/live/activity_live/show.ex:657 +#: lib/who_need_help_web/live/activity_live/show.ex:675 #, elixir-autogen, elixir-format msgid "Decline" msgstr "Decline" @@ -642,13 +642,13 @@ msgstr "Double-blind review" msgid "Email" msgstr "Email" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:90 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:97 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:94 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:101 #, elixir-autogen, elixir-format msgid "Email change link is invalid or it has expired." msgstr "Email change link is invalid or it has expired." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:85 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:89 #, elixir-autogen, elixir-format msgid "Email changed successfully." msgstr "Email changed successfully." @@ -704,12 +704,12 @@ msgstr "Expires:" msgid "Explain what is already arranged and what the volunteer needs to do." msgstr "Explain what is already arranged and what the volunteer needs to do." -#: lib/who_need_help_web/live/activity_live/index.ex:327 +#: lib/who_need_help_web/live/activity_live/index.ex:369 #, elixir-autogen, elixir-format msgid "Find people to join" msgstr "Find people to join" -#: lib/who_need_help_web/live/activity_live/show.ex:747 +#: lib/who_need_help_web/live/activity_live/show.ex:765 #: lib/who_need_help_web/live/moderation_live.ex:251 #: lib/who_need_help_web/live/report_live.ex:112 #: lib/who_need_help_web/live/request_live/show.ex:1769 @@ -747,7 +747,7 @@ msgstr "Handle (optional)" msgid "Handover code verified." msgstr "Handover code verified." -#: lib/who_need_help_web/live/activity_live/show.ex:745 +#: lib/who_need_help_web/live/activity_live/show.ex:763 #: lib/who_need_help_web/live/moderation_live.ex:250 #: lib/who_need_help_web/live/report_live.ex:111 #: lib/who_need_help_web/live/request_live/show.ex:1768 @@ -834,7 +834,7 @@ msgstr "I confirm there is no immediate danger, people and vehicles are away fro msgid "I confirm this is not an emergency, there is no immediate danger, and the requested task is lawful." msgstr "I confirm this is not an emergency, there is no immediate danger, and the requested task is lawful." -#: lib/who_need_help_web/live/activity_live/new.ex:526 +#: lib/who_need_help_web/live/activity_live/new.ex:530 #, elixir-autogen, elixir-format msgid "I will approve participants individually, keep sensitive details in the approved group, and use an appropriate public first meeting point." msgstr "I will approve participants individually, keep sensitive details in the approved group, and use an appropriate public first meeting point." @@ -844,14 +844,14 @@ msgstr "I will approve participants individually, keep sensitive details in the msgid "Identity and reputation" msgstr "Identity and reputation" -#: lib/who_need_help_web/controllers/user_session_controller.ex:104 +#: lib/who_need_help_web/controllers/user_session_controller.ex:108 #, elixir-autogen, elixir-format msgid "If your email is in our system, you will receive instructions for logging in shortly." msgstr "If your email is in our system, you will receive instructions for logging in shortly." #: lib/who_need_help_web/controllers/content_removal_html.ex:9 #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:47 -#: lib/who_need_help_web/live/activity_live/show.ex:749 +#: lib/who_need_help_web/live/activity_live/show.ex:767 #: lib/who_need_help_web/live/moderation_live.ex:254 #: lib/who_need_help_web/live/report_live.ex:115 #: lib/who_need_help_web/live/request_live/show.ex:1772 @@ -865,8 +865,8 @@ msgstr "Impersonation" msgid "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops." msgstr "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops." -#: lib/who_need_help_web/live/request_live/index.ex:309 -#: lib/who_need_help_web/live/request_live/index.ex:310 +#: lib/who_need_help_web/live/request_live/index.ex:323 +#: lib/who_need_help_web/live/request_live/index.ex:324 #: lib/who_need_help_web/live/request_live/show.ex:954 #, elixir-autogen, elixir-format msgid "In progress" @@ -883,7 +883,7 @@ msgstr "Interface language" msgid "Internal note" msgstr "Internal note" -#: lib/who_need_help_web/controllers/user_session_controller.ex:153 +#: lib/who_need_help_web/controllers/user_session_controller.ex:157 #, elixir-autogen, elixir-format msgid "Invalid email or password" msgstr "Invalid email or password" @@ -899,14 +899,14 @@ msgstr "Join requests are closed." msgid "Join requests close" msgstr "Join requests close" -#: lib/who_need_help_web/live/activity_live/new.ex:482 +#: lib/who_need_help_web/live/activity_live/new.ex:486 #: lib/who_need_help_web/live/notification_live.ex:686 #: lib/who_need_help_web/live/request_live/new.ex:668 #, elixir-autogen, elixir-format msgid "Latitude" msgstr "Latitude" -#: lib/who_need_help_web/live/activity_live/show.ex:608 +#: lib/who_need_help_web/live/activity_live/show.ex:626 #, elixir-autogen, elixir-format msgid "Leave activity" msgstr "Leave activity" @@ -917,7 +917,7 @@ msgid "Live location" msgstr "Live location" #: lib/who_need_help_web/controllers/page_html/safety.html.heex:51 -#: lib/who_need_help_web/live/activity_live/show.ex:550 +#: lib/who_need_help_web/live/activity_live/show.ex:568 #, elixir-autogen, elixir-format msgid "Location privacy" msgstr "Location privacy" @@ -952,12 +952,12 @@ msgstr "Log in instructions" msgid "Log in only this time" msgstr "Log in only this time" -#: lib/who_need_help_web/controllers/user_session_controller.ex:133 +#: lib/who_need_help_web/controllers/user_session_controller.ex:137 #, elixir-autogen, elixir-format msgid "Logged out successfully." msgstr "Logged out successfully." -#: lib/who_need_help_web/live/activity_live/new.ex:496 +#: lib/who_need_help_web/live/activity_live/new.ex:500 #: lib/who_need_help_web/live/notification_live.ex:700 #: lib/who_need_help_web/live/request_live/new.ex:682 #, elixir-autogen, elixir-format @@ -979,7 +979,7 @@ msgstr "Manage your account email address and password settings" msgid "Manually added links are public and marked unverified. A verified badge is only granted after completing a provider authorization flow." msgstr "Manually added links are public and marked unverified. A verified badge is only granted after completing a provider authorization flow." -#: lib/who_need_help_web/live/activity_live/show.ex:667 +#: lib/who_need_help_web/live/activity_live/show.ex:685 #, elixir-autogen, elixir-format msgid "Mark completed" msgstr "Mark completed" @@ -989,8 +989,8 @@ msgstr "Mark completed" msgid "Match and coordinate" msgstr "Match and coordinate" -#: lib/who_need_help_web/live/request_live/index.ex:307 -#: lib/who_need_help_web/live/request_live/index.ex:308 +#: lib/who_need_help_web/live/request_live/index.ex:321 +#: lib/who_need_help_web/live/request_live/index.ex:322 #: lib/who_need_help_web/live/request_live/show.ex:952 #, elixir-autogen, elixir-format msgid "Matched" @@ -1016,7 +1016,7 @@ msgstr "Medicine pickup remains the first priority. Roadside categories use the msgid "Medicine requests" msgstr "Medicine requests" -#: lib/who_need_help_web/live/activity_live/index.ex:343 +#: lib/who_need_help_web/live/activity_live/index.ex:385 #, elixir-autogen, elixir-format msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe." msgstr "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe." @@ -1067,12 +1067,12 @@ msgstr "Moderator" msgid "Moderator access is required." msgstr "Moderator access is required." -#: lib/who_need_help_web/live/request_live/index.ex:333 +#: lib/who_need_help_web/live/request_live/index.ex:347 #, elixir-autogen, elixir-format msgid "NEARBY MUTUAL AID" msgstr "NEARBY MUTUAL AID" -#: lib/who_need_help_web/live/request_live/index.ex:33 +#: lib/who_need_help_web/live/request_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Nearby help" msgstr "Nearby help" @@ -1116,7 +1116,7 @@ msgstr "No messages yet." msgid "No open signals." msgstr "No open signals." -#: lib/who_need_help_web/live/activity_live/show.ex:662 +#: lib/who_need_help_web/live/activity_live/show.ex:680 #, elixir-autogen, elixir-format msgid "No pending join requests." msgstr "No pending join requests." @@ -1153,9 +1153,9 @@ msgid "Not for emergencies" msgstr "Not for emergencies" #: lib/who_need_help_web/live/notification_live.ex:748 -#: lib/who_need_help_web/live/request_live/index.ex:297 -#: lib/who_need_help_web/live/request_live/index.ex:298 -#: lib/who_need_help_web/live/request_live/index.ex:409 +#: lib/who_need_help_web/live/request_live/index.ex:311 +#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:423 #: lib/who_need_help_web/live/request_live/new.ex:449 #: lib/who_need_help_web/live/request_live/show.ex:943 #: lib/who_need_help_web/live/request_live/show.ex:944 @@ -1163,7 +1163,7 @@ msgstr "Not for emergencies" msgid "Now" msgstr "Now" -#: lib/who_need_help_web/live/activity_live/index.ex:325 +#: lib/who_need_help_web/live/activity_live/index.ex:367 #, elixir-autogen, elixir-format msgid "OPTIONAL SOCIAL PLANS" msgstr "OPTIONAL SOCIAL PLANS" @@ -1181,12 +1181,12 @@ msgstr "Only the organizer and approved participants can read or send these mess #: lib/who_need_help/notifications/text.ex:140 #: lib/who_need_help_web/controllers/content_removal_html.ex:21 #: lib/who_need_help_web/controllers/support_html.ex:16 -#: lib/who_need_help_web/live/activity_live/index.ex:314 +#: lib/who_need_help_web/live/activity_live/index.ex:356 #: lib/who_need_help_web/live/activity_live/show.ex:342 #: lib/who_need_help_web/live/moderation_live.ex:258 #: lib/who_need_help_web/live/report_live.ex:104 -#: lib/who_need_help_web/live/request_live/index.ex:305 -#: lib/who_need_help_web/live/request_live/index.ex:306 +#: lib/who_need_help_web/live/request_live/index.ex:319 +#: lib/who_need_help_web/live/request_live/index.ex:320 #: lib/who_need_help_web/live/request_live/show.ex:951 #: lib/who_need_help_web/live/support_operations_live.ex:237 #: lib/who_need_help_web/live/support_operations_live.ex:438 @@ -1224,7 +1224,7 @@ msgstr "Organizer" msgid "Organizer blocked. Their activities and messages are hidden." msgstr "Organizer blocked. Their activities and messages are hidden." -#: lib/who_need_help_web/live/activity_live/show.ex:613 +#: lib/who_need_help_web/live/activity_live/show.ex:631 #, elixir-autogen, elixir-format msgid "Organizer controls" msgstr "Organizer controls" @@ -1239,7 +1239,7 @@ msgstr "Organizer social links" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:74 #: lib/who_need_help_web/controllers/support_html.ex:13 #: lib/who_need_help_web/controllers/support_html/new.html.heex:38 -#: lib/who_need_help_web/live/activity_live/show.ex:750 +#: lib/who_need_help_web/live/activity_live/show.ex:768 #: lib/who_need_help_web/live/moderation_live.ex:255 #: lib/who_need_help_web/live/profile_live.ex:320 #: lib/who_need_help_web/live/public_profile_live.ex:53 @@ -1263,7 +1263,7 @@ msgstr "Participant" msgid "Password" msgstr "Password" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:61 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:65 #, elixir-autogen, elixir-format msgid "Password updated successfully." msgstr "Password updated successfully." @@ -1383,7 +1383,7 @@ msgstr "Public reputation" msgid "Publish a clear public plan. Join requests stay pending until you approve them." msgstr "Publish a clear public plan. Join requests stay pending until you approve them." -#: lib/who_need_help_web/live/activity_live/new.ex:538 +#: lib/who_need_help_web/live/activity_live/new.ex:542 #, elixir-autogen, elixir-format msgid "Publish activity" msgstr "Publish activity" @@ -1398,7 +1398,7 @@ msgstr "Publish proposal" msgid "Publish request" msgstr "Publish request" -#: lib/who_need_help_web/live/activity_live/new.ex:536 +#: lib/who_need_help_web/live/activity_live/new.ex:540 #: lib/who_need_help_web/live/request_live/new.ex:742 #, elixir-autogen, elixir-format msgid "Publishing…" @@ -1434,7 +1434,7 @@ msgid "Read the" msgstr "Read the" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:42 -#: lib/who_need_help_web/live/activity_live/show.ex:743 +#: lib/who_need_help_web/live/activity_live/show.ex:761 #: lib/who_need_help_web/live/request_live/show.ex:1342 #: lib/who_need_help_web/live/request_live/show.ex:1536 #: lib/who_need_help_web/live/request_live/show.ex:1579 @@ -1518,7 +1518,7 @@ msgstr "Request hidden." msgid "Request restored." msgstr "Request restored." -#: lib/who_need_help_web/live/activity_live/show.ex:570 +#: lib/who_need_help_web/live/activity_live/show.ex:588 #, elixir-autogen, elixir-format msgid "Request to join" msgstr "Request to join" @@ -1603,7 +1603,7 @@ msgstr "Russian description (optional)" msgid "Russian name (optional)" msgstr "Russian name (optional)" -#: lib/who_need_help_web/live/activity_live/show.ex:713 +#: lib/who_need_help_web/live/activity_live/show.ex:731 #: lib/who_need_help_web/live/request_live/show.ex:1727 #, elixir-autogen, elixir-format msgid "Safety controls" @@ -1639,9 +1639,9 @@ msgid "Save profile" msgstr "Save profile" #: lib/who_need_help_web/live/notification_live.ex:750 -#: lib/who_need_help_web/live/request_live/index.ex:301 -#: lib/who_need_help_web/live/request_live/index.ex:302 -#: lib/who_need_help_web/live/request_live/index.ex:411 +#: lib/who_need_help_web/live/request_live/index.ex:315 +#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:425 #: lib/who_need_help_web/live/request_live/new.ex:451 #: lib/who_need_help_web/live/request_live/show.ex:947 #: lib/who_need_help_web/live/request_live/show.ex:948 @@ -1655,7 +1655,7 @@ msgstr "Scheduled" msgid "Send" msgstr "Send" -#: lib/who_need_help_web/live/activity_live/show.ex:759 +#: lib/who_need_help_web/live/activity_live/show.ex:777 #: lib/who_need_help_web/live/request_live/show.ex:1782 #, elixir-autogen, elixir-format msgid "Send report" @@ -1753,7 +1753,7 @@ msgstr "Social verification session expired. Please retry." msgid "Social verification session is invalid." msgstr "Social verification session is invalid." -#: lib/who_need_help_web/live/activity_live/show.ex:748 +#: lib/who_need_help_web/live/activity_live/show.ex:766 #: lib/who_need_help_web/live/moderation_live.ex:253 #: lib/who_need_help_web/live/report_live.ex:114 #: lib/who_need_help_web/live/request_live/show.ex:1771 @@ -1848,12 +1848,12 @@ msgstr "The browser could not share your location." msgid "The last administrator cannot demote themselves." msgstr "The last administrator cannot demote themselves." -#: lib/who_need_help_web/controllers/user_session_controller.ex:51 +#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #, elixir-autogen, elixir-format msgid "The link is invalid or it has expired." msgstr "The link is invalid or it has expired." -#: lib/who_need_help_web/live/activity_live/show.ex:575 +#: lib/who_need_help_web/live/activity_live/show.ex:593 #, elixir-autogen, elixir-format msgid "The organizer has not approved your request yet." msgstr "The organizer has not approved your request yet." @@ -1868,12 +1868,12 @@ msgstr "The pharmacy closes soon and I cannot leave home. The item is already pa msgid "The platform never handles, requires, splits, or guarantees a payment." msgstr "The platform never handles, requires, splits, or guarantees a payment." -#: lib/who_need_help_web/live/activity_live/show.ex:555 +#: lib/who_need_help_web/live/activity_live/show.ex:573 #, elixir-autogen, elixir-format msgid "The public map is approximate or hidden. Exact coordinates appear only after approval." msgstr "The public map is approximate or hidden. Exact coordinates appear only after approval." -#: lib/who_need_help_web/live/activity_live/show.ex:731 +#: lib/who_need_help_web/live/activity_live/show.ex:749 #, elixir-autogen, elixir-format msgid "The report is scoped to the selected group message." msgstr "The report is scoped to the selected group message." @@ -1904,7 +1904,7 @@ msgstr "This activity state has already changed." msgid "This interaction is blocked." msgstr "This interaction is blocked." -#: lib/who_need_help_web/live/request_live/index.ex:349 +#: lib/who_need_help_web/live/request_live/index.ex:363 #: lib/who_need_help_web/live/request_live/show.ex:1022 #, elixir-autogen, elixir-format msgid "This is not an emergency service. Contact local emergency services for immediate danger." @@ -1952,9 +1952,9 @@ msgid "To see sent emails, visit" msgstr "To see sent emails, visit" #: lib/who_need_help_web/live/notification_live.ex:749 -#: lib/who_need_help_web/live/request_live/index.ex:299 -#: lib/who_need_help_web/live/request_live/index.ex:300 -#: lib/who_need_help_web/live/request_live/index.ex:410 +#: lib/who_need_help_web/live/request_live/index.ex:313 +#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:424 #: lib/who_need_help_web/live/request_live/new.ex:450 #: lib/who_need_help_web/live/request_live/show.ex:945 #: lib/who_need_help_web/live/request_live/show.ex:946 @@ -1971,8 +1971,8 @@ msgstr "Today" msgid "Too many actions in the configured time window." msgstr "Too many actions in the configured time window." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:125 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:61 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:130 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:62 #, elixir-autogen, elixir-format msgid "Too many registration attempts in the configured time window." msgstr "Too many registration attempts in the configured time window." @@ -1982,8 +1982,8 @@ msgstr "Too many registration attempts in the configured time window." msgid "Too many requests in the configured time window." msgstr "Too many requests in the configured time window." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:175 -#: lib/who_need_help_web/controllers/user_session_controller.ex:117 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:181 +#: lib/who_need_help_web/controllers/user_session_controller.ex:121 #, elixir-autogen, elixir-format msgid "Too many sign-in emails in the configured time window." msgstr "Too many sign-in emails in the configured time window." @@ -2029,7 +2029,7 @@ msgid "Update email instructions" msgstr "Update email instructions" #: lib/who_need_help_web/live/notification_live.ex:743 -#: lib/who_need_help_web/live/request_live/index.ex:406 +#: lib/who_need_help_web/live/request_live/index.ex:420 #: lib/who_need_help_web/live/request_live/new.ex:447 #, elixir-autogen, elixir-format msgid "Urgency" @@ -2078,7 +2078,7 @@ msgstr "User" msgid "User blocked. Their requests and new messages are hidden." msgstr "User blocked. Their requests and new messages are hidden." -#: lib/who_need_help_web/controllers/user_session_controller.ex:42 +#: lib/who_need_help_web/controllers/user_session_controller.ex:41 #, elixir-autogen, elixir-format msgid "User confirmed successfully." msgstr "User confirmed successfully." @@ -2129,10 +2129,10 @@ msgstr "Visible reviews" msgid "Waiting for a nearby volunteer." msgstr "Waiting for a nearby volunteer." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:349 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:435 -#: lib/who_need_help_web/controllers/user_session_controller.ex:41 -#: lib/who_need_help_web/controllers/user_session_controller.ex:65 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:355 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:441 +#: lib/who_need_help_web/controllers/user_session_controller.ex:40 +#: lib/who_need_help_web/controllers/user_session_controller.ex:69 #, elixir-autogen, elixir-format msgid "Welcome back!" msgstr "Welcome back!" @@ -2142,7 +2142,7 @@ msgstr "Welcome back!" msgid "What Codex contributed" msgstr "What Codex contributed" -#: lib/who_need_help_web/live/activity_live/show.ex:756 +#: lib/who_need_help_web/live/activity_live/show.ex:774 #: lib/who_need_help_web/live/request_live/show.ex:1779 #, elixir-autogen, elixir-format msgid "What happened?" @@ -2174,7 +2174,7 @@ msgstr "Who Need Help home" msgid "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used." msgstr "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used." -#: lib/who_need_help_web/live/request_live/index.ex:334 +#: lib/who_need_help_web/live/request_live/index.ex:348 #, elixir-autogen, elixir-format msgid "Who needs help?" msgstr "Who needs help?" @@ -2214,7 +2214,7 @@ msgstr "You already have a join request for this activity." msgid "You already voted for this proposal." msgstr "You already voted for this proposal." -#: lib/who_need_help_web/live/activity_live/show.ex:553 +#: lib/who_need_help_web/live/activity_live/show.ex:571 #, elixir-autogen, elixir-format msgid "You are approved, so the map can show the exact meeting coordinates." msgstr "You are approved, so the map can show the exact meeting coordinates." @@ -2292,7 +2292,7 @@ msgstr "Your requests" msgid "Your review is revealed only after both participants submit." msgstr "Your review is revealed only after both participants submit." -#: lib/who_need_help_web/live/request_live/index.ex:602 +#: lib/who_need_help_web/live/request_live/index.ex:630 #, elixir-autogen, elixir-format msgid "Your trust summary" msgstr "Your trust summary" @@ -2313,17 +2313,17 @@ msgstr "activity message" msgid "before confirming." msgstr "before confirming." -#: lib/who_need_help_web/live/activity_live/index.ex:464 +#: lib/who_need_help_web/live/activity_live/index.ex:577 #: lib/who_need_help_web/live/category_proposal_live.ex:252 #: lib/who_need_help_web/live/moderation_live.ex:372 -#: lib/who_need_help_web/live/request_live/index.ex:536 +#: lib/who_need_help_web/live/request_live/index.ex:564 #, elixir-autogen, elixir-format msgid "by %{name}" msgstr "by %{name}" #: lib/who_need_help_web/live/profile_live.ex:190 #: lib/who_need_help_web/live/public_profile_live.ex:133 -#: lib/who_need_help_web/live/request_live/index.ex:606 +#: lib/who_need_help_web/live/request_live/index.ex:634 #: lib/who_need_help_web/live/request_live/show.ex:1374 #, elixir-autogen, elixir-format msgid "completed" @@ -2349,7 +2349,7 @@ msgstr "no movement evidence" msgid "no proximity evidence" msgstr "no proximity evidence" -#: lib/who_need_help_web/live/activity_live/show.ex:683 +#: lib/who_need_help_web/live/activity_live/show.ex:701 #, elixir-autogen, elixir-format msgid "organizer" msgstr "organizer" @@ -2359,7 +2359,7 @@ msgstr "organizer" msgid "participants only" msgstr "participants only" -#: lib/who_need_help_web/live/request_live/index.ex:610 +#: lib/who_need_help_web/live/request_live/index.ex:638 #: lib/who_need_help_web/live/request_live/show.ex:1378 #, elixir-autogen, elixir-format msgid "people" @@ -2399,8 +2399,8 @@ msgid "unique people" msgstr "unique people" #: lib/who_need_help_web/live/activity_live/show.ex:426 -#: lib/who_need_help_web/live/activity_live/show.ex:641 -#: lib/who_need_help_web/live/activity_live/show.ex:702 +#: lib/who_need_help_web/live/activity_live/show.ex:659 +#: lib/who_need_help_web/live/activity_live/show.ex:720 #: lib/who_need_help_web/live/profile_live.ex:286 #: lib/who_need_help_web/live/public_profile_live.ex:98 #: lib/who_need_help_web/live/request_live/show.ex:1094 @@ -2415,11 +2415,11 @@ msgid "urgent" msgstr "urgent" #: lib/who_need_help_web/live/activity_live/show.ex:425 -#: lib/who_need_help_web/live/activity_live/show.ex:640 -#: lib/who_need_help_web/live/activity_live/show.ex:701 +#: lib/who_need_help_web/live/activity_live/show.ex:658 +#: lib/who_need_help_web/live/activity_live/show.ex:719 #: lib/who_need_help_web/live/profile_live.ex:285 #: lib/who_need_help_web/live/public_profile_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:614 +#: lib/who_need_help_web/live/request_live/index.ex:642 #: lib/who_need_help_web/live/request_live/show.ex:1093 #: lib/who_need_help_web/live/request_live/show.ex:1476 #, elixir-autogen, elixir-format @@ -2452,8 +2452,8 @@ msgstr "← Requests" msgid "Approved" msgstr "Approved" -#: lib/who_need_help_web/live/request_live/index.ex:315 -#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:329 +#: lib/who_need_help_web/live/request_live/index.ex:330 #, elixir-autogen, elixir-format msgid "Expired" msgstr "Expired" @@ -2506,8 +2506,8 @@ msgstr "Unusual action rate" msgid "Load earlier messages" msgstr "Load earlier messages" -#: lib/who_need_help_web/live/activity_live/index.ex:476 -#: lib/who_need_help_web/live/activity_live/index.ex:564 +#: lib/who_need_help_web/live/activity_live/index.ex:589 +#: lib/who_need_help_web/live/activity_live/index.ex:679 #: lib/who_need_help_web/live/category_proposal_live.ex:284 #: lib/who_need_help_web/live/leaderboard_live.ex:91 #: lib/who_need_help_web/live/moderation_live.ex:465 @@ -2519,21 +2519,21 @@ msgstr "Load earlier messages" #: lib/who_need_help_web/live/profile_live.ex:363 #: lib/who_need_help_web/live/public_profile_live.ex:178 #: lib/who_need_help_web/live/report_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:671 +#: lib/who_need_help_web/live/request_live/index.ex:699 #: lib/who_need_help_web/live/support_operations_live.ex:306 #: lib/who_need_help_web/live/support_operations_live.ex:505 #, elixir-autogen, elixir-format msgid "Load more" msgstr "Load more" -#: lib/who_need_help_web/live/activity_live/show.ex:543 -#: lib/who_need_help_web/live/request_live/index.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:544 +#: lib/who_need_help_web/live/request_live/index.ex:610 #: lib/who_need_help_web/live/request_live/show.ex:1275 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Request details remain usable." msgstr "The map is unavailable in this browser. Request details remain usable." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:114 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:118 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:9 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:21 #, elixir-autogen, elixir-format @@ -2592,7 +2592,7 @@ msgstr "The organizer cannot leave the activity." msgid "The organizer is already attending." msgstr "The organizer is already attending." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:70 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:71 #, elixir-autogen, elixir-format msgid "The registration form is invalid." msgstr "The registration form is invalid." @@ -2603,12 +2603,12 @@ msgstr "The registration form is invalid." msgid "The selected record is no longer available." msgstr "The selected record is no longer available." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:73 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:77 #, elixir-autogen, elixir-format msgid "The settings form is invalid." msgstr "The settings form is invalid." -#: lib/who_need_help_web/controllers/user_session_controller.ex:126 +#: lib/who_need_help_web/controllers/user_session_controller.ex:130 #, elixir-autogen, elixir-format msgid "The sign-in form is invalid." msgstr "The sign-in form is invalid." @@ -2634,7 +2634,7 @@ msgstr "This proposal is no longer available." msgid "This request has expired." msgstr "This request has expired." -#: lib/who_need_help_web/controllers/user_session_controller.ex:75 +#: lib/who_need_help_web/controllers/user_session_controller.ex:79 #, elixir-autogen, elixir-format msgid "Too many sign-in attempts in the configured time window." msgstr "Too many sign-in attempts in the configured time window." @@ -2644,12 +2644,12 @@ msgstr "Too many sign-in attempts in the configured time window." msgid "Your secure email link is ready. Continue to sign in." msgstr "Your secure email link is ready. Continue to sign in." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:140 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:144 #, elixir-autogen, elixir-format msgid "The confirmation email could not be sent. Please try again." msgstr "The confirmation email could not be sent. Please try again." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:119 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:120 #, elixir-autogen, elixir-format msgid "If this address can be registered or signed in, login instructions will arrive shortly." msgstr "If this address can be registered or signed in, login instructions will arrive shortly." @@ -2659,7 +2659,7 @@ msgstr "If this address can be registered or signed in, login instructions will msgid "This account cannot perform that action." msgstr "This account cannot perform that action." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:41 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:45 #, elixir-autogen, elixir-format msgid "Too many email-change requests in the configured time window." msgstr "Too many email-change requests in the configured time window." @@ -2669,7 +2669,7 @@ msgstr "Too many email-change requests in the configured time window." msgid "A Google account is connected. You can use it to sign in." msgstr "A Google account is connected. You can use it to sign in." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:382 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:388 #, elixir-autogen, elixir-format msgid "A different Google account is already connected." msgstr "A different Google account is already connected." @@ -2699,13 +2699,13 @@ msgstr "Create account and email me a link" msgid "Email me a sign-in link" msgstr "Email me a sign-in link" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:390 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:396 #, elixir-autogen, elixir-format msgid "Google account connection session is invalid." msgstr "Google account connection session is invalid." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:218 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:288 #, elixir-autogen, elixir-format msgid "Google did not provide a verified email address." msgstr "Google did not provide a verified email address." @@ -2715,38 +2715,38 @@ msgstr "Google did not provide a verified email address." msgid "Google sign-in" msgstr "Google sign-in" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:372 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:378 #, elixir-autogen, elixir-format msgid "Google sign-in connected." msgstr "Google sign-in connected." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:315 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:321 #, elixir-autogen, elixir-format msgid "Google sign-in could not be started." msgstr "Google sign-in could not be started." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:217 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:287 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:294 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:223 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:293 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:300 #, elixir-autogen, elixir-format msgid "Google sign-in failed. Please try again." msgstr "Google sign-in failed. Please try again." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:320 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:326 #, elixir-autogen, elixir-format msgid "Google sign-in is not configured yet." msgstr "Google sign-in is not configured yet." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:87 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:202 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:272 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:529 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:86 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:208 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:278 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 #, elixir-autogen, elixir-format msgid "Google sign-in session expired. Please start again." msgstr "Google sign-in session expired. Please start again." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:206 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:276 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 #, elixir-autogen, elixir-format msgid "Google sign-in session is invalid." msgstr "Google sign-in session is invalid." @@ -2766,18 +2766,18 @@ msgstr "Sending link..." msgid "Sign up with Google" msgstr "Sign up with Google" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:377 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:383 #, elixir-autogen, elixir-format msgid "That Google account belongs to another local account." msgstr "That Google account belongs to another local account." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:36 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:150 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:35 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:155 #, elixir-autogen, elixir-format, fuzzy msgid "The Google registration form is invalid." msgstr "The Google registration form is invalid." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:541 #, elixir-autogen, elixir-format msgid "This Google account cannot be used right now." msgstr "This Google account cannot be used right now." @@ -2797,7 +2797,7 @@ msgstr "Use a password instead" msgid "We will send a one-time sign-in link. No password is required." msgstr "We will send a one-time sign-in link. No password is required." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:105 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:110 #, elixir-autogen, elixir-format msgid "Your account was created with Google." msgstr "Your account was created with Google." @@ -2822,12 +2822,12 @@ msgstr "Disconnect Google" msgid "Disconnect Google sign-in from this account?" msgstr "Disconnect Google sign-in from this account?" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:51 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:50 #, elixir-autogen, elixir-format msgid "Google sign-in disconnected." msgstr "Google sign-in disconnected." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:56 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:55 #, elixir-autogen, elixir-format msgid "No Google account is connected." msgstr "No Google account is connected." @@ -3262,7 +3262,7 @@ msgstr "Stated basis:" msgid "Subject" msgstr "Subject" -#: lib/who_need_help_web/live/activity_live/show.ex:767 +#: lib/who_need_help_web/live/activity_live/show.ex:785 #: lib/who_need_help_web/live/request_live/show.ex:1790 #, elixir-autogen, elixir-format msgid "Submit a legal content-removal notice" @@ -3819,7 +3819,7 @@ msgstr "Cancel and use another sign-in method" msgid "Confirm access once so nobody can attach Google to your account using only a matching email address." msgstr "Confirm access once so nobody can attach Google to your account using only a matching email address." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:491 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:497 #: lib/who_need_help_web/controllers/google_auth_html/complete.html.heex:10 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:82 #, elixir-autogen, elixir-format @@ -3846,7 +3846,7 @@ msgstr "Creating account..." msgid "Email me a verification link" msgstr "Email me a verification link" -#: lib/who_need_help_web/controllers/user_session_controller.ex:179 +#: lib/who_need_help_web/controllers/user_session_controller.ex:183 #, elixir-autogen, elixir-format msgid "Google sign-in connected. You can use it next time." msgstr "Google sign-in connected. You can use it next time." @@ -3876,8 +3876,8 @@ msgstr "Sign in to %{email} once to connect Google. You can use the email link o msgid "This email already has an account" msgstr "This email already has an account" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:117 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:139 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:122 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:144 #, elixir-autogen, elixir-format msgid "This email already has an account. Confirm it once to connect Google." msgstr "This email already has an account. Confirm it once to connect Google." @@ -3893,12 +3893,12 @@ msgstr "Use my password instead" msgid "Verified" msgstr "Verified" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:164 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:170 #, elixir-autogen, elixir-format msgid "We sent a verification link to %{email}. Open it to finish connecting Google." msgstr "We sent a verification link to %{email}. Open it to finish connecting Google." -#: lib/who_need_help_web/controllers/user_session_controller.ex:192 +#: lib/who_need_help_web/controllers/user_session_controller.ex:196 #, elixir-autogen, elixir-format msgid "You are signed in, but Google could not be connected. Try again in account settings." msgstr "You are signed in, but Google could not be connected. Try again in account settings." @@ -4017,7 +4017,7 @@ msgstr "" msgid "Date" msgstr "" -#: lib/who_need_help_web/live/activity_live/new.ex:474 +#: lib/who_need_help_web/live/activity_live/new.ex:478 #: lib/who_need_help_web/live/notification_live.ex:683 #: lib/who_need_help_web/live/request_live/new.ex:660 #, elixir-autogen, elixir-format @@ -4216,7 +4216,7 @@ msgstr "" msgid "Request expires" msgstr "Request expires" -#: lib/who_need_help_web/live/activity_live/show.ex:569 +#: lib/who_need_help_web/live/activity_live/show.ex:587 #, elixir-autogen, elixir-format, fuzzy msgid "Request to join again" msgstr "Request to join again" @@ -4431,12 +4431,12 @@ msgstr "" msgid "Where is help needed?" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:584 +#: lib/who_need_help_web/live/activity_live/show.ex:602 #, elixir-autogen, elixir-format msgid "You are no longer a participant and cannot access the group chat or exact meeting point. You can request to join again while registration is open." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:595 +#: lib/who_need_help_web/live/activity_live/show.ex:613 #, elixir-autogen, elixir-format msgid "You are not a participant. You can send a new request while registration is open." msgstr "" @@ -4451,7 +4451,7 @@ msgstr "" msgid "You have not submitted any content-removal notices while signed in." msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:600 #, elixir-autogen, elixir-format msgid "You left this activity" msgstr "" @@ -4466,7 +4466,7 @@ msgstr "" msgid "Your exact position is not shown" msgstr "" -#: lib/who_need_help_web/live/activity_live/show.ex:593 +#: lib/who_need_help_web/live/activity_live/show.ex:611 #, elixir-autogen, elixir-format, fuzzy msgid "Your join request was declined" msgstr "Your join request was declined" @@ -4516,42 +4516,44 @@ msgstr "" msgid "Exact point selected. Drag the red pin or click the map to move it." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:434 +#: lib/who_need_help_web/live/request_live/index.ex:453 #, elixir-autogen, elixir-format msgid "%{count} request in this map area" msgid_plural "%{count} requests in this map area" msgstr[0] "" msgstr[1] "" -#: lib/who_need_help_web/live/activity_live/index.ex:396 -#: lib/who_need_help_web/live/request_live/index.ex:416 +#: lib/who_need_help_web/live/activity_live/index.ex:438 +#: lib/who_need_help_web/live/request_live/index.ex:430 #, elixir-autogen, elixir-format msgid "Area or city" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:412 -#: lib/who_need_help_web/live/request_live/index.ex:487 +#: lib/who_need_help_web/live/activity_live/index.ex:525 +#: lib/who_need_help_web/live/request_live/index.ex:515 #, elixir-autogen, elixir-format msgid "Choose an area on the map" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:336 +#: lib/who_need_help_web/live/request_live/index.ex:350 #, elixir-autogen, elixir-format msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:455 +#: lib/who_need_help_web/live/activity_live/index.ex:484 +#: lib/who_need_help_web/live/request_live/index.ex:474 #, elixir-autogen, elixir-format msgid "List" msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:558 +#: lib/who_need_help_web/live/request_live/index.ex:586 #, elixir-autogen, elixir-format msgid "Load more in this area" msgstr "Load more in this area" #: lib/who_need_help_web/components/layouts/root.html.heex:5 -#: lib/who_need_help_web/live/request_live/index.ex:475 +#: lib/who_need_help_web/live/activity_live/index.ex:504 +#: lib/who_need_help_web/live/request_live/index.ex:494 #, elixir-autogen, elixir-format msgid "Map" msgstr "" @@ -4561,51 +4563,53 @@ msgstr "" msgid "Map preview" msgstr "Map preview" -#: lib/who_need_help_web/live/activity_live/index.ex:424 -#: lib/who_need_help_web/live/request_live/index.ex:499 +#: lib/who_need_help_web/live/activity_live/index.ex:537 +#: lib/who_need_help_web/live/request_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Move or zoom the map to search somewhere else." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:442 +#: lib/who_need_help_web/live/activity_live/index.ex:471 +#: lib/who_need_help_web/live/request_live/index.ex:461 #, elixir-autogen, elixir-format msgid "Move the map, then search the newly visible area." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:497 +#: lib/who_need_help_web/live/request_live/index.ex:525 #, elixir-autogen, elixir-format msgid "No open requests in this area" msgstr "No open requests in this area" -#: lib/who_need_help_web/live/request_live/index.ex:489 +#: lib/who_need_help_web/live/request_live/index.ex:517 #, elixir-autogen, elixir-format msgid "Only requests from the selected map area will be loaded." msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:8 -#: lib/who_need_help_web/live/request_live/index.ex:585 +#: lib/who_need_help_web/live/request_live/index.ex:613 #, elixir-autogen, elixir-format msgid "Open request" msgstr "Open request" -#: lib/who_need_help_web/live/activity_live/index.ex:397 -#: lib/who_need_help_web/live/request_live/index.ex:417 +#: lib/who_need_help_web/live/activity_live/index.ex:439 +#: lib/who_need_help_web/live/request_live/index.ex:431 #, elixir-autogen, elixir-format msgid "Optional text search" msgstr "Optional text search" -#: lib/who_need_help_web/live/request_live/index.ex:564 +#: lib/who_need_help_web/live/request_live/index.ex:592 #, elixir-autogen, elixir-format msgid "Requests without map coordinates appear only when their public area description matches the text search." msgstr "" -#: lib/who_need_help_web/live/request_live/index.ex:446 +#: lib/who_need_help_web/live/activity_live/index.ex:475 +#: lib/who_need_help_web/live/request_live/index.ex:465 #, elixir-autogen, elixir-format msgid "Results layout" msgstr "" -#: lib/who_need_help_web/live/activity_live/index.ex:508 -#: lib/who_need_help_web/live/request_live/index.ex:597 +#: lib/who_need_help_web/live/activity_live/index.ex:621 +#: lib/who_need_help_web/live/request_live/index.ex:625 #, elixir-autogen, elixir-format msgid "Search this area" msgstr "" @@ -4615,20 +4619,21 @@ msgstr "" msgid "Static preview" msgstr "Static preview" -#: lib/who_need_help_web/live/request_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/request_live/index.ex:484 #, elixir-autogen, elixir-format msgid "Together" msgstr "" #: lib/who_need_help_web/components/layouts/root.html.heex:6 #: lib/who_need_help_web/controllers/page_html/home.html.heex:160 -#: lib/who_need_help_web/live/activity_live/index.ex:496 +#: lib/who_need_help_web/live/activity_live/index.ex:609 #: lib/who_need_help_web/live/activity_live/new.ex:355 #: lib/who_need_help_web/live/activity_live/new.ex:414 -#: lib/who_need_help_web/live/activity_live/show.ex:545 +#: lib/who_need_help_web/live/activity_live/show.ex:546 #: lib/who_need_help_web/live/notification_live.ex:602 #: lib/who_need_help_web/live/notification_live.ex:621 -#: lib/who_need_help_web/live/request_live/index.ex:584 +#: lib/who_need_help_web/live/request_live/index.ex:612 #: lib/who_need_help_web/live/request_live/new.ex:485 #: lib/who_need_help_web/live/request_live/new.ex:563 #: lib/who_need_help_web/live/request_live/show.ex:1277 @@ -5017,7 +5022,7 @@ msgid "Nearby alert deleted." msgstr "" #: lib/who_need_help_web/live/notification_live.ex:456 -#: lib/who_need_help_web/live/request_live/index.ex:362 +#: lib/who_need_help_web/live/request_live/index.ex:376 #, elixir-autogen, elixir-format msgid "Nearby requests" msgstr "" @@ -5357,8 +5362,8 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec msgstr "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map." #: lib/who_need_help_web/components/layouts/root.html.heex:7 -#: lib/who_need_help_web/live/activity_live/index.ex:497 -#: lib/who_need_help_web/live/activity_live/show.ex:546 +#: lib/who_need_help_web/live/activity_live/index.ex:610 +#: lib/who_need_help_web/live/activity_live/show.ex:547 #: lib/who_need_help_web/live/request_live/show.ex:1278 #, elixir-autogen, elixir-format msgid "Show location on map" @@ -5922,7 +5927,7 @@ msgstr "Your review is now visible" msgid "Delivery settings" msgstr "Delivery settings" -#: lib/who_need_help_web/live/request_live/index.ex:541 +#: lib/who_need_help_web/live/request_live/index.ex:569 #, elixir-autogen, elixir-format msgid "Expires" msgstr "Expires" @@ -5942,29 +5947,29 @@ msgstr "Nearby alerts" msgid "Notification sections" msgstr "Notification sections" -#: lib/who_need_help_web/live/activity_live/index.ex:492 +#: lib/who_need_help_web/live/activity_live/index.ex:605 #, elixir-autogen, elixir-format msgid "%{count} activities" msgstr "%{count} activities" -#: lib/who_need_help_web/live/activity_live/index.ex:512 +#: lib/who_need_help_web/live/activity_live/index.ex:463 #, elixir-autogen, elixir-format msgid "%{count} activity in this map area" msgid_plural "%{count} activities in this map area" msgstr[0] "%{count} activity in this map area" msgstr[1] "%{count} activities in this map area" -#: lib/who_need_help_web/live/request_live/index.ex:586 +#: lib/who_need_help_web/live/request_live/index.ex:614 #, elixir-autogen, elixir-format msgid "%{count} requests" msgstr "%{count} requests" -#: lib/who_need_help_web/live/activity_live/index.ex:530 +#: lib/who_need_help_web/live/activity_live/index.ex:645 #, elixir-autogen, elixir-format msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list." msgstr "Activities you create or ask to join will appear here. Leaving removes an activity from this list." -#: lib/who_need_help_web/live/activity_live/index.ex:351 +#: lib/who_need_help_web/live/activity_live/index.ex:393 #, elixir-autogen, elixir-format msgid "Activity views" msgstr "Activity views" @@ -5979,12 +5984,12 @@ msgstr "Choose a category and describe the task clearly." msgid "Confirm the safety guidance before asking nearby people for help." msgstr "Confirm the safety guidance before asking nearby people for help." -#: lib/who_need_help_web/live/request_live/index.ex:631 +#: lib/who_need_help_web/live/request_live/index.ex:659 #, elixir-autogen, elixir-format msgid "Create request" msgstr "Create request" -#: lib/who_need_help_web/live/request_live/index.ex:645 +#: lib/who_need_help_web/live/request_live/index.ex:673 #, elixir-autogen, elixir-format msgid "Create your first request" msgstr "Create your first request" @@ -5999,43 +6004,43 @@ msgstr "Help details" msgid "Location" msgstr "Location" -#: lib/who_need_help_web/live/activity_live/index.ex:357 +#: lib/who_need_help_web/live/activity_live/index.ex:399 #, elixir-autogen, elixir-format msgid "My activities" msgstr "My activities" -#: lib/who_need_help_web/live/request_live/index.ex:363 -#: lib/who_need_help_web/live/request_live/index.ex:625 +#: lib/who_need_help_web/live/request_live/index.ex:377 +#: lib/who_need_help_web/live/request_live/index.ex:653 #, elixir-autogen, elixir-format msgid "My requests" msgstr "My requests" -#: lib/who_need_help_web/live/activity_live/index.ex:356 +#: lib/who_need_help_web/live/activity_live/index.ex:398 #, elixir-autogen, elixir-format msgid "Nearby activities" msgstr "Nearby activities" -#: lib/who_need_help_web/live/activity_live/index.ex:528 +#: lib/who_need_help_web/live/activity_live/index.ex:643 #, elixir-autogen, elixir-format msgid "No activities yet" msgstr "No activities yet" -#: lib/who_need_help_web/live/activity_live/index.ex:422 +#: lib/who_need_help_web/live/activity_live/index.ex:535 #, elixir-autogen, elixir-format msgid "No open activities in this area" msgstr "No open activities in this area" -#: lib/who_need_help_web/live/request_live/index.ex:640 +#: lib/who_need_help_web/live/request_live/index.ex:668 #, elixir-autogen, elixir-format msgid "No requests yet" msgstr "No requests yet" -#: lib/who_need_help_web/live/activity_live/index.ex:414 +#: lib/who_need_help_web/live/activity_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Only activities from the selected map area will be loaded." msgstr "Only activities from the selected map area will be loaded." -#: lib/who_need_help_web/live/activity_live/index.ex:491 +#: lib/who_need_help_web/live/activity_live/index.ex:604 #, elixir-autogen, elixir-format msgid "Open activity" msgstr "Open activity" @@ -6045,12 +6050,12 @@ msgstr "Open activity" msgid "Request form steps" msgstr "Request form steps" -#: lib/who_need_help_web/live/request_live/index.ex:357 +#: lib/who_need_help_web/live/request_live/index.ex:371 #, elixir-autogen, elixir-format msgid "Request views" msgstr "Request views" -#: lib/who_need_help_web/live/request_live/index.ex:642 +#: lib/who_need_help_web/live/request_live/index.ex:670 #, elixir-autogen, elixir-format msgid "Requests you publish or accept will appear here." msgstr "Requests you publish or accept will appear here." @@ -6075,7 +6080,7 @@ msgstr "Set urgency and when this request should close." msgid "Share only as much location detail as helpers need." msgstr "Share only as much location detail as helpers need." -#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/activity_live/index.ex:607 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Activity details remain usable." msgstr "The map is unavailable in this browser. Activity details remain usable." @@ -6085,7 +6090,7 @@ msgstr "The map is unavailable in this browser. Activity details remain usable." msgid "Timing" msgstr "Timing" -#: lib/who_need_help_web/live/request_live/index.ex:627 +#: lib/who_need_help_web/live/request_live/index.ex:655 #, elixir-autogen, elixir-format msgid "Track requests you created and requests you accepted as a helper." msgstr "Track requests you created and requests you accepted as a helper." @@ -6690,6 +6695,7 @@ msgid "People can find it by the area description" msgstr "People can find it by the area description" #: lib/who_need_help_web/live/activity_live/new.ex:455 +#: lib/who_need_help_web/live/activity_live/show.ex:560 #, elixir-autogen, elixir-format msgid "People can search by the public area description. Approved participants receive the exact point." msgstr "People can search by the public area description. Approved participants receive the exact point." @@ -6725,11 +6731,12 @@ msgid "The public map stays approximate" msgstr "The public map stays approximate" #: lib/who_need_help_web/live/activity_live/new.ex:453 +#: lib/who_need_help_web/live/activity_live/show.ex:558 #, elixir-autogen, elixir-format msgid "The public map stays hidden" msgstr "The public map stays hidden" -#: lib/who_need_help_web/live/activity_live/new.ex:476 +#: lib/who_need_help_web/live/activity_live/new.ex:480 #, elixir-autogen, elixir-format msgid "These coordinates are the exact meeting point. They are revealed only after approval." msgstr "These coordinates are the exact meeting point. They are revealed only after approval." @@ -6743,3 +6750,12 @@ msgstr "What should people see before approval?" #, elixir-autogen, elixir-format msgid "Choose the meeting point on the map or use your current location." msgstr "Choose the meeting point on the map or use your current location." + +#: lib/who_need_help_web/live/activity_live/index.ex:458 +#: lib/who_need_help_web/live/activity_live/index.ex:517 +#: lib/who_need_help_web/live/activity_live/index.ex:635 +#: lib/who_need_help_web/live/request_live/index.ex:448 +#: lib/who_need_help_web/live/request_live/index.ex:507 +#, elixir-autogen, elixir-format +msgid "Loading results…" +msgstr "Loading results…" diff --git a/priv/gettext/ru/LC_MESSAGES/default.po b/priv/gettext/ru/LC_MESSAGES/default.po index 5ae05dc..3497e18 100644 --- a/priv/gettext/ru/LC_MESSAGES/default.po +++ b/priv/gettext/ru/LC_MESSAGES/default.po @@ -13,7 +13,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:142 #: lib/who_need_help_web/components/layouts.ex:387 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:115 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:119 #, elixir-autogen, elixir-format msgid "Account settings" msgstr "Настройки аккаунта" @@ -45,7 +45,7 @@ msgstr "Категории" #: lib/who_need_help_web/components/layouts.ex:95 #: lib/who_need_help_web/components/layouts.ex:171 #: lib/who_need_help_web/controllers/user_registration_html/new.html.heex:14 -#: lib/who_need_help_web/controllers/user_session_controller.ex:211 +#: lib/who_need_help_web/controllers/user_session_controller.ex:215 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:10 #, elixir-autogen, elixir-format msgid "Log in" @@ -66,7 +66,7 @@ msgstr "Профиль" #: lib/who_need_help_web/components/layouts.ex:92 #: lib/who_need_help_web/components/layouts.ex:168 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:141 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:142 #, elixir-autogen, elixir-format msgid "Register" msgstr "Регистрация" @@ -131,7 +131,7 @@ msgstr "4 — хорошо" msgid "5 — excellent" msgstr "5 — отлично" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:130 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:134 #, elixir-autogen, elixir-format msgid "A link to confirm your email change has been sent to the new address." msgstr "" @@ -184,7 +184,7 @@ msgid "Active" msgstr "Активный" #: lib/who_need_help_web/components/layouts.ex:231 -#: lib/who_need_help_web/live/activity_live/index.ex:29 +#: lib/who_need_help_web/live/activity_live/index.ex:33 #, elixir-autogen, elixir-format msgid "Activities" msgstr "Активности" @@ -226,7 +226,7 @@ msgstr "Активность опубликована. Вы одобряете msgid "Activity restored." msgstr "Активность восстановлена." -#: lib/who_need_help_web/live/activity_live/index.ex:385 +#: lib/who_need_help_web/live/activity_live/index.ex:427 #: lib/who_need_help_web/live/activity_live/new.ex:204 #, elixir-autogen, elixir-format msgid "Activity type" @@ -243,12 +243,12 @@ msgstr "Добавить неподтверждённую ссылку" msgid "Administrator" msgstr "Администратор" -#: lib/who_need_help_web/live/activity_live/index.ex:387 +#: lib/who_need_help_web/live/activity_live/index.ex:429 #, elixir-autogen, elixir-format msgid "All activities" msgstr "Все активности" -#: lib/who_need_help_web/live/request_live/index.ex:393 +#: lib/who_need_help_web/live/request_live/index.ex:407 #, elixir-autogen, elixir-format msgid "All categories" msgstr "Все категории" @@ -263,17 +263,17 @@ msgstr "Уже зарегистрированы?" msgid "Another helper already accepted this request." msgstr "Другой помощник уже принял эту заявку." -#: lib/who_need_help_web/live/request_live/index.ex:408 +#: lib/who_need_help_web/live/request_live/index.ex:422 #, elixir-autogen, elixir-format msgid "Any urgency" msgstr "Любая срочность" -#: lib/who_need_help_web/live/activity_live/show.ex:573 +#: lib/who_need_help_web/live/activity_live/show.ex:591 #, elixir-autogen, elixir-format msgid "Approval pending" msgstr "Ожидает одобрения" -#: lib/who_need_help_web/live/activity_live/show.ex:650 +#: lib/who_need_help_web/live/activity_live/show.ex:668 #, elixir-autogen, elixir-format msgid "Approve" msgstr "Одобрить" @@ -283,7 +283,7 @@ msgstr "Одобрить" msgid "Approved group chat" msgstr "Чат одобренной группы" -#: lib/who_need_help_web/live/activity_live/show.ex:676 +#: lib/who_need_help_web/live/activity_live/show.ex:694 #, elixir-autogen, elixir-format msgid "Approved participants" msgstr "Одобренные участники" @@ -331,7 +331,7 @@ msgstr "" msgid "Block and report" msgstr "Блокировка и жалоба" -#: lib/who_need_help_web/live/activity_live/show.ex:719 +#: lib/who_need_help_web/live/activity_live/show.ex:737 #, elixir-autogen, elixir-format msgid "Block organizer" msgstr "Заблокировать организатора" @@ -372,7 +372,7 @@ msgstr "ДОВЕРИЕ СООБЩЕСТВА" msgid "COMMUNITY-DRIVEN TAXONOMY" msgstr "КАТЕГОРИИ ОТ СООБЩЕСТВА" -#: lib/who_need_help_web/live/activity_live/show.ex:670 +#: lib/who_need_help_web/live/activity_live/show.ex:688 #, elixir-autogen, elixir-format msgid "Cancel activity" msgstr "Отменить активность" @@ -383,16 +383,16 @@ msgstr "Отменить активность" msgid "Cancel request" msgstr "Отменить заявку" -#: lib/who_need_help_web/live/activity_live/index.ex:316 +#: lib/who_need_help_web/live/activity_live/index.ex:358 #: lib/who_need_help_web/live/activity_live/show.ex:344 -#: lib/who_need_help_web/live/request_live/index.ex:313 -#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:327 +#: lib/who_need_help_web/live/request_live/index.ex:328 #: lib/who_need_help_web/live/request_live/show.ex:956 #, elixir-autogen, elixir-format msgid "Cancelled" msgstr "Отменено" -#: lib/who_need_help_web/live/request_live/index.ex:391 +#: lib/who_need_help_web/live/request_live/index.ex:405 #: lib/who_need_help_web/live/request_live/new.ex:302 #, elixir-autogen, elixir-format msgid "Category" @@ -460,7 +460,7 @@ msgstr "Выберите…" msgid "City centre, near the main entrance" msgstr "Центр города, рядом с главным входом" -#: lib/who_need_help_web/live/activity_live/show.ex:737 +#: lib/who_need_help_web/live/activity_live/show.ex:755 #: lib/who_need_help_web/live/request_live/show.ex:1759 #, elixir-autogen, elixir-format msgid "Clear" @@ -476,7 +476,7 @@ msgstr "Закрыть" msgid "Coffee and conversation" msgstr "Кофе и разговоры" -#: lib/who_need_help_web/live/activity_live/index.ex:330 +#: lib/who_need_help_web/live/activity_live/index.ex:372 #, elixir-autogen, elixir-format msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings." msgstr "" @@ -493,19 +493,19 @@ msgstr "Комментарий (необязательно)" msgid "Community helpers" msgstr "Помощники сообщества" -#: lib/who_need_help_web/live/activity_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:578 #: lib/who_need_help_web/live/activity_live/show.ex:403 -#: lib/who_need_help_web/live/activity_live/show.ex:621 -#: lib/who_need_help_web/live/activity_live/show.ex:680 -#: lib/who_need_help_web/live/request_live/index.ex:537 +#: lib/who_need_help_web/live/activity_live/show.ex:639 +#: lib/who_need_help_web/live/activity_live/show.ex:698 +#: lib/who_need_help_web/live/request_live/index.ex:565 #, elixir-autogen, elixir-format msgid "Community member" msgstr "Член сообщества" -#: lib/who_need_help_web/live/activity_live/index.ex:315 +#: lib/who_need_help_web/live/activity_live/index.ex:357 #: lib/who_need_help_web/live/activity_live/show.ex:343 -#: lib/who_need_help_web/live/request_live/index.ex:311 -#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:325 +#: lib/who_need_help_web/live/request_live/index.ex:326 #: lib/who_need_help_web/live/request_live/show.ex:955 #: lib/who_need_help_web/live/request_live/show.ex:974 #, elixir-autogen, elixir-format @@ -558,13 +558,13 @@ msgstr "Инструкции по подтверждению" msgid "Confirming..." msgstr "Подтверждение…" -#: lib/who_need_help_web/live/activity_live/index.ex:336 +#: lib/who_need_help_web/live/activity_live/index.ex:378 #: lib/who_need_help_web/live/activity_live/new.ex:12 #, elixir-autogen, elixir-format msgid "Create activity" msgstr "Создать активность" -#: lib/who_need_help_web/live/activity_live/index.ex:535 +#: lib/who_need_help_web/live/activity_live/index.ex:650 #: lib/who_need_help_web/live/activity_live/new.ex:180 #, elixir-autogen, elixir-format msgid "Create an activity" @@ -575,12 +575,12 @@ msgstr "Создать активность" msgid "Create category" msgstr "Создать категорию" -#: lib/who_need_help_web/live/request_live/index.ex:342 +#: lib/who_need_help_web/live/request_live/index.ex:356 #, elixir-autogen, elixir-format msgid "Create urgent request" msgstr "Создать срочную заявку" -#: lib/who_need_help_web/live/activity_live/show.ex:746 +#: lib/who_need_help_web/live/activity_live/show.ex:764 #, elixir-autogen, elixir-format msgid "Dangerous plan" msgstr "Опасный план" @@ -609,7 +609,7 @@ msgstr "" "Решения и доступ к материалам жалоб на переписку записываются в журнал " "аудита." -#: lib/who_need_help_web/live/activity_live/show.ex:657 +#: lib/who_need_help_web/live/activity_live/show.ex:675 #, elixir-autogen, elixir-format msgid "Decline" msgstr "Отклонить" @@ -670,13 +670,13 @@ msgstr "Двусторонний скрытый отзыв" msgid "Email" msgstr "Электронная почта" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:90 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:97 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:94 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:101 #, elixir-autogen, elixir-format msgid "Email change link is invalid or it has expired." msgstr "Ссылка на изменение электронной почты недействительна или истекла." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:85 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:89 #, elixir-autogen, elixir-format msgid "Email changed successfully." msgstr "Адрес электронной почты успешно изменён." @@ -737,12 +737,12 @@ msgstr "Истекает:" msgid "Explain what is already arranged and what the volunteer needs to do." msgstr "Объясните, что уже сделано и что нужно сделать волонтеру." -#: lib/who_need_help_web/live/activity_live/index.ex:327 +#: lib/who_need_help_web/live/activity_live/index.ex:369 #, elixir-autogen, elixir-format msgid "Find people to join" msgstr "Найти компанию" -#: lib/who_need_help_web/live/activity_live/show.ex:747 +#: lib/who_need_help_web/live/activity_live/show.ex:765 #: lib/who_need_help_web/live/moderation_live.ex:251 #: lib/who_need_help_web/live/report_live.ex:112 #: lib/who_need_help_web/live/request_live/show.ex:1769 @@ -782,7 +782,7 @@ msgstr "Имя пользователя (необязательно)" msgid "Handover code verified." msgstr "Код передачи подтверждён." -#: lib/who_need_help_web/live/activity_live/show.ex:745 +#: lib/who_need_help_web/live/activity_live/show.ex:763 #: lib/who_need_help_web/live/moderation_live.ex:250 #: lib/who_need_help_web/live/report_live.ex:111 #: lib/who_need_help_web/live/request_live/show.ex:1768 @@ -899,7 +899,7 @@ msgstr "" "Я подтверждаю, что это не экстренная ситуация, непосредственной опасности " "нет, а запрашиваемая помощь законна." -#: lib/who_need_help_web/live/activity_live/new.ex:526 +#: lib/who_need_help_web/live/activity_live/new.ex:530 #, elixir-autogen, elixir-format msgid "I will approve participants individually, keep sensitive details in the approved group, and use an appropriate public first meeting point." msgstr "" @@ -912,7 +912,7 @@ msgstr "" msgid "Identity and reputation" msgstr "Личность и репутация" -#: lib/who_need_help_web/controllers/user_session_controller.ex:104 +#: lib/who_need_help_web/controllers/user_session_controller.ex:108 #, elixir-autogen, elixir-format msgid "If your email is in our system, you will receive instructions for logging in shortly." msgstr "" @@ -921,7 +921,7 @@ msgstr "" #: lib/who_need_help_web/controllers/content_removal_html.ex:9 #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:47 -#: lib/who_need_help_web/live/activity_live/show.ex:749 +#: lib/who_need_help_web/live/activity_live/show.ex:767 #: lib/who_need_help_web/live/moderation_live.ex:254 #: lib/who_need_help_web/live/report_live.ex:115 #: lib/who_need_help_web/live/request_live/show.ex:1772 @@ -938,8 +938,8 @@ msgstr "" "Android показывает постоянное уведомление и может продолжать передачу после " "сворачивания. При остановке передачи исходные координаты удаляются." -#: lib/who_need_help_web/live/request_live/index.ex:309 -#: lib/who_need_help_web/live/request_live/index.ex:310 +#: lib/who_need_help_web/live/request_live/index.ex:323 +#: lib/who_need_help_web/live/request_live/index.ex:324 #: lib/who_need_help_web/live/request_live/show.ex:954 #, elixir-autogen, elixir-format msgid "In progress" @@ -956,7 +956,7 @@ msgstr "Язык интерфейса" msgid "Internal note" msgstr "Внутренняя заметка" -#: lib/who_need_help_web/controllers/user_session_controller.ex:153 +#: lib/who_need_help_web/controllers/user_session_controller.ex:157 #, elixir-autogen, elixir-format msgid "Invalid email or password" msgstr "Неверная электронная почта или пароль" @@ -972,14 +972,14 @@ msgstr "Приём заявок на участие закрыт." msgid "Join requests close" msgstr "Приём заявок завершается" -#: lib/who_need_help_web/live/activity_live/new.ex:482 +#: lib/who_need_help_web/live/activity_live/new.ex:486 #: lib/who_need_help_web/live/notification_live.ex:686 #: lib/who_need_help_web/live/request_live/new.ex:668 #, elixir-autogen, elixir-format msgid "Latitude" msgstr "Широта" -#: lib/who_need_help_web/live/activity_live/show.ex:608 +#: lib/who_need_help_web/live/activity_live/show.ex:626 #, elixir-autogen, elixir-format msgid "Leave activity" msgstr "Покинуть активность" @@ -990,7 +990,7 @@ msgid "Live location" msgstr "Геопозиция в реальном времени" #: lib/who_need_help_web/controllers/page_html/safety.html.heex:51 -#: lib/who_need_help_web/live/activity_live/show.ex:550 +#: lib/who_need_help_web/live/activity_live/show.ex:568 #, elixir-autogen, elixir-format msgid "Location privacy" msgstr "Конфиденциальность геопозиции" @@ -1027,12 +1027,12 @@ msgstr "Инструкции для входа" msgid "Log in only this time" msgstr "Войти только сейчас" -#: lib/who_need_help_web/controllers/user_session_controller.ex:133 +#: lib/who_need_help_web/controllers/user_session_controller.ex:137 #, elixir-autogen, elixir-format msgid "Logged out successfully." msgstr "Вы успешно вышли." -#: lib/who_need_help_web/live/activity_live/new.ex:496 +#: lib/who_need_help_web/live/activity_live/new.ex:500 #: lib/who_need_help_web/live/notification_live.ex:700 #: lib/who_need_help_web/live/request_live/new.ex:682 #, elixir-autogen, elixir-format @@ -1059,7 +1059,7 @@ msgstr "" "подтверждения появляется только после авторизации у соответствующего " "провайдера." -#: lib/who_need_help_web/live/activity_live/show.ex:667 +#: lib/who_need_help_web/live/activity_live/show.ex:685 #, elixir-autogen, elixir-format msgid "Mark completed" msgstr "Отметить завершённой" @@ -1069,8 +1069,8 @@ msgstr "Отметить завершённой" msgid "Match and coordinate" msgstr "Найдите помощника и согласуйте детали" -#: lib/who_need_help_web/live/request_live/index.ex:307 -#: lib/who_need_help_web/live/request_live/index.ex:308 +#: lib/who_need_help_web/live/request_live/index.ex:321 +#: lib/who_need_help_web/live/request_live/index.ex:322 #: lib/who_need_help_web/live/request_live/show.ex:952 #, elixir-autogen, elixir-format msgid "Matched" @@ -1099,7 +1099,7 @@ msgstr "" msgid "Medicine requests" msgstr "Заявки на лекарства" -#: lib/who_need_help_web/live/activity_live/index.ex:343 +#: lib/who_need_help_web/live/activity_live/index.ex:385 #, elixir-autogen, elixir-format msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe." msgstr "" @@ -1153,12 +1153,12 @@ msgstr "Модератор" msgid "Moderator access is required." msgstr "Требуется доступ модератора." -#: lib/who_need_help_web/live/request_live/index.ex:333 +#: lib/who_need_help_web/live/request_live/index.ex:347 #, elixir-autogen, elixir-format msgid "NEARBY MUTUAL AID" msgstr "ВЗАИМОПОМОЩЬ РЯДОМ" -#: lib/who_need_help_web/live/request_live/index.ex:33 +#: lib/who_need_help_web/live/request_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Nearby help" msgstr "Помощь рядом" @@ -1202,7 +1202,7 @@ msgstr "Сообщений пока нет." msgid "No open signals." msgstr "Открытых сигналов нет." -#: lib/who_need_help_web/live/activity_live/show.ex:662 +#: lib/who_need_help_web/live/activity_live/show.ex:680 #, elixir-autogen, elixir-format msgid "No pending join requests." msgstr "Нет ожидающих заявок на участие." @@ -1241,9 +1241,9 @@ msgid "Not for emergencies" msgstr "Не для чрезвычайных ситуаций" #: lib/who_need_help_web/live/notification_live.ex:748 -#: lib/who_need_help_web/live/request_live/index.ex:297 -#: lib/who_need_help_web/live/request_live/index.ex:298 -#: lib/who_need_help_web/live/request_live/index.ex:409 +#: lib/who_need_help_web/live/request_live/index.ex:311 +#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:423 #: lib/who_need_help_web/live/request_live/new.ex:449 #: lib/who_need_help_web/live/request_live/show.ex:943 #: lib/who_need_help_web/live/request_live/show.ex:944 @@ -1251,7 +1251,7 @@ msgstr "Не для чрезвычайных ситуаций" msgid "Now" msgstr "Сейчас" -#: lib/who_need_help_web/live/activity_live/index.ex:325 +#: lib/who_need_help_web/live/activity_live/index.ex:367 #, elixir-autogen, elixir-format msgid "OPTIONAL SOCIAL PLANS" msgstr "НЕОБЯЗАТЕЛЬНЫЕ СОВМЕСТНЫЕ ПЛАНЫ" @@ -1271,12 +1271,12 @@ msgstr "" #: lib/who_need_help/notifications/text.ex:140 #: lib/who_need_help_web/controllers/content_removal_html.ex:21 #: lib/who_need_help_web/controllers/support_html.ex:16 -#: lib/who_need_help_web/live/activity_live/index.ex:314 +#: lib/who_need_help_web/live/activity_live/index.ex:356 #: lib/who_need_help_web/live/activity_live/show.ex:342 #: lib/who_need_help_web/live/moderation_live.ex:258 #: lib/who_need_help_web/live/report_live.ex:104 -#: lib/who_need_help_web/live/request_live/index.ex:305 -#: lib/who_need_help_web/live/request_live/index.ex:306 +#: lib/who_need_help_web/live/request_live/index.ex:319 +#: lib/who_need_help_web/live/request_live/index.ex:320 #: lib/who_need_help_web/live/request_live/show.ex:951 #: lib/who_need_help_web/live/support_operations_live.ex:237 #: lib/who_need_help_web/live/support_operations_live.ex:438 @@ -1314,7 +1314,7 @@ msgstr "Организатор" msgid "Organizer blocked. Their activities and messages are hidden." msgstr "Организатор заблокирован. Его активности и сообщения скрыты." -#: lib/who_need_help_web/live/activity_live/show.ex:613 +#: lib/who_need_help_web/live/activity_live/show.ex:631 #, elixir-autogen, elixir-format msgid "Organizer controls" msgstr "Управление организатора" @@ -1329,7 +1329,7 @@ msgstr "Ссылки организатора на соцсети" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:74 #: lib/who_need_help_web/controllers/support_html.ex:13 #: lib/who_need_help_web/controllers/support_html/new.html.heex:38 -#: lib/who_need_help_web/live/activity_live/show.ex:750 +#: lib/who_need_help_web/live/activity_live/show.ex:768 #: lib/who_need_help_web/live/moderation_live.ex:255 #: lib/who_need_help_web/live/profile_live.ex:320 #: lib/who_need_help_web/live/public_profile_live.ex:53 @@ -1353,7 +1353,7 @@ msgstr "Участник" msgid "Password" msgstr "Пароль" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:61 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:65 #, elixir-autogen, elixir-format msgid "Password updated successfully." msgstr "Пароль успешно обновлён." @@ -1486,7 +1486,7 @@ msgstr "" "Опубликуйте понятный публичный план. Заявки на участие будут ожидать, пока " "вы их не одобрите." -#: lib/who_need_help_web/live/activity_live/new.ex:538 +#: lib/who_need_help_web/live/activity_live/new.ex:542 #, elixir-autogen, elixir-format msgid "Publish activity" msgstr "Опубликовать активность" @@ -1501,7 +1501,7 @@ msgstr "Опубликовать предложение" msgid "Publish request" msgstr "Опубликовать заявку" -#: lib/who_need_help_web/live/activity_live/new.ex:536 +#: lib/who_need_help_web/live/activity_live/new.ex:540 #: lib/who_need_help_web/live/request_live/new.ex:742 #, elixir-autogen, elixir-format msgid "Publishing…" @@ -1537,7 +1537,7 @@ msgid "Read the" msgstr "Прочитайте" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:42 -#: lib/who_need_help_web/live/activity_live/show.ex:743 +#: lib/who_need_help_web/live/activity_live/show.ex:761 #: lib/who_need_help_web/live/request_live/show.ex:1342 #: lib/who_need_help_web/live/request_live/show.ex:1536 #: lib/who_need_help_web/live/request_live/show.ex:1579 @@ -1623,7 +1623,7 @@ msgstr "Заявка скрыта." msgid "Request restored." msgstr "Заявка восстановлена." -#: lib/who_need_help_web/live/activity_live/show.ex:570 +#: lib/who_need_help_web/live/activity_live/show.ex:588 #, elixir-autogen, elixir-format msgid "Request to join" msgstr "Подать заявку на участие" @@ -1709,7 +1709,7 @@ msgstr "Описание на русском (необязательно)" msgid "Russian name (optional)" msgstr "Название на русском (необязательно)" -#: lib/who_need_help_web/live/activity_live/show.ex:713 +#: lib/who_need_help_web/live/activity_live/show.ex:731 #: lib/who_need_help_web/live/request_live/show.ex:1727 #, elixir-autogen, elixir-format msgid "Safety controls" @@ -1748,9 +1748,9 @@ msgid "Save profile" msgstr "Сохранить профиль" #: lib/who_need_help_web/live/notification_live.ex:750 -#: lib/who_need_help_web/live/request_live/index.ex:301 -#: lib/who_need_help_web/live/request_live/index.ex:302 -#: lib/who_need_help_web/live/request_live/index.ex:411 +#: lib/who_need_help_web/live/request_live/index.ex:315 +#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:425 #: lib/who_need_help_web/live/request_live/new.ex:451 #: lib/who_need_help_web/live/request_live/show.ex:947 #: lib/who_need_help_web/live/request_live/show.ex:948 @@ -1764,7 +1764,7 @@ msgstr "Запланировано" msgid "Send" msgstr "Отправить" -#: lib/who_need_help_web/live/activity_live/show.ex:759 +#: lib/who_need_help_web/live/activity_live/show.ex:777 #: lib/who_need_help_web/live/request_live/show.ex:1782 #, elixir-autogen, elixir-format msgid "Send report" @@ -1866,7 +1866,7 @@ msgstr "Сеанс подтверждения соцсети завершилс msgid "Social verification session is invalid." msgstr "Сеанс социальной проверки недействителен." -#: lib/who_need_help_web/live/activity_live/show.ex:748 +#: lib/who_need_help_web/live/activity_live/show.ex:766 #: lib/who_need_help_web/live/moderation_live.ex:253 #: lib/who_need_help_web/live/report_live.ex:114 #: lib/who_need_help_web/live/request_live/show.ex:1771 @@ -1961,12 +1961,12 @@ msgstr "Браузеру не удалось передать вашу геоп msgid "The last administrator cannot demote themselves." msgstr "Последний администратор не может понизить собственную роль." -#: lib/who_need_help_web/controllers/user_session_controller.ex:51 +#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #, elixir-autogen, elixir-format msgid "The link is invalid or it has expired." msgstr "Ссылка недействительна или просрочена." -#: lib/who_need_help_web/live/activity_live/show.ex:575 +#: lib/who_need_help_web/live/activity_live/show.ex:593 #, elixir-autogen, elixir-format msgid "The organizer has not approved your request yet." msgstr "Организатор ещё не одобрил вашу заявку." @@ -1984,14 +1984,14 @@ msgstr "" "Платформа никогда не обрабатывает, не требует, не разделяет и не гарантирует" " оплату." -#: lib/who_need_help_web/live/activity_live/show.ex:555 +#: lib/who_need_help_web/live/activity_live/show.ex:573 #, elixir-autogen, elixir-format msgid "The public map is approximate or hidden. Exact coordinates appear only after approval." msgstr "" "Публичная карта показывает приблизительную область либо скрыта. Точные " "координаты появляются только после одобрения." -#: lib/who_need_help_web/live/activity_live/show.ex:731 +#: lib/who_need_help_web/live/activity_live/show.ex:749 #, elixir-autogen, elixir-format msgid "The report is scoped to the selected group message." msgstr "Жалоба относится к выбранному сообщению группы." @@ -2022,7 +2022,7 @@ msgstr "Состояние этой активности уже изменило msgid "This interaction is blocked." msgstr "Это взаимодействие заблокировано." -#: lib/who_need_help_web/live/request_live/index.ex:349 +#: lib/who_need_help_web/live/request_live/index.ex:363 #: lib/who_need_help_web/live/request_live/show.ex:1022 #, elixir-autogen, elixir-format msgid "This is not an emergency service. Contact local emergency services for immediate danger." @@ -2075,9 +2075,9 @@ msgid "To see sent emails, visit" msgstr "Чтобы посмотреть отправленные письма, откройте" #: lib/who_need_help_web/live/notification_live.ex:749 -#: lib/who_need_help_web/live/request_live/index.ex:299 -#: lib/who_need_help_web/live/request_live/index.ex:300 -#: lib/who_need_help_web/live/request_live/index.ex:410 +#: lib/who_need_help_web/live/request_live/index.ex:313 +#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:424 #: lib/who_need_help_web/live/request_live/new.ex:450 #: lib/who_need_help_web/live/request_live/show.ex:945 #: lib/who_need_help_web/live/request_live/show.ex:946 @@ -2094,8 +2094,8 @@ msgstr "Сегодня" msgid "Too many actions in the configured time window." msgstr "Слишком много действий за настроенный промежуток времени." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:125 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:61 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:130 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:62 #, elixir-autogen, elixir-format msgid "Too many registration attempts in the configured time window." msgstr "Слишком много попыток регистрации за настроенный промежуток времени." @@ -2105,8 +2105,8 @@ msgstr "Слишком много попыток регистрации за н msgid "Too many requests in the configured time window." msgstr "Слишком много запросов за настроенный промежуток времени." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:175 -#: lib/who_need_help_web/controllers/user_session_controller.ex:117 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:181 +#: lib/who_need_help_web/controllers/user_session_controller.ex:121 #, elixir-autogen, elixir-format msgid "Too many sign-in emails in the configured time window." msgstr "Слишком много писем для входа за настроенный промежуток времени." @@ -2152,7 +2152,7 @@ msgid "Update email instructions" msgstr "Инструкции для изменения электронной почты" #: lib/who_need_help_web/live/notification_live.ex:743 -#: lib/who_need_help_web/live/request_live/index.ex:406 +#: lib/who_need_help_web/live/request_live/index.ex:420 #: lib/who_need_help_web/live/request_live/new.ex:447 #, elixir-autogen, elixir-format msgid "Urgency" @@ -2206,7 +2206,7 @@ msgstr "Пользователь" msgid "User blocked. Their requests and new messages are hidden." msgstr "Пользователь заблокирован. Его заявки и новые сообщения скрыты." -#: lib/who_need_help_web/controllers/user_session_controller.ex:42 +#: lib/who_need_help_web/controllers/user_session_controller.ex:41 #, elixir-autogen, elixir-format msgid "User confirmed successfully." msgstr "Пользователь успешно подтверждён." @@ -2257,10 +2257,10 @@ msgstr "Открытые отзывы" msgid "Waiting for a nearby volunteer." msgstr "Ожидаем помощника поблизости." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:349 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:435 -#: lib/who_need_help_web/controllers/user_session_controller.ex:41 -#: lib/who_need_help_web/controllers/user_session_controller.ex:65 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:355 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:441 +#: lib/who_need_help_web/controllers/user_session_controller.ex:40 +#: lib/who_need_help_web/controllers/user_session_controller.ex:69 #, elixir-autogen, elixir-format msgid "Welcome back!" msgstr "С возвращением!" @@ -2270,7 +2270,7 @@ msgstr "С возвращением!" msgid "What Codex contributed" msgstr "Что сделал Codex" -#: lib/who_need_help_web/live/activity_live/show.ex:756 +#: lib/who_need_help_web/live/activity_live/show.ex:774 #: lib/who_need_help_web/live/request_live/show.ex:1779 #, elixir-autogen, elixir-format msgid "What happened?" @@ -2308,7 +2308,7 @@ msgstr "" "через подписку ChatGPT разработчика. Ключ OpenAI API и резервный вариант API" " с оплатой по использованию не применяются." -#: lib/who_need_help_web/live/request_live/index.ex:334 +#: lib/who_need_help_web/live/request_live/index.ex:348 #, elixir-autogen, elixir-format msgid "Who needs help?" msgstr "Кому нужна помощь?" @@ -2348,7 +2348,7 @@ msgstr "Вы уже подали заявку на участие в этой а msgid "You already voted for this proposal." msgstr "Вы уже проголосовали за это предложение." -#: lib/who_need_help_web/live/activity_live/show.ex:553 +#: lib/who_need_help_web/live/activity_live/show.ex:571 #, elixir-autogen, elixir-format msgid "You are approved, so the map can show the exact meeting coordinates." msgstr "" @@ -2432,7 +2432,7 @@ msgid "Your review is revealed only after both participants submit." msgstr "" "Ваш отзыв откроется только после того, как его отправят оба участника." -#: lib/who_need_help_web/live/request_live/index.ex:602 +#: lib/who_need_help_web/live/request_live/index.ex:630 #, elixir-autogen, elixir-format msgid "Your trust summary" msgstr "Ваши показатели доверия" @@ -2453,17 +2453,17 @@ msgstr "сообщение активности" msgid "before confirming." msgstr "перед подтверждением." -#: lib/who_need_help_web/live/activity_live/index.ex:464 +#: lib/who_need_help_web/live/activity_live/index.ex:577 #: lib/who_need_help_web/live/category_proposal_live.ex:252 #: lib/who_need_help_web/live/moderation_live.ex:372 -#: lib/who_need_help_web/live/request_live/index.ex:536 +#: lib/who_need_help_web/live/request_live/index.ex:564 #, elixir-autogen, elixir-format msgid "by %{name}" msgstr "автор: %{name}" #: lib/who_need_help_web/live/profile_live.ex:190 #: lib/who_need_help_web/live/public_profile_live.ex:133 -#: lib/who_need_help_web/live/request_live/index.ex:606 +#: lib/who_need_help_web/live/request_live/index.ex:634 #: lib/who_need_help_web/live/request_live/show.ex:1374 #, elixir-autogen, elixir-format msgid "completed" @@ -2489,7 +2489,7 @@ msgstr "нет подтверждения движения" msgid "no proximity evidence" msgstr "нет подтверждения близости" -#: lib/who_need_help_web/live/activity_live/show.ex:683 +#: lib/who_need_help_web/live/activity_live/show.ex:701 #, elixir-autogen, elixir-format msgid "organizer" msgstr "организатор" @@ -2499,7 +2499,7 @@ msgstr "организатор" msgid "participants only" msgstr "только участники" -#: lib/who_need_help_web/live/request_live/index.ex:610 +#: lib/who_need_help_web/live/request_live/index.ex:638 #: lib/who_need_help_web/live/request_live/show.ex:1378 #, elixir-autogen, elixir-format msgid "people" @@ -2539,8 +2539,8 @@ msgid "unique people" msgstr "Уникальные люди" #: lib/who_need_help_web/live/activity_live/show.ex:426 -#: lib/who_need_help_web/live/activity_live/show.ex:641 -#: lib/who_need_help_web/live/activity_live/show.ex:702 +#: lib/who_need_help_web/live/activity_live/show.ex:659 +#: lib/who_need_help_web/live/activity_live/show.ex:720 #: lib/who_need_help_web/live/profile_live.ex:286 #: lib/who_need_help_web/live/public_profile_live.ex:98 #: lib/who_need_help_web/live/request_live/show.ex:1094 @@ -2555,11 +2555,11 @@ msgid "urgent" msgstr "срочно" #: lib/who_need_help_web/live/activity_live/show.ex:425 -#: lib/who_need_help_web/live/activity_live/show.ex:640 -#: lib/who_need_help_web/live/activity_live/show.ex:701 +#: lib/who_need_help_web/live/activity_live/show.ex:658 +#: lib/who_need_help_web/live/activity_live/show.ex:719 #: lib/who_need_help_web/live/profile_live.ex:285 #: lib/who_need_help_web/live/public_profile_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:614 +#: lib/who_need_help_web/live/request_live/index.ex:642 #: lib/who_need_help_web/live/request_live/show.ex:1093 #: lib/who_need_help_web/live/request_live/show.ex:1476 #, elixir-autogen, elixir-format @@ -2592,8 +2592,8 @@ msgstr "← Заявки" msgid "Approved" msgstr "Одобрено" -#: lib/who_need_help_web/live/request_live/index.ex:315 -#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:329 +#: lib/who_need_help_web/live/request_live/index.ex:330 #, elixir-autogen, elixir-format msgid "Expired" msgstr "Истекло" @@ -2646,8 +2646,8 @@ msgstr "Необычная частота действий" msgid "Load earlier messages" msgstr "Загрузить предыдущие сообщения" -#: lib/who_need_help_web/live/activity_live/index.ex:476 -#: lib/who_need_help_web/live/activity_live/index.ex:564 +#: lib/who_need_help_web/live/activity_live/index.ex:589 +#: lib/who_need_help_web/live/activity_live/index.ex:679 #: lib/who_need_help_web/live/category_proposal_live.ex:284 #: lib/who_need_help_web/live/leaderboard_live.ex:91 #: lib/who_need_help_web/live/moderation_live.ex:465 @@ -2659,21 +2659,21 @@ msgstr "Загрузить предыдущие сообщения" #: lib/who_need_help_web/live/profile_live.ex:363 #: lib/who_need_help_web/live/public_profile_live.ex:178 #: lib/who_need_help_web/live/report_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:671 +#: lib/who_need_help_web/live/request_live/index.ex:699 #: lib/who_need_help_web/live/support_operations_live.ex:306 #: lib/who_need_help_web/live/support_operations_live.ex:505 #, elixir-autogen, elixir-format msgid "Load more" msgstr "Загрузить ещё" -#: lib/who_need_help_web/live/activity_live/show.ex:543 -#: lib/who_need_help_web/live/request_live/index.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:544 +#: lib/who_need_help_web/live/request_live/index.ex:610 #: lib/who_need_help_web/live/request_live/show.ex:1275 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Request details remain usable." msgstr "Карта недоступна в этом браузере. Остальная информация по-прежнему доступна." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:114 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:118 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:9 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:21 #, elixir-autogen, elixir-format @@ -2732,7 +2732,7 @@ msgstr "Организатор не может покинуть активнос msgid "The organizer is already attending." msgstr "Организатор уже участвует." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:70 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:71 #, elixir-autogen, elixir-format msgid "The registration form is invalid." msgstr "Форма регистрации заполнена неверно." @@ -2743,12 +2743,12 @@ msgstr "Форма регистрации заполнена неверно." msgid "The selected record is no longer available." msgstr "Выбранная запись больше недоступна." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:73 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:77 #, elixir-autogen, elixir-format msgid "The settings form is invalid." msgstr "Форма настроек заполнена неверно." -#: lib/who_need_help_web/controllers/user_session_controller.ex:126 +#: lib/who_need_help_web/controllers/user_session_controller.ex:130 #, elixir-autogen, elixir-format msgid "The sign-in form is invalid." msgstr "Форма входа заполнена неверно." @@ -2774,7 +2774,7 @@ msgstr "Это предложение больше недоступно." msgid "This request has expired." msgstr "Срок действия этой заявки истёк." -#: lib/who_need_help_web/controllers/user_session_controller.ex:75 +#: lib/who_need_help_web/controllers/user_session_controller.ex:79 #, elixir-autogen, elixir-format msgid "Too many sign-in attempts in the configured time window." msgstr "Слишком много попыток входа за настроенный промежуток времени." @@ -2784,12 +2784,12 @@ msgstr "Слишком много попыток входа за настрое msgid "Your secure email link is ready. Continue to sign in." msgstr "Защищённая ссылка из письма готова. Продолжите вход." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:140 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:144 #, elixir-autogen, elixir-format msgid "The confirmation email could not be sent. Please try again." msgstr "Не удалось отправить письмо с подтверждением. Попробуйте ещё раз." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:119 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:120 #, elixir-autogen, elixir-format msgid "If this address can be registered or signed in, login instructions will arrive shortly." msgstr "Если этот адрес можно зарегистрировать или использовать для входа, инструкции для входа скоро придут." @@ -2799,7 +2799,7 @@ msgstr "Если этот адрес можно зарегистрировать msgid "This account cannot perform that action." msgstr "Эта учётная запись не может выполнить это действие." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:41 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:45 #, elixir-autogen, elixir-format msgid "Too many email-change requests in the configured time window." msgstr "Слишком много запросов на смену email за настроенный промежуток времени." @@ -2809,7 +2809,7 @@ msgstr "Слишком много запросов на смену email за н msgid "A Google account is connected. You can use it to sign in." msgstr "Аккаунт Google подключён. Теперь с его помощью можно входить." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:382 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:388 #, elixir-autogen, elixir-format msgid "A different Google account is already connected." msgstr "Уже подключён другой аккаунт Google." @@ -2839,13 +2839,13 @@ msgstr "Создать аккаунт и отправить ссылку на em msgid "Email me a sign-in link" msgstr "Отправить ссылку для входа на email" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:390 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:396 #, elixir-autogen, elixir-format msgid "Google account connection session is invalid." msgstr "Сессия подключения аккаунта Google недействительна." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:218 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:288 #, elixir-autogen, elixir-format msgid "Google did not provide a verified email address." msgstr "Google не предоставил подтверждённый адрес email." @@ -2855,38 +2855,38 @@ msgstr "Google не предоставил подтверждённый адре msgid "Google sign-in" msgstr "Вход через Google" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:372 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:378 #, elixir-autogen, elixir-format msgid "Google sign-in connected." msgstr "Вход через Google подключён." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:315 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:321 #, elixir-autogen, elixir-format msgid "Google sign-in could not be started." msgstr "Не удалось начать вход через Google." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:217 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:287 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:294 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:223 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:293 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:300 #, elixir-autogen, elixir-format msgid "Google sign-in failed. Please try again." msgstr "Не удалось войти через Google. Попробуйте ещё раз." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:320 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:326 #, elixir-autogen, elixir-format msgid "Google sign-in is not configured yet." msgstr "Вход через Google пока не настроен." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:87 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:202 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:272 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:529 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:86 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:208 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:278 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 #, elixir-autogen, elixir-format msgid "Google sign-in session expired. Please start again." msgstr "Сессия входа через Google истекла. Начните заново." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:206 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:276 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 #, elixir-autogen, elixir-format msgid "Google sign-in session is invalid." msgstr "Сессия входа через Google недействительна." @@ -2906,18 +2906,18 @@ msgstr "Отправляем ссылку..." msgid "Sign up with Google" msgstr "Зарегистрироваться через Google" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:377 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:383 #, elixir-autogen, elixir-format msgid "That Google account belongs to another local account." msgstr "Этот аккаунт Google принадлежит другой локальной учётной записи." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:36 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:150 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:35 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:155 #, elixir-autogen, elixir-format msgid "The Google registration form is invalid." msgstr "Форма регистрации через Google заполнена неверно." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:541 #, elixir-autogen, elixir-format msgid "This Google account cannot be used right now." msgstr "Этот аккаунт Google сейчас нельзя использовать." @@ -2937,7 +2937,7 @@ msgstr "Войти по паролю" msgid "We will send a one-time sign-in link. No password is required." msgstr "Мы отправим одноразовую ссылку для входа. Пароль не требуется." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:105 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:110 #, elixir-autogen, elixir-format msgid "Your account was created with Google." msgstr "Ваша учётная запись создана через Google." @@ -2962,12 +2962,12 @@ msgstr "Отключить Google" msgid "Disconnect Google sign-in from this account?" msgstr "Отключить вход через Google для этой учётной записи?" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:51 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:50 #, elixir-autogen, elixir-format msgid "Google sign-in disconnected." msgstr "Вход через Google отключён." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:56 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:55 #, elixir-autogen, elixir-format msgid "No Google account is connected." msgstr "Аккаунт Google не подключён." @@ -3402,7 +3402,7 @@ msgstr "Указанное основание:" msgid "Subject" msgstr "Тема" -#: lib/who_need_help_web/live/activity_live/show.ex:767 +#: lib/who_need_help_web/live/activity_live/show.ex:785 #: lib/who_need_help_web/live/request_live/show.ex:1790 #, elixir-autogen, elixir-format msgid "Submit a legal content-removal notice" @@ -3959,7 +3959,7 @@ msgstr "Отменить и выбрать другой способ входа" msgid "Confirm access once so nobody can attach Google to your account using only a matching email address." msgstr "Один раз подтвердите доступ, чтобы никто не смог привязать Google к вашему аккаунту лишь по совпадающему email." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:491 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:497 #: lib/who_need_help_web/controllers/google_auth_html/complete.html.heex:10 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:82 #, elixir-autogen, elixir-format @@ -3986,7 +3986,7 @@ msgstr "Создаём аккаунт..." msgid "Email me a verification link" msgstr "Отправить ссылку для подтверждения на email" -#: lib/who_need_help_web/controllers/user_session_controller.ex:179 +#: lib/who_need_help_web/controllers/user_session_controller.ex:183 #, elixir-autogen, elixir-format msgid "Google sign-in connected. You can use it next time." msgstr "Вход через Google подключён. В следующий раз можно использовать его." @@ -4016,8 +4016,8 @@ msgstr "Один раз войдите в %{email}, чтобы подключи msgid "This email already has an account" msgstr "Для этого email уже есть аккаунт" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:117 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:139 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:122 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:144 #, elixir-autogen, elixir-format msgid "This email already has an account. Confirm it once to connect Google." msgstr "Для этого email уже есть аккаунт. Один раз подтвердите его, чтобы подключить Google." @@ -4033,12 +4033,12 @@ msgstr "Использовать пароль" msgid "Verified" msgstr "Подтверждено" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:164 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:170 #, elixir-autogen, elixir-format msgid "We sent a verification link to %{email}. Open it to finish connecting Google." msgstr "Мы отправили ссылку для подтверждения на %{email}. Откройте её, чтобы завершить подключение Google." -#: lib/who_need_help_web/controllers/user_session_controller.ex:192 +#: lib/who_need_help_web/controllers/user_session_controller.ex:196 #, elixir-autogen, elixir-format msgid "You are signed in, but Google could not be connected. Try again in account settings." msgstr "Вы вошли, но подключить Google не удалось. Повторите попытку в настройках аккаунта." @@ -4157,7 +4157,7 @@ msgstr "Не удалось скопировать" msgid "Date" msgstr "Дата" -#: lib/who_need_help_web/live/activity_live/new.ex:474 +#: lib/who_need_help_web/live/activity_live/new.ex:478 #: lib/who_need_help_web/live/notification_live.ex:683 #: lib/who_need_help_web/live/request_live/new.ex:660 #, elixir-autogen, elixir-format @@ -4356,7 +4356,7 @@ msgstr "Запрос на регистрацию принят" msgid "Request expires" msgstr "Заявка действует до" -#: lib/who_need_help_web/live/activity_live/show.ex:569 +#: lib/who_need_help_web/live/activity_live/show.ex:587 #, elixir-autogen, elixir-format msgid "Request to join again" msgstr "Подать заявку на участие" @@ -4571,12 +4571,12 @@ msgstr "Что должны видеть другие?" msgid "Where is help needed?" msgstr "Где нужна помощь?" -#: lib/who_need_help_web/live/activity_live/show.ex:584 +#: lib/who_need_help_web/live/activity_live/show.ex:602 #, elixir-autogen, elixir-format msgid "You are no longer a participant and cannot access the group chat or exact meeting point. You can request to join again while registration is open." msgstr "Вы больше не участник и не можете открыть групповой чат или точное место встречи. Пока регистрация открыта, можно снова подать заявку." -#: lib/who_need_help_web/live/activity_live/show.ex:595 +#: lib/who_need_help_web/live/activity_live/show.ex:613 #, elixir-autogen, elixir-format msgid "You are not a participant. You can send a new request while registration is open." msgstr "Вы не участвуете. Пока регистрация открыта, можно отправить новую заявку." @@ -4591,7 +4591,7 @@ msgstr "Можно отдельно поделиться геопозицией msgid "You have not submitted any content-removal notices while signed in." msgstr "После входа вы ещё не отправляли уведомления об удалении контента." -#: lib/who_need_help_web/live/activity_live/show.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:600 #, elixir-autogen, elixir-format msgid "You left this activity" msgstr "Вы вышли из этой активности" @@ -4606,7 +4606,7 @@ msgstr "Ваше подтверждение завершения сохране msgid "Your exact position is not shown" msgstr "Ваша точная позиция не показывается" -#: lib/who_need_help_web/live/activity_live/show.ex:593 +#: lib/who_need_help_web/live/activity_live/show.ex:611 #, elixir-autogen, elixir-format msgid "Your join request was declined" msgstr "Ваша заявка на участие отклонена" @@ -4656,7 +4656,7 @@ msgstr "Перетащите, чтобы переместить точную т msgid "Exact point selected. Drag the red pin or click the map to move it." msgstr "Точная точка выбрана. Перетащите красный маркер или нажмите на карту, чтобы переместить её." -#: lib/who_need_help_web/live/request_live/index.ex:434 +#: lib/who_need_help_web/live/request_live/index.ex:453 #, elixir-autogen, elixir-format msgid "%{count} request in this map area" msgid_plural "%{count} requests in this map area" @@ -4664,35 +4664,37 @@ msgstr[0] "%{count} заявка в этой области карты" msgstr[1] "%{count} заявки в этой области карты" msgstr[2] "%{count} заявок в этой области карты" -#: lib/who_need_help_web/live/activity_live/index.ex:396 -#: lib/who_need_help_web/live/request_live/index.ex:416 +#: lib/who_need_help_web/live/activity_live/index.ex:438 +#: lib/who_need_help_web/live/request_live/index.ex:430 #, elixir-autogen, elixir-format msgid "Area or city" msgstr "Район или город" -#: lib/who_need_help_web/live/activity_live/index.ex:412 -#: lib/who_need_help_web/live/request_live/index.ex:487 +#: lib/who_need_help_web/live/activity_live/index.ex:525 +#: lib/who_need_help_web/live/request_live/index.ex:515 #, elixir-autogen, elixir-format msgid "Choose an area on the map" msgstr "Выберите область на карте" -#: lib/who_need_help_web/live/request_live/index.ex:336 +#: lib/who_need_help_web/live/request_live/index.ex:350 #, elixir-autogen, elixir-format msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters." msgstr "Просматривайте безопасные для приватности области и точные публичные места встречи, выбранные авторами заявок." -#: lib/who_need_help_web/live/request_live/index.ex:455 +#: lib/who_need_help_web/live/activity_live/index.ex:484 +#: lib/who_need_help_web/live/request_live/index.ex:474 #, elixir-autogen, elixir-format msgid "List" msgstr "Список" -#: lib/who_need_help_web/live/request_live/index.ex:558 +#: lib/who_need_help_web/live/request_live/index.ex:586 #, elixir-autogen, elixir-format msgid "Load more in this area" msgstr "Загрузить ещё в этой области" #: lib/who_need_help_web/components/layouts/root.html.heex:5 -#: lib/who_need_help_web/live/request_live/index.ex:475 +#: lib/who_need_help_web/live/activity_live/index.ex:504 +#: lib/who_need_help_web/live/request_live/index.ex:494 #, elixir-autogen, elixir-format msgid "Map" msgstr "Карта" @@ -4702,51 +4704,53 @@ msgstr "Карта" msgid "Map preview" msgstr "Предпросмотр карты" -#: lib/who_need_help_web/live/activity_live/index.ex:424 -#: lib/who_need_help_web/live/request_live/index.ex:499 +#: lib/who_need_help_web/live/activity_live/index.ex:537 +#: lib/who_need_help_web/live/request_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Move or zoom the map to search somewhere else." msgstr "Переместите или масштабируйте карту, чтобы искать в другом месте." -#: lib/who_need_help_web/live/request_live/index.ex:442 +#: lib/who_need_help_web/live/activity_live/index.ex:471 +#: lib/who_need_help_web/live/request_live/index.ex:461 #, elixir-autogen, elixir-format msgid "Move the map, then search the newly visible area." msgstr "Переместите карту, затем выполните поиск в новой видимой области." -#: lib/who_need_help_web/live/request_live/index.ex:497 +#: lib/who_need_help_web/live/request_live/index.ex:525 #, elixir-autogen, elixir-format msgid "No open requests in this area" msgstr "В этой области нет открытых заявок" -#: lib/who_need_help_web/live/request_live/index.ex:489 +#: lib/who_need_help_web/live/request_live/index.ex:517 #, elixir-autogen, elixir-format msgid "Only requests from the selected map area will be loaded." msgstr "Будут загружены только заявки из выбранной области карты." #: lib/who_need_help_web/components/layouts/root.html.heex:8 -#: lib/who_need_help_web/live/request_live/index.ex:585 +#: lib/who_need_help_web/live/request_live/index.ex:613 #, elixir-autogen, elixir-format msgid "Open request" msgstr "Открыть заявку" -#: lib/who_need_help_web/live/activity_live/index.ex:397 -#: lib/who_need_help_web/live/request_live/index.ex:417 +#: lib/who_need_help_web/live/activity_live/index.ex:439 +#: lib/who_need_help_web/live/request_live/index.ex:431 #, elixir-autogen, elixir-format msgid "Optional text search" msgstr "Необязательный текстовый поиск" -#: lib/who_need_help_web/live/request_live/index.ex:564 +#: lib/who_need_help_web/live/request_live/index.ex:592 #, elixir-autogen, elixir-format msgid "Requests without map coordinates appear only when their public area description matches the text search." msgstr "Заявки без координат на карте отображаются только тогда, когда описание публичной области совпадает с текстовым поиском." -#: lib/who_need_help_web/live/request_live/index.ex:446 +#: lib/who_need_help_web/live/activity_live/index.ex:475 +#: lib/who_need_help_web/live/request_live/index.ex:465 #, elixir-autogen, elixir-format msgid "Results layout" msgstr "Расположение результатов" -#: lib/who_need_help_web/live/activity_live/index.ex:508 -#: lib/who_need_help_web/live/request_live/index.ex:597 +#: lib/who_need_help_web/live/activity_live/index.ex:621 +#: lib/who_need_help_web/live/request_live/index.ex:625 #, elixir-autogen, elixir-format msgid "Search this area" msgstr "Искать в этой области" @@ -4756,20 +4760,21 @@ msgstr "Искать в этой области" msgid "Static preview" msgstr "Статический просмотр" -#: lib/who_need_help_web/live/request_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/request_live/index.ex:484 #, elixir-autogen, elixir-format msgid "Together" msgstr "Вместе" #: lib/who_need_help_web/components/layouts/root.html.heex:6 #: lib/who_need_help_web/controllers/page_html/home.html.heex:160 -#: lib/who_need_help_web/live/activity_live/index.ex:496 +#: lib/who_need_help_web/live/activity_live/index.ex:609 #: lib/who_need_help_web/live/activity_live/new.ex:355 #: lib/who_need_help_web/live/activity_live/new.ex:414 -#: lib/who_need_help_web/live/activity_live/show.ex:545 +#: lib/who_need_help_web/live/activity_live/show.ex:546 #: lib/who_need_help_web/live/notification_live.ex:602 #: lib/who_need_help_web/live/notification_live.ex:621 -#: lib/who_need_help_web/live/request_live/index.ex:584 +#: lib/who_need_help_web/live/request_live/index.ex:612 #: lib/who_need_help_web/live/request_live/new.ex:485 #: lib/who_need_help_web/live/request_live/new.ex:563 #: lib/who_need_help_web/live/request_live/show.ex:1277 @@ -5159,7 +5164,7 @@ msgid "Nearby alert deleted." msgstr "Уведомление поблизости удалено." #: lib/who_need_help_web/live/notification_live.ex:456 -#: lib/who_need_help_web/live/request_live/index.ex:362 +#: lib/who_need_help_web/live/request_live/index.ex:376 #, elixir-autogen, elixir-format msgid "Nearby requests" msgstr "Заявки поблизости" @@ -5499,8 +5504,8 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec msgstr "WebGL недоступен в этом браузере, поэтому полная карта не может запуститься. Выберите пример ниже или попробуйте запустить карту ещё раз." #: lib/who_need_help_web/components/layouts/root.html.heex:7 -#: lib/who_need_help_web/live/activity_live/index.ex:497 -#: lib/who_need_help_web/live/activity_live/show.ex:546 +#: lib/who_need_help_web/live/activity_live/index.ex:610 +#: lib/who_need_help_web/live/activity_live/show.ex:547 #: lib/who_need_help_web/live/request_live/show.ex:1278 #, elixir-autogen, elixir-format msgid "Show location on map" @@ -6064,7 +6069,7 @@ msgstr "Теперь ваш отзыв виден" msgid "Delivery settings" msgstr "Настройки доставки" -#: lib/who_need_help_web/live/request_live/index.ex:541 +#: lib/who_need_help_web/live/request_live/index.ex:569 #, elixir-autogen, elixir-format msgid "Expires" msgstr "Истекает" @@ -6084,12 +6089,12 @@ msgstr "Оповещения рядом" msgid "Notification sections" msgstr "Разделы уведомлений" -#: lib/who_need_help_web/live/activity_live/index.ex:492 +#: lib/who_need_help_web/live/activity_live/index.ex:605 #, elixir-autogen, elixir-format msgid "%{count} activities" msgstr "Активностей: %{count}" -#: lib/who_need_help_web/live/activity_live/index.ex:512 +#: lib/who_need_help_web/live/activity_live/index.ex:463 #, elixir-autogen, elixir-format msgid "%{count} activity in this map area" msgid_plural "%{count} activities in this map area" @@ -6097,17 +6102,17 @@ msgstr[0] "%{count} активность в этой области карты" msgstr[1] "%{count} активности в этой области карты" msgstr[2] "%{count} активностей в этой области карты" -#: lib/who_need_help_web/live/request_live/index.ex:586 +#: lib/who_need_help_web/live/request_live/index.ex:614 #, elixir-autogen, elixir-format msgid "%{count} requests" msgstr "Заявок: %{count}" -#: lib/who_need_help_web/live/activity_live/index.ex:530 +#: lib/who_need_help_web/live/activity_live/index.ex:645 #, elixir-autogen, elixir-format msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list." msgstr "Созданные вами активности и те, к которым вы попросили присоединиться, появятся здесь. После выхода активность исчезнет из списка." -#: lib/who_need_help_web/live/activity_live/index.ex:351 +#: lib/who_need_help_web/live/activity_live/index.ex:393 #, elixir-autogen, elixir-format msgid "Activity views" msgstr "Разделы активностей" @@ -6122,12 +6127,12 @@ msgstr "Выберите категорию и понятно опишите з msgid "Confirm the safety guidance before asking nearby people for help." msgstr "Подтвердите правила безопасности перед публикацией заявки." -#: lib/who_need_help_web/live/request_live/index.ex:631 +#: lib/who_need_help_web/live/request_live/index.ex:659 #, elixir-autogen, elixir-format msgid "Create request" msgstr "Создать заявку" -#: lib/who_need_help_web/live/request_live/index.ex:645 +#: lib/who_need_help_web/live/request_live/index.ex:673 #, elixir-autogen, elixir-format msgid "Create your first request" msgstr "Создать первую заявку" @@ -6142,43 +6147,43 @@ msgstr "Что нужно сделать" msgid "Location" msgstr "Место" -#: lib/who_need_help_web/live/activity_live/index.ex:357 +#: lib/who_need_help_web/live/activity_live/index.ex:399 #, elixir-autogen, elixir-format msgid "My activities" msgstr "Мои активности" -#: lib/who_need_help_web/live/request_live/index.ex:363 -#: lib/who_need_help_web/live/request_live/index.ex:625 +#: lib/who_need_help_web/live/request_live/index.ex:377 +#: lib/who_need_help_web/live/request_live/index.ex:653 #, elixir-autogen, elixir-format msgid "My requests" msgstr "Мои заявки" -#: lib/who_need_help_web/live/activity_live/index.ex:356 +#: lib/who_need_help_web/live/activity_live/index.ex:398 #, elixir-autogen, elixir-format msgid "Nearby activities" msgstr "Активности рядом" -#: lib/who_need_help_web/live/activity_live/index.ex:528 +#: lib/who_need_help_web/live/activity_live/index.ex:643 #, elixir-autogen, elixir-format msgid "No activities yet" msgstr "Активностей пока нет" -#: lib/who_need_help_web/live/activity_live/index.ex:422 +#: lib/who_need_help_web/live/activity_live/index.ex:535 #, elixir-autogen, elixir-format msgid "No open activities in this area" msgstr "В этой области нет открытых активностей" -#: lib/who_need_help_web/live/request_live/index.ex:640 +#: lib/who_need_help_web/live/request_live/index.ex:668 #, elixir-autogen, elixir-format msgid "No requests yet" msgstr "Заявок пока нет" -#: lib/who_need_help_web/live/activity_live/index.ex:414 +#: lib/who_need_help_web/live/activity_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Only activities from the selected map area will be loaded." msgstr "Будут загружены только активности из выбранной области карты." -#: lib/who_need_help_web/live/activity_live/index.ex:491 +#: lib/who_need_help_web/live/activity_live/index.ex:604 #, elixir-autogen, elixir-format msgid "Open activity" msgstr "Открыть активность" @@ -6188,12 +6193,12 @@ msgstr "Открыть активность" msgid "Request form steps" msgstr "Шаги создания заявки" -#: lib/who_need_help_web/live/request_live/index.ex:357 +#: lib/who_need_help_web/live/request_live/index.ex:371 #, elixir-autogen, elixir-format msgid "Request views" msgstr "Разделы заявок" -#: lib/who_need_help_web/live/request_live/index.ex:642 +#: lib/who_need_help_web/live/request_live/index.ex:670 #, elixir-autogen, elixir-format msgid "Requests you publish or accept will appear here." msgstr "Здесь появятся заявки, которые вы создали или приняли." @@ -6218,7 +6223,7 @@ msgstr "Укажите срочность и время закрытия зая msgid "Share only as much location detail as helpers need." msgstr "Покажите помощникам только необходимую информацию о месте." -#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/activity_live/index.ex:607 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Activity details remain usable." msgstr "Карта недоступна в этом браузере. Информация об активностях по-прежнему доступна." @@ -6228,7 +6233,7 @@ msgstr "Карта недоступна в этом браузере. Инфор msgid "Timing" msgstr "Время" -#: lib/who_need_help_web/live/request_live/index.ex:627 +#: lib/who_need_help_web/live/request_live/index.ex:655 #, elixir-autogen, elixir-format msgid "Track requests you created and requests you accepted as a helper." msgstr "Следите за созданными заявками и за теми, которые вы приняли как помощник." @@ -6833,6 +6838,7 @@ msgid "People can find it by the area description" msgstr "Активность можно найти по описанию района" #: lib/who_need_help_web/live/activity_live/new.ex:455 +#: lib/who_need_help_web/live/activity_live/show.ex:560 #, elixir-autogen, elixir-format msgid "People can search by the public area description. Approved participants receive the exact point." msgstr "Люди могут искать по публичному описанию района. Одобренные участники получат точное место." @@ -6868,11 +6874,12 @@ msgid "The public map stays approximate" msgstr "На публичной карте показана только примерная область" #: lib/who_need_help_web/live/activity_live/new.ex:453 +#: lib/who_need_help_web/live/activity_live/show.ex:558 #, elixir-autogen, elixir-format msgid "The public map stays hidden" msgstr "Публичная карта остаётся скрытой" -#: lib/who_need_help_web/live/activity_live/new.ex:476 +#: lib/who_need_help_web/live/activity_live/new.ex:480 #, elixir-autogen, elixir-format msgid "These coordinates are the exact meeting point. They are revealed only after approval." msgstr "Эти координаты задают точное место встречи. Они раскрываются только после одобрения." @@ -6886,3 +6893,12 @@ msgstr "Что должны видеть люди до одобрения?" #, elixir-autogen, elixir-format msgid "Choose the meeting point on the map or use your current location." msgstr "Выберите место встречи на карте или используйте своё текущее местоположение." + +#: lib/who_need_help_web/live/activity_live/index.ex:458 +#: lib/who_need_help_web/live/activity_live/index.ex:517 +#: lib/who_need_help_web/live/activity_live/index.ex:635 +#: lib/who_need_help_web/live/request_live/index.ex:448 +#: lib/who_need_help_web/live/request_live/index.ex:507 +#, elixir-autogen, elixir-format +msgid "Loading results…" +msgstr "Загружаем результаты…" diff --git a/priv/gettext/uk/LC_MESSAGES/default.po b/priv/gettext/uk/LC_MESSAGES/default.po index fae0176..a1620f1 100644 --- a/priv/gettext/uk/LC_MESSAGES/default.po +++ b/priv/gettext/uk/LC_MESSAGES/default.po @@ -13,7 +13,7 @@ msgstr "" #: lib/who_need_help_web/components/layouts.ex:142 #: lib/who_need_help_web/components/layouts.ex:387 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:115 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:119 #, elixir-autogen, elixir-format msgid "Account settings" msgstr "Параметри облікового запису" @@ -45,7 +45,7 @@ msgstr "Категорії" #: lib/who_need_help_web/components/layouts.ex:95 #: lib/who_need_help_web/components/layouts.ex:171 #: lib/who_need_help_web/controllers/user_registration_html/new.html.heex:14 -#: lib/who_need_help_web/controllers/user_session_controller.ex:211 +#: lib/who_need_help_web/controllers/user_session_controller.ex:215 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:10 #, elixir-autogen, elixir-format msgid "Log in" @@ -66,7 +66,7 @@ msgstr "Профіль" #: lib/who_need_help_web/components/layouts.ex:92 #: lib/who_need_help_web/components/layouts.ex:168 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:141 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:142 #, elixir-autogen, elixir-format msgid "Register" msgstr "Зареєструватися" @@ -131,7 +131,7 @@ msgstr "4 — добре" msgid "5 — excellent" msgstr "5 — відмінно" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:130 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:134 #, elixir-autogen, elixir-format msgid "A link to confirm your email change has been sent to the new address." msgstr "" @@ -184,7 +184,7 @@ msgid "Active" msgstr "Активний" #: lib/who_need_help_web/components/layouts.ex:231 -#: lib/who_need_help_web/live/activity_live/index.ex:29 +#: lib/who_need_help_web/live/activity_live/index.ex:33 #, elixir-autogen, elixir-format msgid "Activities" msgstr "Активності" @@ -226,7 +226,7 @@ msgstr "Активність опубліковано. Ви схвалюєте msgid "Activity restored." msgstr "Активність відновлено." -#: lib/who_need_help_web/live/activity_live/index.ex:385 +#: lib/who_need_help_web/live/activity_live/index.ex:427 #: lib/who_need_help_web/live/activity_live/new.ex:204 #, elixir-autogen, elixir-format msgid "Activity type" @@ -243,12 +243,12 @@ msgstr "Додати непідтверджене посилання" msgid "Administrator" msgstr "Адміністратор" -#: lib/who_need_help_web/live/activity_live/index.ex:387 +#: lib/who_need_help_web/live/activity_live/index.ex:429 #, elixir-autogen, elixir-format msgid "All activities" msgstr "Усі активності" -#: lib/who_need_help_web/live/request_live/index.ex:393 +#: lib/who_need_help_web/live/request_live/index.ex:407 #, elixir-autogen, elixir-format msgid "All categories" msgstr "Усі категорії" @@ -263,17 +263,17 @@ msgstr "Уже зареєстровані?" msgid "Another helper already accepted this request." msgstr "Інший помічник уже прийняв цю заявку." -#: lib/who_need_help_web/live/request_live/index.ex:408 +#: lib/who_need_help_web/live/request_live/index.ex:422 #, elixir-autogen, elixir-format msgid "Any urgency" msgstr "Будь-яка терміновість" -#: lib/who_need_help_web/live/activity_live/show.ex:573 +#: lib/who_need_help_web/live/activity_live/show.ex:591 #, elixir-autogen, elixir-format msgid "Approval pending" msgstr "Очікує схвалення" -#: lib/who_need_help_web/live/activity_live/show.ex:650 +#: lib/who_need_help_web/live/activity_live/show.ex:668 #, elixir-autogen, elixir-format msgid "Approve" msgstr "Схвалити" @@ -283,7 +283,7 @@ msgstr "Схвалити" msgid "Approved group chat" msgstr "Чат схваленої групи" -#: lib/who_need_help_web/live/activity_live/show.ex:676 +#: lib/who_need_help_web/live/activity_live/show.ex:694 #, elixir-autogen, elixir-format msgid "Approved participants" msgstr "Схвалені учасники" @@ -331,7 +331,7 @@ msgstr "" msgid "Block and report" msgstr "Блокування та скарга" -#: lib/who_need_help_web/live/activity_live/show.ex:719 +#: lib/who_need_help_web/live/activity_live/show.ex:737 #, elixir-autogen, elixir-format msgid "Block organizer" msgstr "Заблокувати організатора" @@ -372,7 +372,7 @@ msgstr "ДОВІРА СПІЛЬНОТИ" msgid "COMMUNITY-DRIVEN TAXONOMY" msgstr "КАТЕГОРІЇ ВІД СПІЛЬНОТИ" -#: lib/who_need_help_web/live/activity_live/show.ex:670 +#: lib/who_need_help_web/live/activity_live/show.ex:688 #, elixir-autogen, elixir-format msgid "Cancel activity" msgstr "Скасувати активність" @@ -383,16 +383,16 @@ msgstr "Скасувати активність" msgid "Cancel request" msgstr "Скасувати запит" -#: lib/who_need_help_web/live/activity_live/index.ex:316 +#: lib/who_need_help_web/live/activity_live/index.ex:358 #: lib/who_need_help_web/live/activity_live/show.ex:344 -#: lib/who_need_help_web/live/request_live/index.ex:313 -#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:327 +#: lib/who_need_help_web/live/request_live/index.ex:328 #: lib/who_need_help_web/live/request_live/show.ex:956 #, elixir-autogen, elixir-format msgid "Cancelled" msgstr "Скасовано" -#: lib/who_need_help_web/live/request_live/index.ex:391 +#: lib/who_need_help_web/live/request_live/index.ex:405 #: lib/who_need_help_web/live/request_live/new.ex:302 #, elixir-autogen, elixir-format msgid "Category" @@ -460,7 +460,7 @@ msgstr "Оберіть…" msgid "City centre, near the main entrance" msgstr "Центр міста, біля головного входу" -#: lib/who_need_help_web/live/activity_live/show.ex:737 +#: lib/who_need_help_web/live/activity_live/show.ex:755 #: lib/who_need_help_web/live/request_live/show.ex:1759 #, elixir-autogen, elixir-format msgid "Clear" @@ -476,7 +476,7 @@ msgstr "Закрити" msgid "Coffee and conversation" msgstr "Кава й розмова" -#: lib/who_need_help_web/live/activity_live/index.ex:330 +#: lib/who_need_help_web/live/activity_live/index.ex:372 #, elixir-autogen, elixir-format msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings." msgstr "" @@ -493,19 +493,19 @@ msgstr "Коментар (необов’язково)" msgid "Community helpers" msgstr "Помічники спільноти" -#: lib/who_need_help_web/live/activity_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:578 #: lib/who_need_help_web/live/activity_live/show.ex:403 -#: lib/who_need_help_web/live/activity_live/show.ex:621 -#: lib/who_need_help_web/live/activity_live/show.ex:680 -#: lib/who_need_help_web/live/request_live/index.ex:537 +#: lib/who_need_help_web/live/activity_live/show.ex:639 +#: lib/who_need_help_web/live/activity_live/show.ex:698 +#: lib/who_need_help_web/live/request_live/index.ex:565 #, elixir-autogen, elixir-format msgid "Community member" msgstr "Член спільноти" -#: lib/who_need_help_web/live/activity_live/index.ex:315 +#: lib/who_need_help_web/live/activity_live/index.ex:357 #: lib/who_need_help_web/live/activity_live/show.ex:343 -#: lib/who_need_help_web/live/request_live/index.ex:311 -#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:325 +#: lib/who_need_help_web/live/request_live/index.ex:326 #: lib/who_need_help_web/live/request_live/show.ex:955 #: lib/who_need_help_web/live/request_live/show.ex:974 #, elixir-autogen, elixir-format @@ -559,13 +559,13 @@ msgstr "Інструкції для підтвердження" msgid "Confirming..." msgstr "Підтвердження…" -#: lib/who_need_help_web/live/activity_live/index.ex:336 +#: lib/who_need_help_web/live/activity_live/index.ex:378 #: lib/who_need_help_web/live/activity_live/new.ex:12 #, elixir-autogen, elixir-format msgid "Create activity" msgstr "Створити активність" -#: lib/who_need_help_web/live/activity_live/index.ex:535 +#: lib/who_need_help_web/live/activity_live/index.ex:650 #: lib/who_need_help_web/live/activity_live/new.ex:180 #, elixir-autogen, elixir-format msgid "Create an activity" @@ -576,12 +576,12 @@ msgstr "Створити активність" msgid "Create category" msgstr "Створити категорію" -#: lib/who_need_help_web/live/request_live/index.ex:342 +#: lib/who_need_help_web/live/request_live/index.ex:356 #, elixir-autogen, elixir-format msgid "Create urgent request" msgstr "Створити термінову заявку" -#: lib/who_need_help_web/live/activity_live/show.ex:746 +#: lib/who_need_help_web/live/activity_live/show.ex:764 #, elixir-autogen, elixir-format msgid "Dangerous plan" msgstr "Небезпечний план" @@ -610,7 +610,7 @@ msgstr "" "Рішення та доступ до матеріалів зі скарг на листування записуються до " "журналу аудиту." -#: lib/who_need_help_web/live/activity_live/show.ex:657 +#: lib/who_need_help_web/live/activity_live/show.ex:675 #, elixir-autogen, elixir-format msgid "Decline" msgstr "Відхилити" @@ -670,13 +670,13 @@ msgstr "Двосторонній прихований відгук" msgid "Email" msgstr "Електронна пошта" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:90 -#: lib/who_need_help_web/controllers/user_settings_controller.ex:97 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:94 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:101 #, elixir-autogen, elixir-format msgid "Email change link is invalid or it has expired." msgstr "Посилання для зміни електронної пошти недійсне або прострочене." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:85 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:89 #, elixir-autogen, elixir-format msgid "Email changed successfully." msgstr "Адресу електронної пошти успішно змінено." @@ -736,12 +736,12 @@ msgstr "Спливає:" msgid "Explain what is already arranged and what the volunteer needs to do." msgstr "Поясніть, що вже організовано й що має зробити волонтер." -#: lib/who_need_help_web/live/activity_live/index.ex:327 +#: lib/who_need_help_web/live/activity_live/index.ex:369 #, elixir-autogen, elixir-format msgid "Find people to join" msgstr "Знайти компанію" -#: lib/who_need_help_web/live/activity_live/show.ex:747 +#: lib/who_need_help_web/live/activity_live/show.ex:765 #: lib/who_need_help_web/live/moderation_live.ex:251 #: lib/who_need_help_web/live/report_live.ex:112 #: lib/who_need_help_web/live/request_live/show.ex:1769 @@ -781,7 +781,7 @@ msgstr "Ім’я користувача (необов’язково)" msgid "Handover code verified." msgstr "Код передачі підтверджено." -#: lib/who_need_help_web/live/activity_live/show.ex:745 +#: lib/who_need_help_web/live/activity_live/show.ex:763 #: lib/who_need_help_web/live/moderation_live.ex:250 #: lib/who_need_help_web/live/report_live.ex:111 #: lib/who_need_help_web/live/request_live/show.ex:1768 @@ -898,7 +898,7 @@ msgstr "" "Я підтверджую, що це не екстрена ситуація, безпосередньої небезпеки немає, а" " запитувана допомога є законною." -#: lib/who_need_help_web/live/activity_live/new.ex:526 +#: lib/who_need_help_web/live/activity_live/new.ex:530 #, elixir-autogen, elixir-format msgid "I will approve participants individually, keep sensitive details in the approved group, and use an appropriate public first meeting point." msgstr "" @@ -910,7 +910,7 @@ msgstr "" msgid "Identity and reputation" msgstr "Особа й репутація" -#: lib/who_need_help_web/controllers/user_session_controller.ex:104 +#: lib/who_need_help_web/controllers/user_session_controller.ex:108 #, elixir-autogen, elixir-format msgid "If your email is in our system, you will receive instructions for logging in shortly." msgstr "" @@ -919,7 +919,7 @@ msgstr "" #: lib/who_need_help_web/controllers/content_removal_html.ex:9 #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:47 -#: lib/who_need_help_web/live/activity_live/show.ex:749 +#: lib/who_need_help_web/live/activity_live/show.ex:767 #: lib/who_need_help_web/live/moderation_live.ex:254 #: lib/who_need_help_web/live/report_live.ex:115 #: lib/who_need_help_web/live/request_live/show.ex:1772 @@ -936,8 +936,8 @@ msgstr "" "Android показує постійне сповіщення й може продовжувати передавання після " "згортання. Після зупинення передавання вихідні координати видаляються." -#: lib/who_need_help_web/live/request_live/index.ex:309 -#: lib/who_need_help_web/live/request_live/index.ex:310 +#: lib/who_need_help_web/live/request_live/index.ex:323 +#: lib/who_need_help_web/live/request_live/index.ex:324 #: lib/who_need_help_web/live/request_live/show.ex:954 #, elixir-autogen, elixir-format msgid "In progress" @@ -954,7 +954,7 @@ msgstr "Мова інтерфейсу" msgid "Internal note" msgstr "Внутрішня нотатка" -#: lib/who_need_help_web/controllers/user_session_controller.ex:153 +#: lib/who_need_help_web/controllers/user_session_controller.ex:157 #, elixir-autogen, elixir-format msgid "Invalid email or password" msgstr "Неправильна електронна пошта або пароль" @@ -970,14 +970,14 @@ msgstr "Подання заявок на участь закрито." msgid "Join requests close" msgstr "Приймання заявок завершується" -#: lib/who_need_help_web/live/activity_live/new.ex:482 +#: lib/who_need_help_web/live/activity_live/new.ex:486 #: lib/who_need_help_web/live/notification_live.ex:686 #: lib/who_need_help_web/live/request_live/new.ex:668 #, elixir-autogen, elixir-format msgid "Latitude" msgstr "Широта" -#: lib/who_need_help_web/live/activity_live/show.ex:608 +#: lib/who_need_help_web/live/activity_live/show.ex:626 #, elixir-autogen, elixir-format msgid "Leave activity" msgstr "Залишити активність" @@ -988,7 +988,7 @@ msgid "Live location" msgstr "Геопозиція в реальному часі" #: lib/who_need_help_web/controllers/page_html/safety.html.heex:51 -#: lib/who_need_help_web/live/activity_live/show.ex:550 +#: lib/who_need_help_web/live/activity_live/show.ex:568 #, elixir-autogen, elixir-format msgid "Location privacy" msgstr "Приватність геопозиції" @@ -1025,12 +1025,12 @@ msgstr "Інструкції для входу" msgid "Log in only this time" msgstr "Увійти лише зараз" -#: lib/who_need_help_web/controllers/user_session_controller.ex:133 +#: lib/who_need_help_web/controllers/user_session_controller.ex:137 #, elixir-autogen, elixir-format msgid "Logged out successfully." msgstr "Ви успішно вийшли." -#: lib/who_need_help_web/live/activity_live/new.ex:496 +#: lib/who_need_help_web/live/activity_live/new.ex:500 #: lib/who_need_help_web/live/notification_live.ex:700 #: lib/who_need_help_web/live/request_live/new.ex:682 #, elixir-autogen, elixir-format @@ -1055,7 +1055,7 @@ msgstr "" "Значок підтвердження з’являється лише після авторизації у відповідного " "провайдера." -#: lib/who_need_help_web/live/activity_live/show.ex:667 +#: lib/who_need_help_web/live/activity_live/show.ex:685 #, elixir-autogen, elixir-format msgid "Mark completed" msgstr "Позначити завершеною" @@ -1065,8 +1065,8 @@ msgstr "Позначити завершеною" msgid "Match and coordinate" msgstr "Знайдіть помічника й узгодьте деталі" -#: lib/who_need_help_web/live/request_live/index.ex:307 -#: lib/who_need_help_web/live/request_live/index.ex:308 +#: lib/who_need_help_web/live/request_live/index.ex:321 +#: lib/who_need_help_web/live/request_live/index.ex:322 #: lib/who_need_help_web/live/request_live/show.ex:952 #, elixir-autogen, elixir-format msgid "Matched" @@ -1095,7 +1095,7 @@ msgstr "" msgid "Medicine requests" msgstr "Запити на ліки" -#: lib/who_need_help_web/live/activity_live/index.ex:343 +#: lib/who_need_help_web/live/activity_live/index.ex:385 #, elixir-autogen, elixir-format msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe." msgstr "" @@ -1149,12 +1149,12 @@ msgstr "Модератор" msgid "Moderator access is required." msgstr "Потрібен доступ модератора." -#: lib/who_need_help_web/live/request_live/index.ex:333 +#: lib/who_need_help_web/live/request_live/index.ex:347 #, elixir-autogen, elixir-format msgid "NEARBY MUTUAL AID" msgstr "ВЗАЄМОДОПОМОГА ПОРУЧ" -#: lib/who_need_help_web/live/request_live/index.ex:33 +#: lib/who_need_help_web/live/request_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Nearby help" msgstr "Допомога поруч" @@ -1198,7 +1198,7 @@ msgstr "Ще немає повідомлень." msgid "No open signals." msgstr "Немає відкритих сигналів." -#: lib/who_need_help_web/live/activity_live/show.ex:662 +#: lib/who_need_help_web/live/activity_live/show.ex:680 #, elixir-autogen, elixir-format msgid "No pending join requests." msgstr "Немає заявок на участь, що очікують." @@ -1237,9 +1237,9 @@ msgid "Not for emergencies" msgstr "Не для екстрених ситуацій" #: lib/who_need_help_web/live/notification_live.ex:748 -#: lib/who_need_help_web/live/request_live/index.ex:297 -#: lib/who_need_help_web/live/request_live/index.ex:298 -#: lib/who_need_help_web/live/request_live/index.ex:409 +#: lib/who_need_help_web/live/request_live/index.ex:311 +#: lib/who_need_help_web/live/request_live/index.ex:312 +#: lib/who_need_help_web/live/request_live/index.ex:423 #: lib/who_need_help_web/live/request_live/new.ex:449 #: lib/who_need_help_web/live/request_live/show.ex:943 #: lib/who_need_help_web/live/request_live/show.ex:944 @@ -1247,7 +1247,7 @@ msgstr "Не для екстрених ситуацій" msgid "Now" msgstr "Зараз" -#: lib/who_need_help_web/live/activity_live/index.ex:325 +#: lib/who_need_help_web/live/activity_live/index.ex:367 #, elixir-autogen, elixir-format msgid "OPTIONAL SOCIAL PLANS" msgstr "НЕОБОВ’ЯЗКОВІ СПІЛЬНІ ПЛАНИ" @@ -1267,12 +1267,12 @@ msgstr "" #: lib/who_need_help/notifications/text.ex:140 #: lib/who_need_help_web/controllers/content_removal_html.ex:21 #: lib/who_need_help_web/controllers/support_html.ex:16 -#: lib/who_need_help_web/live/activity_live/index.ex:314 +#: lib/who_need_help_web/live/activity_live/index.ex:356 #: lib/who_need_help_web/live/activity_live/show.ex:342 #: lib/who_need_help_web/live/moderation_live.ex:258 #: lib/who_need_help_web/live/report_live.ex:104 -#: lib/who_need_help_web/live/request_live/index.ex:305 -#: lib/who_need_help_web/live/request_live/index.ex:306 +#: lib/who_need_help_web/live/request_live/index.ex:319 +#: lib/who_need_help_web/live/request_live/index.ex:320 #: lib/who_need_help_web/live/request_live/show.ex:951 #: lib/who_need_help_web/live/support_operations_live.ex:237 #: lib/who_need_help_web/live/support_operations_live.ex:438 @@ -1310,7 +1310,7 @@ msgstr "Організатор" msgid "Organizer blocked. Their activities and messages are hidden." msgstr "Організатора заблоковано. Його активності й повідомлення приховано." -#: lib/who_need_help_web/live/activity_live/show.ex:613 +#: lib/who_need_help_web/live/activity_live/show.ex:631 #, elixir-autogen, elixir-format msgid "Organizer controls" msgstr "Керування організатора" @@ -1325,7 +1325,7 @@ msgstr "Посилання організатора на соцмережі" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:74 #: lib/who_need_help_web/controllers/support_html.ex:13 #: lib/who_need_help_web/controllers/support_html/new.html.heex:38 -#: lib/who_need_help_web/live/activity_live/show.ex:750 +#: lib/who_need_help_web/live/activity_live/show.ex:768 #: lib/who_need_help_web/live/moderation_live.ex:255 #: lib/who_need_help_web/live/profile_live.ex:320 #: lib/who_need_help_web/live/public_profile_live.ex:53 @@ -1349,7 +1349,7 @@ msgstr "Учасник" msgid "Password" msgstr "Пароль" -#: lib/who_need_help_web/controllers/user_settings_controller.ex:61 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:65 #, elixir-autogen, elixir-format msgid "Password updated successfully." msgstr "Пароль успішно оновлено." @@ -1482,7 +1482,7 @@ msgstr "" "Опублікуйте зрозумілий публічний план. Заявки на участь очікуватимуть, доки " "ви їх не схвалите." -#: lib/who_need_help_web/live/activity_live/new.ex:538 +#: lib/who_need_help_web/live/activity_live/new.ex:542 #, elixir-autogen, elixir-format msgid "Publish activity" msgstr "Опублікувати активність" @@ -1497,7 +1497,7 @@ msgstr "Оприлюднити пропозицію" msgid "Publish request" msgstr "Опублікувати заявку" -#: lib/who_need_help_web/live/activity_live/new.ex:536 +#: lib/who_need_help_web/live/activity_live/new.ex:540 #: lib/who_need_help_web/live/request_live/new.ex:742 #, elixir-autogen, elixir-format msgid "Publishing…" @@ -1533,7 +1533,7 @@ msgid "Read the" msgstr "Прочитайте" #: lib/who_need_help_web/controllers/content_removal_html/new.html.heex:42 -#: lib/who_need_help_web/live/activity_live/show.ex:743 +#: lib/who_need_help_web/live/activity_live/show.ex:761 #: lib/who_need_help_web/live/request_live/show.ex:1342 #: lib/who_need_help_web/live/request_live/show.ex:1536 #: lib/who_need_help_web/live/request_live/show.ex:1579 @@ -1619,7 +1619,7 @@ msgstr "Заявку приховано." msgid "Request restored." msgstr "Заявку відновлено." -#: lib/who_need_help_web/live/activity_live/show.ex:570 +#: lib/who_need_help_web/live/activity_live/show.ex:588 #, elixir-autogen, elixir-format msgid "Request to join" msgstr "Подати заявку на участь" @@ -1706,7 +1706,7 @@ msgstr "Опис російською (необов’язково)" msgid "Russian name (optional)" msgstr "Назва російською (необов’язково)" -#: lib/who_need_help_web/live/activity_live/show.ex:713 +#: lib/who_need_help_web/live/activity_live/show.ex:731 #: lib/who_need_help_web/live/request_live/show.ex:1727 #, elixir-autogen, elixir-format msgid "Safety controls" @@ -1745,9 +1745,9 @@ msgid "Save profile" msgstr "Зберегти профіль" #: lib/who_need_help_web/live/notification_live.ex:750 -#: lib/who_need_help_web/live/request_live/index.ex:301 -#: lib/who_need_help_web/live/request_live/index.ex:302 -#: lib/who_need_help_web/live/request_live/index.ex:411 +#: lib/who_need_help_web/live/request_live/index.ex:315 +#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:425 #: lib/who_need_help_web/live/request_live/new.ex:451 #: lib/who_need_help_web/live/request_live/show.ex:947 #: lib/who_need_help_web/live/request_live/show.ex:948 @@ -1761,7 +1761,7 @@ msgstr "Заплановано" msgid "Send" msgstr "Надіслати" -#: lib/who_need_help_web/live/activity_live/show.ex:759 +#: lib/who_need_help_web/live/activity_live/show.ex:777 #: lib/who_need_help_web/live/request_live/show.ex:1782 #, elixir-autogen, elixir-format msgid "Send report" @@ -1863,7 +1863,7 @@ msgstr "Сеанс підтвердження соцмережі заверши msgid "Social verification session is invalid." msgstr "Сеанс соціальної перевірки є некоректним." -#: lib/who_need_help_web/live/activity_live/show.ex:748 +#: lib/who_need_help_web/live/activity_live/show.ex:766 #: lib/who_need_help_web/live/moderation_live.ex:253 #: lib/who_need_help_web/live/report_live.ex:114 #: lib/who_need_help_web/live/request_live/show.ex:1771 @@ -1958,12 +1958,12 @@ msgstr "Браузеру не вдалося передати вашу геоп msgid "The last administrator cannot demote themselves." msgstr "Останній адміністратор не може понизити власну роль." -#: lib/who_need_help_web/controllers/user_session_controller.ex:51 +#: lib/who_need_help_web/controllers/user_session_controller.ex:50 #, elixir-autogen, elixir-format msgid "The link is invalid or it has expired." msgstr "Посилання недійсне або прострочене." -#: lib/who_need_help_web/live/activity_live/show.ex:575 +#: lib/who_need_help_web/live/activity_live/show.ex:593 #, elixir-autogen, elixir-format msgid "The organizer has not approved your request yet." msgstr "Організатор ще не схвалив ваш запит." @@ -1981,14 +1981,14 @@ msgstr "" "Платформа ніколи не обробляє, не вимагає, не розподіляє й не гарантує " "оплату." -#: lib/who_need_help_web/live/activity_live/show.ex:555 +#: lib/who_need_help_web/live/activity_live/show.ex:573 #, elixir-autogen, elixir-format msgid "The public map is approximate or hidden. Exact coordinates appear only after approval." msgstr "" "Публічна карта показує приблизну область або прихована. Точні координати " "з’являються лише після схвалення." -#: lib/who_need_help_web/live/activity_live/show.ex:731 +#: lib/who_need_help_web/live/activity_live/show.ex:749 #, elixir-autogen, elixir-format msgid "The report is scoped to the selected group message." msgstr "Скарга стосується вибраного повідомлення групи." @@ -2019,7 +2019,7 @@ msgstr "Цей стан активності вже змінено." msgid "This interaction is blocked." msgstr "Ця взаємодія заблокована." -#: lib/who_need_help_web/live/request_live/index.ex:349 +#: lib/who_need_help_web/live/request_live/index.ex:363 #: lib/who_need_help_web/live/request_live/show.ex:1022 #, elixir-autogen, elixir-format msgid "This is not an emergency service. Contact local emergency services for immediate danger." @@ -2072,9 +2072,9 @@ msgid "To see sent emails, visit" msgstr "Щоб переглянути надіслані листи, відкрийте" #: lib/who_need_help_web/live/notification_live.ex:749 -#: lib/who_need_help_web/live/request_live/index.ex:299 -#: lib/who_need_help_web/live/request_live/index.ex:300 -#: lib/who_need_help_web/live/request_live/index.ex:410 +#: lib/who_need_help_web/live/request_live/index.ex:313 +#: lib/who_need_help_web/live/request_live/index.ex:314 +#: lib/who_need_help_web/live/request_live/index.ex:424 #: lib/who_need_help_web/live/request_live/new.ex:450 #: lib/who_need_help_web/live/request_live/show.ex:945 #: lib/who_need_help_web/live/request_live/show.ex:946 @@ -2091,8 +2091,8 @@ msgstr "Сьогодні" msgid "Too many actions in the configured time window." msgstr "Забагато дій протягом налаштованого проміжку часу." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:125 -#: lib/who_need_help_web/controllers/user_registration_controller.ex:61 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:130 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:62 #, elixir-autogen, elixir-format msgid "Too many registration attempts in the configured time window." msgstr "Забагато спроб реєстрації протягом налаштованого проміжку часу." @@ -2102,8 +2102,8 @@ msgstr "Забагато спроб реєстрації протягом нал msgid "Too many requests in the configured time window." msgstr "Забагато запитів протягом налаштованого проміжку часу." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:175 -#: lib/who_need_help_web/controllers/user_session_controller.ex:117 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:181 +#: lib/who_need_help_web/controllers/user_session_controller.ex:121 #, elixir-autogen, elixir-format msgid "Too many sign-in emails in the configured time window." msgstr "Забагато листів для входу протягом налаштованого проміжку часу." @@ -2149,7 +2149,7 @@ msgid "Update email instructions" msgstr "Інструкції для зміни електронної пошти" #: lib/who_need_help_web/live/notification_live.ex:743 -#: lib/who_need_help_web/live/request_live/index.ex:406 +#: lib/who_need_help_web/live/request_live/index.ex:420 #: lib/who_need_help_web/live/request_live/new.ex:447 #, elixir-autogen, elixir-format msgid "Urgency" @@ -2202,7 +2202,7 @@ msgstr "Користувач" msgid "User blocked. Their requests and new messages are hidden." msgstr "Користувача заблоковано. Його заявки й нові повідомлення приховано." -#: lib/who_need_help_web/controllers/user_session_controller.ex:42 +#: lib/who_need_help_web/controllers/user_session_controller.ex:41 #, elixir-autogen, elixir-format msgid "User confirmed successfully." msgstr "Користувача успішно підтверджено." @@ -2253,10 +2253,10 @@ msgstr "Відкриті відгуки" msgid "Waiting for a nearby volunteer." msgstr "Очікуємо помічника поруч." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:349 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:435 -#: lib/who_need_help_web/controllers/user_session_controller.ex:41 -#: lib/who_need_help_web/controllers/user_session_controller.ex:65 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:355 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:441 +#: lib/who_need_help_web/controllers/user_session_controller.ex:40 +#: lib/who_need_help_web/controllers/user_session_controller.ex:69 #, elixir-autogen, elixir-format msgid "Welcome back!" msgstr "З поверненням!" @@ -2266,7 +2266,7 @@ msgstr "З поверненням!" msgid "What Codex contributed" msgstr "Що зробив Codex" -#: lib/who_need_help_web/live/activity_live/show.ex:756 +#: lib/who_need_help_web/live/activity_live/show.ex:774 #: lib/who_need_help_web/live/request_live/show.ex:1779 #, elixir-autogen, elixir-format msgid "What happened?" @@ -2304,7 +2304,7 @@ msgstr "" "через підписку ChatGPT розробника. Ключ OpenAI API та резервний варіант API " "з оплатою за використання не застосовуються." -#: lib/who_need_help_web/live/request_live/index.ex:334 +#: lib/who_need_help_web/live/request_live/index.ex:348 #, elixir-autogen, elixir-format msgid "Who needs help?" msgstr "Хто потребує допомоги?" @@ -2344,7 +2344,7 @@ msgstr "Ви вже подали заявку на участь у цій акт msgid "You already voted for this proposal." msgstr "Ви вже проголосували за цю пропозицію." -#: lib/who_need_help_web/live/activity_live/show.ex:553 +#: lib/who_need_help_web/live/activity_live/show.ex:571 #, elixir-autogen, elixir-format msgid "You are approved, so the map can show the exact meeting coordinates." msgstr "Вас схвалено, тому карта може показувати точні координати зустрічі." @@ -2426,7 +2426,7 @@ msgid "Your review is revealed only after both participants submit." msgstr "" "Ваш відгук відкриється лише після того, як його надішлють обидва учасники." -#: lib/who_need_help_web/live/request_live/index.ex:602 +#: lib/who_need_help_web/live/request_live/index.ex:630 #, elixir-autogen, elixir-format msgid "Your trust summary" msgstr "Ваші показники довіри" @@ -2447,17 +2447,17 @@ msgstr "повідомлення активності" msgid "before confirming." msgstr "перед підтвердженням." -#: lib/who_need_help_web/live/activity_live/index.ex:464 +#: lib/who_need_help_web/live/activity_live/index.ex:577 #: lib/who_need_help_web/live/category_proposal_live.ex:252 #: lib/who_need_help_web/live/moderation_live.ex:372 -#: lib/who_need_help_web/live/request_live/index.ex:536 +#: lib/who_need_help_web/live/request_live/index.ex:564 #, elixir-autogen, elixir-format msgid "by %{name}" msgstr "автор: %{name}" #: lib/who_need_help_web/live/profile_live.ex:190 #: lib/who_need_help_web/live/public_profile_live.ex:133 -#: lib/who_need_help_web/live/request_live/index.ex:606 +#: lib/who_need_help_web/live/request_live/index.ex:634 #: lib/who_need_help_web/live/request_live/show.ex:1374 #, elixir-autogen, elixir-format msgid "completed" @@ -2483,7 +2483,7 @@ msgstr "немає підтвердження руху" msgid "no proximity evidence" msgstr "немає підтвердження близькості" -#: lib/who_need_help_web/live/activity_live/show.ex:683 +#: lib/who_need_help_web/live/activity_live/show.ex:701 #, elixir-autogen, elixir-format msgid "organizer" msgstr "організатор" @@ -2493,7 +2493,7 @@ msgstr "організатор" msgid "participants only" msgstr "лише учасники" -#: lib/who_need_help_web/live/request_live/index.ex:610 +#: lib/who_need_help_web/live/request_live/index.ex:638 #: lib/who_need_help_web/live/request_live/show.ex:1378 #, elixir-autogen, elixir-format msgid "people" @@ -2533,8 +2533,8 @@ msgid "unique people" msgstr "унікальні люди" #: lib/who_need_help_web/live/activity_live/show.ex:426 -#: lib/who_need_help_web/live/activity_live/show.ex:641 -#: lib/who_need_help_web/live/activity_live/show.ex:702 +#: lib/who_need_help_web/live/activity_live/show.ex:659 +#: lib/who_need_help_web/live/activity_live/show.ex:720 #: lib/who_need_help_web/live/profile_live.ex:286 #: lib/who_need_help_web/live/public_profile_live.ex:98 #: lib/who_need_help_web/live/request_live/show.ex:1094 @@ -2549,11 +2549,11 @@ msgid "urgent" msgstr "терміново" #: lib/who_need_help_web/live/activity_live/show.ex:425 -#: lib/who_need_help_web/live/activity_live/show.ex:640 -#: lib/who_need_help_web/live/activity_live/show.ex:701 +#: lib/who_need_help_web/live/activity_live/show.ex:658 +#: lib/who_need_help_web/live/activity_live/show.ex:719 #: lib/who_need_help_web/live/profile_live.ex:285 #: lib/who_need_help_web/live/public_profile_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:614 +#: lib/who_need_help_web/live/request_live/index.ex:642 #: lib/who_need_help_web/live/request_live/show.ex:1093 #: lib/who_need_help_web/live/request_live/show.ex:1476 #, elixir-autogen, elixir-format @@ -2586,8 +2586,8 @@ msgstr "← Заявки" msgid "Approved" msgstr "Схвалено" -#: lib/who_need_help_web/live/request_live/index.ex:315 -#: lib/who_need_help_web/live/request_live/index.ex:316 +#: lib/who_need_help_web/live/request_live/index.ex:329 +#: lib/who_need_help_web/live/request_live/index.ex:330 #, elixir-autogen, elixir-format msgid "Expired" msgstr "Спливло" @@ -2640,8 +2640,8 @@ msgstr "Незвична частота дій" msgid "Load earlier messages" msgstr "Завантажити попередні повідомлення" -#: lib/who_need_help_web/live/activity_live/index.ex:476 -#: lib/who_need_help_web/live/activity_live/index.ex:564 +#: lib/who_need_help_web/live/activity_live/index.ex:589 +#: lib/who_need_help_web/live/activity_live/index.ex:679 #: lib/who_need_help_web/live/category_proposal_live.ex:284 #: lib/who_need_help_web/live/leaderboard_live.ex:91 #: lib/who_need_help_web/live/moderation_live.ex:465 @@ -2653,21 +2653,21 @@ msgstr "Завантажити попередні повідомлення" #: lib/who_need_help_web/live/profile_live.ex:363 #: lib/who_need_help_web/live/public_profile_live.ex:178 #: lib/who_need_help_web/live/report_live.ex:98 -#: lib/who_need_help_web/live/request_live/index.ex:671 +#: lib/who_need_help_web/live/request_live/index.ex:699 #: lib/who_need_help_web/live/support_operations_live.ex:306 #: lib/who_need_help_web/live/support_operations_live.ex:505 #, elixir-autogen, elixir-format msgid "Load more" msgstr "Завантажити ще" -#: lib/who_need_help_web/live/activity_live/show.ex:543 -#: lib/who_need_help_web/live/request_live/index.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:544 +#: lib/who_need_help_web/live/request_live/index.ex:610 #: lib/who_need_help_web/live/request_live/show.ex:1275 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Request details remain usable." msgstr "Карта недоступна в цьому браузері. Решта інформації залишається доступною." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:114 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:118 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:9 #: lib/who_need_help_web/controllers/user_settings_html/confirm_email.html.heex:21 #, elixir-autogen, elixir-format @@ -2726,7 +2726,7 @@ msgstr "Організатор не може залишити активніст msgid "The organizer is already attending." msgstr "Організатор уже бере участь." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:70 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:71 #, elixir-autogen, elixir-format msgid "The registration form is invalid." msgstr "Форму реєстрації заповнено неправильно." @@ -2737,12 +2737,12 @@ msgstr "Форму реєстрації заповнено неправильн msgid "The selected record is no longer available." msgstr "Вибраний запис більше недоступний." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:73 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:77 #, elixir-autogen, elixir-format msgid "The settings form is invalid." msgstr "Форму налаштувань заповнено неправильно." -#: lib/who_need_help_web/controllers/user_session_controller.ex:126 +#: lib/who_need_help_web/controllers/user_session_controller.ex:130 #, elixir-autogen, elixir-format msgid "The sign-in form is invalid." msgstr "Форму входу заповнено неправильно." @@ -2768,7 +2768,7 @@ msgstr "Ця пропозиція більше недоступна." msgid "This request has expired." msgstr "Термін дії цієї заявки минув." -#: lib/who_need_help_web/controllers/user_session_controller.ex:75 +#: lib/who_need_help_web/controllers/user_session_controller.ex:79 #, elixir-autogen, elixir-format msgid "Too many sign-in attempts in the configured time window." msgstr "Забагато спроб входу протягом налаштованого проміжку часу." @@ -2778,12 +2778,12 @@ msgstr "Забагато спроб входу протягом налаштов msgid "Your secure email link is ready. Continue to sign in." msgstr "Захищене посилання з листа готове. Продовжте вхід." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:140 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:144 #, elixir-autogen, elixir-format msgid "The confirmation email could not be sent. Please try again." msgstr "Не вдалося надіслати лист із підтвердженням. Спробуйте ще раз." -#: lib/who_need_help_web/controllers/user_registration_controller.ex:119 +#: lib/who_need_help_web/controllers/user_registration_controller.ex:120 #, elixir-autogen, elixir-format msgid "If this address can be registered or signed in, login instructions will arrive shortly." msgstr "Якщо цю адресу можна зареєструвати або використати для входу, інструкції для входу незабаром надійдуть." @@ -2793,7 +2793,7 @@ msgstr "Якщо цю адресу можна зареєструвати або msgid "This account cannot perform that action." msgstr "Цей обліковий запис не може виконати цю дію." -#: lib/who_need_help_web/controllers/user_settings_controller.ex:41 +#: lib/who_need_help_web/controllers/user_settings_controller.ex:45 #, elixir-autogen, elixir-format msgid "Too many email-change requests in the configured time window." msgstr "Забагато запитів на зміну email протягом налаштованого проміжку часу." @@ -2803,7 +2803,7 @@ msgstr "Забагато запитів на зміну email протягом msgid "A Google account is connected. You can use it to sign in." msgstr "Обліковий запис Google підключено. Тепер за його допомогою можна входити." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:382 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:388 #, elixir-autogen, elixir-format msgid "A different Google account is already connected." msgstr "Уже підключено інший обліковий запис Google." @@ -2833,13 +2833,13 @@ msgstr "Створити обліковий запис і надіслати п msgid "Email me a sign-in link" msgstr "Надіслати посилання для входу на email" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:390 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:396 #, elixir-autogen, elixir-format msgid "Google account connection session is invalid." msgstr "Сесія підключення облікового запису Google недійсна." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:218 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:288 #, elixir-autogen, elixir-format msgid "Google did not provide a verified email address." msgstr "Google не надав підтверджену адресу email." @@ -2849,38 +2849,38 @@ msgstr "Google не надав підтверджену адресу email." msgid "Google sign-in" msgstr "Вхід через Google" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:372 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:378 #, elixir-autogen, elixir-format msgid "Google sign-in connected." msgstr "Вхід через Google підключено." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:315 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:321 #, elixir-autogen, elixir-format msgid "Google sign-in could not be started." msgstr "Не вдалося розпочати вхід через Google." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:217 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:287 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:294 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:223 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:293 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:300 #, elixir-autogen, elixir-format msgid "Google sign-in failed. Please try again." msgstr "Не вдалося увійти через Google. Спробуйте ще раз." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:320 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:326 #, elixir-autogen, elixir-format msgid "Google sign-in is not configured yet." msgstr "Вхід через Google ще не налаштовано." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:87 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:202 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:272 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:529 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:86 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:208 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:278 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 #, elixir-autogen, elixir-format msgid "Google sign-in session expired. Please start again." msgstr "Сесія входу через Google завершилася. Почніть заново." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:206 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:276 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:212 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:282 #, elixir-autogen, elixir-format msgid "Google sign-in session is invalid." msgstr "Сесія входу через Google недійсна." @@ -2900,18 +2900,18 @@ msgstr "Надсилаємо посилання..." msgid "Sign up with Google" msgstr "Зареєструватися через Google" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:377 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:383 #, elixir-autogen, elixir-format msgid "That Google account belongs to another local account." msgstr "Цей обліковий запис Google належить іншому локальному обліковому запису." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:36 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:150 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:35 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:155 #, elixir-autogen, elixir-format msgid "The Google registration form is invalid." msgstr "Форму реєстрації через Google заповнено неправильно." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:535 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:541 #, elixir-autogen, elixir-format msgid "This Google account cannot be used right now." msgstr "Цей обліковий запис Google зараз не можна використовувати." @@ -2931,7 +2931,7 @@ msgstr "Увійти за паролем" msgid "We will send a one-time sign-in link. No password is required." msgstr "Ми надішлемо одноразове посилання для входу. Пароль не потрібен." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:105 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:110 #, elixir-autogen, elixir-format msgid "Your account was created with Google." msgstr "Ваш обліковий запис створено через Google." @@ -2956,12 +2956,12 @@ msgstr "Відключити Google" msgid "Disconnect Google sign-in from this account?" msgstr "Відключити вхід через Google для цього облікового запису?" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:51 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:50 #, elixir-autogen, elixir-format msgid "Google sign-in disconnected." msgstr "Вхід через Google відключено." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:56 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:55 #, elixir-autogen, elixir-format msgid "No Google account is connected." msgstr "Обліковий запис Google не підключено." @@ -3396,7 +3396,7 @@ msgstr "Зазначена підстава:" msgid "Subject" msgstr "Тема" -#: lib/who_need_help_web/live/activity_live/show.ex:767 +#: lib/who_need_help_web/live/activity_live/show.ex:785 #: lib/who_need_help_web/live/request_live/show.ex:1790 #, elixir-autogen, elixir-format msgid "Submit a legal content-removal notice" @@ -3953,7 +3953,7 @@ msgstr "Скасувати й вибрати інший спосіб входу" msgid "Confirm access once so nobody can attach Google to your account using only a matching email address." msgstr "Один раз підтвердьте доступ, щоб ніхто не зміг прив’язати Google до вашого облікового запису лише за збігом email." -#: lib/who_need_help_web/controllers/google_auth_controller.ex:491 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:497 #: lib/who_need_help_web/controllers/google_auth_html/complete.html.heex:10 #: lib/who_need_help_web/controllers/user_session_html/new.html.heex:82 #, elixir-autogen, elixir-format @@ -3980,7 +3980,7 @@ msgstr "Створюємо обліковий запис..." msgid "Email me a verification link" msgstr "Надіслати посилання для підтвердження на email" -#: lib/who_need_help_web/controllers/user_session_controller.ex:179 +#: lib/who_need_help_web/controllers/user_session_controller.ex:183 #, elixir-autogen, elixir-format msgid "Google sign-in connected. You can use it next time." msgstr "Вхід через Google підключено. Наступного разу можна скористатися ним." @@ -4010,8 +4010,8 @@ msgstr "Один раз увійдіть до %{email}, щоб підключи msgid "This email already has an account" msgstr "Для цього email уже є обліковий запис" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:117 -#: lib/who_need_help_web/controllers/google_auth_controller.ex:139 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:122 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:144 #, elixir-autogen, elixir-format msgid "This email already has an account. Confirm it once to connect Google." msgstr "Для цього email уже є обліковий запис. Один раз підтвердьте його, щоб підключити Google." @@ -4027,12 +4027,12 @@ msgstr "Скористатися паролем" msgid "Verified" msgstr "Підтверджено" -#: lib/who_need_help_web/controllers/google_auth_controller.ex:164 +#: lib/who_need_help_web/controllers/google_auth_controller.ex:170 #, elixir-autogen, elixir-format msgid "We sent a verification link to %{email}. Open it to finish connecting Google." msgstr "Ми надіслали посилання для підтвердження на %{email}. Відкрийте його, щоб завершити підключення Google." -#: lib/who_need_help_web/controllers/user_session_controller.ex:192 +#: lib/who_need_help_web/controllers/user_session_controller.ex:196 #, elixir-autogen, elixir-format msgid "You are signed in, but Google could not be connected. Try again in account settings." msgstr "Ви ввійшли, але підключити Google не вдалося. Повторіть спробу в налаштуваннях облікового запису." @@ -4151,7 +4151,7 @@ msgstr "Не вдалося скопіювати" msgid "Date" msgstr "Дата" -#: lib/who_need_help_web/live/activity_live/new.ex:474 +#: lib/who_need_help_web/live/activity_live/new.ex:478 #: lib/who_need_help_web/live/notification_live.ex:683 #: lib/who_need_help_web/live/request_live/new.ex:660 #, elixir-autogen, elixir-format @@ -4350,7 +4350,7 @@ msgstr "Запит на реєстрацію прийнято" msgid "Request expires" msgstr "Заявка діє до" -#: lib/who_need_help_web/live/activity_live/show.ex:569 +#: lib/who_need_help_web/live/activity_live/show.ex:587 #, elixir-autogen, elixir-format msgid "Request to join again" msgstr "Подати заявку на участь" @@ -4565,12 +4565,12 @@ msgstr "Що мають бачити інші?" msgid "Where is help needed?" msgstr "Де потрібна допомога?" -#: lib/who_need_help_web/live/activity_live/show.ex:584 +#: lib/who_need_help_web/live/activity_live/show.ex:602 #, elixir-autogen, elixir-format msgid "You are no longer a participant and cannot access the group chat or exact meeting point. You can request to join again while registration is open." msgstr "Ви більше не учасник і не маєте доступу до групового чату чи точного місця зустрічі. Поки реєстрація відкрита, можна знову подати заявку." -#: lib/who_need_help_web/live/activity_live/show.ex:595 +#: lib/who_need_help_web/live/activity_live/show.ex:613 #, elixir-autogen, elixir-format msgid "You are not a participant. You can send a new request while registration is open." msgstr "Ви не берете участі. Поки реєстрація відкрита, можна надіслати нову заявку." @@ -4585,7 +4585,7 @@ msgstr "Можна окремо поділитися геопозицією з msgid "You have not submitted any content-removal notices while signed in." msgstr "Після входу ви ще не надсилали повідомлень про видалення контенту." -#: lib/who_need_help_web/live/activity_live/show.ex:582 +#: lib/who_need_help_web/live/activity_live/show.ex:600 #, elixir-autogen, elixir-format msgid "You left this activity" msgstr "Ви вийшли з цієї активності" @@ -4600,7 +4600,7 @@ msgstr "Ваше підтвердження завершення збереже msgid "Your exact position is not shown" msgstr "Вашу точну позицію не показано" -#: lib/who_need_help_web/live/activity_live/show.ex:593 +#: lib/who_need_help_web/live/activity_live/show.ex:611 #, elixir-autogen, elixir-format msgid "Your join request was declined" msgstr "Вашу заявку на участь відхилено" @@ -4650,7 +4650,7 @@ msgstr "Перетягніть, щоб перемістити точну точ msgid "Exact point selected. Drag the red pin or click the map to move it." msgstr "Точну точку вибрано. Перетягніть червоний маркер або натисніть на карту, щоб перемістити її." -#: lib/who_need_help_web/live/request_live/index.ex:434 +#: lib/who_need_help_web/live/request_live/index.ex:453 #, elixir-autogen, elixir-format msgid "%{count} request in this map area" msgid_plural "%{count} requests in this map area" @@ -4658,35 +4658,37 @@ msgstr[0] "%{count} заявка в цій області карти" msgstr[1] "%{count} заявки в цій області карти" msgstr[2] "%{count} заявок у цій області карти" -#: lib/who_need_help_web/live/activity_live/index.ex:396 -#: lib/who_need_help_web/live/request_live/index.ex:416 +#: lib/who_need_help_web/live/activity_live/index.ex:438 +#: lib/who_need_help_web/live/request_live/index.ex:430 #, elixir-autogen, elixir-format msgid "Area or city" msgstr "Район або місто" -#: lib/who_need_help_web/live/activity_live/index.ex:412 -#: lib/who_need_help_web/live/request_live/index.ex:487 +#: lib/who_need_help_web/live/activity_live/index.ex:525 +#: lib/who_need_help_web/live/request_live/index.ex:515 #, elixir-autogen, elixir-format msgid "Choose an area on the map" msgstr "Виберіть область на карті" -#: lib/who_need_help_web/live/request_live/index.ex:336 +#: lib/who_need_help_web/live/request_live/index.ex:350 #, elixir-autogen, elixir-format msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters." msgstr "Переглядайте приватні області та точні публічні місця зустрічі, вибрані авторами заявок." -#: lib/who_need_help_web/live/request_live/index.ex:455 +#: lib/who_need_help_web/live/activity_live/index.ex:484 +#: lib/who_need_help_web/live/request_live/index.ex:474 #, elixir-autogen, elixir-format msgid "List" msgstr "Список" -#: lib/who_need_help_web/live/request_live/index.ex:558 +#: lib/who_need_help_web/live/request_live/index.ex:586 #, elixir-autogen, elixir-format msgid "Load more in this area" msgstr "Завантажити ще в цій області" #: lib/who_need_help_web/components/layouts/root.html.heex:5 -#: lib/who_need_help_web/live/request_live/index.ex:475 +#: lib/who_need_help_web/live/activity_live/index.ex:504 +#: lib/who_need_help_web/live/request_live/index.ex:494 #, elixir-autogen, elixir-format msgid "Map" msgstr "Карта" @@ -4696,51 +4698,53 @@ msgstr "Карта" msgid "Map preview" msgstr "Попередній перегляд карти" -#: lib/who_need_help_web/live/activity_live/index.ex:424 -#: lib/who_need_help_web/live/request_live/index.ex:499 +#: lib/who_need_help_web/live/activity_live/index.ex:537 +#: lib/who_need_help_web/live/request_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Move or zoom the map to search somewhere else." msgstr "Перемістіть або масштабуйте карту, щоб шукати в іншому місці." -#: lib/who_need_help_web/live/request_live/index.ex:442 +#: lib/who_need_help_web/live/activity_live/index.ex:471 +#: lib/who_need_help_web/live/request_live/index.ex:461 #, elixir-autogen, elixir-format msgid "Move the map, then search the newly visible area." msgstr "Перемістіть карту, а потім виконайте пошук у новій видимій області." -#: lib/who_need_help_web/live/request_live/index.ex:497 +#: lib/who_need_help_web/live/request_live/index.ex:525 #, elixir-autogen, elixir-format msgid "No open requests in this area" msgstr "У цій області немає відкритих заявок" -#: lib/who_need_help_web/live/request_live/index.ex:489 +#: lib/who_need_help_web/live/request_live/index.ex:517 #, elixir-autogen, elixir-format msgid "Only requests from the selected map area will be loaded." msgstr "Буде завантажено лише заявки з вибраної області карти." #: lib/who_need_help_web/components/layouts/root.html.heex:8 -#: lib/who_need_help_web/live/request_live/index.ex:585 +#: lib/who_need_help_web/live/request_live/index.ex:613 #, elixir-autogen, elixir-format msgid "Open request" msgstr "Відкрити заявку" -#: lib/who_need_help_web/live/activity_live/index.ex:397 -#: lib/who_need_help_web/live/request_live/index.ex:417 +#: lib/who_need_help_web/live/activity_live/index.ex:439 +#: lib/who_need_help_web/live/request_live/index.ex:431 #, elixir-autogen, elixir-format msgid "Optional text search" msgstr "Необов’язковий текстовий пошук" -#: lib/who_need_help_web/live/request_live/index.ex:564 +#: lib/who_need_help_web/live/request_live/index.ex:592 #, elixir-autogen, elixir-format msgid "Requests without map coordinates appear only when their public area description matches the text search." msgstr "Заявки без координат на карті з’являються лише тоді, коли опис публічної області збігається з текстовим пошуком." -#: lib/who_need_help_web/live/request_live/index.ex:446 +#: lib/who_need_help_web/live/activity_live/index.ex:475 +#: lib/who_need_help_web/live/request_live/index.ex:465 #, elixir-autogen, elixir-format msgid "Results layout" msgstr "Розташування результатів" -#: lib/who_need_help_web/live/activity_live/index.ex:508 -#: lib/who_need_help_web/live/request_live/index.ex:597 +#: lib/who_need_help_web/live/activity_live/index.ex:621 +#: lib/who_need_help_web/live/request_live/index.ex:625 #, elixir-autogen, elixir-format msgid "Search this area" msgstr "Шукати в цій області" @@ -4750,20 +4754,21 @@ msgstr "Шукати в цій області" msgid "Static preview" msgstr "Статичний перегляд" -#: lib/who_need_help_web/live/request_live/index.ex:465 +#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/request_live/index.ex:484 #, elixir-autogen, elixir-format msgid "Together" msgstr "Разом" #: lib/who_need_help_web/components/layouts/root.html.heex:6 #: lib/who_need_help_web/controllers/page_html/home.html.heex:160 -#: lib/who_need_help_web/live/activity_live/index.ex:496 +#: lib/who_need_help_web/live/activity_live/index.ex:609 #: lib/who_need_help_web/live/activity_live/new.ex:355 #: lib/who_need_help_web/live/activity_live/new.ex:414 -#: lib/who_need_help_web/live/activity_live/show.ex:545 +#: lib/who_need_help_web/live/activity_live/show.ex:546 #: lib/who_need_help_web/live/notification_live.ex:602 #: lib/who_need_help_web/live/notification_live.ex:621 -#: lib/who_need_help_web/live/request_live/index.ex:584 +#: lib/who_need_help_web/live/request_live/index.ex:612 #: lib/who_need_help_web/live/request_live/new.ex:485 #: lib/who_need_help_web/live/request_live/new.ex:563 #: lib/who_need_help_web/live/request_live/show.ex:1277 @@ -5153,7 +5158,7 @@ msgid "Nearby alert deleted." msgstr "Сповіщення поблизу видалено." #: lib/who_need_help_web/live/notification_live.ex:456 -#: lib/who_need_help_web/live/request_live/index.ex:362 +#: lib/who_need_help_web/live/request_live/index.ex:376 #, elixir-autogen, elixir-format msgid "Nearby requests" msgstr "Заявки поблизу" @@ -5493,8 +5498,8 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec msgstr "WebGL недоступний у цьому браузері, тому повна карта не може запуститися. Виберіть приклад нижче або спробуйте запустити карту ще раз." #: lib/who_need_help_web/components/layouts/root.html.heex:7 -#: lib/who_need_help_web/live/activity_live/index.ex:497 -#: lib/who_need_help_web/live/activity_live/show.ex:546 +#: lib/who_need_help_web/live/activity_live/index.ex:610 +#: lib/who_need_help_web/live/activity_live/show.ex:547 #: lib/who_need_help_web/live/request_live/show.ex:1278 #, elixir-autogen, elixir-format msgid "Show location on map" @@ -6058,7 +6063,7 @@ msgstr "Тепер ваш відгук видно" msgid "Delivery settings" msgstr "Налаштування доставки" -#: lib/who_need_help_web/live/request_live/index.ex:541 +#: lib/who_need_help_web/live/request_live/index.ex:569 #, elixir-autogen, elixir-format msgid "Expires" msgstr "Спливає" @@ -6078,12 +6083,12 @@ msgstr "Сповіщення поруч" msgid "Notification sections" msgstr "Розділи сповіщень" -#: lib/who_need_help_web/live/activity_live/index.ex:492 +#: lib/who_need_help_web/live/activity_live/index.ex:605 #, elixir-autogen, elixir-format msgid "%{count} activities" msgstr "Активностей: %{count}" -#: lib/who_need_help_web/live/activity_live/index.ex:512 +#: lib/who_need_help_web/live/activity_live/index.ex:463 #, elixir-autogen, elixir-format msgid "%{count} activity in this map area" msgid_plural "%{count} activities in this map area" @@ -6091,17 +6096,17 @@ msgstr[0] "%{count} активність у цій області карти" msgstr[1] "%{count} активності в цій області карти" msgstr[2] "%{count} активностей у цій області карти" -#: lib/who_need_help_web/live/request_live/index.ex:586 +#: lib/who_need_help_web/live/request_live/index.ex:614 #, elixir-autogen, elixir-format msgid "%{count} requests" msgstr "Запитів: %{count}" -#: lib/who_need_help_web/live/activity_live/index.ex:530 +#: lib/who_need_help_web/live/activity_live/index.ex:645 #, elixir-autogen, elixir-format msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list." msgstr "Створені вами активності й ті, до яких ви попросили приєднатися, з’являться тут. Після виходу активність зникне зі списку." -#: lib/who_need_help_web/live/activity_live/index.ex:351 +#: lib/who_need_help_web/live/activity_live/index.ex:393 #, elixir-autogen, elixir-format msgid "Activity views" msgstr "Розділи активностей" @@ -6116,12 +6121,12 @@ msgstr "Оберіть категорію та зрозуміло опишіть msgid "Confirm the safety guidance before asking nearby people for help." msgstr "Підтвердьте правила безпеки перед публікацією запиту." -#: lib/who_need_help_web/live/request_live/index.ex:631 +#: lib/who_need_help_web/live/request_live/index.ex:659 #, elixir-autogen, elixir-format msgid "Create request" msgstr "Створити запит" -#: lib/who_need_help_web/live/request_live/index.ex:645 +#: lib/who_need_help_web/live/request_live/index.ex:673 #, elixir-autogen, elixir-format msgid "Create your first request" msgstr "Створити перший запит" @@ -6136,43 +6141,43 @@ msgstr "Що потрібно зробити" msgid "Location" msgstr "Місце" -#: lib/who_need_help_web/live/activity_live/index.ex:357 +#: lib/who_need_help_web/live/activity_live/index.ex:399 #, elixir-autogen, elixir-format msgid "My activities" msgstr "Мої активності" -#: lib/who_need_help_web/live/request_live/index.ex:363 -#: lib/who_need_help_web/live/request_live/index.ex:625 +#: lib/who_need_help_web/live/request_live/index.ex:377 +#: lib/who_need_help_web/live/request_live/index.ex:653 #, elixir-autogen, elixir-format msgid "My requests" msgstr "Мої запити" -#: lib/who_need_help_web/live/activity_live/index.ex:356 +#: lib/who_need_help_web/live/activity_live/index.ex:398 #, elixir-autogen, elixir-format msgid "Nearby activities" msgstr "Активності поруч" -#: lib/who_need_help_web/live/activity_live/index.ex:528 +#: lib/who_need_help_web/live/activity_live/index.ex:643 #, elixir-autogen, elixir-format msgid "No activities yet" msgstr "Активностей поки немає" -#: lib/who_need_help_web/live/activity_live/index.ex:422 +#: lib/who_need_help_web/live/activity_live/index.ex:535 #, elixir-autogen, elixir-format msgid "No open activities in this area" msgstr "У цій області немає відкритих активностей" -#: lib/who_need_help_web/live/request_live/index.ex:640 +#: lib/who_need_help_web/live/request_live/index.ex:668 #, elixir-autogen, elixir-format msgid "No requests yet" msgstr "Запитів поки немає" -#: lib/who_need_help_web/live/activity_live/index.ex:414 +#: lib/who_need_help_web/live/activity_live/index.ex:527 #, elixir-autogen, elixir-format msgid "Only activities from the selected map area will be loaded." msgstr "Буде завантажено лише активності з вибраної області карти." -#: lib/who_need_help_web/live/activity_live/index.ex:491 +#: lib/who_need_help_web/live/activity_live/index.ex:604 #, elixir-autogen, elixir-format msgid "Open activity" msgstr "Відкрити активність" @@ -6182,12 +6187,12 @@ msgstr "Відкрити активність" msgid "Request form steps" msgstr "Кроки створення запиту" -#: lib/who_need_help_web/live/request_live/index.ex:357 +#: lib/who_need_help_web/live/request_live/index.ex:371 #, elixir-autogen, elixir-format msgid "Request views" msgstr "Розділи запитів" -#: lib/who_need_help_web/live/request_live/index.ex:642 +#: lib/who_need_help_web/live/request_live/index.ex:670 #, elixir-autogen, elixir-format msgid "Requests you publish or accept will appear here." msgstr "Тут з’являться запити, які ви створили або прийняли." @@ -6212,7 +6217,7 @@ msgstr "Укажіть терміновість і час закриття за msgid "Share only as much location detail as helpers need." msgstr "Покажіть помічникам лише необхідну інформацію про місце." -#: lib/who_need_help_web/live/activity_live/index.ex:494 +#: lib/who_need_help_web/live/activity_live/index.ex:607 #, elixir-autogen, elixir-format msgid "The map is unavailable in this browser. Activity details remain usable." msgstr "Карта недоступна в цьому браузері. Інформація про активності залишається доступною." @@ -6222,7 +6227,7 @@ msgstr "Карта недоступна в цьому браузері. Інфо msgid "Timing" msgstr "Час" -#: lib/who_need_help_web/live/request_live/index.ex:627 +#: lib/who_need_help_web/live/request_live/index.ex:655 #, elixir-autogen, elixir-format msgid "Track requests you created and requests you accepted as a helper." msgstr "Стежте за створеними запитами й тими, які ви прийняли як помічник." @@ -6827,6 +6832,7 @@ msgid "People can find it by the area description" msgstr "Активність можна знайти за описом району" #: lib/who_need_help_web/live/activity_live/new.ex:455 +#: lib/who_need_help_web/live/activity_live/show.ex:560 #, elixir-autogen, elixir-format msgid "People can search by the public area description. Approved participants receive the exact point." msgstr "Люди можуть шукати за публічним описом району. Схвалені учасники отримають точне місце." @@ -6862,11 +6868,12 @@ msgid "The public map stays approximate" msgstr "На публічній карті показано лише приблизну область" #: lib/who_need_help_web/live/activity_live/new.ex:453 +#: lib/who_need_help_web/live/activity_live/show.ex:558 #, elixir-autogen, elixir-format msgid "The public map stays hidden" msgstr "Публічна карта залишається прихованою" -#: lib/who_need_help_web/live/activity_live/new.ex:476 +#: lib/who_need_help_web/live/activity_live/new.ex:480 #, elixir-autogen, elixir-format msgid "These coordinates are the exact meeting point. They are revealed only after approval." msgstr "Ці координати задають точне місце зустрічі. Їх буде розкрито лише після схвалення." @@ -6880,3 +6887,12 @@ msgstr "Що мають бачити люди до схвалення?" #, elixir-autogen, elixir-format msgid "Choose the meeting point on the map or use your current location." msgstr "Оберіть місце зустрічі на карті або скористайтеся своїм поточним місцезнаходженням." + +#: lib/who_need_help_web/live/activity_live/index.ex:458 +#: lib/who_need_help_web/live/activity_live/index.ex:517 +#: lib/who_need_help_web/live/activity_live/index.ex:635 +#: lib/who_need_help_web/live/request_live/index.ex:448 +#: lib/who_need_help_web/live/request_live/index.ex:507 +#, elixir-autogen, elixir-format +msgid "Loading results…" +msgstr "Завантажуємо результати…" diff --git a/scripts/load-cycle.sh b/scripts/load-cycle.sh index 09474cd..1493486 100755 --- a/scripts/load-cycle.sh +++ b/scripts/load-cycle.sh @@ -57,6 +57,29 @@ set_env_value APP_IMAGE "who-need-help:load-$safe_id" set_env_value SOCKET_PROXY_IMAGE "who-need-help:socket-proxy-load-$safe_id" set_env_value POSTGIS_IMAGE "who-need-help:postgis-load-$safe_id" +apply_positive_integer_override() { + local override_name=$1 + local target_name=$2 + local value=${!override_name:-} + + if [[ -z "$value" ]]; then + return + fi + + if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then + echo "$override_name must be a positive integer." >&2 + exit 1 + fi + + set_env_value "$target_name" "$value" +} + +apply_positive_integer_override WNH_LOAD_WEB_REPLICAS_OVERRIDE LOAD_WEB_REPLICAS +apply_positive_integer_override WNH_LOAD_WORKER_REPLICAS_OVERRIDE LOAD_WORKER_REPLICAS +apply_positive_integer_override WNH_LOAD_HTTP_VUS_OVERRIDE LOAD_HTTP_VUS +apply_positive_integer_override WNH_LOAD_WS_VUS_OVERRIDE LOAD_WS_VUS +apply_positive_integer_override WNH_LOAD_AUTH_VUS_OVERRIDE LOAD_AUTH_VUS + set -a # shellcheck source=/dev/null . "$temporary_env" diff --git a/scripts/load-run.sh b/scripts/load-run.sh index 81af27e..c0a291a 100755 --- a/scripts/load-run.sh +++ b/scripts/load-run.sh @@ -765,7 +765,18 @@ trap cleanup_on_exit EXIT HUP INT TERM printf 'fixture_count=%s\n' "$fixture_count" printf 'authenticated_websocket_timeout_ms=%s\n' "$LOAD_AUTH_WS_TIMEOUT_MS" printf 'authenticated_think_seconds=%s\n' "$LOAD_AUTH_THINK_SECONDS" + printf 'cpu_replay=%s\n' "${WNH_LOAD_CPU_REPLAY:-false}" + printf 'cpu_replay_cpuset=%s\n' "${CPU_REPLAY_CPUSET:-unconstrained}" + printf 'cpu_replay_web_cpus=%s\n' "${CPU_REPLAY_WEB_CPUS:-unconstrained}" + printf 'cpu_replay_worker_cpus=%s\n' "${CPU_REPLAY_WORKER_CPUS:-unconstrained}" + printf 'cpu_replay_web_schedulers=%s\n' \ + "${CPU_REPLAY_WEB_SCHEDULERS:-unconstrained}" + printf 'cpu_replay_worker_schedulers=%s\n' \ + "${CPU_REPLAY_WORKER_SCHEDULERS:-unconstrained}" docker info --format 'docker_cpus={{.NCPU}} docker_memory_bytes={{.MemTotal}} docker_server={{.ServerVersion}}' + docker inspect \ + --format 'container={{.Name}} nano_cpus={{.HostConfig.NanoCpus}} cpuset={{.HostConfig.CpusetCpus}}' \ + "${measured_containers[@]}" docker compose version uname -a } >"$output_dir/environment.txt" diff --git a/scripts/load-stack-up.sh b/scripts/load-stack-up.sh index ad9a572..c6ca524 100755 --- a/scripts/load-stack-up.sh +++ b/scripts/load-stack-up.sh @@ -17,6 +17,8 @@ set +a : "${LOAD_HOST:?LOAD_HOST is missing from the generated load runtime environment}" : "${LOAD_WEB_REPLICAS:?LOAD_WEB_REPLICAS is missing from the generated load runtime environment}" : "${LOAD_WORKER_REPLICAS:?LOAD_WORKER_REPLICAS is missing from the generated load runtime environment}" +: "${LOAD_RESILIENCE_RECOVERY_TIMEOUT_SECONDS:?LOAD_RESILIENCE_RECOVERY_TIMEOUT_SECONDS is missing from the generated load runtime environment}" +: "${LOAD_RESILIENCE_PROBE_INTERVAL_SECONDS:?LOAD_RESILIENCE_PROBE_INTERVAL_SECONDS is missing from the generated load runtime environment}" if [ "$LOAD_PROJECT" = who_need_help ]; then echo "The load profile must not use the staging Compose project." >&2 @@ -36,12 +38,22 @@ esac cd "$ROOT" compose() { - docker compose \ - --env-file "$ENV_FILE" \ - -p "$LOAD_PROJECT" \ - -f compose.yaml \ - -f compose.load.yaml \ - "$@" + if [ "${WNH_LOAD_CPU_REPLAY:-false}" = true ]; then + docker compose \ + --env-file "$ENV_FILE" \ + -p "$LOAD_PROJECT" \ + -f compose.yaml \ + -f compose.load.yaml \ + -f compose.cpu-replay.yaml \ + "$@" + else + docker compose \ + --env-file "$ENV_FILE" \ + -p "$LOAD_PROJECT" \ + -f compose.yaml \ + -f compose.load.yaml \ + "$@" + fi } cleanup_failed_start() { @@ -94,13 +106,26 @@ compose up -d --build --wait \ COMPOSE_PROJECT_NAME=$LOAD_PROJECT "$ROOT/scripts/verify-realtime-cluster.sh" compose COMPOSE_PROJECT_NAME=$LOAD_PROJECT "$ROOT/scripts/verify-beam-runtime.sh" compose -status=$( - curl --silent --show-error \ - --header "Host: $LOAD_HOST" \ - --output /dev/null \ - --write-out '%{http_code}' \ - "http://localhost:$HTTP_PORT/healthz/ready" -) +deadline=$(( $(date +%s) + LOAD_RESILIENCE_RECOVERY_TIMEOUT_SECONDS )) +status=000 + +while [ "$(date +%s)" -le "$deadline" ]; do + if ! status=$( + curl --silent \ + --header "Host: $LOAD_HOST" \ + --output /dev/null \ + --write-out '%{http_code}' \ + "http://localhost:$HTTP_PORT/healthz/ready" + ); then + status=000 + fi + + if [ "$status" = 200 ]; then + break + fi + + sleep "$LOAD_RESILIENCE_PROBE_INTERVAL_SECONDS" +done if [ "$status" != 200 ]; then echo "Load-profile readiness returned HTTP $status instead of 200." >&2 diff --git a/scripts/quality.sh b/scripts/quality.sh index 8f920d6..f0fdfe0 100755 --- a/scripts/quality.sh +++ b/scripts/quality.sh @@ -1126,6 +1126,7 @@ WEB_REPLICAS=1 WORKER_REPLICAS=1 \ .services.web.deploy.replicas == 1 and .services.worker.deploy.replicas == 1 ' >/dev/null +CPU_REPLAY_CPUSET=0 \ CPU_REPLAY_WEB_CPUS=1 \ CPU_REPLAY_WORKER_CPUS=1 \ CPU_REPLAY_WEB_SCHEDULERS=1 \ diff --git a/scripts/scale-common.sh b/scripts/scale-common.sh new file mode 100755 index 0000000..19a5562 --- /dev/null +++ b/scripts/scale-common.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# Variables assigned here are exported for Docker Compose and child containers. +# The literal backticks belong to Traefik's rule syntax. +# shellcheck disable=SC2034,SC2016 + +scale_load_environment() { + SCALE_ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) + SCALE_ENV_FILE=${WNH_SCALE_ENV_FILE:-"$SCALE_ROOT/output/runtime/scale.env"} + + WNH_LOAD_ENV_FILE=$SCALE_ENV_FILE "$SCALE_ROOT/scripts/ensure-local-load-env.sh" + + set -a + # shellcheck source=/dev/null + . "$SCALE_ENV_FILE" + + LOAD_PROJECT=who_need_help_scale + COMPOSE_PROJECT_NAME=$LOAD_PROJECT + LOAD_HOST=scale.localhost + HTTP_BIND_ADDRESS=127.0.0.1 + HTTP_PORT=${WNH_SCALE_HTTP_PORT:-4120} + SCALE_HTTPS_BIND_ADDRESS=127.0.0.1 + SCALE_HTTPS_PORT=${WNH_SCALE_HTTPS_PORT:-4121} + MAILPIT_BIND_ADDRESS=127.0.0.1 + MAILPIT_PORT=${WNH_SCALE_MAILPIT_PORT:-8129} + PHX_HOST=$LOAD_HOST + PHX_SCHEME=https + PHX_URL_PORT=$SCALE_HTTPS_PORT + POSTGRES_DB=who_need_help_scale + POSTGRES_USER=wnh_scale + DATABASE_URL="ecto://$POSTGRES_USER:$POSTGRES_PASSWORD@db/$POSTGRES_DB" + APP_IMAGE=who-need-help:scale + SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-scale + POSTGIS_IMAGE=who-need-help:postgis-scale + TRAEFIK_PROJECT_CONSTRAINT=$LOAD_PROJECT + TRAEFIK_APP_NAME=who-need-help-scale + TRAEFIK_DOCKER_NETWORK="${LOAD_PROJECT}_ingress" + TRAEFIK_ROUTER_RULE='Host(`scale.localhost`)' + WEB_REPLICAS=1 + WORKER_REPLICAS=1 + LOAD_WEB_REPLICAS=1 + LOAD_WORKER_REPLICAS=1 + CPU_REPLAY_CPUSET=${WNH_SCALE_CPUSET:-0} + CPU_REPLAY_WEB_CPUS=1 + CPU_REPLAY_WORKER_CPUS=1 + CPU_REPLAY_WEB_SCHEDULERS=1 + CPU_REPLAY_WORKER_SCHEDULERS=1 + SCALE_VIEWER_PASSWORD=${SCALE_VIEWER_PASSWORD:-$LOAD_FIXTURE_PASSWORD} + WNH_SCALE_EXPECTED_DATABASE=$POSTGRES_DB + WNH_SCALE_FIXTURE_CONFIRM=persistent-local-scale + set +a +} + +scale_compose() { + docker compose \ + --env-file "$SCALE_ENV_FILE" \ + -p "$LOAD_PROJECT" \ + -f "$SCALE_ROOT/compose.yaml" \ + -f "$SCALE_ROOT/compose.load.yaml" \ + -f "$SCALE_ROOT/compose.scale.yaml" \ + -f "$SCALE_ROOT/compose.cpu-replay.yaml" \ + "$@" +} diff --git a/scripts/scale-readonly-load.sh b/scripts/scale-readonly-load.sh new file mode 100755 index 0000000..7b4da98 --- /dev/null +++ b/scripts/scale-readonly-load.sh @@ -0,0 +1,161 @@ +#!/bin/sh +set -eu +umask 077 + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=scripts/scale-common.sh +. "$ROOT/scripts/scale-common.sh" +scale_load_environment + +K6_IMAGE="grafana/k6@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669" +STATE_FILE=${WNH_SCALE_BROWSER_STATE:-"$ROOT/output/runtime/scale-browser-state.json"} +VUS=${WNH_SCALE_READONLY_VUS:?WNH_SCALE_READONLY_VUS is required} +DURATION=${WNH_SCALE_READONLY_DURATION:?WNH_SCALE_READONLY_DURATION is required} +THINK_SECONDS=${WNH_SCALE_READONLY_THINK_SECONDS:?WNH_SCALE_READONLY_THINK_SECONDS is required} +LABEL=${WNH_SCALE_READONLY_LABEL:-"scale-readonly-$(date -u +%Y%m%dT%H%M%SZ)"} +OUTPUT_DIR="$ROOT/output/performance/$LABEL" + +case "$VUS" in + ''|*[!0-9]*|0) + echo "WNH_SCALE_READONLY_VUS must be a positive integer." >&2 + exit 2 + ;; +esac + +if ! printf '%s\n' "$DURATION" | grep -Eq '^[1-9][0-9]*(ms|s|m|h)$'; then + echo "WNH_SCALE_READONLY_DURATION must be a positive k6 duration." >&2 + exit 2 +fi + +if ! printf '%s\n' "$THINK_SECONDS" | + grep -Eq '^([0-9]+([.][0-9]+)?|[.][0-9]+)$'; then + echo "WNH_SCALE_READONLY_THINK_SECONDS must be a non-negative number." >&2 + exit 2 +fi + +case "$LABEL" in + ''|*[!A-Za-z0-9._-]*) + echo "WNH_SCALE_READONLY_LABEL contains unsupported characters." >&2 + exit 2 + ;; +esac + +if [ ! -f "$STATE_FILE" ]; then + echo "Missing authenticated scale browser state: $STATE_FILE" >&2 + exit 2 +fi + +session_cookie=$( + jq -r ' + .cookies[] + | select(.domain == "scale.localhost" and .name == "_who_need_help_key") + | "\(.name)=\(.value)" + ' "$STATE_FILE" +) + +if [ -z "$session_cookie" ]; then + echo "The browser state does not contain the scale session cookie." >&2 + exit 2 +fi + +web_id=$(scale_compose ps -q web) +worker_id=$(scale_compose ps -q worker) +db_id=$(scale_compose ps -q db) + +for pair in "web:$web_id" "worker:$worker_id" "db:$db_id"; do + service=${pair%%:*} + container=${pair#*:} + + if [ -z "$container" ] || [ "$(docker inspect --format '{{.State.Status}}' "$container")" != "running" ]; then + echo "Scale service $service is not running." >&2 + exit 2 + fi +done + +actual_database=$( + scale_compose exec -T db \ + psql -XAt -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ + -c 'SELECT current_database()' +) + +if [ "$actual_database" != "$WNH_SCALE_EXPECTED_DATABASE" ]; then + echo "Refusing unexpected database: $actual_database" >&2 + exit 2 +fi + +if [ -e "$OUTPUT_DIR" ]; then + echo "Output path already exists: $OUTPUT_DIR" >&2 + exit 2 +fi + +mkdir -p "$OUTPUT_DIR" +chmod 700 "$ROOT/output" "$ROOT/output/performance" "$OUTPUT_DIR" + +snapshot_counts() { + destination=$1 + + scale_compose exec -T db \ + psql -XAt -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ + -c " + SELECT json_build_object( + 'captured_at', clock_timestamp(), + 'database', current_database(), + 'users', (SELECT count(*) FROM users), + 'users_tokens', (SELECT count(*) FROM users_tokens), + 'help_requests', (SELECT count(*) FROM help_requests), + 'activities', (SELECT count(*) FROM activities), + 'help_assignments', (SELECT count(*) FROM help_assignments), + 'activity_participants', (SELECT count(*) FROM activity_participants), + 'messages', (SELECT count(*) FROM messages), + 'activity_messages', (SELECT count(*) FROM activity_messages), + 'rate_limit_buckets', (SELECT count(*) FROM rate_limit_buckets) + ) + " >"$destination" +} + +snapshot_counts "$OUTPUT_DIR/counts-before.json" + +cat >"$OUTPUT_DIR/scope.txt" </dev/null + +docker run --rm \ + --volume "$OUTPUT_DIR:/output" \ + alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 \ + chown -R "$(id -u):$(id -g)" /output + +printf 'Read-only scale result: %s\n' "$OUTPUT_DIR" diff --git a/scripts/scale-seed.sh b/scripts/scale-seed.sh new file mode 100755 index 0000000..c6be0d6 --- /dev/null +++ b/scripts/scale-seed.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=scripts/scale-common.sh +. "$ROOT/scripts/scale-common.sh" +scale_load_environment + +REQUESTS=${WNH_SCALE_REQUESTS:-1000000} +ACTIVITIES=${WNH_SCALE_ACTIVITIES:-1000000} +OUTPUT_DIR="$ROOT/output/scale" +SUMMARY="$OUTPUT_DIR/summary.json" +TOOLS_IMAGE=who-need-help:scale-tools + +for value in "$REQUESTS" "$ACTIVITIES"; do + case "$value" in + ''|*[!0-9]*|0) + echo "WNH_SCALE_REQUESTS and WNH_SCALE_ACTIVITIES must be positive integers." >&2 + exit 2 + ;; + esac +done + +mkdir -p "$OUTPUT_DIR" +docker build --target load_tools --tag "$TOOLS_IMAGE" "$ROOT" + +docker run --rm \ + --cpuset-cpus "$CPU_REPLAY_CPUSET" \ + --network "${LOAD_PROJECT}_internal" \ + --env APP_ROLE=migrate \ + --env DATABASE_URL \ + --env POOL_SIZE=2 \ + --env "ERL_ZFLAGS=+Q ${ERLANG_PORT_LIMIT:-65536} +S 1:1" \ + --env SECRET_KEY_BASE \ + --env HANDOVER_SECRET \ + --env RELEASE_COOKIE \ + --env METRICS_TOKEN \ + --env PHX_HOST \ + --env PHX_SCHEME \ + --env PHX_URL_PORT \ + --env EMAIL_FROM_NAME="Who Need Help scale profile" \ + --env EMAIL_FROM_ADDRESS=scale@example.invalid \ + --env WNH_SCALE_EXPECTED_DATABASE \ + --env WNH_SCALE_FIXTURE_CONFIRM \ + --env SCALE_VIEWER_PASSWORD \ + --volume "$OUTPUT_DIR:/output" \ + "$TOOLS_IMAGE" \ + mix wnh.scale_fixtures prepare \ + --requests "$REQUESTS" \ + --activities "$ACTIVITIES" \ + --output /output/summary.json + +docker run --rm \ + --volume "$OUTPUT_DIR:/output" \ + alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 \ + chown -R "$(id -u):$(id -g)" /output + +printf 'Persistent scale summary: %s\n' "$SUMMARY" diff --git a/scripts/scale-stack-stop.sh b/scripts/scale-stack-stop.sh new file mode 100755 index 0000000..0fe06db --- /dev/null +++ b/scripts/scale-stack-stop.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=scripts/scale-common.sh +. "$ROOT/scripts/scale-common.sh" +scale_load_environment + +scale_compose stop +printf 'Stopped %s; its PostgreSQL volume was preserved.\n' "$LOAD_PROJECT" diff --git a/scripts/scale-stack-up.sh b/scripts/scale-stack-up.sh new file mode 100755 index 0000000..5b0568d --- /dev/null +++ b/scripts/scale-stack-up.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=scripts/scale-common.sh +. "$ROOT/scripts/scale-common.sh" +scale_load_environment + +case "$CPU_REPLAY_CPUSET" in + ''|*[!0-9,-]*) + echo "WNH_SCALE_CPUSET must be a Docker cpuset expression." >&2 + exit 2 + ;; +esac + +cd "$ROOT" +scale_compose up -d --build --wait --scale web=1 --scale worker=1 + +COMPOSE_PROJECT_NAME=$LOAD_PROJECT "$ROOT/scripts/verify-realtime-cluster.sh" compose +COMPOSE_PROJECT_NAME=$LOAD_PROJECT "$ROOT/scripts/verify-beam-runtime.sh" compose + +deadline=$(( $(date +%s) + LOAD_RESILIENCE_RECOVERY_TIMEOUT_SECONDS )) +status=000 + +while [ "$(date +%s)" -le "$deadline" ]; do + if ! status=$( + curl --insecure --silent \ + --resolve "$LOAD_HOST:$SCALE_HTTPS_PORT:127.0.0.1" \ + --output /dev/null \ + --write-out '%{http_code}' \ + "https://$LOAD_HOST:$SCALE_HTTPS_PORT/healthz/ready" + ); then + status=000 + fi + + [ "$status" = 200 ] && break + sleep "$LOAD_RESILIENCE_PROBE_INTERVAL_SECONDS" +done + +if [ "$status" != 200 ]; then + echo "Scale-profile readiness returned HTTP $status instead of 200." >&2 + exit 1 +fi + +curl --fail --silent --show-error --insecure \ + --resolve "$LOAD_HOST:$SCALE_HTTPS_PORT:127.0.0.1" \ + "https://$LOAD_HOST:$SCALE_HTTPS_PORT/healthz/ready" + +printf '\nPersistent scale stack: https://%s:%s\n' "$LOAD_HOST" "$SCALE_HTTPS_PORT" +printf 'Mailpit: http://127.0.0.1:%s\n' "$MAILPIT_PORT" diff --git a/scripts/scale-verify.sh b/scripts/scale-verify.sh new file mode 100755 index 0000000..bb9ac13 --- /dev/null +++ b/scripts/scale-verify.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=scripts/scale-common.sh +. "$ROOT/scripts/scale-common.sh" +scale_load_environment + +REQUESTS=${WNH_SCALE_REQUESTS:-1000000} +ACTIVITIES=${WNH_SCALE_ACTIVITIES:-1000000} +SAMPLES=${WNH_SCALE_SAMPLES:-3} +OUTPUT_DIR="$ROOT/output/scale" +TOOLS_IMAGE=who-need-help:scale-tools + +for value in "$REQUESTS" "$ACTIVITIES" "$SAMPLES"; do + case "$value" in + ''|*[!0-9]*|0) + echo "WNH_SCALE_REQUESTS, WNH_SCALE_ACTIVITIES and WNH_SCALE_SAMPLES must be positive integers." >&2 + exit 2 + ;; + esac +done + +mkdir -p "$OUTPUT_DIR" +docker build --target load_tools --tag "$TOOLS_IMAGE" "$ROOT" + +docker run --rm \ + --cpuset-cpus "$CPU_REPLAY_CPUSET" \ + --network "${LOAD_PROJECT}_internal" \ + --env APP_ROLE=migrate \ + --env DATABASE_URL \ + --env POOL_SIZE=2 \ + --env "ERL_ZFLAGS=+Q ${ERLANG_PORT_LIMIT:-65536} +S 1:1" \ + --env SECRET_KEY_BASE \ + --env HANDOVER_SECRET \ + --env RELEASE_COOKIE \ + --env METRICS_TOKEN \ + --env PHX_HOST \ + --env PHX_SCHEME \ + --env PHX_URL_PORT \ + --env EMAIL_FROM_NAME="Who Need Help scale profile" \ + --env EMAIL_FROM_ADDRESS=scale@example.invalid \ + --env WNH_SCALE_EXPECTED_DATABASE \ + --env WNH_SCALE_FIXTURE_CONFIRM \ + --env SCALE_VIEWER_PASSWORD \ + --volume "$OUTPUT_DIR:/output" \ + "$TOOLS_IMAGE" \ + mix wnh.scale_fixtures verify \ + --requests "$REQUESTS" \ + --activities "$ACTIVITIES" \ + --samples "$SAMPLES" \ + --output /output/verification.json + +docker run --rm \ + --volume "$OUTPUT_DIR:/output" \ + alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 \ + chown -R "$(id -u):$(id -g)" /output + +printf 'Scale verification: %s/output/scale/verification.json\n' "$ROOT" diff --git a/scripts/verify-beam-runtime.sh b/scripts/verify-beam-runtime.sh index a24e1ac..b9da2e3 100755 --- a/scripts/verify-beam-runtime.sh +++ b/scripts/verify-beam-runtime.sh @@ -60,7 +60,7 @@ case "$MODE" in exit 1 fi - live_flags=$( + mapfile -t live_flag_values < <( for target in "${targets[@]}"; do docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \ "$target" | @@ -69,12 +69,19 @@ case "$MODE" in sort -u ) - if [[ ! "$live_flags" =~ ^\+Q[[:space:]]+([0-9]+)$ ]]; then + if [[ "${#live_flag_values[@]}" -ne 1 ]]; then echo "The live Compose replicas do not expose one unambiguous +Q limit." >&2 exit 1 fi - expected=${BASH_REMATCH[1]} + live_flags=${live_flag_values[0]} + + if [[ ! "$live_flags" =~ (^|[[:space:]])\+Q[[:space:]]+([0-9]+)($|[[:space:]]) ]]; then + echo "The live Compose replicas do not expose one unambiguous +Q limit." >&2 + exit 1 + fi + + expected=${BASH_REMATCH[2]} for target in "${targets[@]}"; do component=$( diff --git a/test/who_need_help/activities_test.exs b/test/who_need_help/activities_test.exs index f9dca70..4bb0ac4 100644 --- a/test/who_need_help/activities_test.exs +++ b/test/who_need_help/activities_test.exs @@ -446,7 +446,10 @@ defmodule WhoNeedHelp.ActivitiesTest do viewport!(30.0, 50.0, 31.0, 51.0, 9) ) - assert Enum.any?(items, &match?(%{type: "cluster", count: 3}, &1)) + cluster = Enum.find(items, &match?(%{type: "cluster", count: 3}, &1)) + assert cluster + assert_in_delta cluster.latitude, 50.45, 0.000_001 + assert_in_delta cluster.longitude, 30.52, 0.000_001 assert Enum.any?(items, fn %{type: "activity", id: id} -> id == sparse.id diff --git a/test/who_need_help/discovery_query_cache_test.exs b/test/who_need_help/discovery_query_cache_test.exs new file mode 100644 index 0000000..8b2a03e --- /dev/null +++ b/test/who_need_help/discovery_query_cache_test.exs @@ -0,0 +1,36 @@ +defmodule WhoNeedHelp.DiscoveryQueryCacheTest do + use ExUnit.Case, async: false + + alias WhoNeedHelp.DiscoveryQueryCache + + test "coalesces concurrent reads, briefly reuses the result, and supports invalidation" do + key = {:test, make_ref()} + parent = self() + + fetch = fn -> + DiscoveryQueryCache.fetch(key, fn -> + send(parent, {:query_started, self()}) + + receive do + :finish_query -> :result + end + end) + end + + first = Task.async(fetch) + assert_receive {:query_started, query_pid} + + second = Task.async(fetch) + refute_receive {:query_started, _other_pid}, 50 + + send(query_pid, :finish_query) + assert Task.await(first) == :result + assert Task.await(second) == :result + + assert DiscoveryQueryCache.fetch(key, fn -> flunk("cached read ran twice") end) == :result + + assert :ok = DiscoveryQueryCache.invalidate(key) + + assert DiscoveryQueryCache.fetch(key, fn -> :fresh_result end) == :fresh_result + end +end diff --git a/test/who_need_help/help/discovery_viewport_test.exs b/test/who_need_help/help/discovery_viewport_test.exs index d2f9842..a68fa8a 100644 --- a/test/who_need_help/help/discovery_viewport_test.exs +++ b/test/who_need_help/help/discovery_viewport_test.exs @@ -69,6 +69,46 @@ defmodule WhoNeedHelp.Help.DiscoveryViewportTest do ] end + test "pads spatial-index envelopes without losing antimeridian coverage" do + {:ok, city} = + DiscoveryViewport.cast(%{ + west: 30, + south: 50, + east: 31, + north: 51, + zoom: 10, + width: 800, + height: 600 + }) + + assert [{west, south, east, north}] = DiscoveryViewport.padded_envelopes(city, 2_000) + assert west < city.west + assert south < city.south + assert east > city.east + assert north > city.north + + {:ok, crossing} = + DiscoveryViewport.cast(%{ + west: 170, + south: -10, + east: -170, + north: 10, + zoom: 3, + width: 800, + height: 600 + }) + + padded = DiscoveryViewport.padded_envelopes(crossing, 2_000) + + assert Enum.any?(padded, fn {padded_west, _, padded_east, _} -> + padded_west < 170 and padded_east == 180.0 + end) + + assert Enum.any?(padded, fn {padded_west, _, padded_east, _} -> + padded_west == -180.0 and padded_east > -170 + end) + end + test "derives cluster detail from both geographic bounds and rendered pixels" do {:ok, city} = DiscoveryViewport.cast(%{ diff --git a/test/who_need_help/mutual_aid_flow_test.exs b/test/who_need_help/mutual_aid_flow_test.exs index 11d2580..c40caae 100644 --- a/test/who_need_help/mutual_aid_flow_test.exs +++ b/test/who_need_help/mutual_aid_flow_test.exs @@ -562,10 +562,10 @@ defmodule WhoNeedHelp.MutualAidFlowTest do viewport = viewport!(30.0, 50.0, 31.0, 51.0, 9) items = Help.map_discovery_items(context.helper_scope, %{}, viewport) - assert Enum.any?(items, fn - %{type: "cluster", count: 3} -> true - _item -> false - end) + cluster = Enum.find(items, &match?(%{type: "cluster", count: 3}, &1)) + assert cluster + assert_in_delta cluster.latitude, 50.4501, 0.000_001 + assert_in_delta cluster.longitude, 30.52345, 0.000_001 assert Enum.any?(items, fn %{type: "request", id: id, exact: true} -> id == sparse_request.id 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 0285d28..a9af558 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 @@ -243,6 +243,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do {:ok, request} = Help.create_request(scope, request_attrs(category)) {:ok, view, _html} = live(conn, ~p"/requests") + render_async(view) assert has_element?(view, "#request-discovery") refute has_element?(view, "#my-requests") @@ -250,8 +251,9 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do |> element("button[phx-click='set-scope'][phx-value-scope='mine']") |> render_click() + render_async(view) assert has_element?(view, "#my-requests") - assert has_element?(view, "#my-request-#{request.id}") + assert_eventually(fn -> has_element?(view, "#my-request-#{request.id}") end) refute has_element?(view, "#request-discovery") end @@ -268,6 +270,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do {:ok, activity} = Activities.create_activity(scope, activity_attrs(category)) {:ok, view, _html} = live(conn, ~p"/activities") + render_async(view) assert has_element?(view, "#activity-map") refute has_element?(view, "#my-activities") @@ -275,11 +278,86 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do |> element("button[phx-click='set-scope'][phx-value-scope='mine']") |> render_click() + render_async(view) assert has_element?(view, "#my-activities") - assert has_element?(view, "#my-activity-#{activity.id}") + assert_eventually(fn -> has_element?(view, "#my-activity-#{activity.id}") end) refute has_element?(view, "#activity-map") end + test "request discovery loads only the data needed by the selected layout", %{ + conn: conn, + scope: scope + } do + category = Catalog.seed_defaults() + {:ok, request} = Help.create_request(scope, request_attrs(category)) + path = requests_in_kyiv_path("map") + + static_html = conn |> get(path) |> html_response(200) + assert static_html =~ "Loading results" + assert static_html =~ "Your trust summary" + refute static_html =~ request.title + + mine_html = + conn + |> get(~p"/requests?scope=mine") + |> html_response(200) + + assert mine_html =~ "My requests" + assert mine_html =~ "Loading results" + + {:ok, view, _html} = live(conn, path) + + render_async(view) + assert has_element?(view, "#request-discovery[data-view-mode='map']") + refute has_element?(view, "#open-request-#{request.id}") + refute has_element?(view, "#request-map[data-items='[]']") + + view + |> element("button[phx-click='set-view'][phx-value-view='list']") + |> render_click() + + render_async(view) + assert_patch(view, requests_in_kyiv_path("list")) + assert has_element?(view, "#request-discovery[data-view-mode='list']") + assert has_element?(view, "#open-request-#{request.id}") + assert has_element?(view, "#request-map[data-items='[]']") + end + + test "activity discovery offers list, split, and map without loading hidden results", %{ + conn: conn, + scope: scope + } do + Catalog.seed_defaults() + + category = + Catalog.list_categories(:activity) + |> Enum.find(&(&1.slug == "coffee-meetup")) + + {:ok, activity} = Activities.create_activity(scope, activity_attrs(category)) + path = activities_in_kyiv_path("map") + + static_html = conn |> get(path) |> html_response(200) + assert static_html =~ "Loading results" + refute static_html =~ activity.title + + {:ok, view, _html} = live(conn, path) + + render_async(view) + assert has_element?(view, "#activity-discovery[data-view-mode='map']") + refute has_element?(view, "#open-activity-#{activity.id}") + refute has_element?(view, "#activity-map[data-items='[]']") + + view + |> element("button[phx-click='set-view'][phx-value-view='list']") + |> render_click() + + render_async(view) + assert_patch(view, activities_in_kyiv_path("list")) + assert has_element?(view, "#activity-discovery[data-view-mode='list']") + assert has_element?(view, "#open-activity-#{activity.id}") + assert has_element?(view, "#activity-map[data-items='[]']") + end + test "profile social-link creation uses the shared action limiter", %{conn: conn} do previous = Application.get_env(:who_need_help, :rate_limit_policies) @@ -1665,12 +1743,12 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do } end - defp requests_in_kyiv_path do - ~p"/requests?#{%{west: 30.0, south: 50.0, east: 31.0, north: 51.0, zoom: 10, width: 1200, height: 720, view: "split"}}" + defp requests_in_kyiv_path(view \\ "split") do + ~p"/requests?#{%{west: 30, south: 50, east: 31, north: 51, zoom: 10, width: 1200, height: 720, view: view}}" end - defp activities_in_kyiv_path do - ~p"/activities?#{%{west: 30.0, south: 50.0, east: 31.0, north: 51.0, zoom: 10, width: 1200, height: 720}}" + defp activities_in_kyiv_path(view \\ "split") do + ~p"/activities?#{%{west: 30, south: 50, east: 31, north: 51, zoom: 10, width: 1200, height: 720, view: view}}" end defp assert_eventually(fun, attempts \\ 50)