fix #1469 and # 1475

This commit is contained in:
setop 2024-07-08 21:44:22 +00:00
parent 0218dbe06e
commit 79bd6a5d21
16 changed files with 367 additions and 173 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

10
package-lock.json generated
View file

@ -6,7 +6,7 @@
"packages": { "packages": {
"": { "": {
"name": "mobilizon", "name": "mobilizon",
"version": "4.1.0", "version": "5.0.0-beta.1",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.16", "@apollo/client": "^3.3.16",
@ -16,7 +16,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

@ -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"
> >
@ -38,6 +38,11 @@
<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 { 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";
@ -51,6 +56,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();
@ -64,10 +70,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);
}
}, },
}); });
@ -82,12 +97,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

@ -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

@ -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

@ -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

@ -26,7 +26,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" /> <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 -->
@ -143,7 +148,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>
@ -160,7 +164,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 OnlineEvents from "@/components/Local/OnlineEvents.vue"; import OnlineEvents from "@/components/Local/OnlineEvents.vue";
import { import {
computed, computed,
@ -183,7 +187,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";
@ -239,6 +243,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();
}; };
@ -383,11 +391,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<{
@ -428,6 +432,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>(() => {