Merge remote-tracking branch 'origin/main'

This commit is contained in:
778a69cd 2024-07-09 00:17:59 +02:00
commit 3c3206fd8a
27 changed files with 461 additions and 212 deletions

View file

@ -69,13 +69,31 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@spec list_events(any(), map(), Absinthe.Resolution.t()) :: @spec list_events(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached} {:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached}
def list_events(
_parent,
%{
page: page,
limit: limit,
order_by: order_by,
direction: direction,
longevents: longevents,
location: location,
radius: radius
},
_resolution
)
when limit < @event_max_limit do
{:ok,
Events.list_events(page, limit, order_by, direction, true, longevents, location, radius)}
end
def list_events( def list_events(
_parent, _parent,
%{page: page, limit: limit, order_by: order_by, direction: direction}, %{page: page, limit: limit, order_by: order_by, direction: direction},
_resolution _resolution
) )
when limit < @event_max_limit do when limit < @event_max_limit do
{:ok, Events.list_events(page, limit, order_by, direction)} {:ok, Events.list_events(page, limit, order_by, direction, true)}
end end
def list_events(_parent, %{page: _page, limit: _limit}, _resolution) do def list_events(_parent, %{page: _page, limit: _limit}, _resolution) do

View file

@ -375,6 +375,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
object :event_queries do object :event_queries do
@desc "Get all events" @desc "Get all events"
field :events, :paginated_event_list do field :events, :paginated_event_list do
arg(:location, :string, default_value: nil, description: "A geohash for coordinates")
arg(:radius, :float,
default_value: nil,
description: "Radius around the location to search in"
)
arg(:page, :integer, default_value: 1, description: "The page in the paginated event list") arg(:page, :integer, default_value: 1, description: "The page in the paginated event list")
arg(:limit, :integer, default_value: 10, description: "The limit of events per page") arg(:limit, :integer, default_value: 10, description: "The limit of events per page")
@ -388,6 +395,11 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "Direction for the sort" description: "Direction for the sort"
) )
arg(:longevents, :boolean,
default_value: nil,
description: "if mention filter in or out long events"
)
middleware(Rajska.QueryAuthorization, permit: :all) middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.list_events/3) resolve(&Event.list_events/3)

View file

@ -359,19 +359,34 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Returns the list of events. Returns the list of events.
""" """
@spec list_events(integer | nil, integer | nil, atom, atom, boolean) :: Page.t(Event.t()) @spec list_events(
integer | nil,
integer | nil,
atom,
atom,
boolean,
boolean | nil,
string | nil,
float | nil
) :: Page.t(Event.t())
def list_events( def list_events(
page \\ nil, page \\ nil,
limit \\ nil, limit \\ nil,
sort \\ :begins_on, sort \\ :begins_on,
direction \\ :asc, direction \\ :asc,
is_future \\ true is_future \\ true,
longevents \\ nil,
location \\ nil,
radius \\ nil
) do ) do
Event Event
|> distinct([e], [{^direction, ^sort}, asc: e.id]) |> distinct([e], [{^direction, ^sort}, asc: e.id])
|> preload([:organizer_actor, :participants]) |> preload([:organizer_actor, :participants])
|> sort(sort, direction) |> sort(sort, direction)
|> maybe_join_address(%{location: location, radius: radius})
|> events_for_location(%{location: location, radius: radius})
|> filter_future_events(is_future) |> filter_future_events(is_future)
|> events_for_longevents(longevents)
|> filter_public_visibility() |> filter_public_visibility()
|> filter_draft() |> filter_draft()
|> filter_cancelled_events() |> filter_cancelled_events()
@ -572,7 +587,7 @@ defmodule Mobilizon.Events do
|> events_for_search_query() |> events_for_search_query()
|> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now())) |> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now()))
|> events_for_ends_on(Map.get(args, :ends_on)) |> events_for_ends_on(Map.get(args, :ends_on))
|> events_for_longevents(args) |> events_for_longevents(Map.get(args, :longevents))
|> events_for_category(args) |> events_for_category(args)
|> events_for_categories(args) |> events_for_categories(args)
|> events_for_languages(args) |> events_for_languages(args)
@ -1379,15 +1394,13 @@ defmodule Mobilizon.Events do
end end
end end
@spec events_for_longevents(Ecto.Queryable.t(), map()) :: Ecto.Query.t() @spec events_for_longevents(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t()
defp events_for_longevents(query, args) do defp events_for_longevents(query, longevents) do
duration = Config.get([:instance, :duration_of_long_event], 0) duration = Config.get([:instance, :duration_of_long_event], 0)
if duration <= 0 do if duration <= 0 do
query query
else else
longevents = Map.get(args, :longevents)
case longevents do case longevents do
nil -> nil ->
query query

8
package-lock.json generated
View file

@ -15,7 +15,7 @@
"@fullcalendar/daygrid": "^6.1.10", "@fullcalendar/daygrid": "^6.1.10",
"@fullcalendar/interaction": "^6.1.10", "@fullcalendar/interaction": "^6.1.10",
"@fullcalendar/vue3": "^6.1.10", "@fullcalendar/vue3": "^6.1.10",
"@oruga-ui/oruga-next": "^0.8.10", "@oruga-ui/oruga-next": "0.8.12",
"@oruga-ui/theme-oruga": "^0.2.0", "@oruga-ui/theme-oruga": "^0.2.0",
"@sentry/tracing": "^7.1", "@sentry/tracing": "^7.1",
"@sentry/vue": "^7.1", "@sentry/vue": "^7.1",
@ -3138,9 +3138,9 @@
"dev": true "dev": true
}, },
"node_modules/@oruga-ui/oruga-next": { "node_modules/@oruga-ui/oruga-next": {
"version": "0.8.10", "version": "0.8.12",
"resolved": "https://registry.npmjs.org/@oruga-ui/oruga-next/-/oruga-next-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@oruga-ui/oruga-next/-/oruga-next-0.8.12.tgz",
"integrity": "sha512-ETPSoGZu1parbj8C3V2ZojQnN4ptQMiJEwS9Hx44NcaDzu4q/FDsYkKYiz6G9kx8cDceXXxvydfOUpZePVVdzw==", "integrity": "sha512-I1jcsTA4J6HQdNpSWgK4cNSqv1cHsghQGtJ12p0yXDSJseek0Y8f4vf9+tDRtfONzWHuRyWUGcHIfePsRKVbiQ==",
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.0" "vue": "^3.0.0"
} }

View file

@ -32,11 +32,7 @@
"@apollo/client": "^3.9.5", "@apollo/client": "^3.9.5",
"@framasoft/socket": "^1.0.0", "@framasoft/socket": "^1.0.0",
"@framasoft/socket-apollo-link": "^1.0.0", "@framasoft/socket-apollo-link": "^1.0.0",
"@fullcalendar/core": "^6.1.10", "@oruga-ui/oruga-next": "0.8.12",
"@fullcalendar/daygrid": "^6.1.10",
"@fullcalendar/interaction": "^6.1.10",
"@fullcalendar/vue3": "^6.1.10",
"@oruga-ui/oruga-next": "^0.8.10",
"@oruga-ui/theme-oruga": "^0.2.0", "@oruga-ui/theme-oruga": "^0.2.0",
"@fullcalendar/core": "^6.1.10", "@fullcalendar/core": "^6.1.10",
"@fullcalendar/daygrid": "^6.1.10", "@fullcalendar/daygrid": "^6.1.10",

View file

@ -3,17 +3,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-24 14:40+0000\n" "POT-Creation-Date: 2019-09-24 14:40+0000\n"
"PO-Revision-Date: 2023-10-22 04:28+0000\n" "PO-Revision-Date: 2024-07-04 23:35+0000\n"
"Last-Translator: Jakub Urbanowicz <tex@przeklad.pl>\n" "Last-Translator: ewm <gnu.ewm@protonmail.com>\n"
"Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/" "Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/"
"backend/pl/>\n" "backend/pl/>\n"
"Language: pl\n" "Language: pl\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n" "|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.0.1\n" "X-Generator: Weblate 5.6.2\n"
#: lib/web/templates/email/password_reset.html.heex:66 #: lib/web/templates/email/password_reset.html.heex:66
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2373,30 +2373,32 @@ msgstr "Data rejestracji uczestnika"
#: lib/web/templates/email/event_participation_confirmed.html.heex:122 #: lib/web/templates/email/event_participation_confirmed.html.heex:122
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cancel my attendance" msgid "Cancel my attendance"
msgstr "" msgstr "Anuluj moją obecność"
#: lib/web/templates/email/anonymous_participation_confirmation.html.heex:90 #: lib/web/templates/email/anonymous_participation_confirmation.html.heex:90
#: lib/web/templates/email/event_participation_confirmed.html.heex:99 #: lib/web/templates/email/event_participation_confirmed.html.heex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you wish to cancel your participation, simply click on the link below." msgid "If you wish to cancel your participation, simply click on the link below."
msgstr "" msgstr "Jeśli chcesz anulować swój udział, kliknij poniższy link."
#: lib/web/email/admin.ex:142 #: lib/web/email/admin.ex:142
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email configuration test for %{instance}" msgid "Email configuration test for %{instance}"
msgstr "" msgstr "Test konfiguracji poczty e-mail dla %{instance}"
#: lib/web/templates/email/email_configuration_test.html.heex:47 #: lib/web/templates/email/email_configuration_test.html.heex:47
#: lib/web/templates/email/email_configuration_test.text.eex:3 #: lib/web/templates/email/email_configuration_test.text.eex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "If you received this email, the email configuration seems to be correct." msgid "If you received this email, the email configuration seems to be correct."
msgstr "" msgstr ""
"Jeśli otrzymano tę wiadomość e-mail, konfiguracja poczty wydaje się być "
"poprawna."
#: lib/web/templates/email/email_configuration_test.html.heex:18 #: lib/web/templates/email/email_configuration_test.html.heex:18
#: lib/web/templates/email/email_configuration_test.text.eex:1 #: lib/web/templates/email/email_configuration_test.text.eex:1
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Well done!" msgid "Well done!"
msgstr "" msgstr "Dobra robota!"
#: lib/web/templates/api/terms.html.heex:55 #: lib/web/templates/api/terms.html.heex:55
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy

