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()) ::
{: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(
_parent,
%{page: page, limit: limit, order_by: order_by, direction: direction},
_resolution
)
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
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
@desc "Get all events"
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(: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"
)
arg(:longevents, :boolean,
default_value: nil,
description: "if mention filter in or out long events"
)
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.list_events/3)

View file

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

8
package-lock.json generated
View file

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

View file

@ -32,11 +32,7 @@
"@apollo/client": "^3.9.5",
"@framasoft/socket": "^1.0.0",
"@framasoft/socket-apollo-link": "^1.0.0",
"@fullcalendar/core": "^6.1.10",
"@fullcalendar/daygrid": "^6.1.10",
"@fullcalendar/interaction": "^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",
"@fullcalendar/core": "^6.1.10",
"@fullcalendar/daygrid": "^6.1.10",

View file

@ -3,17 +3,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-24 14:40+0000\n"
"PO-Revision-Date: 2023-10-22 04:28+0000\n"
"Last-Translator: Jakub Urbanowicz <tex@przeklad.pl>\n"
"PO-Revision-Date: 2024-07-04 23:35+0000\n"
"Last-Translator: ewm <gnu.ewm@protonmail.com>\n"
"Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/"
"backend/pl/>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.0.1\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.6.2\n"
#: lib/web/templates/email/password_reset.html.heex:66
#, elixir-autogen, elixir-format
@ -2373,30 +2373,32 @@ msgstr "Data rejestracji uczestnika"
#: lib/web/templates/email/event_participation_confirmed.html.heex:122
#, elixir-autogen, elixir-format
msgid "Cancel my attendance"
msgstr ""
msgstr "Anuluj moją obecność"
#: lib/web/templates/email/anonymous_participation_confirmation.html.heex:90
#: lib/web/templates/email/event_participation_confirmed.html.heex:99
#, elixir-autogen, elixir-format
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
#, elixir-autogen, elixir-format
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.text.eex:3
#, elixir-autogen, elixir-format
msgid "If you received this email, the email configuration seems to be correct."
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.text.eex:1
#, elixir-autogen, elixir-format
msgid "Well done!"
msgstr ""
msgstr "Dobra robota!"
#: lib/web/templates/api/terms.html.heex:55
#, elixir-autogen, elixir-format, fuzzy

View file

