From 79bd6a5d212fca36ed2c8a50473eb71bb86c608a Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Mon, 8 Jul 2024 21:44:22 +0000 Subject: [PATCH] fix #1469 and # 1475 --- lib/graphql/resolvers/event.ex | 20 +++- lib/graphql/schema/event.ex | 12 ++ lib/mobilizon/events/events.ex | 27 +++-- package-lock.json | 10 +- src/components/Home/SearchFields.vue | 26 ++-- src/components/Local/CloseEvents.vue | 120 ++++++++++--------- src/components/Local/LastEvents.vue | 1 + src/composition/apollo/user.ts | 15 +++ src/graphql/event.ts | 6 + src/graphql/search.ts | 1 + src/graphql/user.ts | 14 +++ src/i18n/en_US.json | 2 +- src/utils/location.ts | 82 ++++++++++++- src/views/HomeView.vue | 27 +++-- src/views/SearchView.vue | 6 +- src/views/User/LoginView.vue | 171 ++++++++++++++------------- 16 files changed, 367 insertions(+), 173 deletions(-) diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index c375f0104..fe7aaec69 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -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 diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index e2961794d..4ecd682fe 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -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) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 1009a0197..4d68b9d41 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -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 diff --git a/package-lock.json b/package-lock.json index 2c293227e..47e154224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "mobilizon", - "version": "4.1.0", + "version": "5.0.0-beta.1", "hasInstallScript": true, "dependencies": { "@apollo/client": "^3.3.16", @@ -16,7 +16,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" } diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index ed04c586e..afd74e414 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -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" > @@ -38,6 +38,11 @@ <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"; @@ -51,6 +56,7 @@ const props = defineProps<{ location: IAddress | null; locationDefaultText?: string | null; search: string; + fromLocalStorage?: boolean | false; }>(); const router = useRouter(); @@ -64,10 +70,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); + } }, }); @@ -82,12 +97,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: { diff --git a/src/components/Local/CloseEvents.vue b/src/components/Local/CloseEvents.vue index 2f79a9075..ac9c0e539 100644 --- a/src/components/Local/CloseEvents.vue +++ b/src/components/Local/CloseEvents.vue @@ -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> diff --git a/src/components/Local/LastEvents.vue b/src/components/Local/LastEvents.vue index cb068a82f..959cec8a0 100644 --- a/src/components/Local/LastEvents.vue +++ b/src/components/Local/LastEvents.vue @@ -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: [] } diff --git a/src/composition/apollo/user.ts b/src/composition/apollo/user.ts index 496878402..288645cd9 100644 --- a/src/composition/apollo/user.ts +++ b/src/composition/apollo/user.ts @@ -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> { diff --git a/src/graphql/event.ts b/src/graphql/event.ts index 862e44e55..b2bdabaea 100644 --- a/src/graphql/event.ts +++ b/src/graphql/event.ts @@ -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 { diff --git a/src/graphql/search.ts b/src/graphql/search.ts index aceff8d85..2eff07b5a 100644 --- a/src/graphql/search.ts +++ b/src/graphql/search.ts @@ -218,6 +218,7 @@ export const SEARCH_CALENDAR_EVENTS = gql` endsOn: $endsOn page: $eventPage limit: $limit + longevents: false ) { total elements { diff --git a/src/graphql/user.ts b/src/graphql/user.ts index 1ea561c13..1d21080cd 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -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 { diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index 066ac2382..32ee34114 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -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}", diff --git a/src/utils/location.ts b/src/utils/location.ts index 8e0ece55d..6dc17cf8b 100644 --- a/src/utils/location.ts +++ b/src/utils/location.ts @@ -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"); + } +}; diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index d9f5975cf..ebca1c34d 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -26,7 +26,12 @@ <!-- Unlogged introduction --> <unlogged-introduction :config="config" v-if="config && !isLoggedIn" /> <!-- Search fields --> - <search-fields v-model:search="search" v-model:location="location" /> + <search-fields + v-model:search="search" + v-model:location="location" + :locationDefaultText="location?.description" + :fromLocalStorage="true" + /> <!-- Categories preview --> <categories-preview /> <!-- Welcome back --> @@ -143,7 +148,6 @@ /> <CloseGroups :userLocation="userLocation" @doGeoLoc="performGeoLocation()" /> <OnlineEvents /> - <LastEvents v-if="instanceName" :instanceName="instanceName" /> </template> <script lang="ts" setup> @@ -160,7 +164,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 OnlineEvents from "@/components/Local/OnlineEvents.vue"; import { computed, @@ -183,7 +187,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"; @@ -239,6 +243,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(); }; @@ -383,11 +391,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<{ @@ -428,6 +432,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 && diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 1e29269bd..7400abe15 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -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( diff --git a/src/views/User/LoginView.vue b/src/views/User/LoginView.vue index c832844af..72e12faad 100644 --- a/src/views/User/LoginView.vue +++ b/src/views/User/LoginView.vue @@ -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>(() => {