View file

@ -8,16 +8,16 @@
## to merge POT files into PO files. ## to merge POT files into PO files.
msgid "" msgid ""
msgstr "" msgstr ""
"PO-Revision-Date: 2023-10-22 23:11+0000\n" "PO-Revision-Date: 2024-07-04 23:35+0000\n"
"Last-Translator: Jakub Urbanowicz <tex@przeklad.pl>\n" "Last-Translator: ewm <gnu.ewm@protonmail.com>\n"
"Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/" "Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/"
"backend-errors/pl/>\n" "backend-errors/pl/>\n"
"Language: pl\n" "Language: pl\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n" "|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.0.1\n" "X-Generator: Weblate 5.6.2\n"
#: lib/mobilizon/discussions/discussion.ex:68 #: lib/mobilizon/discussions/discussion.ex:68
#, elixir-autogen #, elixir-autogen
@ -1457,9 +1457,9 @@ msgstr ""
#: lib/graphql/resolvers/conversation.ex:164 #: lib/graphql/resolvers/conversation.ex:164
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Conversation needs to mention at least one participant that's not yourself" msgid "Conversation needs to mention at least one participant that's not yourself"
msgstr "" msgstr "Konwersacja musi zawierać co najmniej jedną wzmiankę o innym uczestniku"
#: lib/graphql/resolvers/participant.ex:401 #: lib/graphql/resolvers/participant.ex:401
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "There are no participants matching the audience you've selected." msgid "There are no participants matching the audience you've selected."
msgstr "" msgstr "Nie ma uczestników pasujących do wybranej grupy odbiorców."

View file

@ -45,7 +45,7 @@ body {
@apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success hover:text-white; @apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success hover:text-white;
} }
.btn-outlined-warning { .btn-outlined-warning {
@apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning; @apply border-2 bg-transparent dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning;
} }
.btn-outlined-danger { .btn-outlined-danger {
@apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger hover:text-white; @apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger hover:text-white;

View file

@ -1,7 +1,7 @@
<template> <template>
<o-taginput <o-taginput
:modelValue="modelValueWithDisplayName" :modelValue="modelValueWithDisplayName"
@update:modelValue="(val: IActor[]) => $emit('update:modelValue', val)" @update:modelValue="updateTags"
:data="availableActors" :data="availableActors"
:allow-autocomplete="true" :allow-autocomplete="true"
:allow-new="false" :allow-new="false"
@ -25,12 +25,16 @@ import { computed, ref } from "vue";
import ActorInline from "./ActorInline.vue"; import ActorInline from "./ActorInline.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const props = defineProps<{ const emit = defineEmits<{
modelValue: IActor[]; "update:modelValue": [value: IActor[]];
}>(); }>();
defineEmits<{ const updateTags = (val: IActor[]) => {
"update:modelValue": [value: IActor[]]; emit("update:modelValue", val);
};
const props = defineProps<{
modelValue: IActor[];
}>(); }>();
const modelValue = computed(() => props.modelValue); const modelValue = computed(() => props.modelValue);

View file