@ -8,16 +8,16 @@
## to merge POT files into PO files.
msgid ""
msgstr ""
"PO-Revision-Date: 2023-10-22 23:11+0000\n"
"Last-Translator: Jakub Urbanowicz <tex@przeklad.pl>\n"
"PO-Revision-Date: 2024-07-04 23:35+0000\n"
"Last-Translator: ewm <gnu.ewm@protonmail.com>\n"
"Language-Team: Polish <https://weblate.framasoft.org/projects/mobilizon/"
"backend-errors/pl/>\n"
"Language: pl\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.0.1\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.6.2\n"
#: lib/mobilizon/discussions/discussion.ex:68
#, elixir-autogen
@ -1457,9 +1457,9 @@ msgstr ""
#: lib/graphql/resolvers/conversation.ex:164
#, elixir-autogen, elixir-format
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
#, elixir-autogen, elixir-format
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;
}
.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 {
@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>
<o-taginput
:modelValue="modelValueWithDisplayName"
@update:modelValue="(val: IActor[]) => $emit('update:modelValue', val)"
@update:modelValue="updateTags"
:data="availableActors"
:allow-autocomplete="true"
:allow-new="false"
@ -25,12 +25,16 @@ import { computed, ref } from "vue";
import ActorInline from "./ActorInline.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
modelValue: IActor[];
const emit = defineEmits<{
"update:modelValue": [value: IActor[]];
}>();
defineEmits<{
"update:modelValue": [value: IActor[]];
const updateTags = (val: IActor[]) => {
emit("update:modelValue", val);
};
const props = defineProps<{
modelValue: IActor[];
}>();
const modelValue = computed(() => props.modelValue);

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<template>
<section
v-if="promotedCategories.length > 1"
class="mx-auto container flex flex-wrap items-center justify-center gap-3 md:gap-5 my-3"
>
<CategoryCard
@ -10,7 +11,6 @@
:imageLazy="false"
/>
<router-link
v-if="promotedCategories.length > 0"
: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"
>
@ -47,13 +47,17 @@ const eventCategoryLabel = (categoryId: string): string | undefined => {
return eventCategories.value?.find(({ id }) => categoryId == id)?.label;
};
const promotedCategories = computed((): CategoryStatsModel[] => {
return shuffle(
categoryStats.value.filter(
({ key, number }) =>
key !== "MEETING" && number >= 1 && categoriesWithPictures.includes(key)
)
)
const promotedCategories = computed((): CategoryStatsModel[] | null => {
const relevant_categories = categoryStats.value.filter(
({ key, number }) =>
key !== "MEETING" && number >= 1 && categoriesWithPictures.includes(key)
);
if (relevant_categories.length <= 1) {
return [];
}
return shuffle(relevant_categories)
.map(({ key, number }) => ({
key,
number,

View file

@ -1,7 +1,7 @@
<template>
<form
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"
@submit.prevent="submit"
>
@ -25,6 +25,12 @@
<script lang="ts" setup>
import { IAddress } from "@/types/address.model";
import { AddressSearchType } from "@/types/enums";
import {
addressToLocation,
getLocationFromLocal,
storeLocationInLocal,
} from "@/utils/location";
import { computed, defineAsyncComponent } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter, useRoute } from "vue-router";
@ -38,6 +44,7 @@ const props = defineProps<{
location: IAddress | null;
locationDefaultText?: string | null;
search: string;
fromLocalStorage?: boolean | false;
}>();
const router = useRouter();
@ -51,10 +58,19 @@ const emit = defineEmits<{
const location = computed({
get(): IAddress | null {
return props.location;
if (props.location) {
return props.location;
}
if (props.fromLocalStorage) {
return getLocationFromLocal();
}
return null;
},
set(newLocation: IAddress | null) {
emit("update:location", newLocation);
if (props.fromLocalStorage) {
storeLocationInLocal(newLocation);
}
},
});
@ -69,12 +85,7 @@ const search = computed({
const submit = () => {
emit("submit");
const lat = location.value?.geom
? parseFloat(location.value?.geom?.split(";")?.[1])
: undefined;
const lon = location.value?.geom
? parseFloat(location.value?.geom?.split(";")?.[0])
: undefined;
const { lat, lon } = addressToLocation(location.value);
router.push({
name: RouteName.SEARCH,
query: {

View file

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

View file

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

View file

@ -209,7 +209,7 @@
: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"
>{{ t("Calendar")
}}<span class="absolute right-0 text-sm"
}}<span class="absolute right-0 text-xs"
><br />(beta)</span
></router-link
>

View file

@ -2,6 +2,7 @@ import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor";
import {
CURRENT_USER_CLIENT,
LOGGED_USER,
LOGGED_USER_LOCATION,
SET_USER_SETTINGS,
UPDATE_USER_LOCALE,
USER_SETTINGS,
@ -51,6 +52,20 @@ export function useUserSettings() {
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(
variables: Record<string, unknown>
): 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`
query FetchPerson($username: String!) {
fetchPerson(preferredUsername: $username) {
...ActorFragment
suspended
mediaSize
avatar {
id
name

View file

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

View file

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

View file

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

View file

@ -1626,7 +1626,7 @@
"With {participants}": "With {participants}",
"Conversations": "Conversations",
"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",
"List of conversations": "List of conversations",
"Conversation with {participants}": "Conversation with {participants}",

View file

@ -79,6 +79,7 @@
"Add a contact": "Dodaj kontakt",
"Add a new post": "Dodaj nowy wpis",
"Add a note": "Dodaj notatkę",
"Add a recipient": "Dodaj odbiorcę",
"Add a todo": "Dodaj element listy do zrobienia",
"Add an address": "Dodaj adres",
"Add an instance": "Dodaj instancję",
@ -118,6 +119,7 @@
"And {number} comments": "Oraz {number} komentarzy",
"Announcements": "Ogłoszenia",
"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 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ł",
@ -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 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 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 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ąć.",
@ -183,6 +186,7 @@
"By transit": "Transportem publicznym",
"By {group}": "Autorstwa {group}",
"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.",
"Cancel": "Anuluj",
"Cancel anonymous participation": "Anuluj anonimowy udział",
@ -193,6 +197,7 @@
"Cancel membership request": "Anulowanie prośby o członkostwo",
"Cancel my participation request…": "Anuluj moje zgłoszenie udziału…",
"Cancel my participation…": "Anuluj mój udział…",
"Cancel participation": "Anuluj swój udział",
"Cancelled": "Anulowane",
"Cancelled: Won't happen": "Anulowano: Nie odbędzie się",
"Categories": "Kategorie",
@ -229,6 +234,8 @@
"Comment body": "Treść komentarza",
"Comment deleted": "Usunięto komentarz",
"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 text can't be empty": "Tekst komentarza nie morze być pusty",
"Comment under event {eventTitle}": "Komentarz pod wydarzeniem {eventTitle}",
@ -246,6 +253,8 @@
"Contact": "Kontakt",
"Continue": "Kontynuuj",
"Continue editing": "Kontynuuj edycję",
"Conversation with {participants}": "Konwersacja z {participants}",
"Conversations": "Konwersacje",
"Cookies and Local storage": "Pliki cookies i pamięć lokalna",
"Copy URL to clipboard": "Kopiowanie adresu URL do schowka",
"Copy details to clipboard": "Kopiowanie szczegółów do schowka",
@ -299,6 +308,7 @@
"Default": "Domyślne",
"Default Mobilizon privacy policy": "Domyślna polityka prywatności Mobilizon",
"Default Mobilizon terms": "Domyślne warunki użytkowania Mobilizon",
"Default Picture": "Obraz domyślny",
"Delete": "Usuń",
"Delete account": "Usuń konto",
"Delete comment": "Usuń komentarz",
@ -318,6 +328,7 @@
"Delete my account": "Usuń moje konto",
"Delete post": "Usuń wpis",
"Delete profiles": "Usuwanie profili",
"Delete this conversation": "Usuń tę konwersację",
"Delete this discussion": "Usunięcie tej dyskusji",
"Delete this identity": "Usuń tę 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}?",
"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 or instance name": "Nazwa domeny lub instancji",
"Draft": "Szkic",
"Drafts": "Szkice",
"Due on": "Zaplanowane na",
@ -381,6 +393,7 @@
"Error details copied!": "Szczegóły błędu zostały skopiowane!",
"Error message": "Komunikat o błędzie",
"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 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.",
@ -429,6 +442,7 @@
"External registration": "Rejestracja zewnętrzna",
"Failed to get location.": "Nie udało się uzyskać lokalizacji.",
"Failed to save admin settings": "Nie udało się zapisać ustawień administratora (-ki)",
"Favicon": "Favicon",
"Featured events": "Wyróżnione wydarzenia",
"Federated Group Name": "Sfederowana nazwa grupy",
"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} at {endTime}": "Od {startDate} o {startTime} do {endDate} o {endTime}",
"From the {startDate} to the {endDate}": "Od {startDate} do {endDate}",
"From this instance only": "Tylko z tej instancji",
"From yourself": "Od Ciebie",
"Fully accessible with a wheelchair": "W pełni dostępne na wózku",
"Gather ⋅ Organize ⋅ Mobilize": "Gromadźcie się ⋅ Organizujcie się⋅ Mobilizujcie się",
@ -610,6 +625,7 @@
"Light": "Jasny",
"Limited number of places": "Ograniczona liczba miejsc",
"List": "Lista",
"List of conversations": "Lista konwersacji",
"List title": "Tytuł listy",
"Live": "Na żywo",
"Load more": "Załaduj więcej",
@ -627,6 +643,8 @@
"Login on Mobilizon!": "Zaloguj się na Mobilizon!",
"Login on {instance}": "Zaloguj się na {instance}",
"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)",
"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",
@ -684,6 +702,7 @@
"Name": "Nazwa",
"Navigated to {pageTitle}": "Przejście do {pageTitle}",
"Never used": "Nigdy nie użyty",
"New announcement": "Nowe ogłoszenie",
"New discussion": "Nowa dyskusja",
"New email": "Nowy adres e-mail",
"New folder": "Nowy katalog",
@ -692,11 +711,13 @@
"New note": "Nowa notatka",
"New password": "Nowe hasło",
"New post": "Nowy wpis",
"New private message": "Nowa wiadomość prywatna",
"New profile": "Nowy profil",
"Next": "Następny",
"Next month": "W kolejnym miesiącu",
"Next page": "Następna strona",
"Next week": "W następnym tygodniu",
"No activities found": "Nie znaleziono żadnych aktywności",
"No address defined": "Nie określono adresu",
"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.",
@ -868,6 +889,7 @@
"Previous month": "Poprzedni miesiąc",
"Previous page": "Poprzednia strona",
"Price sheet": "Cennik",
"Primary Color": "Kolor podstawowy",
"Privacy": "Prywatność",
"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",
"Resources": "Zasoby",
"Restricted": "Ograniczona",
"Return to the event page": "Wróć do strony wydarzenia",
"Return to the group page": "Powrót do strony grupy",
"Revoke": "Cofnięcie uprawnień",
"Right now": "Właśnie teraz",
@ -982,6 +1005,7 @@
"Search events, groups, etc.": "Szukaj wydarzeń, grup itp.",
"Search target": "Cel wyszukiwania",
"Searching…": "Wyszukiwanie…",
"Secondary Color": "Kolor dodatkowy",
"Select a category": "Wybierz kategorię",
"Select a language": "Wybierz język",
"Select a radius": "Wybierz promień",
@ -1021,6 +1045,7 @@
"Smoke free": "Strefa wolna od dymu tytoniowego",
"Smoking allowed": "Palenie dozwolone",
"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ć:",
"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",
@ -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 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's no announcements yet": "Nie ma jeszcze żadnych ogłoszeń",
"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 events may interest you": "Te wydarzenia mogą Cię zainteresować",
@ -1312,6 +1338,7 @@
"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 (public)": "Widoczne w całym internecie (publiczne)",
"Visit {instance_domain}": "Odwiedź {instance_domain}",
"Waiting for organization team approval.": "Oczekiwanie na zatwierdzenie przez organizatorów.",
"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.",
@ -1346,6 +1373,8 @@
"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.",
"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}",
"Write a new comment": "Napisz nowy komentarz",
"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 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 need to enter a text": "Należy wprowadzić tekst",
"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 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 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 participation has been cancelled": "Twój udział został anulowany",
"Your participation has been confirmed": "Twój udział został potwierdzony",
"Your participation has been rejected": "Twoje uczestnictwo zostało odrzucone",
"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 is being validated": "Twoje uczestnictwo jest weryfikowane",
"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} participants": "Jeszcze nie ma uczestników / uczestniczek|Jeden uczestnik / uczestniczka|{count} uczestników / uczestniczek",
"{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ń",
"{folder} - Resources": "{folder} Zasoby",
"{groupsCount} groups found": "Nie znaleziono żadnych grup|Znaleziono jedną grupę|Znaleziono {groupsCount} grup",

View file

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

View file

@ -1,10 +1,11 @@
<template>
<div class="max-w-4xl mx-auto">
<SearchFields
<search-fields
class="md:ml-10 mr-2"
v-model:search="search"
v-model:location="location"
:locationDefaultText="locationName"
:fromLocalStorage="true"
/>
</div>
<div
@ -917,6 +918,9 @@ const groupPage = useRouteQuery("groupPage", 1, integerTransformer);
const latitude = useRouteQuery("lat", 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 when = useRouteQuery("when", "any");
const contentType = useRouteQuery(

View file

@ -127,15 +127,17 @@
<script setup lang="ts">
import { LOGIN } from "@/graphql/auth";
import { LOGIN_CONFIG } from "@/graphql/config";
import { LOGGED_USER_LOCATION } from "@/graphql/user";
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
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 { storeUserLocationAndRadiusFromUserSettings } from "@/utils/location";
import {
initializeCurrentActor,
NoIdentitiesException,
} 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 { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
@ -153,14 +155,14 @@ const route = useRoute();
const { currentUser } = useCurrentUserClient();
const { result: configResult } = useQuery<{
const configQuery = useQuery<{
config: Pick<
IConfig,
"auth" | "registrationsOpen" | "registrationsAllowlist"
>;
}>(LOGIN_CONFIG);
const config = computed(() => configResult.value?.config);
const config = computed(() => configQuery.result.value?.config);
const canRegister = computed(() => {
return (
@ -181,85 +183,90 @@ const credentials = reactive({
const redirect = useRouteQuery("redirect", "");
const errorCode = useRouteQuery("code", null, enumTransformer(LoginErrorCode));
const {
onDone: onLoginMutationDone,
onError: onLoginMutationError,
mutate: loginMutation,
} = useMutation(LOGIN);
// Login
const loginMutation = useMutation(LOGIN);
// Load user identities
const currentUserIdentitiesQuery = useLazyCurrentUserIdentities();
// 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) => {
const data = result.data;
submitted.value = false;
if (data == null) {
throw new Error("Data is undefined");
}
saveUserData(data.login);
await setupClientUserAndActors(data.login);
if (redirect.value) {
console.debug("We have a redirect", redirect.value);
router.push(redirect.value);
return;
}
console.debug("No redirect, going to homepage");
if (window.localStorage) {
console.debug("Has localstorage, setting welcome back");
window.localStorage.setItem("welcome-back", "yes");
}
router.replace({ name: RouteName.HOME });
return;
});
onLoginMutationError((err) => {
console.error(err);
submitted.value = false;
if (err.graphQLErrors) {
err.graphQLErrors.forEach(({ message }: { message: string }) => {
errors.value.push(message);
});
} else if (err.networkError) {
errors.value.push(err.networkError.message);
}
});
const loginAction = (e: Event) => {
// form submit action
const loginAction = async (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);
// Step 1: login the user
const { data: loginData } = await loginMutation.mutate({
email: credentials.email,
password: credentials.password,
});
submitted.value = false;
if (loginData == null) {
throw new Error("Login: user's data is undefined");
}
// Login saved to local storage
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) {
console.debug("We have a redirect", redirect.value);
router.push(redirect.value);
return;
}
console.debug("No redirect, going to homepage");
if (window.localStorage) {
console.debug("Has localstorage, setting welcome back");
window.localStorage.setItem("welcome-back", "yes");
}
router.replace({ name: RouteName.HOME });
// 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: {
@ -268,19 +275,17 @@ onCurrentUserMutationDone(async () => {
},
});
} else {
throw err;
console.error(err);
submitted.value = false;
if (err.graphQLErrors) {
err.graphQLErrors.forEach(({ message }: { message: string }) => {
errors.value.push(message);
});
} else if (err.networkError) {
errors.value.push(err.networkError.message);
}
}
}
});
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>(() => {