@ -23,6 +23,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { localeShortWeekDayNames } from "@/utils/datetime"; import { localeShortWeekDayNames } from "@/utils/datetime";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { locale } = useI18n({ useScope: "global" });
const localeConverted = locale.replace("_", "-");
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -35,15 +40,15 @@ const props = withDefaults(
const dateObj = computed<Date>(() => new Date(props.date)); const dateObj = computed<Date>(() => new Date(props.date));
const month = computed<string>(() => const month = computed<string>(() =>
dateObj.value.toLocaleString(undefined, { month: "short" }) dateObj.value.toLocaleString(localeConverted, { month: "short" })
); );
const day = computed<string>(() => const day = computed<string>(() =>
dateObj.value.toLocaleString(undefined, { day: "numeric" }) dateObj.value.toLocaleString(localeConverted, { day: "numeric" })
); );
const weekday = computed<string>(() => const weekday = computed<string>(() =>
dateObj.value.toLocaleString(undefined, { weekday: "short" }) dateObj.value.toLocaleString(localeConverted, { weekday: "short" })
); );
const smallStyle = computed<string>(() => (props.small ? "1.2" : "2")); const smallStyle = computed<string>(() => (props.small ? "1.2" : "2"));

View file

@ -13,8 +13,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n";
import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue"; import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue";
const { locale } = useI18n({ useScope: "global" });
const localeConverted = locale.replace("_", "-");
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
date: string; date: string;
@ -26,7 +32,7 @@ const props = withDefaults(
const dateObj = computed<Date>(() => new Date(props.date)); const dateObj = computed<Date>(() => new Date(props.date));
const time = computed<string>(() => const time = computed<string>(() =>
dateObj.value.toLocaleTimeString(undefined, { dateObj.value.toLocaleTimeString(localeConverted, {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
}) })

View file

@ -1,5 +1,6 @@
<template> <template>
<section <section
v-if="promotedCategories.length > 1"
class="mx-auto container flex flex-wrap items-center justify-center gap-3 md:gap-5 my-3" class="mx-auto container flex flex-wrap items-center justify-center gap-3 md:gap-5 my-3"
> >
<CategoryCard <CategoryCard
@ -10,7 +11,6 @@
:imageLazy="false" :imageLazy="false"
/> />
<router-link <router-link
v-if="promotedCategories.length > 0"
:to="{ name: RouteName.CATEGORIES }" :to="{ name: RouteName.CATEGORIES }"
class="flex items-end brightness-85 h-36 w-36 md:h-52 md:w-52 rounded-lg font-semibold text-lg md:text-xl p-4 text-white bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500 hover:text-slate-200" class="flex items-end brightness-85 h-36 w-36 md:h-52 md:w-52 rounded-lg font-semibold text-lg md:text-xl p-4 text-white bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500 hover:text-slate-200"
> >
@ -47,13 +47,17 @@ const eventCategoryLabel = (categoryId: string): string | undefined => {
return eventCategories.value?.find(({ id }) => categoryId == id)?.label; return eventCategories.value?.find(({ id }) => categoryId == id)?.label;
}; };
const promotedCategories = computed((): CategoryStatsModel[] => { const promotedCategories = computed((): CategoryStatsModel[] | null => {
return shuffle( const relevant_categories = categoryStats.value.filter(
categoryStats.value.filter(
({ key, number }) => ({ key, number }) =>
key !== "MEETING" && number >= 1 && categoriesWithPictures.includes(key) key !== "MEETING" && number >= 1 && categoriesWithPictures.includes(key)
) );
)
if (relevant_categories.length <= 1) {
return [];
}
return shuffle(relevant_categories)
.map(({ key, number }) => ({ .map(({ key, number }) => ({
key, key,
number, number,

View file

@ -1,7 +1,7 @@
<template> <template>
<form <form
id="search-anchor" id="search-anchor"
class="container mx-auto my-3 px-2 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center items-center justify-center dark:text-slate-100" class="container mx-auto my-3 px-2 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center justify-center dark:text-slate-100"
role="search" role="search"
@submit.prevent="submit" @submit.prevent="submit"
> >
@ -25,6 +25,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { IAddress } from "@/types/address.model"; import { IAddress } from "@/types/address.model";
import { AddressSearchType } from "@/types/enums";
import {
addressToLocation,
getLocationFromLocal,
storeLocationInLocal,
} from "@/utils/location";
import { computed, defineAsyncComponent } from "vue"; import { computed, defineAsyncComponent } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
@ -38,6 +44,7 @@ const props = defineProps<{
location: IAddress | null; location: IAddress | null;
locationDefaultText?: string | null; locationDefaultText?: string | null;
search: string; search: string;
fromLocalStorage?: boolean | false;
}>(); }>();
const router = useRouter(); const router = useRouter();
@ -51,10 +58,19 @@ const emit = defineEmits<{
const location = computed({ const location = computed({
get(): IAddress | null { get(): IAddress | null {
if (props.location) {
return props.location; return props.location;
}
if (props.fromLocalStorage) {
return getLocationFromLocal();
}
return null;
}, },
set(newLocation: IAddress | null) { set(newLocation: IAddress | null) {
emit("update:location", newLocation); emit("update:location", newLocation);
if (props.fromLocalStorage) {
storeLocationInLocal(newLocation);
}
}, },
}); });
@ -69,12 +85,7 @@ const search = computed({
const submit = () => { const submit = () => {
emit("submit"); emit("submit");
const lat = location.value?.geom const { lat, lon } = addressToLocation(location.value);
? parseFloat(location.value?.geom?.split(";")?.[1])
: undefined;
const lon = location.value?.geom
? parseFloat(location.value?.geom?.split(";")?.[0])
: undefined;
router.push({ router.push({
name: RouteName.SEARCH, name: RouteName.SEARCH,
query: { query: {

View file

@ -1,20 +1,36 @@
<template> <template>
<close-content <close-content
class="container mx-auto px-2" class="container mx-auto px-2"
v-show="loading || (events && events.total > 0)" :suggestGeoloc="false"
:suggestGeoloc="suggestGeoloc"
v-on="attrs" v-on="attrs"
@doGeoLoc="emit('doGeoLoc')"
:doingGeoloc="doingGeoloc"
> >
<template #title> <template #title>
<template v-if="userLocationName"> <template v-if="userLocation?.name">
{{ t("Events nearby {position}", { position: userLocationName }) }} {{
t("Incoming events and activities nearby {position}", {
position: userLocation?.name,
})
}}
</template> </template>
<template v-else> <template v-else>
{{ t("Events close to you") }} {{ t("Incoming events and activities") }}
</template> </template>
</template> </template>
<template #subtitle>
<div v-if="!loading && events.total == 0">
<template v-if="userLocation?.name">
{{
t(
"No events found nearby {position}. Try removing your position to see all events!",
{ position: userLocation?.name }
)
}}
</template>
<template v-else>
{{ t("No events found") }}
</template>
</div>
</template>
<template #content> <template #content>
<skeleton-event-result <skeleton-event-result
v-for="i in 6" v-for="i in 6"
@ -28,25 +44,36 @@
:key="event.uuid" :key="event.uuid"
/> />
<more-content <more-content
v-if="userLocationName && userLocation?.lat && userLocation?.lon" v-if="userLocation?.name && userLocation?.lat && userLocation?.lon"
:to="{ :to="{
name: RouteName.SEARCH, name: RouteName.SEARCH,
query: { query: {
locationName: userLocationName, locationName: userLocation?.name,
lat: userLocation.lat?.toString(), lat: userLocation.lat?.toString(),
lon: userLocation.lon?.toString(), lon: userLocation.lon?.toString(),
contentType: 'EVENTS', contentType: 'ALL',
distance: `${distance}_km`, distance: `${distance}_km`,
}, },
}" }"
:picture="userLocation?.picture" :picture="userLocation?.picture"
> >
{{ {{
t("View more events around {position}", { t("View more events and activities around {position}", {
position: userLocationName, position: userLocation?.name,
}) })
}} }}
</more-content> </more-content>
<more-content
v-else
:to="{
name: RouteName.SEARCH,
query: {
contentType: 'ALL',
},
}"
>
{{ t("View more events and activities") }}
</more-content>
</template> </template>
</close-content> </close-content>
</template> </template>
@ -55,76 +82,55 @@
import { LocationType } from "../../types/user-location.model"; import { LocationType } from "../../types/user-location.model";
import MoreContent from "./MoreContent.vue"; import MoreContent from "./MoreContent.vue";
import CloseContent from "./CloseContent.vue"; import CloseContent from "./CloseContent.vue";
import { computed, onMounted, useAttrs } from "vue"; import { watch, computed, useAttrs } from "vue";
import { SEARCH_EVENTS } from "@/graphql/search"; import { FETCH_EVENTS } from "@/graphql/event";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import { useLazyQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import EventCard from "../Event/EventCard.vue"; import EventCard from "../Event/EventCard.vue";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import SkeletonEventResult from "../Event/SkeletonEventResult.vue"; import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { coordsToGeoHash } from "@/utils/location"; import { coordsToGeoHash } from "@/utils/location";
import { roundToNearestMinute } from "@/utils/datetime";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { EventSortField, SortDirection } from "@/types/enums";
const props = defineProps<{ const props = defineProps<{
userLocation: LocationType; userLocation: LocationType;
doingGeoloc?: boolean; doingGeoloc?: boolean;
}>(); }>();
const emit = defineEmits(["doGeoLoc"]); defineEmits(["doGeoLoc"]);
const EVENT_PAGE_LIMIT = 12;
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const attrs = useAttrs(); const attrs = useAttrs();
const userLocation = computed(() => props.userLocation); const userLocation = computed(() => props.userLocation);
const userLocationName = computed(() => { const geoHash = computed(() => {
return userLocation.value?.name; console.debug("userLocation updated", userLocation.value);
const geo = coordsToGeoHash(userLocation.value.lat, userLocation.value.lon);
console.debug("geohash:", geo);
return geo;
}); });
const suggestGeoloc = computed(() => userLocation.value?.isIPLocation);
const geoHash = computed(() => const distance = computed<number>(() =>
coordsToGeoHash(props.userLocation.lat, props.userLocation.lon) userLocation.value?.isIPLocation ? 150 : 25
); );
const distance = computed<number>(() => (suggestGeoloc.value ? 150 : 25)); const eventsQuery = useQuery<{
const now = computed(() => roundToNearestMinute(new Date()));
const searchEnabled = computed(() => geoHash.value != undefined);
const {
result: eventsResult,
loading: loadingEvents,
load: load,
} = useLazyQuery<{
searchEvents: Paginate<IEvent>; searchEvents: Paginate<IEvent>;
}>( }>(FETCH_EVENTS, () => ({
SEARCH_EVENTS, orderBy: EventSortField.BEGINS_ON,
() => ({ direction: SortDirection.ASC,
longevents: false,
location: geoHash.value, location: geoHash.value,
beginsOn: now.value,
endsOn: undefined,
radius: distance.value, radius: distance.value,
eventPage: 1, }));
limit: EVENT_PAGE_LIMIT,
type: "IN_PERSON",
}),
() => ({
enabled: searchEnabled.value,
fetchPolicy: "cache-first",
})
);
const events = computed( const events = computed(
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 } () => eventsQuery.result.value?.events ?? { elements: [], total: 0 }
); );
watch(events, (e) => console.debug("events: ", e));
onMounted(async () => { const loading = computed(() => props.doingGeoloc || eventsQuery.loading.value);
await load(); watch(loading, (l) => console.debug("loading: ", l));
});
const loading = computed(() => props.doingGeoloc || loadingEvents.value);
</script> </script>

View file

@ -60,6 +60,7 @@ const { result: resultEvents, loading: loadingEvents } = useQuery<{
}>(FETCH_EVENTS, { }>(FETCH_EVENTS, {
orderBy: EventSortField.BEGINS_ON, orderBy: EventSortField.BEGINS_ON,
direction: SortDirection.ASC, direction: SortDirection.ASC,
longevents: false,
}); });
const events = computed( const events = computed(
() => resultEvents.value?.events ?? { total: 0, elements: [] } () => resultEvents.value?.events ?? { total: 0, elements: [] }

View file

@ -209,7 +209,7 @@
:to="{ name: RouteName.EVENT_CALENDAR }" :to="{ name: RouteName.EVENT_CALENDAR }"
class="block relative py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700" class="block relative py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("Calendar") >{{ t("Calendar")
}}<span class="absolute right-0 text-sm" }}<span class="absolute right-0 text-xs"
><br />(beta)</span ><br />(beta)</span
></router-link ></router-link
> >

View file

@ -2,6 +2,7 @@ import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor";
import { import {
CURRENT_USER_CLIENT, CURRENT_USER_CLIENT,
LOGGED_USER, LOGGED_USER,
LOGGED_USER_LOCATION,
SET_USER_SETTINGS, SET_USER_SETTINGS,
UPDATE_USER_LOCALE, UPDATE_USER_LOCALE,
USER_SETTINGS, USER_SETTINGS,
@ -51,6 +52,20 @@ export function useUserSettings() {
return { loggedUser, error, loading }; return { loggedUser, error, loading };
} }
export function useUserLocation() {
const {
result: userSettingsResult,
error,
loading,
onResult,
} = useQuery<{ loggedUser: IUser }>(LOGGED_USER_LOCATION);
const location = computed(
() => userSettingsResult.value?.loggedUser.settings.location
);
return { location, error, loading, onResult };
}
export async function doUpdateSetting( export async function doUpdateSetting(
variables: Record<string, unknown> variables: Record<string, unknown>
): Promise<void> { ): Promise<void> {

View file

@ -16,12 +16,15 @@ export const ACTOR_FRAGMENT = gql`
} }
`; `;
// Do not request mediaSize here because mediaSize can only be accessed
// by user_himself/moderator/administrator (can_get_actor_size? in media.ex)
// - FETCH_PERSON is used by <NewConversation> and can be used by simple users here
// - FETCH_PERSON is also used in <EditIdentity> but mediaSize is not used there
export const FETCH_PERSON = gql` export const FETCH_PERSON = gql`
query FetchPerson($username: String!) { query FetchPerson($username: String!) {
fetchPerson(preferredUsername: $username) { fetchPerson(preferredUsername: $username) {
...ActorFragment ...ActorFragment
suspended suspended
mediaSize
avatar { avatar {
id id
name name

View file

@ -103,16 +103,22 @@ export const FETCH_EVENT_BASIC = gql`
export const FETCH_EVENTS = gql` export const FETCH_EVENTS = gql`
query FetchEvents( query FetchEvents(
$location: String
$radius: Float
$orderBy: EventOrderBy $orderBy: EventOrderBy
$direction: SortDirection $direction: SortDirection
$page: Int $page: Int
$limit: Int $limit: Int
$longevents: Boolean
) { ) {
events( events(
location: $location
radius: $radius
orderBy: $orderBy orderBy: $orderBy
direction: $direction direction: $direction
page: $page page: $page
limit: $limit limit: $limit
longevents: $longevents
) { ) {
total total
elements { elements {

View file

@ -218,6 +218,7 @@ export const SEARCH_CALENDAR_EVENTS = gql`
endsOn: $endsOn endsOn: $endsOn
page: $eventPage page: $eventPage
limit: $limit limit: $limit
longevents: false
) { ) {
total total
elements { elements {

View file

@ -138,6 +138,20 @@ export const USER_SETTINGS = gql`
${USER_SETTINGS_FRAGMENT} ${USER_SETTINGS_FRAGMENT}
`; `;
export const LOGGED_USER_LOCATION = gql`
query LoggedUserLocation {
loggedUser {
settings {
location {
range
geohash
name
}
}
}
}
`;
export const LOGGED_USER_TIMEZONE = gql` export const LOGGED_USER_TIMEZONE = gql`
query LoggedUserTimezone { query LoggedUserTimezone {
loggedUser { loggedUser {

View file

@ -1626,7 +1626,7 @@
"With {participants}": "With {participants}", "With {participants}": "With {participants}",
"Conversations": "Conversations", "Conversations": "Conversations",
"New private message": "New private message", "New private message": "New private message",
"There's no conversations yet": "There's no conversations yet", "There's no conversations yet": "There are no conversations yet",
"Open conversations": "Open conversations", "Open conversations": "Open conversations",
"List of conversations": "List of conversations", "List of conversations": "List of conversations",
"Conversation with {participants}": "Conversation with {participants}", "Conversation with {participants}": "Conversation with {participants}",

View file

@ -79,6 +79,7 @@
"Add a contact": "Dodaj kontakt", "Add a contact": "Dodaj kontakt",
"Add a new post": "Dodaj nowy wpis", "Add a new post": "Dodaj nowy wpis",
"Add a note": "Dodaj notatkę", "Add a note": "Dodaj notatkę",
"Add a recipient": "Dodaj odbiorcę",
"Add a todo": "Dodaj element listy do zrobienia", "Add a todo": "Dodaj element listy do zrobienia",
"Add an address": "Dodaj adres", "Add an address": "Dodaj adres",
"Add an instance": "Dodaj instancję", "Add an instance": "Dodaj instancję",
@ -118,6 +119,7 @@
"And {number} comments": "Oraz {number} komentarzy", "And {number} comments": "Oraz {number} komentarzy",
"Announcements": "Ogłoszenia", "Announcements": "Ogłoszenia",
"Announcements and mentions notifications are always sent straight away.": "Ogłoszenia i powiadomienia o wzmiankach są zawsze wysyłane natychmiast.", "Announcements and mentions notifications are always sent straight away.": "Ogłoszenia i powiadomienia o wzmiankach są zawsze wysyłane natychmiast.",
"Announcements for {eventTitle}": "Ogłoszenia dla {eventTitle}",
"Anonymous participant": "Anonimowy(-a) uczestnik(-czka)", "Anonymous participant": "Anonimowy(-a) uczestnik(-czka)",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonimowi uczestnicy będą otrzymywać prośbę o potwierdzenie uczestnictwa przez e-mail.", "Anonymous participants will be asked to confirm their participation through e-mail.": "Anonimowi uczestnicy będą otrzymywać prośbę o potwierdzenie uczestnictwa przez e-mail.",
"Anonymous participations": "Anonimowy udział", "Anonymous participations": "Anonimowy udział",
@ -146,6 +148,7 @@
"Are you sure you want to cancel the event creation? You'll lose all modifications.": "Czy na pewno chcesz anulować tworzenie wydarzenia? Utracisz wszystkie zmiany.", "Are you sure you want to cancel the event creation? You'll lose all modifications.": "Czy na pewno chcesz anulować tworzenie wydarzenia? Utracisz wszystkie zmiany.",
"Are you sure you want to cancel the event edition? You'll lose all modifications.": "Czy na pewno chcesz usunąć edycję wydarzenia? Utracisz wszystkie zmiany.", "Are you sure you want to cancel the event edition? You'll lose all modifications.": "Czy na pewno chcesz usunąć edycję wydarzenia? Utracisz wszystkie zmiany.",
"Are you sure you want to cancel your participation at event \"{title}\"?": "Czy na pewno chcesz wycofać swój udział w wydarzeniu „{title}”?", "Are you sure you want to cancel your participation at event \"{title}\"?": "Czy na pewno chcesz wycofać swój udział w wydarzeniu „{title}”?",
"Are you sure you want to delete this entire conversation?": "Czy na pewno chcesz usunąć całą konwersację?",
"Are you sure you want to delete this entire discussion?": "Czy na pewno usunąć tę całą dyskusję?", "Are you sure you want to delete this entire discussion?": "Czy na pewno usunąć tę całą dyskusję?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Czy na pewno chcesz usunąć to wydarzenie? To działanie nie może zostać odwrócone.", "Are you sure you want to delete this event? This action cannot be reverted.": "Czy na pewno chcesz usunąć to wydarzenie? To działanie nie może zostać odwrócone.",
"Are you sure you want to delete this post? This action cannot be reverted.": "Czy na pewno chcesz usunąć ten wpis? Tego działania nie można cofnąć.", "Are you sure you want to delete this post? This action cannot be reverted.": "Czy na pewno chcesz usunąć ten wpis? Tego działania nie można cofnąć.",
@ -183,6 +186,7 @@
"By transit": "Transportem publicznym", "By transit": "Transportem publicznym",
"By {group}": "Autorstwa {group}", "By {group}": "Autorstwa {group}",
"By {username}": "Od {username}", "By {username}": "Od {username}",
"Calendar": "Kalendarz",
"Can be an email or a link, or just plain text.": "Może być adresem e-mail, odnośnikiem lub zwykłym tekstem.", "Can be an email or a link, or just plain text.": "Może być adresem e-mail, odnośnikiem lub zwykłym tekstem.",
"Cancel": "Anuluj", "Cancel": "Anuluj",
"Cancel anonymous participation": "Anuluj anonimowy udział", "Cancel anonymous participation": "Anuluj anonimowy udział",
@ -193,6 +197,7 @@
"Cancel membership request": "Anulowanie prośby o członkostwo", "Cancel membership request": "Anulowanie prośby o członkostwo",
"Cancel my participation request…": "Anuluj moje zgłoszenie udziału…", "Cancel my participation request…": "Anuluj moje zgłoszenie udziału…",
"Cancel my participation…": "Anuluj mój udział…", "Cancel my participation…": "Anuluj mój udział…",
"Cancel participation": "Anuluj swój udział",
"Cancelled": "Anulowane", "Cancelled": "Anulowane",
"Cancelled: Won't happen": "Anulowano: Nie odbędzie się", "Cancelled: Won't happen": "Anulowano: Nie odbędzie się",
"Categories": "Kategorie", "Categories": "Kategorie",
@ -229,6 +234,8 @@
"Comment body": "Treść komentarza", "Comment body": "Treść komentarza",
"Comment deleted": "Usunięto komentarz", "Comment deleted": "Usunięto komentarz",
"Comment deleted and report resolved": "Komentarz usunięto i załatwiono zgłoszenie", "Comment deleted and report resolved": "Komentarz usunięto i załatwiono zgłoszenie",
"Comment from a private conversation": "Komentarz z prywatnej konwersacji",
"Comment from an event announcement": "Komentarz z ogłoszenia o wydarzeniu",
"Comment from {'@'}{username} reported": "Komentarz {'@'}{username} zgłoszony", "Comment from {'@'}{username} reported": "Komentarz {'@'}{username} zgłoszony",
"Comment text can't be empty": "Tekst komentarza nie morze być pusty", "Comment text can't be empty": "Tekst komentarza nie morze być pusty",
"Comment under event {eventTitle}": "Komentarz pod wydarzeniem {eventTitle}", "Comment under event {eventTitle}": "Komentarz pod wydarzeniem {eventTitle}",
@ -246,6 +253,8 @@
"Contact": "Kontakt", "Contact": "Kontakt",
"Continue": "Kontynuuj", "Continue": "Kontynuuj",
"Continue editing": "Kontynuuj edycję", "Continue editing": "Kontynuuj edycję",
"Conversation with {participants}": "Konwersacja z {participants}",
"Conversations": "Konwersacje",
"Cookies and Local storage": "Pliki cookies i pamięć lokalna", "Cookies and Local storage": "Pliki cookies i pamięć lokalna",
"Copy URL to clipboard": "Kopiowanie adresu URL do schowka", "Copy URL to clipboard": "Kopiowanie adresu URL do schowka",
"Copy details to clipboard": "Kopiowanie szczegółów do schowka", "Copy details to clipboard": "Kopiowanie szczegółów do schowka",
@ -299,6 +308,7 @@
"Default": "Domyślne", "Default": "Domyślne",
"Default Mobilizon privacy policy": "Domyślna polityka prywatności Mobilizon", "Default Mobilizon privacy policy": "Domyślna polityka prywatności Mobilizon",
"Default Mobilizon terms": "Domyślne warunki użytkowania Mobilizon", "Default Mobilizon terms": "Domyślne warunki użytkowania Mobilizon",
"Default Picture": "Obraz domyślny",
"Delete": "Usuń", "Delete": "Usuń",
"Delete account": "Usuń konto", "Delete account": "Usuń konto",
"Delete comment": "Usuń komentarz", "Delete comment": "Usuń komentarz",
@ -318,6 +328,7 @@
"Delete my account": "Usuń moje konto", "Delete my account": "Usuń moje konto",
"Delete post": "Usuń wpis", "Delete post": "Usuń wpis",
"Delete profiles": "Usuwanie profili", "Delete profiles": "Usuwanie profili",
"Delete this conversation": "Usuń tę konwersację",
"Delete this discussion": "Usunięcie tej dyskusji", "Delete this discussion": "Usunięcie tej dyskusji",
"Delete this identity": "Usuń tę tożsamość", "Delete this identity": "Usuń tę tożsamość",
"Delete your identity": "Usuń swoją tożsamość", "Delete your identity": "Usuń swoją tożsamość",
@ -349,6 +360,7 @@
"Do you wish to {create_group} or {explore_groups}?": "Czy chcesz {create_group} lub {explore_groups}?", "Do you wish to {create_group} or {explore_groups}?": "Czy chcesz {create_group} lub {explore_groups}?",
"Does the event needs to be confirmed later or is it cancelled?": "Czy wydarzenie wymaga jeszcze późniejszego potwierdzenia, czy też zostało odwołane?", "Does the event needs to be confirmed later or is it cancelled?": "Czy wydarzenie wymaga jeszcze późniejszego potwierdzenia, czy też zostało odwołane?",
"Domain": "Domen", "Domain": "Domen",
"Domain or instance name": "Nazwa domeny lub instancji",
"Draft": "Szkic", "Draft": "Szkic",
"Drafts": "Szkice", "Drafts": "Szkice",
"Due on": "Zaplanowane na", "Due on": "Zaplanowane na",
@ -381,6 +393,7 @@
"Error details copied!": "Szczegóły błędu zostały skopiowane!", "Error details copied!": "Szczegóły błędu zostały skopiowane!",
"Error message": "Komunikat o błędzie", "Error message": "Komunikat o błędzie",
"Error stacktrace": "Ślad stosu błędów", "Error stacktrace": "Ślad stosu błędów",
"Error while cancelling your participation": "Błąd podczas anulowania uczestnictwa",
"Error while changing email": "Wystąpił błąd podczas zmiany adresu e-mail", "Error while changing email": "Wystąpił błąd podczas zmiany adresu e-mail",
"Error while loading the preview": "Błąd podczas ładowania podglądu", "Error while loading the preview": "Błąd podczas ładowania podglądu",
"Error while login with {provider}. Retry or login another way.": "Błąd logowania z {provider}. Spróbuj ponownie lub zaloguj się w inny sposób.", "Error while login with {provider}. Retry or login another way.": "Błąd logowania z {provider}. Spróbuj ponownie lub zaloguj się w inny sposób.",
@ -429,6 +442,7 @@
"External registration": "Rejestracja zewnętrzna", "External registration": "Rejestracja zewnętrzna",
"Failed to get location.": "Nie udało się uzyskać lokalizacji.", "Failed to get location.": "Nie udało się uzyskać lokalizacji.",
"Failed to save admin settings": "Nie udało się zapisać ustawień administratora (-ki)", "Failed to save admin settings": "Nie udało się zapisać ustawień administratora (-ki)",
"Favicon": "Favicon",
"Featured events": "Wyróżnione wydarzenia", "Featured events": "Wyróżnione wydarzenia",
"Federated Group Name": "Sfederowana nazwa grupy", "Federated Group Name": "Sfederowana nazwa grupy",
"Federation": "Federacja", "Federation": "Federacja",
@ -467,6 +481,7 @@
"From the {startDate} at {startTime} to the {endDate}": "Od {startDate} o {startTime} do {endDate}", "From the {startDate} at {startTime} to the {endDate}": "Od {startDate} o {startTime} do {endDate}",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Od {startDate} o {startTime} do {endDate} o {endTime}", "From the {startDate} at {startTime} to the {endDate} at {endTime}": "Od {startDate} o {startTime} do {endDate} o {endTime}",
"From the {startDate} to the {endDate}": "Od {startDate} do {endDate}", "From the {startDate} to the {endDate}": "Od {startDate} do {endDate}",
"From this instance only": "Tylko z tej instancji",
"From yourself": "Od Ciebie", "From yourself": "Od Ciebie",
"Fully accessible with a wheelchair": "W pełni dostępne na wózku", "Fully accessible with a wheelchair": "W pełni dostępne na wózku",
"Gather ⋅ Organize ⋅ Mobilize": "Gromadźcie się ⋅ Organizujcie się⋅ Mobilizujcie się", "Gather ⋅ Organize ⋅ Mobilize": "Gromadźcie się ⋅ Organizujcie się⋅ Mobilizujcie się",
@ -610,6 +625,7 @@
"Light": "Jasny", "Light": "Jasny",
"Limited number of places": "Ograniczona liczba miejsc", "Limited number of places": "Ograniczona liczba miejsc",
"List": "Lista", "List": "Lista",
"List of conversations": "Lista konwersacji",
"List title": "Tytuł listy", "List title": "Tytuł listy",
"Live": "Na żywo", "Live": "Na żywo",
"Load more": "Załaduj więcej", "Load more": "Załaduj więcej",
@ -627,6 +643,8 @@
"Login on Mobilizon!": "Zaloguj się na Mobilizon!", "Login on Mobilizon!": "Zaloguj się na Mobilizon!",
"Login on {instance}": "Zaloguj się na {instance}", "Login on {instance}": "Zaloguj się na {instance}",
"Login status": "Stan logowania", "Login status": "Stan logowania",
"Logo": "Logo",
"Logo of the instance. Defaults to the upstream Mobilizon logo.": "Logo instancji. Domyślnie jest to logo Mobilizon.",
"Main languages you/your moderators speak": "Główne języki którymi posługujesz się Ty / moderatorzy (-ki)", "Main languages you/your moderators speak": "Główne języki którymi posługujesz się Ty / moderatorzy (-ki)",
"Make sure that all words are spelled correctly.": "Upewnij się, że wszystkie słowa są napisane poprawnie.", "Make sure that all words are spelled correctly.": "Upewnij się, że wszystkie słowa są napisane poprawnie.",
"Manage activity settings": "Zarządzanie ustawieniami aktywności", "Manage activity settings": "Zarządzanie ustawieniami aktywności",
@ -684,6 +702,7 @@
"Name": "Nazwa", "Name": "Nazwa",
"Navigated to {pageTitle}": "Przejście do {pageTitle}", "Navigated to {pageTitle}": "Przejście do {pageTitle}",
"Never used": "Nigdy nie użyty", "Never used": "Nigdy nie użyty",
"New announcement": "Nowe ogłoszenie",
"New discussion": "Nowa dyskusja", "New discussion": "Nowa dyskusja",
"New email": "Nowy adres e-mail", "New email": "Nowy adres e-mail",
"New folder": "Nowy katalog", "New folder": "Nowy katalog",
@ -692,11 +711,13 @@
"New note": "Nowa notatka", "New note": "Nowa notatka",
"New password": "Nowe hasło", "New password": "Nowe hasło",
"New post": "Nowy wpis", "New post": "Nowy wpis",
"New private message": "Nowa wiadomość prywatna",
"New profile": "Nowy profil", "New profile": "Nowy profil",
"Next": "Następny", "Next": "Następny",
"Next month": "W kolejnym miesiącu", "Next month": "W kolejnym miesiącu",
"Next page": "Następna strona", "Next page": "Następna strona",
"Next week": "W następnym tygodniu", "Next week": "W następnym tygodniu",
"No activities found": "Nie znaleziono żadnych aktywności",
"No address defined": "Nie określono adresu", "No address defined": "Nie określono adresu",
"No apps authorized yet": "Żadne aplikacje nie zostały jeszcze autoryzowane", "No apps authorized yet": "Żadne aplikacje nie zostały jeszcze autoryzowane",
"No categories with public upcoming events on this instance were found.": "Nie znaleziono żadnych kategorii z publicznymi nadchodzącymi wydarzeniami w tej instancji.", "No categories with public upcoming events on this instance were found.": "Nie znaleziono żadnych kategorii z publicznymi nadchodzącymi wydarzeniami w tej instancji.",
@ -868,6 +889,7 @@
"Previous month": "Poprzedni miesiąc", "Previous month": "Poprzedni miesiąc",
"Previous page": "Poprzednia strona", "Previous page": "Poprzednia strona",
"Price sheet": "Cennik", "Price sheet": "Cennik",
"Primary Color": "Kolor podstawowy",
"Privacy": "Prywatność", "Privacy": "Prywatność",
"Privacy Policy": "Polityka prywatności", "Privacy Policy": "Polityka prywatności",
"Privacy policy": "Polityka prywatności", "Privacy policy": "Polityka prywatności",
@ -968,6 +990,7 @@
"Resource provided is not an URL": "Podany zasób nie jest adresem URL", "Resource provided is not an URL": "Podany zasób nie jest adresem URL",
"Resources": "Zasoby", "Resources": "Zasoby",
"Restricted": "Ograniczona", "Restricted": "Ograniczona",
"Return to the event page": "Wróć do strony wydarzenia",
"Return to the group page": "Powrót do strony grupy", "Return to the group page": "Powrót do strony grupy",
"Revoke": "Cofnięcie uprawnień", "Revoke": "Cofnięcie uprawnień",
"Right now": "Właśnie teraz", "Right now": "Właśnie teraz",
@ -982,6 +1005,7 @@
"Search events, groups, etc.": "Szukaj wydarzeń, grup itp.", "Search events, groups, etc.": "Szukaj wydarzeń, grup itp.",
"Search target": "Cel wyszukiwania", "Search target": "Cel wyszukiwania",
"Searching…": "Wyszukiwanie…", "Searching…": "Wyszukiwanie…",
"Secondary Color": "Kolor dodatkowy",
"Select a category": "Wybierz kategorię", "Select a category": "Wybierz kategorię",
"Select a language": "Wybierz język", "Select a language": "Wybierz język",
"Select a radius": "Wybierz promień", "Select a radius": "Wybierz promień",
@ -1021,6 +1045,7 @@
"Smoke free": "Strefa wolna od dymu tytoniowego", "Smoke free": "Strefa wolna od dymu tytoniowego",
"Smoking allowed": "Palenie dozwolone", "Smoking allowed": "Palenie dozwolone",
"Social": "Społeczne", "Social": "Społeczne",
"Software details: {software_details}": "Szczegóły oprogramowania: {software_details}",
"Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Część terminów z tekstu, technicznych lub innych, może być trudna do zrozumienia. Dlatego utworzyliśmy słownik który pomoże je lepiej zrozumieć:", "Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Część terminów z tekstu, technicznych lub innych, może być trudna do zrozumienia. Dlatego utworzyliśmy słownik który pomoże je lepiej zrozumieć:",
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Przepraszamy, nie mogliśmy zapisać Twoich informacji zwrotnych. Nie martw się, postaramy się naprawić ten błąd.", "Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Przepraszamy, nie mogliśmy zapisać Twoich informacji zwrotnych. Nie martw się, postaramy się naprawić ten błąd.",
"Sort by": "Sortuj według", "Sort by": "Sortuj według",
@ -1122,6 +1147,7 @@
"There will be no way to recover your data.": "Nie będzie możliwości przywrócenia Twoich danych.", "There will be no way to recover your data.": "Nie będzie możliwości przywrócenia Twoich danych.",
"There will be no way to restore the profile's data!": "Nie będzie możliwości przywrócenia danych profilu!", "There will be no way to restore the profile's data!": "Nie będzie możliwości przywrócenia danych profilu!",
"There will be no way to restore the user's data!": "Nie będzie możliwości przywrócenia danych użytkownika!", "There will be no way to restore the user's data!": "Nie będzie możliwości przywrócenia danych użytkownika!",
"There's no announcements yet": "Nie ma jeszcze żadnych ogłoszeń",
"There's no discussions yet": "Nie ma jeszcze dyskusji", "There's no discussions yet": "Nie ma jeszcze dyskusji",
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "Aplikacje te mogą uzyskać dostęp do konta przez API. Jeśli widzisz tutaj aplikacje, których nie rozpoznajesz, które nie działają zgodnie z oczekiwaniami lub których już nie używasz, możesz odebrać im dostęp.", "These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "Aplikacje te mogą uzyskać dostęp do konta przez API. Jeśli widzisz tutaj aplikacje, których nie rozpoznajesz, które nie działają zgodnie z oczekiwaniami lub których już nie używasz, możesz odebrać im dostęp.",
"These events may interest you": "Te wydarzenia mogą Cię zainteresować", "These events may interest you": "Te wydarzenia mogą Cię zainteresować",
@ -1312,6 +1338,7 @@
"Visibility was set to public.": "Widoczność została ustawiona na publiczną.", "Visibility was set to public.": "Widoczność została ustawiona na publiczną.",
"Visible everywhere on the web": "Widoczna w całej sieci", "Visible everywhere on the web": "Widoczna w całej sieci",
"Visible everywhere on the web (public)": "Widoczne w całym internecie (publiczne)", "Visible everywhere on the web (public)": "Widoczne w całym internecie (publiczne)",
"Visit {instance_domain}": "Odwiedź {instance_domain}",
"Waiting for organization team approval.": "Oczekiwanie na zatwierdzenie przez organizatorów.", "Waiting for organization team approval.": "Oczekiwanie na zatwierdzenie przez organizatorów.",
"Warning": "Ostrzeżenie", "Warning": "Ostrzeżenie",
"We collect your feedback and the error information in order to improve this service.": "Zbieramy informacje zwrotne i informacje o błędach w celu ulepszenia tej usługi.", "We collect your feedback and the error information in order to improve this service.": "Zbieramy informacje zwrotne i informacje o błędach w celu ulepszenia tej usługi.",
@ -1346,6 +1373,8 @@
"Why create an account?": "Dlaczego warto założyć konto?", "Why create an account?": "Dlaczego warto założyć konto?",
"Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.": "Pozwoli na wyświetlenie i zarządzanie stanem uczestnictwa na stronie wydarzenia gdy używasz tego urządzenia. Odznacz, jeżeli korzystasz z ogólnodostępnego urządzenia.", "Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.": "Pozwoli na wyświetlenie i zarządzanie stanem uczestnictwa na stronie wydarzenia gdy używasz tego urządzenia. Odznacz, jeżeli korzystasz z ogólnodostępnego urządzenia.",
"With the most participants": "Z największą liczbą uczestników(-czek)", "With the most participants": "Z największą liczbą uczestników(-czek)",
"With unknown participants": "Z nieznanymi uczestnikami",
"With {participants}": "Z {participants}",
"Within {number} kilometers of {place}": "|W promieniu jednego kilometra od {place}|W promieniu {number} kilometrów od {place}", "Within {number} kilometers of {place}": "|W promieniu jednego kilometra od {place}|W promieniu {number} kilometrów od {place}",
"Write a new comment": "Napisz nowy komentarz", "Write a new comment": "Napisz nowy komentarz",
"Write a new message": "Napisz nową wiadomość", "Write a new message": "Napisz nową wiadomość",
@ -1410,6 +1439,7 @@
"You moved the folder {resource} to the root folder.": "Katalog {resource} został przeniesiony do katalogu głównego przez Ciebie.", "You moved the folder {resource} to the root folder.": "Katalog {resource} został przeniesiony do katalogu głównego przez Ciebie.",
"You moved the resource {resource} into {new_path}.": "Zasób {resource} został przez Ciebie przeniesiony do {new_path}.", "You moved the resource {resource} into {new_path}.": "Zasób {resource} został przez Ciebie przeniesiony do {new_path}.",
"You moved the resource {resource} to the root folder.": "Zasób {resource} został przez Ciebie przeniesiony do katalogu głównego.", "You moved the resource {resource} to the root folder.": "Zasób {resource} został przez Ciebie przeniesiony do katalogu głównego.",
"You need to enter a text": "Należy wprowadzić tekst",
"You need to login.": "Musisz się zalogować.", "You need to login.": "Musisz się zalogować.",
"You need to provide the following code to your application. It will only be valid for a few minutes.": "Musisz dostarczyć następujący kod do swojej aplikacji. Będzie on ważny tylko przez kilka minut.", "You need to provide the following code to your application. It will only be valid for a few minutes.": "Musisz dostarczyć następujący kod do swojej aplikacji. Będzie on ważny tylko przez kilka minut.",
"You posted a comment on the event {event}.": "Twój komentarz do wydarzenia {event} został zamieszczony.", "You posted a comment on the event {event}.": "Twój komentarz do wydarzenia {event} został zamieszczony.",
@ -1460,9 +1490,11 @@
"Your federated identity": "Twoja sfederowana tożsamość", "Your federated identity": "Twoja sfederowana tożsamość",
"Your membership is pending approval": "Twoje członkostwo oczekuje na zatwierdzenie", "Your membership is pending approval": "Twoje członkostwo oczekuje na zatwierdzenie",
"Your membership was approved by {profile}.": "Twoje członkostwo zostało zatwierdzone przez {profile}.", "Your membership was approved by {profile}.": "Twoje członkostwo zostało zatwierdzone przez {profile}.",
"Your participation has been cancelled": "Twój udział został anulowany",
"Your participation has been confirmed": "Twój udział został potwierdzony", "Your participation has been confirmed": "Twój udział został potwierdzony",
"Your participation has been rejected": "Twoje uczestnictwo zostało odrzucone", "Your participation has been rejected": "Twoje uczestnictwo zostało odrzucone",
"Your participation has been requested": "Poprosiłeś(-aś) o uczestnictwo", "Your participation has been requested": "Poprosiłeś(-aś) o uczestnictwo",
"Your participation is being cancelled": "Twój udział zostaje anulowany",
"Your participation request has been validated": "Twoje uczestnictwo zostało zweryfikowane", "Your participation request has been validated": "Twoje uczestnictwo zostało zweryfikowane",
"Your participation request is being validated": "Twoje uczestnictwo jest weryfikowane", "Your participation request is being validated": "Twoje uczestnictwo jest weryfikowane",
"Your participation status has been changed": "Stan Twojego uczestnictwa został zmieniony", "Your participation status has been changed": "Stan Twojego uczestnictwa został zmieniony",
@ -1530,6 +1562,7 @@
"{count} members or followers": "Brak członków(--iń) i osób obserwujących|Jeden członek / jedna członkini lub osoba obserwująca|{count} członków / członkinie(-ń) lub osób obserwujących", "{count} members or followers": "Brak członków(--iń) i osób obserwujących|Jeden członek / jedna członkini lub osoba obserwująca|{count} członków / członkinie(-ń) lub osób obserwujących",
"{count} participants": "Jeszcze nie ma uczestników / uczestniczek|Jeden uczestnik / uczestniczka|{count} uczestników / uczestniczek", "{count} participants": "Jeszcze nie ma uczestników / uczestniczek|Jeden uczestnik / uczestniczka|{count} uczestników / uczestniczek",
"{count} requests waiting": "{count} oczekujących zgłoszeń", "{count} requests waiting": "{count} oczekujących zgłoszeń",
"{eventsCount} activities found": "Nie znaleziono żadnych aktywności|Znaleziono jedną aktywność|Znaleziono {eventsCount} aktywności",
"{eventsCount} events found": "Nie znaleziono wydarzeń|Znaleziono jedno wydarzenie|Znaleziono {eventsCount} wydarzenia/wydarzeń", "{eventsCount} events found": "Nie znaleziono wydarzeń|Znaleziono jedno wydarzenie|Znaleziono {eventsCount} wydarzenia/wydarzeń",
"{folder} - Resources": "{folder} Zasoby", "{folder} - Resources": "{folder} Zasoby",
"{groupsCount} groups found": "Nie znaleziono żadnych grup|Znaleziono jedną grupę|Znaleziono {groupsCount} grup", "{groupsCount} groups found": "Nie znaleziono żadnych grup|Znaleziono jedną grupę|Znaleziono {groupsCount} grup",

View file

@ -1,7 +1,24 @@
import ngeohash from "ngeohash"; import ngeohash from "ngeohash";
import { IAddress, Address } from "@/types/address.model";
import { LocationType } from "@/types/user-location.model";
import { IUserPreferredLocation } from "@/types/current-user.model";
const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway
export const addressToLocation = (
address: IAddress
): LocationType | undefined => {
if (!address.geom) return undefined;
const arr = address.geom.split(";");
if (arr.length < 2) return undefined;
return {
lon: parseFloat(arr[0]),
lat: parseFloat(arr[1]),
name: address.description,
};
};
export const coordsToGeoHash = ( export const coordsToGeoHash = (
lat: number | undefined, lat: number | undefined,
lon: number | undefined, lon: number | undefined,
@ -14,9 +31,72 @@ export const coordsToGeoHash = (
}; };
export const geoHashToCoords = ( export const geoHashToCoords = (
geohash: string | undefined geohash: string | undefined | null
): { latitude: number; longitude: number } | undefined => { ): { latitude: number; longitude: number } | undefined => {
if (!geohash) return undefined; if (!geohash) return undefined;
const { latitude, longitude } = ngeohash.decode(geohash); const { latitude, longitude } = ngeohash.decode(geohash);
return latitude && longitude ? { latitude, longitude } : undefined; return latitude && longitude ? { latitude, longitude } : undefined;
}; };
export const storeLocationInLocal = (location: IAddress | null): undefined => {
if (location) {
window.localStorage.setItem("location", JSON.stringify(location));
} else {
window.localStorage.removeItem("location");
}
};
export const getLocationFromLocal = (): IAddress | null => {
const locationString = window.localStorage.getItem("location");
if (!locationString) {
return null;
}
const location = JSON.parse(locationString) as IAddress;
if (!location.description || !location.geom) {
return null;
}
return location;
};
export const storeRadiusInLocal = (radius: number | null): undefined => {
if (radius) {
window.localStorage.setItem("radius", radius.toString());
} else {
window.localStorage.removeItem("radius");
}
};
export const getRadiusFromLocal = (): IAddress | null => {
const locationString = window.localStorage.getItem("location");
if (!locationString) {
return null;
}
const location = JSON.parse(locationString) as IAddress;
if (!location.description || !location.geom) {
return null;
}
return location;
};
export const storeUserLocationAndRadiusFromUserSettings = (
location: IUserPreferredLocation | null
): undefined => {
if (location) {
const latlon = geoHashToCoords(location.geohash);
if (latlon) {
storeLocationInLocal({
...new Address(),
geom: `${latlon.longitude};${latlon.latitude}`,
description: location.name || "",
type: "administrative",
});
}
if (location.range) {
storeRadiusInLocal(location.range);
} else {
console.debug("user has not set a radius");
}
} else {
console.debug("user has not set a location");
}
};

View file

@ -2,6 +2,12 @@
<!-- Unlogged introduction --> <!-- Unlogged introduction -->
<unlogged-introduction :config="config" v-if="config && !isLoggedIn" /> <unlogged-introduction :config="config" v-if="config && !isLoggedIn" />
<!-- Search fields --> <!-- Search fields -->
<search-fields
v-model:search="search"
v-model:location="location"
:locationDefaultText="location?.description"
:fromLocalStorage="true"
/>
<!-- Categories preview <!-- Categories preview
<categories-preview /> --> <categories-preview /> -->
<!-- Welcome back --> <!-- Welcome back -->
@ -118,7 +124,6 @@
/> />
<CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" /> <CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" />
<OnlineEvents /> <OnlineEvents />
<LastEvents v-if="instanceName" :instanceName="instanceName" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -135,7 +140,7 @@ import { IEvent } from "../types/event.model";
// import { IFollowedGroupEvent } from "../types/followedGroupEvent.model"; // import { IFollowedGroupEvent } from "../types/followedGroupEvent.model";
import CloseEvents from "@/components/Local/CloseEvents.vue"; import CloseEvents from "@/components/Local/CloseEvents.vue";
import CloseGroups from "@/components/Local/CloseGroups.vue"; import CloseGroups from "@/components/Local/CloseGroups.vue";
import LastEvents from "@/components/Local/LastEvents.vue"; // import LastEvents from "@/components/Local/LastEvents.vue";
import UpcomingEvents from "@/components/Local/UpcomingEvents.vue"; import UpcomingEvents from "@/components/Local/UpcomingEvents.vue";
import OnlineEvents from "@/components/Local/OnlineEvents.vue"; import OnlineEvents from "@/components/Local/OnlineEvents.vue";
import { import {
@ -159,7 +164,7 @@ import CategoriesPreview from "@/components/Home/CategoriesPreview.vue";
import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue"; import UnloggedIntroduction from "@/components/Home/UnloggedIntroduction.vue";
import SearchFields from "@/components/Home/SearchFields.vue"; import SearchFields from "@/components/Home/SearchFields.vue";
import { useHead } from "@unhead/vue"; import { useHead } from "@unhead/vue";
import { geoHashToCoords } from "@/utils/location"; import { addressToLocation, geoHashToCoords } from "@/utils/location";
import { useServerProvidedLocation } from "@/composition/apollo/config"; import { useServerProvidedLocation } from "@/composition/apollo/config";
import { ABOUT } from "@/graphql/config"; import { ABOUT } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
@ -215,6 +220,10 @@ const currentUserParticipations = computed(
const location = ref(null); const location = ref(null);
const search = ref(""); const search = ref("");
watch(location, (newLoc, oldLoc) =>
console.debug("LOCATION UPDATED from", { ...oldLoc }, " to ", { ...newLoc })
);
const isToday = (date: string): boolean => { const isToday = (date: string): boolean => {
return new Date(date).toDateString() === new Date().toDateString(); return new Date(date).toDateString() === new Date().toDateString();
}; };
@ -359,11 +368,7 @@ const coords = computed(() => {
userSettingsLocationGeoHash.value ?? undefined userSettingsLocationGeoHash.value ?? undefined
); );
if (userSettingsCoords) { return { ...serverLocation.value, isIPLocation: !userSettingsCoords };
return { ...userSettingsCoords, isIPLocation: false };
}
return { ...serverLocation.value, isIPLocation: true };
}); });
const { result: reverseGeocodeResult } = useQuery<{ const { result: reverseGeocodeResult } = useQuery<{
@ -404,6 +409,11 @@ const currentUserLocation = computed(() => {
}); });
const userLocation = computed(() => { const userLocation = computed(() => {
console.debug("new userLocation");
if (location.value) {
console.debug("userLocation is typed location");
return addressToLocation(location.value);
}
if ( if (
!userSettingsLocation.value || !userSettingsLocation.value ||
(userSettingsLocation.value?.isIPLocation && (userSettingsLocation.value?.isIPLocation &&

View file

@ -1,10 +1,11 @@
<template> <template>
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<SearchFields <search-fields
class="md:ml-10 mr-2" class="md:ml-10 mr-2"
v-model:search="search" v-model:search="search"
v-model:location="location" v-model:location="location"
:locationDefaultText="locationName" :locationDefaultText="locationName"
:fromLocalStorage="true"
/> />
</div> </div>
<div <div
@ -917,6 +918,9 @@ const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
const latitude = useRouteQuery("lat", undefined, floatTransformer); const latitude = useRouteQuery("lat", undefined, floatTransformer);
const longitude = useRouteQuery("lon", undefined, floatTransformer); const longitude = useRouteQuery("lon", undefined, floatTransformer);
// TODO
// This should be updated with getRadiusFromLocal if we want to use user's
// preferences
const distance = useRouteQuery("distance", "10_km"); const distance = useRouteQuery("distance", "10_km");
const when = useRouteQuery("when", "any"); const when = useRouteQuery("when", "any");
const contentType = useRouteQuery( const contentType = useRouteQuery(

View file

@ -127,15 +127,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { LOGIN } from "@/graphql/auth"; import { LOGIN } from "@/graphql/auth";
import { LOGIN_CONFIG } from "@/graphql/config"; import { LOGIN_CONFIG } from "@/graphql/config";
import { LOGGED_USER_LOCATION } from "@/graphql/user";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user"; import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { ILogin } from "@/types/login.model"; import { IUser } from "@/types/current-user.model";
import { saveUserData, SELECTED_PROVIDERS } from "@/utils/auth"; import { saveUserData, SELECTED_PROVIDERS } from "@/utils/auth";
import { storeUserLocationAndRadiusFromUserSettings } from "@/utils/location";
import { import {
initializeCurrentActor, initializeCurrentActor,
NoIdentitiesException, NoIdentitiesException,
} from "@/utils/identity"; } from "@/utils/identity";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useLazyQuery, useQuery } from "@vue/apollo-composable";
import { computed, reactive, ref, onMounted } from "vue"; import { computed, reactive, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
@ -153,14 +155,14 @@ const route = useRoute();
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();
const { result: configResult } = useQuery<{ const configQuery = useQuery<{
config: Pick< config: Pick<
IConfig, IConfig,
"auth" | "registrationsOpen" | "registrationsAllowlist" "auth" | "registrationsOpen" | "registrationsAllowlist"
>; >;
}>(LOGIN_CONFIG); }>(LOGIN_CONFIG);
const config = computed(() => configResult.value?.config); const config = computed(() => configQuery.result.value?.config);
const canRegister = computed(() => { const canRegister = computed(() => {
return ( return (
@ -181,22 +183,71 @@ const credentials = reactive({
const redirect = useRouteQuery("redirect", ""); const redirect = useRouteQuery("redirect", "");
const errorCode = useRouteQuery("code", null, enumTransformer(LoginErrorCode)); const errorCode = useRouteQuery("code", null, enumTransformer(LoginErrorCode));
const { // Login
onDone: onLoginMutationDone, const loginMutation = useMutation(LOGIN);
onError: onLoginMutationError, // Load user identities
mutate: loginMutation, const currentUserIdentitiesQuery = useLazyCurrentUserIdentities();
} = useMutation(LOGIN); // Update user in cache
const currentUserMutation = useMutation(UPDATE_CURRENT_USER_CLIENT);
// Retrieve preferred location
const loggedUserLocationQuery = useLazyQuery<{
loggedUser: IUser;
}>(LOGGED_USER_LOCATION);
onLoginMutationDone(async (result) => { // form submit action
const data = result.data; const loginAction = async (e: Event) => {
e.preventDefault();
if (submitted.value) {
return;
}
submitted.value = true;
errors.value = [];
try {
// Step 1: login the user
const { data: loginData } = await loginMutation.mutate({
email: credentials.email,
password: credentials.password,
});
submitted.value = false; submitted.value = false;
if (data == null) { if (loginData == null) {
throw new Error("Data is undefined"); throw new Error("Login: user's data is undefined");
} }
saveUserData(data.login); // Login saved to local storage
await setupClientUserAndActors(data.login); saveUserData(loginData.login);
// Step 2: save login in apollo cache
await currentUserMutation.mutate({
id: loginData.login.user.id,
email: credentials.email,
isLoggedIn: true,
role: loginData.login.user.role,
});
// Step 3a: Retrieving user location
const loggedUserLocationPromise = loggedUserLocationQuery.load();
// Step 3b: Setuping user's identities
// FIXME this promise never resolved the first time
// no idea why !
// this appends even with the last version of apollo-composable (4.0.2)
// may be related to that : https://github.com/vuejs/apollo/issues/1543
// EDIT: now it works :shrug:
const currentUserIdentitiesResult = await currentUserIdentitiesQuery.load();
if (!currentUserIdentitiesResult) {
throw new Error("Loading user's identities failed");
}
await initializeCurrentActor(currentUserIdentitiesResult.loggedUser.actors);
// Step 3a following
const loggedUserLocationResult = await loggedUserLocationPromise;
storeUserLocationAndRadiusFromUserSettings(
loggedUserLocationResult?.loggedUser?.settings?.location
);
// Soft redirect
if (redirect.value) { if (redirect.value) {
console.debug("We have a redirect", redirect.value); console.debug("We have a redirect", redirect.value);
router.push(redirect.value); router.push(redirect.value);
@ -208,10 +259,22 @@ onLoginMutationDone(async (result) => {
window.localStorage.setItem("welcome-back", "yes"); window.localStorage.setItem("welcome-back", "yes");
} }
router.replace({ name: RouteName.HOME }); router.replace({ name: RouteName.HOME });
return;
});
onLoginMutationError((err) => { // Hard redirect
// since we fail to refresh the navbar properly, we force a page reload.
// see the explanation of the bug bellow
// window.location = redirect.value || "/";
} catch (err: any) {
if (err instanceof NoIdentitiesException && currentUser.value) {
console.debug("No identities, redirecting to profile registration");
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: currentUser.value.email,
userAlreadyActivated: "true",
},
});
} else {
console.error(err); console.error(err);
submitted.value = false; submitted.value = false;
if (err.graphQLErrors) { if (err.graphQLErrors) {
@ -221,66 +284,8 @@ onLoginMutationError((err) => {
} else if (err.networkError) { } else if (err.networkError) {
errors.value.push(err.networkError.message); errors.value.push(err.networkError.message);
} }
});
const loginAction = (e: Event) => {
e.preventDefault();
if (submitted.value) {
return;
}
submitted.value = true;
errors.value = [];
loginMutation({
email: credentials.email,
password: credentials.password,
});
};
const { load: loadIdentities } = useLazyCurrentUserIdentities();
const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
useMutation(UPDATE_CURRENT_USER_CLIENT);
onCurrentUserMutationDone(async () => {
console.debug("Current user mutation done, now setuping actors…");
// since we fail to refresh the navbar properly, we force a page reload.
// see the explanation of the bug bellow
window.location = redirect.value || "/";
try {
/* FIXME this promise never resolved the first time
no idea why !
this appends even with the last version of apollo-composable (4.0.2)
may be related to that : https://github.com/vuejs/apollo/issues/1543
*/
const result = await loadIdentities();
console.debug("login, loadIdentities resolved");
if (!result) return;
await initializeCurrentActor(result.loggedUser.actors);
} catch (err: any) {
if (err instanceof NoIdentitiesException && currentUser.value) {
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: currentUser.value.email,
userAlreadyActivated: "true",
},
});
} else {
throw err;
} }
} }
});
const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
console.debug("Setuping client user and actors");
updateCurrentUserMutation({
id: login.user.id,
email: credentials.email,
isLoggedIn: true,
role: login.user.role,
});
}; };
const hasCaseWarning = computed<boolean>(() => { const hasCaseWarning = computed<boolean>(() => {