From 78b7fae91b566d8df192007a8af145148e618ebb Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 7 Oct 2024 13:06:32 +0200 Subject: [PATCH 01/78] Move time display settings near the related datetimepicker --- src/views/Event/EditView.vue | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 804e9b057..b807c86b8 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -61,7 +61,7 @@ </div> <o-field - horizontal + grouped :label="t('Starts on…')" class="items-center" label-for="begins-on-field" @@ -82,10 +82,13 @@ }" > </o-datetimepicker> + <o-switch v-model="eventOptions.showStartTime">{{ + t("Show the time when the event begins") + }}</o-switch> </o-field> <o-field - horizontal + grouped :label="t('Ends on…')" label-for="ends-on-field" class="items-center" @@ -107,10 +110,13 @@ }" > </o-datetimepicker> + <o-switch v-model="eventOptions.showEndTime">{{ + t("Show the time when the event ends") + }}</o-switch> </o-field> <o-button class="block" variant="text" @click="dateSettingsIsOpen = true"> - {{ t("Date parameters") }} + {{ t("Timezone parameters") }} </o-button> <div class="my-6"> @@ -476,7 +482,7 @@ > <form class="p-3"> <header class=""> - <h2 class="">{{ t("Date and time settings") }}</h2> + <h2 class="">{{ t("Timezone") }}</h2> </header> <section class=""> <p> @@ -486,7 +492,7 @@ ) }} </p> - <o-field :label="t('Timezone')" label-for="timezone" expanded> + <o-field expanded> <o-select :placeholder="t('Select a timezone')" :loading="timezoneLoading" @@ -517,16 +523,6 @@ :title="t('Clear timezone field')" /> </o-field> - <o-field :label="t('Event page settings')"> - <o-switch v-model="eventOptions.showStartTime">{{ - t("Show the time when the event begins") - }}</o-switch> - </o-field> - <o-field> - <o-switch v-model="eventOptions.showEndTime">{{ - t("Show the time when the event ends") - }}</o-switch> - </o-field> </section> <footer class="mt-2"> <o-button @click="dateSettingsIsOpen = false"> From 4d9d6b02b67efe6035817444418cd3fbb758fd93 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 11 Oct 2024 13:44:15 +0200 Subject: [PATCH 02/78] The status choice area no longer causes the event page to scroll in mobile view --- src/views/Event/EditView.vue | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index b807c86b8..74a992d8f 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -402,7 +402,7 @@ <section class="my-4"> <h2>{{ t("Status") }}</h2> - <fieldset> + <fieldset id="status"> <legend> {{ t( @@ -1430,4 +1430,27 @@ const registerOption = computed({ padding-left: 3px; } } + +#status .o-field--addons { + flex-wrap: wrap; + gap: 5px; +} + +#status .o-field--addons > label { + flex: 1 1 0; + margin: 0; +} +#status .o-field--addons .mr-2 { + margin: 0; +} + +#status .o-field--addons > label .o-radio__label { + width: 100%; +} + +@media screen and (max-width: 700px) { + #status .o-field--addons { + flex-direction: column; + } +} </style> From 3401a7dd858c6c5dfcff0f2dc350b0a923eea91c Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 11 Oct 2024 14:25:56 +0200 Subject: [PATCH 03/78] The validation button area no longer causes the event page to scroll in mobile view --- src/views/Event/EditView.vue | 53 +++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 74a992d8f..84a9b92eb 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -540,40 +540,43 @@ v-if="hasCurrentActorPermissionsToEdit" > <div class="container mx-auto"> - <div class="flex justify-between items-center"> - <span class="dark:text-gray-900" v-if="isEventModified"> + <div class="lg:flex lg:justify-between lg:items-center lg:flex-wrap"> + <div + class="text-red-900 text-center w-full margin m-1 lg:m-0 lg:w-auto lg:text-left" + v-if="isEventModified" + > {{ t("Unsaved changes") }} - </span> + </div> <div class="flex flex-wrap gap-3 items-center justify-end"> <o-button + expanded variant="text" @click="confirmGoBack" class="dark:!text-black ml-auto" >{{ t("Cancel") }}</o-button > <!-- If an event has been published we can't make it draft anymore --> - <span class="" v-if="event.draft === true"> - <o-button - variant="primary" - class="!text-black hover:!text-white" - outlined - @click="createOrUpdateDraft" - :disabled="saving" - >{{ t("Save draft") }}</o-button - > - </span> - <span class="ml-auto"> - <o-button - variant="primary" - :disabled="saving" - @click="createOrUpdatePublish" - @keyup.enter="createOrUpdatePublish" - > - <span v-if="isUpdate === false">{{ t("Create my event") }}</span> - <span v-else-if="event.draft === true">{{ t("Publish") }}</span> - <span v-else>{{ t("Update my event") }}</span> - </o-button> - </span> + <o-button + v-if="event.draft === true" + expanded + variant="primary" + class="!text-black hover:!text-white" + outlined + @click="createOrUpdateDraft" + :disabled="saving" + >{{ t("Save draft") }}</o-button + > + <o-button + expanded + variant="primary" + :disabled="saving" + @click="createOrUpdatePublish" + @keyup.enter="createOrUpdatePublish" + > + <span v-if="isUpdate === false">{{ t("Create my event") }}</span> + <span v-else-if="event.draft === true">{{ t("Publish") }}</span> + <span v-else>{{ t("Update my event") }}</span> + </o-button> </div> </div> </div> From 37e977404f178d424de44c315e9e9de6489a3149 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 11 Oct 2024 17:00:31 +0200 Subject: [PATCH 04/78] Shorter fr_FR "Add new..." translation to remove a scroll in edit event in mobile view --- src/i18n/fr_FR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 5ad78b06b..043a4343e 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -83,7 +83,7 @@ "Add an address": "Ajouter une adresse", "Add an instance": "Ajouter une instance", "Add link": "Ajouter un lien", - "Add new…": "Ajouter un nouvel élément…", + "Add new…": "Ajouter…", "Add picture": "Ajouter une image", "Add some tags": "Ajouter des tags", "Add to my calendar": "Ajouter à mon agenda", From c859d32cd1578e5a6442b4c0c481d5414b5ba0fb Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 11 Oct 2024 17:43:51 +0200 Subject: [PATCH 05/78] Changing organizer pop-up is now usable in mobile view --- src/components/Event/OrganizerPicker.vue | 1 + src/components/Event/OrganizerPickerWrapper.vue | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Event/OrganizerPicker.vue b/src/components/Event/OrganizerPicker.vue index d2797a0de..912e9320c 100644 --- a/src/components/Event/OrganizerPicker.vue +++ b/src/components/Event/OrganizerPicker.vue @@ -1,6 +1,7 @@ <template> <div class="max-w-md mx-auto"> <o-input + expanded dir="auto" :placeholder="t('Filter by profile or group name')" v-model="actorFilterProxy" diff --git a/src/components/Event/OrganizerPickerWrapper.vue b/src/components/Event/OrganizerPickerWrapper.vue index b898ae79e..86cfcbf86 100644 --- a/src/components/Event/OrganizerPickerWrapper.vue +++ b/src/components/Event/OrganizerPickerWrapper.vue @@ -63,9 +63,10 @@ <h2 class="">{{ $t("Pick a profile or a group") }}</h2> </header> <section class=""> - <div class="flex flex-wrap gap-2 items-center"> - <div class="max-h-[400px] overflow-y-auto flex-1"> + <div class="flex flex-wrap gap-2 items-center flex-col lg:flex-row"> + <div class="max-h-[400px] overflow-y-auto flex-1 w-full"> <organizer-picker + class="p-5 w-3/4" v-if="currentActor" :current-actor="currentActor" :identities="identities ?? []" @@ -80,6 +81,7 @@ <div v-if="isSelectedActorAGroup"> <p>{{ $t("Add a contact") }}</p> <o-input + expanded :placeholder="$t('Filter by name')" :value="contactFilter" @input="debounceSetFilterByName" @@ -137,8 +139,12 @@ </div> </div> </section> - <footer class="my-2"> - <o-button variant="primary" @click="pickActor"> + <footer class="my-2 text-center sm:text-right"> + <o-button + variant="primary" + class="w-full sm:w-auto" + @click="pickActor" + > {{ $t("Pick") }} </o-button> </footer> From d1ce24cb692b0aadcb36376ce1329db3af9e5243 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 14 Oct 2024 16:18:25 +0200 Subject: [PATCH 06/78] #1511 format <start-time-icon> with the timezone and the formatTimeString() function already used for that in Mobilizon --- src/components/Event/StartTimeIcon.vue | 14 ++++---------- src/views/Event/EventView.vue | 1 + 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/Event/StartTimeIcon.vue b/src/components/Event/StartTimeIcon.vue index 7d1fe0a95..38abc97bc 100644 --- a/src/components/Event/StartTimeIcon.vue +++ b/src/components/Event/StartTimeIcon.vue @@ -12,30 +12,24 @@ </div> </template> <script lang="ts" setup> +import { formatTimeString } from "@/filters/datetime"; 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; + timezone?: string; small?: boolean; }>(), - { small: false } + { small: false, timezone: "Etc/UTC" } ); const dateObj = computed<Date>(() => new Date(props.date)); const time = computed<string>(() => - dateObj.value.toLocaleTimeString(localeConverted, { - hour: "2-digit", - minute: "2-digit", - }) + formatTimeString(props.date, props.timezone) ); const smallStyle = computed<string>(() => (props.small ? "0.9" : "2")); diff --git a/src/views/Event/EventView.vue b/src/views/Event/EventView.vue index 0ad9ff065..c8b275991 100755 --- a/src/views/Event/EventView.vue +++ b/src/views/Event/EventView.vue @@ -24,6 +24,7 @@ > <start-time-icon :date="event.beginsOn.toString()" + :timezone="event.options.timezone ?? undefined" class="absolute right-3 -top-16" /> </div> From 5b5f295dc3d823fe3ce449636dab1ffca8e93e88 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Tue, 15 Oct 2024 17:35:27 +0200 Subject: [PATCH 07/78] #1459 Update translation to correctly pluralize the available places for an event --- src/components/Event/EventParticipationCard.vue | 10 +++++++--- src/i18n/en_US.json | 4 ++-- src/i18n/fr_FR.json | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Event/EventParticipationCard.vue b/src/components/Event/EventParticipationCard.vue index 0376ee13e..f16348da1 100644 --- a/src/components/Event/EventParticipationCard.vue +++ b/src/components/Event/EventParticipationCard.vue @@ -124,9 +124,13 @@ <!-- Less than 10 seats left --> <span class="has-text-danger" v-if="lastSeatsLeft"> {{ - t("{number} seats left", { - number: seatsLeft, - }) + t( + "{number} seats left", + { + number: seatsLeft, + }, + seatsLeft ?? 0 + ) }} </span> <span diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index 32ee34114..3b688f59b 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -414,7 +414,7 @@ "Due on": "Due on", "Organizers": "Organizers", "(Masked)": "(Masked)", - "{available}/{capacity} available places": "No places left|{available}/{capacity} available places", + "{available}/{capacity} available places": "No places left|{available}/{capacity} available place left|{available}/{capacity} available places", "No one is participating|One person participating|{going} people participating": "No one is participating|One person participating|{going} people participating", "Date and time": "Date and time", "Location": "Location", @@ -1218,7 +1218,7 @@ "You will receive notifications about this group's public activity depending on %{notification_settings}.": "You will receive notifications about this group's public activity depending on %{notification_settings}.", "Online": "Online", "That you follow or of which you are a member": "That you follow or of which you are a member", - "{number} seats left": "{number} seats left", + "{number} seats left": "No seat left|One seat left|{number} seats left", "Published by {name}": "Published by {name}", "Share this post": "Share this post", "This post is accessible only through it's link. Be careful where you post this link.": "This post is accessible only through it's link. Be careful where you post this link.", diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 043a4343e..4cf9cdf11 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -1562,7 +1562,7 @@ "{'@'}{username}": "{'@'}{username}", "{'@'}{username} ({role})": "{'@'}{username} ({role})", "{approved} / {total} seats": "{approved} / {total} places", - "{available}/{capacity} available places": "Pas de places restantes|{available}/{capacity} places restantes|{available}/{capacity} places restantes", + "{available}/{capacity} available places": "Pas de places restantes|{available}/{capacity} place restante|{available}/{capacity} places restantes", "{count} events": "{count} événements", "{count} km": "{count} km", "{count} members": "Aucun membre|Un·e membre|{count} membres", @@ -1608,7 +1608,7 @@ "{number} organized events": "Aucun événement organisé|Un événement organisé|{number} événements organisés", "{number} participations": "Aucune participation|Une participation|{number} participations", "{number} posts": "Aucun billet|Un billet|{number} billets", - "{number} seats left": "{number} places restantes", + "{number} seats left": "Aucune place restante|Une place restante|{number} places restantes", "{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.", "{profileName} (suspended)": "{profileName} (suspendu·e)", "{profile} (by default)": "{profile} (par défault)", From 3ed4105879916e9020a855d536ae4a821121f246 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 16 Oct 2024 18:36:10 +0200 Subject: [PATCH 08/78] Issue #1560 "Location is lost when navigating" : propagation of query for each route --- src/components/NavBar.vue | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index b4a597942..fae37b5a8 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -189,8 +189,9 @@ <li class="m-auto" v-if="islongEvents"> <router-link :to="{ + ...$route, name: RouteName.SEARCH, - query: { contentType: 'SHORTEVENTS' }, + query: { ...$route.query, contentType: 'SHORTEVENTS' }, }" class="block 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("Events") }}</router-link @@ -198,7 +199,11 @@ </li> <li class="m-auto" v-else> <router-link - :to="{ name: RouteName.SEARCH, query: { contentType: 'EVENTS' } }" + :to="{ + ...$route, + name: RouteName.SEARCH, + query: { ...$route.query, contentType: 'EVENTS' }, + }" class="block 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("Events") }}</router-link > @@ -206,8 +211,9 @@ <li class="m-auto" v-if="islongEvents"> <router-link :to="{ + ...$route, name: RouteName.SEARCH, - query: { contentType: 'LONGEVENTS' }, + query: { ...$route.query, contentType: 'LONGEVENTS' }, }" class="block 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("Activities") }}</router-link @@ -215,7 +221,11 @@ </li> <li class="m-auto"> <router-link - :to="{ name: RouteName.SEARCH, query: { contentType: 'GROUPS' } }" + :to="{ + ...$route, + name: RouteName.SEARCH, + query: { ...$route.query, contentType: 'GROUPS' }, + }" class="block 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("Groups") }}</router-link > From 9403a9d60a0ed3b72536ed3e2a8dbbba43a62bb0 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 23 Oct 2024 16:13:17 +0200 Subject: [PATCH 09/78] correction about issue #1556 : Fix location on homepage and location clearing --- src/components/Event/FullAddressAutoComplete.vue | 2 +- src/components/Home/SearchFields.vue | 5 +++++ src/views/HomeView.vue | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/Event/FullAddressAutoComplete.vue b/src/components/Event/FullAddressAutoComplete.vue index a827684d8..48185b32c 100644 --- a/src/components/Event/FullAddressAutoComplete.vue +++ b/src/components/Event/FullAddressAutoComplete.vue @@ -374,7 +374,7 @@ const asyncData = async (query: string): Promise<void> => { }; const selectedAddressText = computed(() => { - if (!selected) return undefined; + if (!selected || !selected.id) return undefined; return addressFullName(selected); }); diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index afd74e414..c20315bb2 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -27,6 +27,7 @@ :default-text="locationDefaultText" labelClass="sr-only" :placeholder="t('e.g. Nantes, Berlin, Cork, …')" + v-on:update:modelValue="modelValueUpdate" /> <o-button native-type="submit" icon-left="magnify"> <template v-if="search">{{ t("Go!") }}</template> @@ -95,6 +96,10 @@ const search = computed({ }, }); +const modelValueUpdate = (newlocation: IAddress | null) => { + emit("update:location", newlocation); +}; + const submit = () => { emit("submit"); const { lat, lon } = addressToLocation(location.value); diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index aeebdda16..5c45847c5 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -29,7 +29,8 @@ <search-fields v-model:search="search" v-model:location="location" - :locationDefaultText="location?.description" + :locationDefaultText="location?.description ?? userLocation?.name" + v-on:update:location="updateLocation" :fromLocalStorage="true" /> <!-- Categories preview --> @@ -237,6 +238,7 @@ const currentUserParticipations = computed( const location = ref(null); const search = ref(""); +const noLocation = ref(false); watch(location, (newLoc, oldLoc) => console.debug("LOCATION UPDATED from", { ...oldLoc }, " to ", { ...newLoc }) @@ -428,6 +430,13 @@ const currentUserLocation = computed(() => { const userLocation = computed(() => { console.debug("new userLocation"); + if (noLocation.value) { + return { + lon: null, + lat: null, + name: null, + }; + } if (location.value) { console.debug("userLocation is typed location"); return addressToLocation(location.value); @@ -502,6 +511,10 @@ const performGeoLocation = () => { ); }; +const updateLocation = (newlocation: IAddress | null) => { + noLocation.value = newlocation == null; +}; + /** * View Head */ From 41aa81097d33c0909388cab12176d1134b720ab0 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 24 Oct 2024 18:54:02 +0200 Subject: [PATCH 10/78] #1308 update build_page() to permit query with group_by() Using Repo.aggregate() to count the total of elements is incompatible with group_by(). Changing this to a subquery with a count(*) of results permit to use group_by(). --- lib/mobilizon/events/events.ex | 2 +- lib/mobilizon/instances/instances.ex | 2 +- lib/mobilizon/storage/page.ex | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 4d68b9d41..7bf770723 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -602,7 +602,7 @@ defmodule Mobilizon.Events do |> filter_local_or_from_followed_instances_events() |> filter_public_visibility() |> event_order(Map.get(args, :sort_by, :match_desc), search_string) - |> Page.build_page(page, limit, :begins_on) + |> Page.build_page(page, limit) end @doc """ diff --git a/lib/mobilizon/instances/instances.ex b/lib/mobilizon/instances/instances.ex index db0dafd29..8d6557877 100644 --- a/lib/mobilizon/instances/instances.ex +++ b/lib/mobilizon/instances/instances.ex @@ -73,7 +73,7 @@ defmodule Mobilizon.Instances do query end - %Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain) + %Page{elements: elements} = paged_instances = Page.build_page(query, page, limit) %Page{ paged_instances diff --git a/lib/mobilizon/storage/page.ex b/lib/mobilizon/storage/page.ex index 9b094d4e8..9a8537774 100644 --- a/lib/mobilizon/storage/page.ex +++ b/lib/mobilizon/storage/page.ex @@ -23,11 +23,22 @@ defmodule Mobilizon.Storage.Page do `field` is use to define the field that will be used for the count aggregate, which should be the same as the field used for order_by See https://stackoverflow.com/q/12693089/10204399 """ - @spec build_page(Ecto.Queryable.t(), integer | nil, integer | nil, atom()) :: t(any) - def build_page(query, page, limit, field \\ :id) do + @spec build_page(Ecto.Queryable.t(), integer | nil, integer) :: t(any) + def build_page(query, page, limit) do + count_query = + query + # Exclude select because we add a new one below + |> exclude(:select) + # Exclude order_by for perf + |> exclude(:order_by) + # Exclude preloads to avoid error "cannot preload associations in subquery" + |> exclude(:preload) + |> subquery() + |> select([r], count(fragment("*"))) + [total, elements] = [ - fn -> Repo.aggregate(query, :count, field) end, + fn -> Repo.one(count_query) end, fn -> Repo.all(paginate(query, page, limit)) end ] |> Enum.map(&Task.async/1) From 6ff35257641c731df52306ea0e550dfd2768b4c4 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 24 Oct 2024 19:03:55 +0200 Subject: [PATCH 11/78] #1308 Fix group sorting and add new criteria (creation date, last event activity) --- lib/graphql/api/search.ex | 3 +- lib/graphql/schema/search.ex | 6 +++- lib/mobilizon/actors/actors.ex | 60 +++++++++++++++++++++++++++------- src/views/SearchView.vue | 43 +++++++++++++++++++----- 4 files changed, 91 insertions(+), 21 deletions(-) diff --git a/lib/graphql/api/search.ex b/lib/graphql/api/search.ex index eba18c20f..d7ef8da0b 100644 --- a/lib/graphql/api/search.ex +++ b/lib/graphql/api/search.ex @@ -57,7 +57,8 @@ defmodule Mobilizon.GraphQL.API.Search do current_actor_id: Map.get(args, :current_actor_id), exclude_my_groups: Map.get(args, :exclude_my_groups, false), exclude_stale_actors: true, - local_only: Map.get(args, :search_target, :internal) == :self + local_only: Map.get(args, :search_target, :internal) == :self, + sort_by: Map.get(args, :sort_by) ], page, limit diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index f518c13dc..e7742e9fe 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -171,7 +171,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do enum :search_group_sort_options do value(:match_desc, description: "The pertinence of the result") - value(:member_count_desc, description: "The members count of the group") + value(:member_count_asc, description: "The members count of the group ascendant order") + value(:member_count_desc, description: "The members count of the group descendent order") + value(:created_at_asc, description: "When the group was created ascendant order") + value(:created_at_desc, description: "When the group was created descendent order") + value(:last_event_activity, description: "Last event activity of the group") end enum :search_event_sort_options do diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index cc78c0807..1349b32f7 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -13,6 +13,7 @@ defmodule Mobilizon.Actors do alias Mobilizon.Actors.{Actor, Bot, Follower, Member} alias Mobilizon.Addresses.Address alias Mobilizon.Crypto + alias Mobilizon.Events.Event alias Mobilizon.Events.FeedToken alias Mobilizon.Medias alias Mobilizon.Service.Workers @@ -518,7 +519,6 @@ defmodule Mobilizon.Actors do query = from(a in Actor) query - |> distinct([q], q.id) |> actor_by_username_or_name_query(term) |> maybe_join_address( Keyword.get(options, :location), @@ -532,8 +532,56 @@ defmodule Mobilizon.Actors do |> filter_by_minimum_visibility(Keyword.get(options, :minimum_visibility, :public)) |> filter_suspended(false) |> filter_out_anonymous_actor_id(anonymous_actor_id) + # order_by + |> actor_order(Keyword.get(options, :sort_by, :match_desc)) end + # sort by most recent id if "best match" + defp actor_order(query, :match_desc) do + query + |> order_by([q], desc: q.id) + end + + defp actor_order(query, :last_event_activity) do + query + |> join(:left, [q], e in Event, on: e.attributed_to_id == q.id) + |> group_by([q, e], q.id) + |> order_by([q, e], [ + # put groups with no events at the end of the list + fragment("MAX(?) IS NULL", e.updated_at), + # last edited event of the group + desc: max(e.updated_at), + # sort group with no event by id + desc: q.id + ]) + end + + defp actor_order(query, :member_count_asc) do + query + |> join(:left, [q], m in Member, on: m.parent_id == q.id) + |> group_by([q, m], q.id) + |> order_by([q, m], asc: count(m.id), asc: q.id) + end + + defp actor_order(query, :member_count_desc) do + query + |> join(:left, [q], m in Member, on: m.parent_id == q.id) + |> group_by([q, m], q.id) + |> order_by([q, m], desc: count(m.id), desc: q.id) + end + + defp actor_order(query, :created_at_asc) do + query + |> order_by([q], asc: q.inserted_at) + end + + defp actor_order(query, :created_at_desc) do + query + |> order_by([q], desc: q.inserted_at) + end + + defp actor_order(query, _), do: query + @doc """ Gets a group by its title. """ @@ -1394,16 +1442,6 @@ defmodule Mobilizon.Actors do ^username ) ) - |> order_by( - [a], - fragment( - "word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc", - a.preferred_username, - ^username, - a.name, - ^username - ) - ) end @spec maybe_join_address( diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 7400abe15..7b7a1ea35 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -884,26 +884,35 @@ enum ViewMode { enum EventSortValues { MATCH_DESC = "MATCH_DESC", + CREATED_AT_ASC = "CREATED_AT_ASC", + CREATED_AT_DESC = "CREATED_AT_DESC", START_TIME_ASC = "START_TIME_ASC", START_TIME_DESC = "START_TIME_DESC", - CREATED_AT_DESC = "CREATED_AT_DESC", - CREATED_AT_ASC = "CREATED_AT_ASC", PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC", } enum GroupSortValues { MATCH_DESC = "MATCH_DESC", + CREATED_AT_ASC = "CREATED_AT_ASC", + CREATED_AT_DESC = "CREATED_AT_DESC", + MEMBER_COUNT_ASC = "MEMBER_COUNT_ASC", MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC", + LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", } enum SortValues { + //Common MATCH_DESC = "MATCH_DESC", - START_TIME_ASC = "START_TIME_ASC", - START_TIME_DESC = "START_TIME_DESC", CREATED_AT_DESC = "CREATED_AT_DESC", CREATED_AT_ASC = "CREATED_AT_ASC", + // EventSortValues + START_TIME_ASC = "START_TIME_ASC", + START_TIME_DESC = "START_TIME_DESC", PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC", + // GroupSortValues + MEMBER_COUNT_ASC = "MEMBER_COUNT_ASC", MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC", + LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", } const props = defineProps<{ @@ -1261,10 +1270,28 @@ const sortOptions = computed(() => { } if (contentType.value === ContentType.GROUPS) { - options.push({ - key: SortValues.MEMBER_COUNT_DESC, - label: t("Number of members"), - }); + options.push( + { + key: SortValues.MEMBER_COUNT_ASC, + label: t("Increasing number of members"), + }, + { + key: SortValues.MEMBER_COUNT_DESC, + label: t("Decreasing number of members"), + }, + { + key: SortValues.CREATED_AT_ASC, + label: t("Increasing creation date"), + }, + { + key: SortValues.CREATED_AT_DESC, + label: t("Decreasing creation date"), + }, + { + key: SortValues.LAST_EVENT_ACTIVITY, + label: t("Last event activity"), + } + ); } return options; From 057f0a3744f918af307567876f34ee3975a255b3 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 25 Oct 2024 16:43:37 +0200 Subject: [PATCH 12/78] #1545 Remove SearchTargets.GLOBAL from the front-end --- src/views/SearchView.vue | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 7b7a1ea35..55c67453c 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -110,21 +110,6 @@ >{{ t("In this instance's network") }}</label > </div> - <div> - <input - id="globalTarget" - v-model="searchTarget" - type="radio" - name="searchTarget" - :value="SearchTargets.GLOBAL" - class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600" - /> - <label - for="globalTarget" - class="ml-3 font-medium text-gray-900 dark:text-gray-300" - >{{ t("On the Fediverse") }}</label - > - </div> </fieldset> </div> @@ -1310,7 +1295,7 @@ const handleSearchConfigChanged = ( searchConfigChanged?.global?.isEnabled && searchConfigChanged?.global?.isDefault ) { - searchTarget.value = SearchTargets.GLOBAL; + searchTarget.value = SearchTargets.INTERNAL; } }; From ce2d4f44cb87d823c023bdeb88e781ca40dc6cbb Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 25 Oct 2024 17:23:01 +0200 Subject: [PATCH 13/78] #1572 Display when results are loading --- src/i18n/fr_FR.json | 3 ++- src/views/SearchView.vue | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 4cf9cdf11..6503079df 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -1569,7 +1569,8 @@ "{count} members or followers": "Aucun·e membre ou abonné·e|Un·e membre ou abonné·e|{count} membres ou abonné·es", "{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s", "{count} requests waiting": "Une demande en attente|{count} demandes en attente", - "{eventsCount} activities found": "Aucune activité trouvé|Une activité trouvé|{eventsCount} activités trouvés", + "Loading search results...": "Chargement des résultats...", + "{eventsCount} activities found": "Aucune activité trouvée|Une activité trouvée|{eventsCount} activités trouvées", "{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés", "{folder} - Resources": "{folder} - Ressources", "{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés", diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 55c67453c..eaa2e9223 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -438,7 +438,8 @@ id="results-anchor" class="hidden sm:flex items-center justify-between dark:text-slate-100 mb-2" > - <p v-if="totalCount === 0"> + <p v-if="searchLoading">{{ t("Loading search results...") }}</p> + <p v-else-if="totalCount === 0"> <span v-if=" contentType === ContentType.EVENTS || From 042b0f097f2ca8a8414e662eddcb24bad54961f3 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 28 Oct 2024 19:41:30 +0100 Subject: [PATCH 14/78] Fix #1476 by updating the search interface - Removed "Everything" choice for search, default to "Events" - Add radio button for search type selection (events, activities, groups) - Label now have a cursor pointer - Remove the confusing concept of EVENTS, SHORTEVENTS and LONGEVENTS. There is only EVENTS and LONGEVENTS. --- src/components/NavBar.vue | 13 +- src/types/enums.ts | 2 - src/views/SearchView.vue | 431 ++++++++++++-------------------------- 3 files changed, 133 insertions(+), 313 deletions(-) diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index fae37b5a8..31c1e966e 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -186,18 +186,7 @@ v-model:location="location" /> - <li class="m-auto" v-if="islongEvents"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'SHORTEVENTS' }, - }" - class="block 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("Events") }}</router-link - > - </li> - <li class="m-auto" v-else> + <li class="m-auto"> <router-link :to="{ ...$route, diff --git a/src/types/enums.ts b/src/types/enums.ts index 1c4cc3eab..c187bd4fc 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -132,9 +132,7 @@ export enum SearchTabs { } export enum ContentType { - ALL = "ALL", EVENTS = "EVENTS", - SHORTEVENTS = "SHORTEVENTS", LONGEVENTS = "LONGEVENTS", GROUPS = "GROUPS", } diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index eaa2e9223..37d242361 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -34,41 +34,36 @@ <li v-for="content in contentTypeMapping" :key="content.contentType" - class="flex gap-1" + class="flex gap-1 items-center" > - <Magnify - v-if="content.contentType === ContentType.ALL" - :size="24" + <input + :id="'contentType' + content.contentType" + v-model="contentType" + type="radio" + name="contentType" + :value="content.contentType" + class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600" /> - <Calendar - v-if="content.contentType === ContentType.EVENTS" - :size="24" - /> - - <Calendar - v-if="content.contentType === ContentType.SHORTEVENTS" - :size="24" - /> - - <CalendarStar - v-if="content.contentType === ContentType.LONGEVENTS" - :size="24" - /> - - <AccountMultiple - v-if="content.contentType === ContentType.GROUPS" - :size="24" - /> - - <router-link - :to="{ - ...$route, - query: { ...$route.query, contentType: content.contentType }, - }" + <label + :for="'contentType' + content.contentType" + class="cursor-pointer w-full font-medium text-gray-900 dark:text-gray-300 flex gap-1" + > + <Calendar + v-if="content.contentType === ContentType.EVENTS" + :size="24" + /> + + <CalendarStar + v-if="content.contentType === ContentType.LONGEVENTS" + :size="24" + /> + + <AccountMultiple + v-if="content.contentType === ContentType.GROUPS" + :size="24" + /><span>{{ content.label }}</span></label > - {{ content.label }} - </router-link> </li> </ul> @@ -90,7 +85,7 @@ /> <label for="selfTarget" - class="ml-3 font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300" >{{ t("From this instance only") }}</label > </div> @@ -106,7 +101,7 @@ /> <label for="internalTarget" - class="ml-3 font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 font-medium text-gray-900 dark:text-gray-300" >{{ t("In this instance's network") }}</label > </div> @@ -142,7 +137,7 @@ /> <label :for="key" - class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" >{{ eventStartDateRangeOption.label }}</label > </div> @@ -182,7 +177,7 @@ /> <label :for="distanceOption.id" - class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" >{{ distanceOption.label }}</label > </div> @@ -216,7 +211,7 @@ /> <label :for="category.id" - class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" >{{ category.label }}</label > </div> @@ -277,7 +272,7 @@ /> <label :for="eventStatusOption.id" - class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" >{{ eventStatusOption.label }}</label > </div> @@ -324,7 +319,7 @@ /> <label :for="key" - class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" + class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" >{{ language }}</label > </div> @@ -360,61 +355,6 @@ </template> </filter-section> - <!-- - - <div class=""> - <label v-translate class="font-bold" for="host">Mobilizon instance</label> - - <input - id="host" - v-model="formHost" - type="text" - name="host" - placeholder="mobilizon.fr" - class="dark:text-black md:max-w-fit w-full" - /> - </div> - - <div class=""> - <label v-translate class="inline font-bold" for="tagsAllOf">All of these tags</label> - <button - v-if="formTagsAllOf.length !== 0" - v-translate - class="text-sm ml-2" - @click="resetField('tagsAllOf')" - > - Reset - </button> - - <vue-tags-input - v-model="formTagAllOf" - :placeholder="tagsPlaceholder" - :tags="formTagsAllOf" - @tags-changed="(newTags) => (formTagsAllOf = newTags)" - /> - </div> - - <div> - <div> - <label v-translate class="inline font-bold" for="tagsOneOf">One of these tags</label> - <button - v-if="formTagsOneOf.length !== 0" - v-translate - class="text-sm ml-2" - @click="resetField('tagsOneOf')" - > - Reset - </button> - </div> - - <vue-tags-input - v-model="formTagOneOf" - :placeholder="tagsPlaceholder" - :tags="formTagsOneOf" - @tags-changed="(newTags) => (formTagsOneOf = newTags)" - /> - </div>--> - <div class="sr-only"> <button class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" @@ -440,13 +380,9 @@ > <p v-if="searchLoading">{{ t("Loading search results...") }}</p> <p v-else-if="totalCount === 0"> - <span - v-if=" - contentType === ContentType.EVENTS || - contentType === ContentType.SHORTEVENTS - " - >{{ t("No events found") }}</span - > + <span v-if="contentType === ContentType.EVENTS">{{ + t("No events found") + }}</span> <span v-else-if="contentType === ContentType.LONGEVENTS">{{ t("No activities found") }}</span> @@ -456,12 +392,7 @@ <span v-else>{{ t("No results found") }}</span> </p> <p v-else> - <span - v-if=" - contentType === ContentType.EVENTS || - contentType === ContentType.SHORTEVENTS - " - > + <span v-if="contentType === ContentType.EVENTS"> {{ t( "{eventsCount} events found", @@ -503,12 +434,27 @@ t("Sort by") }}</label> <o-select - :placeholder="t('Sort by')" - v-model="sortBy" - id="sortOptionSelect" + v-if="contentType !== ContentType.GROUPS" + :placeholder="t('Sort by events')" + v-model="sortByEvents" + id="sortOptionSelectEvents" > <option - v-for="sortOption in sortOptions" + v-for="sortOption in sortOptionsEvents" + :key="sortOption.key" + :value="sortOption.key" + > + {{ sortOption.label }} + </option> + </o-select> + <o-select + v-if="contentType === ContentType.GROUPS" + :placeholder="t('Sort by groups')" + v-model="sortByGroups" + id="sortOptionSelectGroups" + > + <option + v-for="sortOption in sortOptionsGroups" :key="sortOption.key" :value="sortOption.key" > @@ -533,92 +479,9 @@ </div> </div> <div v-if="mode === ViewMode.LIST"> - <template v-if="contentType === ContentType.ALL"> - <template v-if="searchLoading"> - <SkeletonGroupResultList v-for="i in 2" :key="i" /> - <SkeletonEventResultList v-for="i in 4" :key="i" /> - </template> - <o-notification v-if="features && !features.groups" variant="danger"> - {{ t("Groups are not enabled on this instance.") }} - </o-notification> - <div v-else-if="searchGroups && searchGroups?.total > 0"> - <GroupCard - class="my-2" - v-for="group in searchGroups?.elements" - :group="group" - :key="group.id" - :isRemoteGroup="group.__typename === 'GroupResult'" - :isLoggedIn="currentUser?.isLoggedIn" - mode="row" - /> - </div> - <div v-if="searchEvents && searchEvents.total > 0"> - <event-card - mode="row" - v-for="event in searchEvents?.elements" - :event="event" - :key="event.uuid" - :options="{ - isRemoteEvent: event.__typename === 'EventResult', - isLoggedIn: currentUser?.isLoggedIn, - }" - class="my-4" - /> - </div> - <EmptyContent v-else-if="searchLoading === false" icon="magnify"> - <span v-if="searchIsUrl"> - {{ t("No event found at this address") }} - </span> - <span v-else-if="!search"> - {{ t("No results found") }} - </span> - <i18n-t keypath="No results found for {search}" tag="span" v-else> - <template #search> - <b class="">{{ search }}</b> - </template> - </i18n-t> - <template #desc v-if="searchIsUrl && !currentUser?.id"> - {{ - t( - "Only registered users may fetch remote events from their URL." - ) - }} - </template> - <template #desc v-else> - <p class="my-2 text-start"> - {{ t("Suggestions:") }} - </p> - <ul class="list-disc list-inside text-start"> - <li> - {{ t("Make sure that all words are spelled correctly.") }} - </li> - <li>{{ t("Try different keywords.") }}</li> - <li>{{ t("Try more general keywords.") }}</li> - <li>{{ t("Try fewer keywords.") }}</li> - <li>{{ t("Change the filters.") }}</li> - </ul> - </template> - </EmptyContent> - <o-pagination - v-if=" - (searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT) || - (searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT) - " - :total=" - Math.max(searchEvents?.total ?? 0, searchGroups?.total ?? 0) - " - v-model:current="page" - :per-page="EVENT_PAGE_LIMIT" - :aria-next-label="t('Next page')" - :aria-previous-label="t('Previous page')" - :aria-page-label="t('Page')" - :aria-current-label="t('Current page')" - /> - </template> <template - v-else-if=" + v-if=" contentType === ContentType.EVENTS || - contentType === ContentType.SHORTEVENTS || contentType === ContentType.LONGEVENTS " > @@ -803,7 +666,6 @@ import { import Calendar from "vue-material-design-icons/Calendar.vue"; import CalendarStar from "vue-material-design-icons/CalendarStar.vue"; import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue"; -import Magnify from "vue-material-design-icons/Magnify.vue"; import { useHead } from "@/utils/head"; import type { Locale } from "date-fns"; @@ -886,27 +748,11 @@ enum GroupSortValues { LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", } -enum SortValues { - //Common - MATCH_DESC = "MATCH_DESC", - CREATED_AT_DESC = "CREATED_AT_DESC", - CREATED_AT_ASC = "CREATED_AT_ASC", - // EventSortValues - START_TIME_ASC = "START_TIME_ASC", - START_TIME_DESC = "START_TIME_DESC", - PARTICIPANT_COUNT_DESC = "PARTICIPANT_COUNT_DESC", - // GroupSortValues - MEMBER_COUNT_ASC = "MEMBER_COUNT_ASC", - MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC", - LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", -} - const props = defineProps<{ tag?: string; }>(); const tag = computed(() => props.tag); -const page = useRouteQuery("page", 1, integerTransformer); const eventPage = useRouteQuery("eventPage", 1, integerTransformer); const groupPage = useRouteQuery("groupPage", 1, integerTransformer); @@ -920,10 +766,9 @@ const distance = useRouteQuery("distance", "10_km"); const when = useRouteQuery("when", "any"); const contentType = useRouteQuery( "contentType", - tag.value ? ContentType.EVENTS : ContentType.ALL, + ContentType.EVENTS, enumTransformer(ContentType) ); - const isOnline = useRouteQuery("isOnline", false, booleanTransformer); const categoryOneOf = useRouteQuery("categoryOneOf", [], arrayTransformer); const statusOneOf = useRouteQuery( @@ -932,16 +777,22 @@ const statusOneOf = useRouteQuery( arrayTransformer ); const languageOneOf = useRouteQuery("languageOneOf", [], arrayTransformer); + const searchTarget = useRouteQuery( "target", SearchTargets.INTERNAL, enumTransformer(SearchTargets) ); const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode)); -const sortBy = useRouteQuery( - "sortBy", - SortValues.START_TIME_ASC, - enumTransformer(SortValues) +const sortByEvents = useRouteQuery( + "sortByEvents", + EventSortValues.MATCH_DESC, + enumTransformer(EventSortValues) +); +const sortByGroups = useRouteQuery( + "sortByGroups", + GroupSortValues.MATCH_DESC, + enumTransformer(GroupSortValues) ); const bbox = useRouteQuery("bbox", undefined); const zoom = useRouteQuery("zoom", undefined, integerTransformer); @@ -1069,34 +920,26 @@ const contentTypeMapping = computed(() => { if (islongEvents.value) { return [ { - contentType: "ALL", - label: t("Everything"), - }, - { - contentType: "SHORTEVENTS", + contentType: ContentType.EVENTS, label: t("Events"), }, { - contentType: "LONGEVENTS", + contentType: ContentType.LONGEVENTS, label: t("Activities"), }, { - contentType: "GROUPS", + contentType: ContentType.GROUPS, label: t("Groups"), }, ]; } else { return [ { - contentType: "ALL", - label: t("Everything"), - }, - { - contentType: "EVENTS", + contentType: ContentType.EVENTS, label: t("Events"), }, { - contentType: "GROUPS", + contentType: ContentType.GROUPS, label: t("Groups"), }, ]; @@ -1209,7 +1052,7 @@ const geoHashLocation = computed(() => const radius = computed(() => Number.parseInt(distance.value.slice(0, -3))); const longEvents = computed(() => { - if (contentType.value === ContentType.SHORTEVENTS) { + if (contentType.value === ContentType.EVENTS) { return false; } else if (contentType.value === ContentType.LONGEVENTS) { return true; @@ -1222,63 +1065,60 @@ const totalCount = computed(() => { return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0); }); -const sortOptions = computed(() => { +const sortOptionsGroups = computed(() => { const options = [ { - key: SortValues.MATCH_DESC, + key: GroupSortValues.MATCH_DESC, label: t("Best match"), }, + { + key: GroupSortValues.MEMBER_COUNT_ASC, + label: t("Increasing number of members"), + }, + { + key: GroupSortValues.MEMBER_COUNT_DESC, + label: t("Decreasing number of members"), + }, + { + key: GroupSortValues.CREATED_AT_ASC, + label: t("Increasing creation date"), + }, + { + key: GroupSortValues.CREATED_AT_DESC, + label: t("Decreasing creation date"), + }, + { + key: GroupSortValues.LAST_EVENT_ACTIVITY, + label: t("Last event activity"), + }, ]; - if ( - contentType.value === ContentType.EVENTS || - contentType.value === ContentType.SHORTEVENTS || - contentType.value === ContentType.LONGEVENTS - ) { - options.push( - { - key: SortValues.START_TIME_ASC, - label: t("Event date"), - }, - { - key: SortValues.CREATED_AT_DESC, - label: t("Most recently published"), - }, - { - key: SortValues.CREATED_AT_ASC, - label: t("Least recently published"), - }, - { - key: SortValues.PARTICIPANT_COUNT_DESC, - label: t("With the most participants"), - } - ); - } + return options; +}); - if (contentType.value === ContentType.GROUPS) { - options.push( - { - key: SortValues.MEMBER_COUNT_ASC, - label: t("Increasing number of members"), - }, - { - key: SortValues.MEMBER_COUNT_DESC, - label: t("Decreasing number of members"), - }, - { - key: SortValues.CREATED_AT_ASC, - label: t("Increasing creation date"), - }, - { - key: SortValues.CREATED_AT_DESC, - label: t("Decreasing creation date"), - }, - { - key: SortValues.LAST_EVENT_ACTIVITY, - label: t("Last event activity"), - } - ); - } +const sortOptionsEvents = computed(() => { + const options = [ + { + key: EventSortValues.MATCH_DESC, + label: t("Best match"), + }, + { + key: EventSortValues.START_TIME_ASC, + label: t("Event date"), + }, + { + key: EventSortValues.CREATED_AT_DESC, + label: t("Most recently published"), + }, + { + key: EventSortValues.CREATED_AT_ASC, + label: t("Least recently published"), + }, + { + key: EventSortValues.PARTICIPANT_COUNT_DESC, + label: t("With the most participants"), + }, + ]; return options; }); @@ -1334,11 +1174,11 @@ watch(isOnline, (newIsOnline) => { }); const sortByForType = ( - value: SortValues, - allowed: typeof EventSortValues | typeof GroupSortValues -): SortValues | undefined => { - if (value === SortValues.START_TIME_ASC && when.value === "past") { - value = SortValues.START_TIME_DESC; + value: EventSortValues, + allowed: typeof EventSortValues +): EventSortValues | undefined => { + if (value === EventSortValues.START_TIME_ASC && when.value === "past") { + value = EventSortValues.START_TIME_DESC; } return Object.values(allowed).includes(value) ? value : undefined; }; @@ -1373,20 +1213,15 @@ watch( searchTarget, bbox, zoom, - sortBy, + sortByEvents, + sortByGroups, boostLanguagesQuery, ], ([newContentType]) => { switch (newContentType) { - case ContentType.ALL: - page.value = 1; - break; case ContentType.EVENTS: eventPage.value = 1; break; - case ContentType.SHORTEVENTS: - eventPage.value = 1; - break; case ContentType.LONGEVENTS: eventPage.value = 1; break; @@ -1408,10 +1243,8 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{ endsOn: end.value, longevents: longEvents.value, radius: geoHashLocation.value ? radius.value : undefined, - eventPage: - contentType.value === ContentType.ALL ? page.value : eventPage.value, - groupPage: - contentType.value === ContentType.ALL ? page.value : groupPage.value, + eventPage: eventPage.value, + groupPage: groupPage.value, limit: EVENT_PAGE_LIMIT, type: isOnline.value ? "ONLINE" : undefined, categoryOneOf: categoryOneOf.value, @@ -1420,8 +1253,8 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{ searchTarget: searchTarget.value, bbox: mode.value === ViewMode.MAP ? bbox.value : undefined, zoom: zoom.value, - sortByEvents: sortByForType(sortBy.value, EventSortValues), - sortByGroups: sortByForType(sortBy.value, GroupSortValues), + sortByEvents: sortByForType(sortByEvents.value, EventSortValues), + sortByGroups: sortByGroups.value, boostLanguages: boostLanguagesQuery.value, })); </script> From 9a04041694ca9a07847188dab8df63f88b292a2a Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 28 Oct 2024 19:42:41 +0100 Subject: [PATCH 15/78] Update French translation --- src/i18n/fr_FR.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 6503079df..870b4bead 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -305,6 +305,8 @@ "Deactivate notifications": "Désactiver les notifications", "Decline": "Refuser", "Decrease": "Baisser", + "Decreasing creation date": "Date de création décroissante", + "Decreasing number of members": "Nombre décroissant de membres", "Default": "Défaut", "Default Mobilizon privacy policy": "Politique de confidentialité par défaut de Mobilizon", "Default Mobilizon terms": "Conditions d'utilisation par défaut de Mobilizon", @@ -565,6 +567,8 @@ "In the past": "Dans le passé", "In this instance's network": "Dans le réseau de cette instance", "Increase": "Augmenter", + "Increasing creation date": "Date de création croissante", + "Increasing number of members": "Nombre croissant de membres", "Instance": "Instance", "Instance Long Description": "Description longue de l'instance", "Instance Name": "Nom de l'instance", @@ -605,6 +609,7 @@ "Keyword, event title, group name, etc.": "Mot clé, titre d'un événement, nom d'un groupe, etc.", "Language": "Langue", "Languages": "Langues", + "Last event activity": "Dernière activité événementielle", "Last IP adress": "Dernière adresse IP", "Last group created": "Dernier groupe créé", "Last published event": "Dernier événement publié", @@ -635,6 +640,7 @@ "Load more activities": "Charger plus d'activités", "Loading comments…": "Chargement des commentaires…", "Loading map": "Chargement de la carte", + "Loading search results...": "Chargement des résultats...", "Local": "Local·e", "Local time ({timezone})": "Heure locale ({timezone})", "Local times ({timezone})": "Heures locales ({timezone})", @@ -1569,7 +1575,6 @@ "{count} members or followers": "Aucun·e membre ou abonné·e|Un·e membre ou abonné·e|{count} membres ou abonné·es", "{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s", "{count} requests waiting": "Une demande en attente|{count} demandes en attente", - "Loading search results...": "Chargement des résultats...", "{eventsCount} activities found": "Aucune activité trouvée|Une activité trouvée|{eventsCount} activités trouvées", "{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés", "{folder} - Resources": "{folder} - Ressources", From 65ad3d855bb2ce18c30523043752fb6fd05ce7b1 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 30 Oct 2024 17:29:24 +0100 Subject: [PATCH 16/78] #1492 : add frame to select distance from searching events --- .../Event/FullAddressAutoComplete.vue | 1 + src/components/Home/SearchFields.vue | 133 +++++++++++++++++- src/components/Local/CloseEvents.vue | 7 +- src/components/core/MaterialIcon.vue | 4 + src/views/HomeView.vue | 17 +++ 5 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/components/Event/FullAddressAutoComplete.vue b/src/components/Event/FullAddressAutoComplete.vue index 48185b32c..83793a46f 100644 --- a/src/components/Event/FullAddressAutoComplete.vue +++ b/src/components/Event/FullAddressAutoComplete.vue @@ -63,6 +63,7 @@ </template> </template> </o-autocomplete> + <slot></slot> <o-button :disabled="!queryTextWithDefault" @click="resetAddress" diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index c20315bb2..8739cecc3 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -28,12 +28,37 @@ labelClass="sr-only" :placeholder="t('e.g. Nantes, Berlin, Cork, …')" v-on:update:modelValue="modelValueUpdate" - /> + > + <o-button + v-if="distance" + class="!h-auto" + icon-left="map-marker-distance" + :title="t('Select distance')" + @click="showDistance = !showDistance" + /> + </full-address-auto-complete> <o-button native-type="submit" icon-left="magnify"> <template v-if="search">{{ t("Go!") }}</template> <template v-else>{{ t("Explore!") }}</template> </o-button> </form> + <div + class="border-black border-2 bg-white pt-1 pb-1 w-64 mx-auto" + v-if="showDistance" + > + <span class="mx-2 font-medium text-gray-900 dark:text-slate-100 text-left"> + {{ t("Distance") }} + </span> + <select v-model="distance"> + <option + v-for="distance_item in distanceList" + :value="distance_item.distance" + :key="distance_item.distance" + > + {{ distance_item.label }} + </option> + </select> + </div> </template> <script lang="ts" setup> @@ -44,7 +69,7 @@ import { getLocationFromLocal, storeLocationInLocal, } from "@/utils/location"; -import { computed, defineAsyncComponent } from "vue"; +import { computed, defineAsyncComponent, ref } from "vue"; import { useI18n } from "vue-i18n"; import { useRouter, useRoute } from "vue-router"; import RouteName from "@/router/name"; @@ -57,6 +82,7 @@ const props = defineProps<{ location: IAddress | null; locationDefaultText?: string | null; search: string; + distance: number | null; fromLocalStorage?: boolean | false; }>(); @@ -66,6 +92,7 @@ const route = useRoute(); const emit = defineEmits<{ (event: "update:location", location: IAddress | null): void; (event: "update:search", newSearch: string): void; + (event: "update:distance", newDistance: number): void; (event: "submit"): void; }>(); @@ -96,21 +123,113 @@ const search = computed({ }, }); +const showDistance = ref(false); + +const distance = computed({ + get(): number { + return props.distance; + }, + set(newDistance: number) { + emit("update:distance", newDistance); + showDistance.value = false; + }, +}); + +const distanceList = computed(() => { + return [ + { + distance: 5, + label: t( + "{number} kilometers", + { + number: 5, + }, + 5 + ), + }, + { + distance: 10, + label: t( + "{number} kilometers", + { + number: 10, + }, + 10 + ), + }, + { + distance: 25, + label: t( + "{number} kilometers", + { + number: 25, + }, + 25 + ), + }, + { + distance: 50, + label: t( + "{number} kilometers", + { + number: 50, + }, + 50 + ), + }, + { + distance: 100, + label: t( + "{number} kilometers", + { + number: 100, + }, + 100 + ), + }, + { + distance: 150, + label: t( + "{number} kilometers", + { + number: 150, + }, + 150 + ), + }, + ]; +}); + +console.debug("initial", distance.value, search.value, location.value); + const modelValueUpdate = (newlocation: IAddress | null) => { emit("update:location", newlocation); }; const submit = () => { emit("submit"); - const { lat, lon } = addressToLocation(location.value); + const search_query = { + locationName: undefined, + lat: undefined, + lon: undefined, + search: search.value, + distance: undefined, + }; + if (distance.value != null) { + search_query.distance = distance.value.toString() + "_km"; + } + if (location.value) { + const { lat, lon } = addressToLocation(location.value); + search_query.locationName = + location.value.locality ?? location.value.region; + search_query.lat = lat; + search_query.lon = lon; + } router.push({ name: RouteName.SEARCH, query: { ...route.query, - locationName: location.value?.locality ?? location.value?.region, - lat, - lon, - search: search.value, + ...search_query, }, }); }; diff --git a/src/components/Local/CloseEvents.vue b/src/components/Local/CloseEvents.vue index f6cdfb580..03437d43b 100644 --- a/src/components/Local/CloseEvents.vue +++ b/src/components/Local/CloseEvents.vue @@ -97,6 +97,7 @@ import { EventSortField, SortDirection } from "@/types/enums"; const props = defineProps<{ userLocation: LocationType; doingGeoloc?: boolean; + distance: number | null; }>(); defineEmits(["doGeoLoc"]); @@ -112,9 +113,9 @@ const geoHash = computed(() => { return geo; }); -const distance = computed<number>(() => - userLocation.value?.isIPLocation ? 150 : 25 -); +const distance = computed<number>(() => { + return props.distance | 25; +}); const eventsQuery = useQuery<{ searchEvents: Paginate<IEvent>; diff --git a/src/components/core/MaterialIcon.vue b/src/components/core/MaterialIcon.vue index 7d6d4ed63..22a3b4438 100644 --- a/src/components/core/MaterialIcon.vue +++ b/src/components/core/MaterialIcon.vue @@ -111,6 +111,10 @@ const icons: Record<string, () => Promise<any>> = { Map: () => import(`../../../node_modules/vue-material-design-icons/Map.vue`), MapMarker: () => import(`../../../node_modules/vue-material-design-icons/MapMarker.vue`), + MapMarkerDistance: () => + import( + `../../../node_modules/vue-material-design-icons/MapMarkerDistance.vue` + ), Close: () => import(`../../../node_modules/vue-material-design-icons/Close.vue`), Magnify: () => diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 5c45847c5..eb65cfad7 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -29,6 +29,7 @@ <search-fields v-model:search="search" v-model:location="location" + v-model:distance="distance" :locationDefaultText="location?.description ?? userLocation?.name" v-on:update:location="updateLocation" :fromLocalStorage="true" @@ -146,6 +147,7 @@ @doGeoLoc="performGeoLocation()" :userLocation="userLocation" :doingGeoloc="doingGeoloc" + :distance="distance" /> </template> @@ -239,6 +241,7 @@ const currentUserParticipations = computed( const location = ref(null); const search = ref(""); const noLocation = ref(false); +const current_distance = ref(null); watch(location, (newLoc, oldLoc) => console.debug("LOCATION UPDATED from", { ...oldLoc }, " to ", { ...newLoc }) @@ -451,6 +454,20 @@ const userLocation = computed(() => { return userSettingsLocation.value; }); +const distance = computed({ + get(): number | null { + if (noLocation.value) { + return null; + } else if (current_distance.value == null) { + return userLocation.value?.isIPLocation ? 150 : 25; + } + return current_distance.value; + }, + set(newDistance: number) { + current_distance.value = newDistance; + }, +}); + const { mutate: saveCurrentUserLocation } = useMutation<any, LocationType>( UPDATE_CURRENT_USER_LOCATION_CLIENT ); From 906985478dd6fa5b83d4aa77d8ecf897847f1371 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 30 Oct 2024 17:06:24 +0100 Subject: [PATCH 17/78] #1546 Remove private section mbz-purple color --- src/components/Group/GroupSection.vue | 11 ++--------- src/components/Group/Sections/EventsSection.vue | 1 - src/components/Group/Sections/PostsSection.vue | 1 - .../unit/specs/components/Group/GroupSection.spec.ts | 10 +--------- 4 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/components/Group/GroupSection.vue b/src/components/Group/GroupSection.vue index 038409c04..c5db0c99f 100644 --- a/src/components/Group/GroupSection.vue +++ b/src/components/Group/GroupSection.vue @@ -1,11 +1,5 @@ <template> - <section - class="flex flex-col mb-3 border-2" - :class="{ - 'border-mbz-purple': privateSection, - 'border-yellow-1': !privateSection, - }" - > + <section class="flex flex-col mb-3 border-2 border-yellow-1"> <div class="flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title"> <div class="flex flex-1 gap-1"> <o-icon :icon="icon" custom-size="36" /> @@ -31,10 +25,9 @@ withDefaults( defineProps<{ title: string; icon: string; - privateSection?: boolean; route: { name: string; params: { preferredUsername: string } }; }>(), - { privateSection: true } + {} ); const { t } = useI18n({ useScope: "global" }); </script> diff --git a/src/components/Group/Sections/EventsSection.vue b/src/components/Group/Sections/EventsSection.vue index 3aa84cf07..4f65f8af5 100644 --- a/src/components/Group/Sections/EventsSection.vue +++ b/src/components/Group/Sections/EventsSection.vue @@ -2,7 +2,6 @@ <group-section :title="t('Events')" icon="calendar" - :privateSection="false" :route="{ name: RouteName.GROUP_EVENTS, params: { preferredUsername: usernameWithDomain(group) }, diff --git a/src/components/Group/Sections/PostsSection.vue b/src/components/Group/Sections/PostsSection.vue index 3ee6812f8..12f5c9631 100644 --- a/src/components/Group/Sections/PostsSection.vue +++ b/src/components/Group/Sections/PostsSection.vue @@ -2,7 +2,6 @@ <group-section :title="t('Announcements')" icon="bullhorn" - :privateSection="false" :route="{ name: RouteName.POSTS, params: { preferredUsername: usernameWithDomain(group) }, diff --git a/tests/unit/specs/components/Group/GroupSection.spec.ts b/tests/unit/specs/components/Group/GroupSection.spec.ts index 73b1a06bd..06d47ca6d 100644 --- a/tests/unit/specs/components/Group/GroupSection.spec.ts +++ b/tests/unit/specs/components/Group/GroupSection.spec.ts @@ -29,7 +29,6 @@ const createSlotButtonText = "+ Create a post"; type Props = { title?: string; icon?: string; - privateSection?: boolean; route?: { name: string; params: { preferredUsername: string } }; }; @@ -73,10 +72,6 @@ describe("GroupSection", () => { expect(wrapper.find("a").attributes("href")).toBe(`/@${groupUsername}/p`); - // expect(wrapper.find(".group-section-title").classes("privateSection")).toBe( - // true - // ); - expect(wrapper.find("section > div.flex-1").text()).toBe(defaultSlotText); expect(wrapper.find(".flex.justify-end.p-2 a").text()).toBe( createSlotButtonText @@ -88,11 +83,8 @@ describe("GroupSection", () => { }); it("renders public group section", () => { - const wrapper = generateWrapper({ privateSection: false }); + const wrapper = generateWrapper(); - // expect(wrapper.find(".group-section-title").classes("privateSection")).toBe( - // false - // ); expect(wrapper.html()).toMatchSnapshot(); }); }); From 861c445b636c7085090b5835937e668931f3eb3b Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 30 Oct 2024 19:59:30 +0100 Subject: [PATCH 18/78] #1546 Harmonization between public and private view + improved CSS --- src/components/Group/GroupSection.vue | 8 +- .../Group/Sections/EventsSection.vue | 13 +- .../Group/Sections/PostsSection.vue | 18 +- src/components/Post/MultiPostListItem.vue | 2 +- src/components/core/MaterialIcon.vue | 4 + src/i18n/fr_FR.json | 3 + src/views/Group/GroupView.vue | 476 +++++++----------- 7 files changed, 231 insertions(+), 293 deletions(-) diff --git a/src/components/Group/GroupSection.vue b/src/components/Group/GroupSection.vue index c5db0c99f..9c4c05de9 100644 --- a/src/components/Group/GroupSection.vue +++ b/src/components/Group/GroupSection.vue @@ -1,15 +1,15 @@ <template> - <section class="flex flex-col mb-3 border-2 border-yellow-1"> + <section class="flex flex-col border-2 border-yellow-1 rounded-lg"> <div class="flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title"> <div class="flex flex-1 gap-1"> <o-icon :icon="icon" custom-size="36" /> <h2 class="text-2xl font-medium mt-0">{{ title }}</h2> </div> - <router-link class="self-center" :to="route">{{ + <router-link v-if="route" class="self-center" :to="route">{{ t("View all") }}</router-link> </div> - <div class="flex-1"> + <div class="flex-1 min-h-40"> <slot></slot> </div> <div class="flex justify-end p-2"> @@ -27,7 +27,7 @@ withDefaults( icon: string; route: { name: string; params: { preferredUsername: string } }; }>(), - {} + { route: undefined } ); const { t } = useI18n({ useScope: "global" }); </script> diff --git a/src/components/Group/Sections/EventsSection.vue b/src/components/Group/Sections/EventsSection.vue index 4f65f8af5..a1572677c 100644 --- a/src/components/Group/Sections/EventsSection.vue +++ b/src/components/Group/Sections/EventsSection.vue @@ -9,7 +9,7 @@ > <template #default> <div - class="flex flex-wrap gap-2 py-1" + class="flex flex-wrap gap-2 p-2" v-if="group && group.organizedEvents.total > 0" > <event-minimalist-card @@ -24,6 +24,17 @@ <!-- <o-skeleton animated v-else></o-skeleton> --> </template> <template #create> + <o-button + tag="router-link" + class="button" + variant="text" + :to="{ + name: RouteName.GROUP_EVENTS, + params: { preferredUsername: usernameWithDomain(group) }, + query: { showPassedEvents: true }, + }" + >{{ t("View past events") }}</o-button + > <o-button tag="router-link" v-if="isModerator" diff --git a/src/components/Group/Sections/PostsSection.vue b/src/components/Group/Sections/PostsSection.vue index 12f5c9631..4f2ac7f6e 100644 --- a/src/components/Group/Sections/PostsSection.vue +++ b/src/components/Group/Sections/PostsSection.vue @@ -8,9 +8,22 @@ }" > <template #default> - <div class="p-1"> + <div class="p-2"> <multi-post-list-item - v-if="group?.posts?.total ?? 0 > 0" + v-if=" + !isMember && + group?.posts.elements.filter( + (post) => !post.draft && post.visibility === PostVisibility.PUBLIC + ).length > 0 + " + :posts=" + group?.posts.elements.filter( + (post) => !post.draft && post.visibility === PostVisibility.PUBLIC + ) + " + /> + <multi-post-list-item + v-else-if="group?.posts?.total ?? 0 > 0" :posts="(group?.posts?.elements ?? []).slice(0, 3)" :isCurrentActorMember="isMember" /> @@ -42,6 +55,7 @@ import { useI18n } from "vue-i18n"; import EmptyContent from "@/components/Utils/EmptyContent.vue"; import MultiPostListItem from "@/components/Post/MultiPostListItem.vue"; import GroupSection from "@/components/Group/GroupSection.vue"; +import { PostVisibility } from "@/types/enums"; const { t } = useI18n({ useScope: "global" }); diff --git a/src/components/Post/MultiPostListItem.vue b/src/components/Post/MultiPostListItem.vue index c57604853..41e003b16 100644 --- a/src/components/Post/MultiPostListItem.vue +++ b/src/components/Post/MultiPostListItem.vue @@ -1,5 +1,5 @@ <template> - <div class="posts-wrapper grid gap-4"> + <div class="posts-wrapper grid gap-2"> <post-list-item v-for="post in posts" :key="post.id" diff --git a/src/components/core/MaterialIcon.vue b/src/components/core/MaterialIcon.vue index 22a3b4438..390eb0fc0 100644 --- a/src/components/core/MaterialIcon.vue +++ b/src/components/core/MaterialIcon.vue @@ -29,6 +29,10 @@ const icons: Record<string, () => Promise<any>> = { import(`../../../node_modules/vue-material-design-icons/LinkOff.vue`), Image: () => import(`../../../node_modules/vue-material-design-icons/Image.vue`), + Information: () => + import( + `../../../node_modules/vue-material-design-icons/InformationVariant.vue` + ), FormatListBulleted: () => import( `../../../node_modules/vue-material-design-icons/FormatListBulleted.vue` diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 870b4bead..7a8edd8d7 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -638,6 +638,7 @@ "Live": "Direct", "Load more": "Voir plus", "Load more activities": "Charger plus d'activités", + "Loading…": "Chargement…", "Loading comments…": "Chargement des commentaires…", "Loading map": "Chargement de la carte", "Loading search results...": "Chargement des résultats...", @@ -726,6 +727,7 @@ "Next month": "Le mois-prochain", "Next page": "Page suivante", "Next week": "La semaine prochaine", + "No about content yet":"À propos n'est pas encore renseigné", "No activities found": "Aucun activité trouvé", "No address defined": "Aucune adresse définie", "No apps authorized yet": "Aucune application autorisée pour le moment", @@ -753,6 +755,7 @@ "No instance to remove|Remove instance|Remove {number} instances": "Pas d'instances à supprimer|Supprimer une instance|Supprimer {number} instances", "No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?", "No languages found": "Aucune langue trouvée", + "No location yet":"Localisation non renseignée", "No member matches the filters": "Aucun·e membre ne correspond aux filtres", "No members found": "Aucun·e membre trouvé·e", "No memberships found": "Aucune adhésion trouvée", diff --git a/src/views/Group/GroupView.vue b/src/views/Group/GroupView.vue index ccc8ffdbf..f32f8268c 100644 --- a/src/views/Group/GroupView.vue +++ b/src/views/Group/GroupView.vue @@ -1,8 +1,13 @@ <template> <div class="container mx-auto is-widescreen"> - <div class="header flex flex-col"> + <o-notification v-if="groupLoading" variant="info"> + {{ t("Loading…") }} + </o-notification> + <o-notification v-if="!group && groupLoading === false" variant="danger"> + {{ t("No group found") }} + </o-notification> + <div class="header flex flex-col" v-if="group"> <breadcrumbs-nav - v-if="group" :links="[ { name: RouteName.MY_GROUPS, text: t('My groups') }, { @@ -12,8 +17,7 @@ }, ]" /> - <!-- <o-loading v-model:active="$apollo.loading"></o-loading> --> - <header class="block-container presentation" v-if="group"> + <header class="block-container presentation"> <div class="banner-container"> <lazy-image-wrapper :picture="group.banner" /> </div> @@ -31,64 +35,14 @@ <AccountGroup v-else :size="128" /> </div> <div class="title-container flex flex-1 flex-col text-center"> - <h1 class="m-0" v-if="group.name"> + <h1 class="m-1" v-if="group.name"> {{ group.name }} </h1> - <!-- <o-skeleton v-else :animated="true" /> --> - <span dir="ltr" class="" v-if="group.preferredUsername" + <span dir="ltr" class="m-1" v-if="group.preferredUsername" >@{{ usernameWithDomain(group) }}</span > - <!-- <o-skeleton v-else :animated="true" /> --> - <br /> </div> <div class="flex flex-wrap justify-center flex-col md:flex-row"> - <div - class="flex flex-col items-center flex-1 m-0" - v-if="isCurrentActorAGroupMember && !previewPublic && members" - > - <div class="flex"> - <figure - :title=" - t(`{'@'}{username} ({role})`, { - username: usernameWithDomain(member.actor), - role: member.role, - }) - " - v-for="member in members.elements" - :key="member.actor.id" - class="-mr-3" - > - <img - class="rounded-full h-8" - :src="member.actor.avatar.url" - v-if="member.actor.avatar" - alt="" - width="32" - height="32" - /> - <AccountCircle v-else :size="32" /> - </figure> - </div> - <p> - {{ - t( - "{count} members", - { - count: group.members?.total, - }, - group.members?.total - ) - }} - <router-link - v-if="isCurrentActorAGroupAdmin" - :to="{ - name: RouteName.GROUP_MEMBERS_SETTINGS, - params: { preferredUsername: usernameWithDomain(group) }, - }" - >{{ t("Add / Remove…") }}</router-link - > - </p> - </div> <div class="flex flex-wrap gap-3 justify-center"> <o-button outlined @@ -375,12 +329,14 @@ :invitations="[groupMember]" /> <o-notification + class="my-2" v-if="isCurrentActorARejectedGroupMember" variant="danger" > {{ t("You have been removed from this group's members.") }} </o-notification> <o-notification + class="my-2" v-if=" isCurrentActorAGroupMember && isCurrentActorARecentMember && @@ -394,47 +350,11 @@ ) }} </o-notification> - </div> - </header> - </div> - <div - v-if="isCurrentActorAGroupMember && !previewPublic && group" - class="block-container flex gap-2 flex-wrap mt-3" - > - <!-- Private things --> - <div class="flex-1 m-0 flex flex-col flex-wrap gap-2"> - <!-- Group discussions --> - <Discussions :group="discussionGroup ?? group" class="flex-1" /> - <!-- Resources --> - <Resources :group="resourcesGroup ?? group" class="flex-1" /> - </div> - <!-- Public things --> - <div class="flex-1 m-0 flex flex-col flex-wrap gap-2"> - <!-- Events --> - <Events - :group="group" - :isModerator="isCurrentActorAGroupModerator" - class="flex-1" - /> - <!-- Posts --> - <Posts - :group="group" - :isModerator="isCurrentActorAGroupModerator" - :isMember="isCurrentActorAGroupMember" - class="flex-1" - /> - </div> - </div> - <o-notification - v-else-if="!group && groupLoading === false" - variant="danger" - > - {{ t("No group found") }} - </o-notification> - <div v-else-if="group" class="public-container flex flex-col"> - <aside class="group-metadata"> - <div class="sticky"> - <o-notification v-if="group.domain && !isCurrentActorAGroupMember"> + <o-notification + class="my-2" + v-if="group && group.domain && !isCurrentActorAGroupMember" + variant="info" + > <p> {{ t( @@ -450,173 +370,196 @@ >{{ t("View full profile") }}</o-button > </o-notification> - <event-metadata-block - :title="t('About')" - v-if="group.summary && group.summary !== '<p></p>'" + </div> + </header> + </div> + <div v-if="group"> + <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2"> + <!-- Private thing: Group discussions --> + <Discussions + v-if="isCurrentActorAGroupMember && !previewPublic" + :group="discussionGroup ?? group" + /> + <!-- Private thing: Resources --> + <Resources + v-if="isCurrentActorAGroupMember && !previewPublic" + :group="resourcesGroup ?? group" + /> + <!-- Public thing: Events --> + <Events + :group="group" + :isModerator="isCurrentActorAGroupModerator && !previewPublic" + /> + <!-- Public thing: Posts --> + <Posts + :group="group" + :isModerator="isCurrentActorAGroupModerator && !previewPublic" + :isMember="isCurrentActorAGroupMember && !previewPublic" + /> + </div> + <div class="grid grid-cols-1 md:grid-cols-3 gap-2"> + <!-- Public thing: Members --> + <group-section :title="t('Members')" icon="account-group"> + <template #default> + <div class="flex flex-col justify-center h-full"> + <div + class="flex flex-col items-center" + v-if="isCurrentActorAGroupMember && !previewPublic && members" + > + <div class="flex"> + <figure + :title=" + t(`{'@'}{username} ({role})`, { + username: usernameWithDomain(member.actor), + role: member.role, + }) + " + v-for="member in members.elements" + :key="member.actor.id" + class="-mr-3" + > + <img + class="rounded-full h-8" + :src="member.actor.avatar.url" + v-if="member.actor.avatar" + alt="" + width="32" + height="32" + /> + <AccountCircle v-else :size="32" /> + </figure> + </div> + </div> + <div class=""> + <h2 class="text-center"> + {{ + t( + "{count} members", + { + count: group.members?.total, + }, + group.members?.total + ) + }} + </h2> + </div> + </div></template > + <template #create> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_MEMBERS_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Add / Remove…") }}</o-button + > + </template> + </group-section> + <!-- Public thing: About --> + <group-section :title="t('About')" icon="information"> + <template #default> <div + v-if="group.summary" dir="auto" - class="prose lg:prose-xl dark:prose-invert" + class="prose lg:prose-xl dark:prose-invert p-2" v-html="group.summary" - /> - </event-metadata-block> - <event-metadata-block :title="t('Members')"> - <template #icon> - <AccountGroup :size="48" /> - </template> - {{ - t( - "{count} members", - { - count: group.members?.total, - }, - group.members?.total - ) - }} - </event-metadata-block> - <event-metadata-block - v-if="physicalAddress && physicalAddress.url" - :title="t('Location')" - > - <template #icon> + ></div> + <empty-content v-else icon="information" :inline="true"> + {{ t("No about content yet") }} + </empty-content> + </template> + <template #create> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_PUBLIC_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Edit") }}</o-button + > + </template> + </group-section> + <!-- Public thing: Location --> + <group-section :title="t('Location')" icon="earth"> + <template #default + ><div + class="flex flex-col justify-center h-full" + v-if="physicalAddress && physicalAddress.url" + > <o-icon v-if="physicalAddress.poiInfos.poiIcon.icon" :icon="physicalAddress.poiInfos.poiIcon.icon" customSize="48" /> <Earth v-else :size="48" /> - </template> - <div class="address-wrapper"> - <span - v-if="!physicalAddress || !addressFullName(physicalAddress)" - >{{ t("No address defined") }}</span - > - <div class="address" v-if="physicalAddress"> - <div> - <address dir="auto"> - <p - class="addressDescription" - :title="physicalAddress.poiInfos.name" - > - {{ physicalAddress.poiInfos.name }} - </p> - <p class="has-text-grey-dark"> - {{ physicalAddress.poiInfos.alternativeName }} - </p> - </address> + <div class="address-wrapper"> + <div class="address"> + <div class="text-center"> + <span v-if="!addressFullName(physicalAddress)">{{ + t("No address defined") + }}</span> + <address dir="auto"> + <p + class="addressDescription" + :title="physicalAddress.poiInfos.name" + > + {{ physicalAddress.poiInfos.name }} + </p> + <p class="has-text-grey-dark"> + {{ physicalAddress.poiInfos.alternativeName }} + </p> + </address> + </div> </div> - <o-button - class="map-show-button" - variant="text" - @click="showMap = !showMap" - @keyup.enter="showMap = !showMap" - v-if="physicalAddress.geom" - > - {{ t("Show map") }} - </o-button> </div> </div> - </event-metadata-block> - </div> - </aside> - <div class="main-content min-w-min flex-auto py-0 px-2"> - <section> - <h2 class="text-2xl font-bold">{{ t("Upcoming events") }}</h2> - <div - class="flex flex-col gap-3" - v-if="group && organizedEvents.elements.length > 0" + <empty-content v-else icon="earth" :inline="true"> + {{ t("No location yet") }} + </empty-content></template > - <event-minimalist-card - v-for="event in organizedEvents.elements.slice(0, 3)" - :event="event" - :key="event.uuid" - class="organized-event" - /> - </div> - <empty-content - v-else-if="group" - icon="calendar" - :inline="true" - description-classes="flex flex-col items-stretch" - > - {{ t("No public upcoming events") }} - <template #desc> - <template v-if="isCurrentActorFollowing"> - <i18n-t - keypath="You will receive notifications about this group's public activity depending on %{notification_settings}." - > - <template #notification_settings> - <router-link :to="{ name: RouteName.NOTIFICATIONS }">{{ - t("your notification settings") - }}</router-link> - </template> - </i18n-t> - </template> - <o-button - tag="router-link" - class="my-2 self-center" - variant="text" - :to="{ - name: RouteName.GROUP_EVENTS, - params: { preferredUsername: usernameWithDomain(group) }, - query: { showPassedEvents: true }, - }" - >{{ t("View past events") }}</o-button - > - </template> - </empty-content> - <!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> --> - <div class="flex justify-center"> + <template #create> <o-button - tag="router-link" - class="my-4" + v-if="physicalAddress && physicalAddress.geom" variant="text" - v-if="organizedEvents.total > 0" - :to="{ - name: RouteName.GROUP_EVENTS, - params: { preferredUsername: usernameWithDomain(group) }, - query: { - showPassedEvents: organizedEvents.elements.length === 0, - }, - }" - >{{ t("View all events") }}</o-button + @click="showMap = !showMap" + @keyup.enter="showMap = !showMap" > - </div> - </section> - <section class="flex flex-col items-stretch"> - <h2 class="ml-0 text-2xl font-bold">{{ t("Latest posts") }}</h2> - - <multi-post-list-item - v-if=" - posts.elements.filter( - (post) => - !post.draft && post.visibility === PostVisibility.PUBLIC - ).length > 0 - " - :posts=" - posts.elements.filter( - (post) => - !post.draft && post.visibility === PostVisibility.PUBLIC - ) - " - /> - <empty-content v-else-if="group" icon="bullhorn" :inline="true"> - {{ t("No posts yet") }} - </empty-content> - <!-- <o-skeleton animated v-else-if="$apollo.loading"></o-skeleton> --> - <o-button - class="self-center my-2" - v-if="posts.total > 0" - tag="router-link" - variant="text" - :to="{ - name: RouteName.POSTS, - params: { preferredUsername: usernameWithDomain(group) }, - }" - >{{ t("View all posts") }}</o-button - > - </section> + {{ t("Show map") }} + </o-button> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_PUBLIC_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Edit") }}</o-button + > + </template> + </group-section> </div> + </div> + <div class="my-2"> + <template v-if="isCurrentActorFollowing"> + <i18n-t + class="my-2" + keypath="You will receive notifications about this group's public activity depending on %{notification_settings}." + > + <template #notification_settings> + <router-link :to="{ name: RouteName.NOTIFICATIONS }">{{ + t("your notification settings") + }}</router-link> + </template> + </i18n-t> + </template> + </div> + <div v-if="group" class="public-container flex flex-col"> <o-modal v-if="physicalAddress && physicalAddress.geom" v-model:active="showMap" @@ -654,7 +597,6 @@ </template> <script lang="ts" setup> -// import EventCard from "@/components/Event/EventCard.vue"; import { displayName, IActor, @@ -662,14 +604,11 @@ import { IPerson, usernameWithDomain, } from "@/types/actor"; -// import CompactTodo from "@/components/Todo/CompactTodo.vue"; -import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue"; -import MultiPostListItem from "@/components/Post/MultiPostListItem.vue"; import { Address, addressFullName } from "@/types/address.model"; import InvitationsList from "@/components/Group/InvitationsList.vue"; import { addMinutes } from "date-fns"; import { JOIN_GROUP } from "@/graphql/member"; -import { MemberRole, Openness, PostVisibility } from "@/types/enums"; +import { MemberRole, Openness } from "@/types/enums"; import { IMember } from "@/types/actor/member.model"; import RouteName from "../../router/name"; import ReportModal from "@/components/Report/ReportModal.vue"; @@ -678,11 +617,7 @@ import { PERSON_STATUS_GROUP, } from "@/graphql/actor"; import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue"; -import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue"; import EmptyContent from "../../components/Utils/EmptyContent.vue"; -import { Paginate } from "@/types/paginate"; -import { IEvent } from "@/types/event.model"; -import { IPost } from "@/types/post.model"; import { FOLLOW_GROUP, UNFOLLOW_GROUP, @@ -715,6 +650,7 @@ import { Dialog } from "@/plugins/dialog"; import { Notifier } from "@/plugins/notifier"; import { useGroupResourcesList } from "@/composition/apollo/resources"; import { useGroupMembers } from "@/composition/apollo/members"; +import GroupSection from "@/components/Group/GroupSection.vue"; const props = defineProps<{ preferredUsername: string; @@ -1092,32 +1028,6 @@ const ableToReport = computed((): boolean => { ); }); -const organizedEvents = computed((): Paginate<IEvent> => { - return { - total: group.value?.organizedEvents.total ?? 0, - elements: - group.value?.organizedEvents.elements.filter((event: IEvent) => { - if (previewPublic.value) { - return !event.draft; // TODO when events get visibility access add visibility constraint like below for posts - } - return true; - }) ?? [], - }; -}); - -const posts = computed((): Paginate<IPost> => { - return { - total: group.value?.posts.total ?? 0, - elements: - group.value?.posts.elements.filter((post: IPost) => { - if (previewPublic.value || !isCurrentActorAGroupMember.value) { - return !post.draft && post.visibility == PostVisibility.PUBLIC; - } - return true; - }) ?? [], - }; -}); - const showFollowButton = computed((): boolean => { return !isCurrentActorFollowing.value || previewPublic.value; }); @@ -1244,10 +1154,6 @@ div.container { justify-content: flex-end; display: flex; - .map-show-button { - cursor: pointer; - } - address { font-style: normal; From 8044dbde86ff4254a74ce15536075f9efbe28023 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Thu, 31 Oct 2024 10:52:47 +0100 Subject: [PATCH 19/78] #1492 & #1556 : small corrections about those issues --- src/components/Home/SearchFields.vue | 52 +++++++++++----------------- src/views/HomeView.vue | 4 +-- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index 8739cecc3..58551fd9e 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -29,36 +29,26 @@ :placeholder="t('e.g. Nantes, Berlin, Cork, …')" v-on:update:modelValue="modelValueUpdate" > - <o-button - v-if="distance" - class="!h-auto" - icon-left="map-marker-distance" - :title="t('Select distance')" - @click="showDistance = !showDistance" - /> + <o-dropdown v-model="distance" position="bottom-right" v-if="distance"> + <template #trigger> + <o-button + icon-left="map-marker-distance" + :title="t('Select distance')" + /> + </template> + <o-dropdown-item + v-for="distance_item in distanceList" + :value="distance_item.distance" + :label="distance_item.label" + :key="distance_item.distance" + /> + </o-dropdown> </full-address-auto-complete> <o-button native-type="submit" icon-left="magnify"> <template v-if="search">{{ t("Go!") }}</template> <template v-else>{{ t("Explore!") }}</template> </o-button> </form> - <div - class="border-black border-2 bg-white pt-1 pb-1 w-64 mx-auto" - v-if="showDistance" - > - <span class="mx-2 font-medium text-gray-900 dark:text-slate-100 text-left"> - {{ t("Distance") }} - </span> - <select v-model="distance"> - <option - v-for="distance_item in distanceList" - :value="distance_item.distance" - :key="distance_item.distance" - > - {{ distance_item.label }} - </option> - </select> - </div> </template> <script lang="ts" setup> @@ -69,7 +59,7 @@ import { getLocationFromLocal, storeLocationInLocal, } from "@/utils/location"; -import { computed, defineAsyncComponent, ref } from "vue"; +import { computed, defineAsyncComponent } from "vue"; import { useI18n } from "vue-i18n"; import { useRouter, useRoute } from "vue-router"; import RouteName from "@/router/name"; @@ -123,15 +113,12 @@ const search = computed({ }, }); -const showDistance = ref(false); - const distance = computed({ get(): number { return props.distance; }, set(newDistance: number) { emit("update:distance", newDistance); - showDistance.value = false; }, }); @@ -212,11 +199,11 @@ const submit = () => { locationName: undefined, lat: undefined, lon: undefined, - search: search.value, + search: undefined, distance: undefined, }; - if (distance.value != null) { - search_query.distance = distance.value.toString() + "_km"; + if (search.value != "") { + search_query.search = search.value; } if (location.value) { const { lat, lon } = addressToLocation(location.value); @@ -224,6 +211,9 @@ const submit = () => { location.value.locality ?? location.value.region; search_query.lat = lat; search_query.lon = lon; + if (distance.value != null) { + search_query.distance = distance.value.toString() + "_km"; + } } router.push({ name: RouteName.SEARCH, diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index eb65cfad7..74d46735b 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -30,7 +30,7 @@ v-model:search="search" v-model:location="location" v-model:distance="distance" - :locationDefaultText="location?.description ?? userLocation?.name" + :locationDefaultText="userLocation?.name" v-on:update:location="updateLocation" :fromLocalStorage="true" /> @@ -456,7 +456,7 @@ const userLocation = computed(() => { const distance = computed({ get(): number | null { - if (noLocation.value) { + if (noLocation.value || !userLocation.value?.name) { return null; } else if (current_distance.value == null) { return userLocation.value?.isIPLocation ? 150 : 25; From 6c189b2d6c992c3877bf70b41d3fa40838165d10 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 1 Nov 2024 13:50:24 +0100 Subject: [PATCH 20/78] Issue #1511 Restore timezone consideration - tz-offset is not a valid parameter for <o-datetimepicker> so the offset was ignored. - also remove invalid horizontal-time-picker and first-day-of-week parameters from <o-datetimepicker> --- src/views/Event/EditView.vue | 79 +++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 84a9b92eb..a1be78408 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -72,9 +72,6 @@ icon="calendar-today" :locale="$i18n.locale.replace('_', '-')" v-model="beginsOn" - horizontal-time-picker - :tz-offset="tzOffset(beginsOn)" - :first-day-of-week="firstDayOfWeek" :datepicker="{ id: 'begins-on-field', 'aria-next-label': t('Next month'), @@ -99,10 +96,7 @@ icon="calendar-today" :locale="$i18n.locale.replace('_', '-')" v-model="endsOn" - horizontal-time-picker :min-datetime="beginsOn" - :tz-offset="tzOffset(endsOn)" - :first-day-of-week="firstDayOfWeek" :datepicker="{ id: 'ends-on-field', 'aria-next-label': t('Next month'), @@ -671,7 +665,6 @@ import { Dialog } from "@/plugins/dialog"; import { Notifier } from "@/plugins/notifier"; import { useHead } from "@/utils/head"; import { useOruga } from "@oruga-ui/oruga-next"; -import type { Locale } from "date-fns"; import sortBy from "lodash/sortBy"; import { escapeHtml } from "@/utils/html"; @@ -1210,37 +1203,63 @@ const isEventModified = computed((): boolean => { const beginsOn = computed({ get(): Date | null { - // if (this.timezone && this.event.beginsOn) { - // return utcToZonedTime(this.event.beginsOn, this.timezone); - // } - return event.value.beginsOn ? new Date(event.value.beginsOn) : null; + if (!event.value.beginsOn) { + return null; + } + // return event.value.beginsOn taking care of timezone + const date = new Date(event.value.beginsOn); + date.setMinutes(date.getMinutes() + tzOffset(date)); + return date; }, set(newBeginsOn: Date | null) { - event.value.beginsOn = newBeginsOn?.toISOString() ?? null; - if (!event.value.endsOn || !newBeginsOn) return; - const dateBeginsOn = new Date(newBeginsOn); - const dateEndsOn = new Date(event.value.endsOn); - let endsOn = new Date(event.value.endsOn); - if (dateEndsOn < dateBeginsOn) { - endsOn = dateBeginsOn; - endsOn.setHours(dateBeginsOn.getHours() + 1); + if (!newBeginsOn) { + event.value.beginsOn = null; + return; } - if (dateEndsOn === dateBeginsOn) { - endsOn.setHours(dateEndsOn.getHours() + 1); + + // usefull for comparaison + newBeginsOn.setSeconds(0); + newBeginsOn.setMilliseconds(0); + + // update event.value.beginsOn taking care of timezone + const date = new Date(newBeginsOn.getTime()); + date.setMinutes(date.getMinutes() - tzOffset(newBeginsOn)); + event.value.beginsOn = date.toISOString(); + + // Update endsOn to make sure endsOn is later than beginsOn + if (endsOn.value && endsOn.value <= newBeginsOn) { + const newEndsOn = new Date(newBeginsOn); + newEndsOn.setHours(newBeginsOn.getHours() + 1); + endsOn.value = newEndsOn; } - event.value.endsOn = endsOn.toISOString(); }, }); const endsOn = computed({ get(): Date | null { - // if (this.event.endsOn && this.timezone) { - // return utcToZonedTime(this.event.endsOn, this.timezone); - // } - return event.value.endsOn ? new Date(event.value.endsOn) : null; + if (!event.value.endsOn) { + return null; + } + + // return event.value.endsOn taking care of timezone + const date = new Date(event.value.endsOn); + date.setMinutes(date.getMinutes() + tzOffset(date)); + return date; }, set(newEndsOn: Date | null) { - event.value.endsOn = newEndsOn?.toISOString() ?? null; + if (!newEndsOn) { + event.value.endsOn = null; + return; + } + + // usefull for comparaison + newEndsOn.setSeconds(0); + newEndsOn.setMilliseconds(0); + + // update event.value.endsOn taking care of timezone + const date = new Date(newEndsOn.getTime()); + date.setMinutes(date.getMinutes() - tzOffset(newEndsOn)); + event.value.endsOn = date.toISOString(); }, }); @@ -1355,12 +1374,6 @@ const maximumAttendeeCapacity = computed({ }, }); -const dateFnsLocale = inject<Locale>("dateFnsLocale"); - -const firstDayOfWeek = computed((): number => { - return dateFnsLocale?.options?.weekStartsOn || 0; -}); - const { event: fetchedEvent, onResult: onFetchEventResult } = useFetchEvent( eventId.value ); From 0073a771676971b6fb680b7a424b555e1c880182 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 1 Nov 2024 14:10:45 +0100 Subject: [PATCH 21/78] Update French translation --- src/i18n/fr_FR.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 7a8edd8d7..b6edb8399 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -529,10 +529,12 @@ "Headline picture": "Image à la une", "Hide filters": "Masquer les filtres", "Hide replies": "Masquer les réponses", + "Hide the number of participants":"Cacher le nombre de participants", "Home": "Accueil", "Home to {number} users": "Abrite {number} utilisateur·rice·s", "Homepage": "Page d'accueil", "Hourly email summary": "E-mail récapitulatif chaque heure", + "How to register":"Gestion des participants", "I agree to the {instanceRules} and {termsOfService}": "J'accepte les {instanceRules} et les {termsOfService}", "I create an identity": "Je crée une identité", "I don't have a Mobilizon account": "Je n'ai pas de compte Mobilizon", @@ -542,7 +544,8 @@ "I participate": "Je participe", "I want to allow people to participate without an account.": "Je veux permettre aux gens de participer sans avoir un compte.", "I want to approve every participation request": "Je veux approuver chaque demande de participation", - "I want to manage the registration with an external provider": "Je souhaite gérer l'enregistrement auprès d'un fournisseur externe", + "I want to manage the registration on Mobilizon": "Je souhaite gérer les inscriptions avec Mobilizon", + "I want to manage the registration with an external provider": "Je souhaite gérer les inscriptions auprès d'un fournisseur externe", "I've been mentionned in a comment under an event": "J'ai été mentionné·e dans un commentaire sous un événement", "I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation", "I've been mentionned in a group discussion": "J'ai été mentionné·e dans une discussion d'un groupe", @@ -1052,6 +1055,7 @@ "Show the time when the event ends": "Afficher l'heure de fin de l'événement", "Showing events before": "Afficher les événements avant", "Showing events starting on": "Afficher les événements à partir de", + "Showing participants":"Affichage des participants", "Sign Language": "Langue des signes", "Sign in with": "Se connecter avec", "Sign up": "S'enregistrer", @@ -1257,6 +1261,7 @@ "Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})", "Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})", "Timezone": "Fuseau horaire", + "Timezone parameters": "Paramétrer le fuseau horaire", "Timezone detected as {timezone}.": "Fuseau horaire détecté en tant que {timezone}.", "Title": "Titre", "To activate more notifications, head over to the notification settings.": "Pour activer plus de notifications, rendez-vous dans vos paramètres de notification.", From 3f65781d27a32d5feb8620a54957dabab3f4a8c9 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 1 Nov 2024 19:04:39 +0100 Subject: [PATCH 22/78] Issue #1511 tzOffset need to use browserTimeZone and not userActualTimezone Javascript Date() objects are based on "the host environment (user's device)". https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date --- src/views/Event/EditView.vue | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index a1be78408..478cbda5b 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -1313,24 +1313,35 @@ const timezone = computed({ }, }); +// Timezone specified in user settings const userTimezone = computed((): string | undefined => { return loggedUser.value?.settings?.timezone; }); +const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + +// Timezone specified in user settings or local timezone browser if unavailable const userActualTimezone = computed((): string => { if (userTimezone.value) { return userTimezone.value; } - return Intl.DateTimeFormat().resolvedOptions().timeZone; + return browserTimeZone; }); const tzOffset = (date: Date | null): number => { - if (timezone.value && date) { - const eventUTCOffset = getTimezoneOffset(timezone.value, date); - const localUTCOffset = getTimezoneOffset(userActualTimezone.value, date); - return (eventUTCOffset - localUTCOffset) / (60 * 1000); + if (!timezone.value || !date) { + return 0; } - return 0; + // diff between UTC and selected timezone + // example: Asia/Shanghai is + 8 hours + const eventUTCOffset = getTimezoneOffset(timezone.value, date); + + // diff between UTC and local browser timezone + // example: Europe/Paris is + 1 hour (or +2 depending of daylight saving time) + const localUTCOffset = getTimezoneOffset(browserTimeZone, date); + + // example : offset is 8-1=7 + return (eventUTCOffset - localUTCOffset) / (60 * 1000); }; const eventPhysicalAddress = computed({ From b440e129a929f62733f72cf1f799d04f060dc71a Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 1 Nov 2024 19:09:38 +0100 Subject: [PATCH 23/78] Issue #1511 : Harmonization to use UTC functions for all dates "The above approach attempts to manipulate the Date object's time zone by shifting the Unix timestamp by some other time zone offset. However, since the Date object only tracks time in UTC, it actually just makes the Date object represent a different point in time. [...] When doing so, any access to non-UTC properties must be avoided. " See: https://stackoverflow.com/a/15171030 --- src/views/Event/EditView.vue | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 478cbda5b..a072d13a3 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -718,9 +718,11 @@ const saving = ref(false); const initializeEvent = () => { const roundUpTo15Minutes = (time: Date) => { - time.setMilliseconds(Math.round(time.getMilliseconds() / 1000) * 1000); - time.setSeconds(Math.round(time.getSeconds() / 60) * 60); - time.setMinutes(Math.round(time.getMinutes() / 15) * 15); + time.setUTCMilliseconds( + Math.round(time.getUTCMilliseconds() / 1000) * 1000 + ); + time.setUTCSeconds(Math.round(time.getUTCSeconds() / 60) * 60); + time.setUTCMinutes(Math.round(time.getUTCMinutes() / 15) * 15); return time; }; @@ -1208,7 +1210,7 @@ const beginsOn = computed({ } // return event.value.beginsOn taking care of timezone const date = new Date(event.value.beginsOn); - date.setMinutes(date.getMinutes() + tzOffset(date)); + date.setUTCMinutes(date.getUTCMinutes() + tzOffset(date)); return date; }, set(newBeginsOn: Date | null) { @@ -1218,18 +1220,18 @@ const beginsOn = computed({ } // usefull for comparaison - newBeginsOn.setSeconds(0); - newBeginsOn.setMilliseconds(0); + newBeginsOn.setUTCSeconds(0); + newBeginsOn.setUTCMilliseconds(0); // update event.value.beginsOn taking care of timezone const date = new Date(newBeginsOn.getTime()); - date.setMinutes(date.getMinutes() - tzOffset(newBeginsOn)); + date.setUTCMinutes(date.getUTCMinutes() - tzOffset(newBeginsOn)); event.value.beginsOn = date.toISOString(); // Update endsOn to make sure endsOn is later than beginsOn if (endsOn.value && endsOn.value <= newBeginsOn) { const newEndsOn = new Date(newBeginsOn); - newEndsOn.setHours(newBeginsOn.getHours() + 1); + newEndsOn.setUTCHours(newBeginsOn.getUTCHours() + 1); endsOn.value = newEndsOn; } }, @@ -1243,7 +1245,7 @@ const endsOn = computed({ // return event.value.endsOn taking care of timezone const date = new Date(event.value.endsOn); - date.setMinutes(date.getMinutes() + tzOffset(date)); + date.setUTCMinutes(date.getUTCMinutes() + tzOffset(date)); return date; }, set(newEndsOn: Date | null) { @@ -1253,12 +1255,12 @@ const endsOn = computed({ } // usefull for comparaison - newEndsOn.setSeconds(0); - newEndsOn.setMilliseconds(0); + newEndsOn.setUTCSeconds(0); + newEndsOn.setUTCMilliseconds(0); // update event.value.endsOn taking care of timezone const date = new Date(newEndsOn.getTime()); - date.setMinutes(date.getMinutes() - tzOffset(newEndsOn)); + date.setUTCMinutes(date.getUTCMinutes() - tzOffset(newEndsOn)); event.value.endsOn = date.toISOString(); }, }); From c0e906023cb3e0cbc8da462319dcde3ecb07ea32 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 1 Nov 2024 19:17:05 +0100 Subject: [PATCH 24/78] Issue #1511 : Use the user account timezone setting by default for the timezone of the event --- src/views/Event/EditView.vue | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index a072d13a3..751deafff 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -716,7 +716,19 @@ const dateSettingsIsOpen = ref(false); const saving = ref(false); +const setEventTimezoneToUserTimezoneIfUnset = () => { + if (userTimezone.value && event.value.options.timezone == null) { + event.value.options.timezone = userTimezone.value; + } +}; + +// usefull if the page is loaded from scratch +watch(loggedUser, setEventTimezoneToUserTimezoneIfUnset); + const initializeEvent = () => { + // usefull if the data is already cached + setEventTimezoneToUserTimezoneIfUnset(); + const roundUpTo15Minutes = (time: Date) => { time.setUTCMilliseconds( Math.round(time.getUTCMilliseconds() / 1000) * 1000 From ae02e281296b67c465175d77292c74832e469499 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Sat, 2 Nov 2024 15:32:54 +0100 Subject: [PATCH 25/78] Issue #1511: Date components no longer change their value when the timezone is updated --- src/views/Event/EditView.vue | 126 +++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 751deafff..ebb9bacb5 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -743,8 +743,8 @@ const initializeEvent = () => { end.setUTCHours(now.getUTCHours() + 3); - event.value.beginsOn = now.toISOString(); - event.value.endsOn = end.toISOString(); + beginsOn.value = now; + endsOn.value = end; }; const organizerActor = computed({ @@ -1215,66 +1215,54 @@ const isEventModified = computed((): boolean => { ); }); -const beginsOn = computed({ - get(): Date | null { - if (!event.value.beginsOn) { - return null; - } - // return event.value.beginsOn taking care of timezone - const date = new Date(event.value.beginsOn); - date.setUTCMinutes(date.getUTCMinutes() + tzOffset(date)); - return date; - }, - set(newBeginsOn: Date | null) { - if (!newBeginsOn) { - event.value.beginsOn = null; - return; - } +const beginsOn = ref(new Date()); +const endsOn = ref(new Date()); - // usefull for comparaison - newBeginsOn.setUTCSeconds(0); - newBeginsOn.setUTCMilliseconds(0); +const updateEventDateRelatedToTimezone = () => { + // update event.value.beginsOn taking care of timezone + const dateBeginsOn = new Date(beginsOn.value.getTime()); + dateBeginsOn.setUTCMinutes(dateBeginsOn.getUTCMinutes() - tzOffset.value); + event.value.beginsOn = dateBeginsOn.toISOString(); - // update event.value.beginsOn taking care of timezone - const date = new Date(newBeginsOn.getTime()); - date.setUTCMinutes(date.getUTCMinutes() - tzOffset(newBeginsOn)); - event.value.beginsOn = date.toISOString(); + // update event.value.endsOn taking care of timezone + const dateEndsOn = new Date(endsOn.value.getTime()); + dateEndsOn.setUTCMinutes(dateEndsOn.getUTCMinutes() - tzOffset.value); + event.value.endsOn = dateEndsOn.toISOString(); +}; - // Update endsOn to make sure endsOn is later than beginsOn - if (endsOn.value && endsOn.value <= newBeginsOn) { - const newEndsOn = new Date(newBeginsOn); - newEndsOn.setUTCHours(newBeginsOn.getUTCHours() + 1); - endsOn.value = newEndsOn; - } - }, +watch(beginsOn, (newBeginsOn) => { + if (!newBeginsOn) { + event.value.beginsOn = null; + return; + } + + // usefull for comparaison + newBeginsOn.setUTCSeconds(0); + newBeginsOn.setUTCMilliseconds(0); + + // update event.value.beginsOn taking care of timezone + updateEventDateRelatedToTimezone(); + + // Update endsOn to make sure endsOn is later than beginsOn + if (endsOn.value && endsOn.value <= newBeginsOn) { + const newEndsOn = new Date(newBeginsOn); + newEndsOn.setUTCHours(newBeginsOn.getUTCHours() + 1); + endsOn.value = newEndsOn; + } }); -const endsOn = computed({ - get(): Date | null { - if (!event.value.endsOn) { - return null; - } +watch(endsOn, (newEndsOn) => { + if (!newEndsOn) { + event.value.endsOn = null; + return; + } - // return event.value.endsOn taking care of timezone - const date = new Date(event.value.endsOn); - date.setUTCMinutes(date.getUTCMinutes() + tzOffset(date)); - return date; - }, - set(newEndsOn: Date | null) { - if (!newEndsOn) { - event.value.endsOn = null; - return; - } + // usefull for comparaison + newEndsOn.setUTCSeconds(0); + newEndsOn.setUTCMilliseconds(0); - // usefull for comparaison - newEndsOn.setUTCSeconds(0); - newEndsOn.setUTCMilliseconds(0); - - // update event.value.endsOn taking care of timezone - const date = new Date(newEndsOn.getTime()); - date.setUTCMinutes(date.getUTCMinutes() - tzOffset(newEndsOn)); - event.value.endsOn = date.toISOString(); - }, + // update event.value.endsOn taking care of timezone + updateEventDateRelatedToTimezone(); }); const { timezones: rawTimezones, loading: timezoneLoading } = useTimezones(); @@ -1342,10 +1330,13 @@ const userActualTimezone = computed((): string => { return browserTimeZone; }); -const tzOffset = (date: Date | null): number => { - if (!timezone.value || !date) { +const tzOffset = computed((): number => { + if (!timezone.value) { return 0; } + + const date = new Date(); + // diff between UTC and selected timezone // example: Asia/Shanghai is + 8 hours const eventUTCOffset = getTimezoneOffset(timezone.value, date); @@ -1356,7 +1347,12 @@ const tzOffset = (date: Date | null): number => { // example : offset is 8-1=7 return (eventUTCOffset - localUTCOffset) / (60 * 1000); -}; +}); + +watch(tzOffset, () => { + // tzOffset has been changed, we need to update the event dates + updateEventDateRelatedToTimezone(); +}); const eventPhysicalAddress = computed({ get(): IAddress | null { @@ -1403,6 +1399,20 @@ const { event: fetchedEvent, onResult: onFetchEventResult } = useFetchEvent( eventId.value ); +// update the date components if the event changed (after fetching it, for example) +watch(event, () => { + if (event.value.beginsOn) { + const date = new Date(event.value.beginsOn); + date.setUTCMinutes(date.getUTCMinutes() + tzOffset.value); + beginsOn.value = date; + } + if (event.value.endsOn) { + const date = new Date(event.value.endsOn); + date.setUTCMinutes(date.getUTCMinutes() + tzOffset.value); + endsOn.value = date; + } +}); + watch( fetchedEvent, () => { From 02687e68bb9b0c354548ab82cf41b3d12d52cabe Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Sat, 2 Nov 2024 16:06:17 +0100 Subject: [PATCH 26/78] Issue #1546 Change block order --- src/views/Group/GroupView.vue | 314 +++++++++++++++++----------------- 1 file changed, 157 insertions(+), 157 deletions(-) diff --git a/src/views/Group/GroupView.vue b/src/views/Group/GroupView.vue index f32f8268c..6d4d718f6 100644 --- a/src/views/Group/GroupView.vue +++ b/src/views/Group/GroupView.vue @@ -373,18 +373,155 @@ </div> </header> </div> + <div class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2"> + <!-- Public thing: Members --> + <group-section :title="t('Members')" icon="account-group"> + <template #default> + <div class="flex flex-col justify-center h-full"> + <div + class="flex flex-col items-center" + v-if="isCurrentActorAGroupMember && !previewPublic && members" + > + <div class="flex"> + <figure + :title=" + t(`{'@'}{username} ({role})`, { + username: usernameWithDomain(member.actor), + role: member.role, + }) + " + v-for="member in members.elements" + :key="member.actor.id" + class="-mr-3" + > + <img + class="rounded-full h-8" + :src="member.actor.avatar.url" + v-if="member.actor.avatar" + alt="" + width="32" + height="32" + /> + <AccountCircle v-else :size="32" /> + </figure> + </div> + </div> + <div class=""> + <h2 class="text-center"> + {{ + t( + "{count} members", + { + count: group.members?.total, + }, + group.members?.total + ) + }} + </h2> + </div> + </div></template + > + <template #create> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_MEMBERS_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Add / Remove…") }}</o-button + > + </template> + </group-section> + <!-- Public thing: About --> + <group-section :title="t('About')" icon="information"> + <template #default> + <div + v-if="group.summary" + dir="auto" + class="prose lg:prose-xl dark:prose-invert p-2" + v-html="group.summary" + ></div> + <empty-content v-else icon="information" :inline="true"> + {{ t("No about content yet") }} + </empty-content> + </template> + <template #create> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_PUBLIC_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Edit") }}</o-button + > + </template> + </group-section> + <!-- Public thing: Location --> + <group-section :title="t('Location')" icon="earth"> + <template #default + ><div + class="flex flex-col justify-center h-full" + v-if="physicalAddress && physicalAddress.url" + > + <o-icon + v-if="physicalAddress.poiInfos.poiIcon.icon" + :icon="physicalAddress.poiInfos.poiIcon.icon" + customSize="48" + /> + <Earth v-else :size="48" /> + <div class="address-wrapper"> + <div class="address"> + <div class="text-center"> + <span v-if="!addressFullName(physicalAddress)">{{ + t("No address defined") + }}</span> + <address dir="auto"> + <p + class="addressDescription" + :title="physicalAddress.poiInfos.name" + > + {{ physicalAddress.poiInfos.name }} + </p> + <p class="has-text-grey-dark"> + {{ physicalAddress.poiInfos.alternativeName }} + </p> + </address> + </div> + </div> + </div> + </div> + <empty-content v-else icon="earth" :inline="true"> + {{ t("No location yet") }} + </empty-content></template + > + <template #create> + <o-button + v-if="physicalAddress && physicalAddress.geom" + variant="text" + @click="showMap = !showMap" + @keyup.enter="showMap = !showMap" + > + {{ t("Show map") }} + </o-button> + <o-button + v-if="isCurrentActorAGroupAdmin && !previewPublic" + tag="router-link" + :to="{ + name: RouteName.GROUP_PUBLIC_SETTINGS, + params: { preferredUsername: usernameWithDomain(group) }, + }" + class="button is-primary" + >{{ t("Edit") }}</o-button + > + </template> + </group-section> + </div> <div v-if="group"> <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2"> - <!-- Private thing: Group discussions --> - <Discussions - v-if="isCurrentActorAGroupMember && !previewPublic" - :group="discussionGroup ?? group" - /> - <!-- Private thing: Resources --> - <Resources - v-if="isCurrentActorAGroupMember && !previewPublic" - :group="resourcesGroup ?? group" - /> <!-- Public thing: Events --> <Events :group="group" @@ -396,153 +533,16 @@ :isModerator="isCurrentActorAGroupModerator && !previewPublic" :isMember="isCurrentActorAGroupMember && !previewPublic" /> - </div> - <div class="grid grid-cols-1 md:grid-cols-3 gap-2"> - <!-- Public thing: Members --> - <group-section :title="t('Members')" icon="account-group"> - <template #default> - <div class="flex flex-col justify-center h-full"> - <div - class="flex flex-col items-center" - v-if="isCurrentActorAGroupMember && !previewPublic && members" - > - <div class="flex"> - <figure - :title=" - t(`{'@'}{username} ({role})`, { - username: usernameWithDomain(member.actor), - role: member.role, - }) - " - v-for="member in members.elements" - :key="member.actor.id" - class="-mr-3" - > - <img - class="rounded-full h-8" - :src="member.actor.avatar.url" - v-if="member.actor.avatar" - alt="" - width="32" - height="32" - /> - <AccountCircle v-else :size="32" /> - </figure> - </div> - </div> - <div class=""> - <h2 class="text-center"> - {{ - t( - "{count} members", - { - count: group.members?.total, - }, - group.members?.total - ) - }} - </h2> - </div> - </div></template - > - <template #create> - <o-button - v-if="isCurrentActorAGroupAdmin && !previewPublic" - tag="router-link" - :to="{ - name: RouteName.GROUP_MEMBERS_SETTINGS, - params: { preferredUsername: usernameWithDomain(group) }, - }" - class="button is-primary" - >{{ t("Add / Remove…") }}</o-button - > - </template> - </group-section> - <!-- Public thing: About --> - <group-section :title="t('About')" icon="information"> - <template #default> - <div - v-if="group.summary" - dir="auto" - class="prose lg:prose-xl dark:prose-invert p-2" - v-html="group.summary" - ></div> - <empty-content v-else icon="information" :inline="true"> - {{ t("No about content yet") }} - </empty-content> - </template> - <template #create> - <o-button - v-if="isCurrentActorAGroupAdmin && !previewPublic" - tag="router-link" - :to="{ - name: RouteName.GROUP_PUBLIC_SETTINGS, - params: { preferredUsername: usernameWithDomain(group) }, - }" - class="button is-primary" - >{{ t("Edit") }}</o-button - > - </template> - </group-section> - <!-- Public thing: Location --> - <group-section :title="t('Location')" icon="earth"> - <template #default - ><div - class="flex flex-col justify-center h-full" - v-if="physicalAddress && physicalAddress.url" - > - <o-icon - v-if="physicalAddress.poiInfos.poiIcon.icon" - :icon="physicalAddress.poiInfos.poiIcon.icon" - customSize="48" - /> - <Earth v-else :size="48" /> - <div class="address-wrapper"> - <div class="address"> - <div class="text-center"> - <span v-if="!addressFullName(physicalAddress)">{{ - t("No address defined") - }}</span> - <address dir="auto"> - <p - class="addressDescription" - :title="physicalAddress.poiInfos.name" - > - {{ physicalAddress.poiInfos.name }} - </p> - <p class="has-text-grey-dark"> - {{ physicalAddress.poiInfos.alternativeName }} - </p> - </address> - </div> - </div> - </div> - </div> - <empty-content v-else icon="earth" :inline="true"> - {{ t("No location yet") }} - </empty-content></template - > - <template #create> - <o-button - v-if="physicalAddress && physicalAddress.geom" - variant="text" - @click="showMap = !showMap" - @keyup.enter="showMap = !showMap" - > - {{ t("Show map") }} - </o-button> - <o-button - v-if="isCurrentActorAGroupAdmin && !previewPublic" - tag="router-link" - :to="{ - name: RouteName.GROUP_PUBLIC_SETTINGS, - params: { preferredUsername: usernameWithDomain(group) }, - }" - class="button is-primary" - >{{ t("Edit") }}</o-button - > - </template> - </group-section> + <!-- Private thing: Group discussions --> + <Discussions + v-if="isCurrentActorAGroupMember && !previewPublic" + :group="discussionGroup ?? group" + /> + <!-- Private thing: Resources --> + <Resources + v-if="isCurrentActorAGroupMember && !previewPublic" + :group="resourcesGroup ?? group" + /> </div> </div> <div class="my-2"> From edac95ef4a65bb72bb07a63e1974ef1c8963d9d6 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Sat, 2 Nov 2024 16:17:44 +0100 Subject: [PATCH 27/78] Issue #1546 Small CSS tweaks for mobile view --- src/components/Group/GroupSection.vue | 2 +- src/views/Group/GroupView.vue | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Group/GroupSection.vue b/src/components/Group/GroupSection.vue index 9c4c05de9..1e30e8d50 100644 --- a/src/components/Group/GroupSection.vue +++ b/src/components/Group/GroupSection.vue @@ -12,7 +12,7 @@ <div class="flex-1 min-h-40"> <slot></slot> </div> - <div class="flex justify-end p-2"> + <div class="flex flex-wrap justify-end p-2"> <slot name="create"></slot> </div> </section> diff --git a/src/views/Group/GroupView.vue b/src/views/Group/GroupView.vue index 6d4d718f6..e7d01dba2 100644 --- a/src/views/Group/GroupView.vue +++ b/src/views/Group/GroupView.vue @@ -443,7 +443,12 @@ class="prose lg:prose-xl dark:prose-invert p-2" v-html="group.summary" ></div> - <empty-content v-else icon="information" :inline="true"> + <empty-content + v-else + icon="information" + :inline="true" + :center="true" + > {{ t("No about content yet") }} </empty-content> </template> @@ -494,7 +499,7 @@ </div> </div> </div> - <empty-content v-else icon="earth" :inline="true"> + <empty-content v-else icon="earth" :inline="true" :center="true"> {{ t("No location yet") }} </empty-content></template > From 841cc4e0b6f0bc91e5d99d339b69ea834479d5f9 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 17:11:04 +0100 Subject: [PATCH 28/78] Issue #1571 Replace <o-datetimepicker> by <input type="datetime-local"> Solves the "date is not updated issue" on iOS 17 --- src/views/Event/EditView.vue | 92 +++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index ebb9bacb5..a4b634e68 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -66,19 +66,13 @@ class="items-center" label-for="begins-on-field" > - <o-datetimepicker - class="datepicker starts-on" - :placeholder="t('Type or select a date…')" - icon="calendar-today" - :locale="$i18n.locale.replace('_', '-')" - v-model="beginsOn" - :datepicker="{ - id: 'begins-on-field', - 'aria-next-label': t('Next month'), - 'aria-previous-label': t('Previous month'), - }" - > - </o-datetimepicker> + <input + type="datetime-local" + class="rounded" + v-model="beginsOnComponentDateTime" + @blur="consistencyBeginsOnBeforeEndsOn" + /> + <o-switch v-model="eventOptions.showStartTime">{{ t("Show the time when the event begins") }}</o-switch> @@ -90,20 +84,13 @@ label-for="ends-on-field" class="items-center" > - <o-datetimepicker - class="datepicker ends-on" - :placeholder="t('Type or select a date…')" - icon="calendar-today" - :locale="$i18n.locale.replace('_', '-')" - v-model="endsOn" - :min-datetime="beginsOn" - :datepicker="{ - id: 'ends-on-field', - 'aria-next-label': t('Next month'), - 'aria-previous-label': t('Previous month'), - }" - > - </o-datetimepicker> + <input + type="datetime-local" + class="rounded" + v-model="endsOnComponentDateTime" + :min="beginsOnComponentDateTime" + @blur="consistencyBeginsOnBeforeEndsOn" + /> <o-switch v-model="eventOptions.showEndTime">{{ t("Show the time when the event ends") }}</o-switch> @@ -1218,6 +1205,34 @@ const isEventModified = computed((): boolean => { const beginsOn = ref(new Date()); const endsOn = ref(new Date()); +const beginsOnComponentDateTime = computed({ + get() { + // UTC to local + const localDate = new Date( + beginsOn.value.getTime() - beginsOn.value.getTimezoneOffset() * 60000 + ); + return localDate.toISOString().slice(0, 16); // Format to 'YYYY-MM-DDTHH:MM' + }, + set(value) { + // Local timezone + beginsOn.value = new Date(value); + }, +}); + +const endsOnComponentDateTime = computed({ + get() { + // UTC to local + const localDate = new Date( + endsOn.value.getTime() - endsOn.value.getTimezoneOffset() * 60000 + ); + return localDate.toISOString().slice(0, 16); // Format to 'YYYY-MM-DDTHH:MM' + }, + set(value) { + // Local timezone + endsOn.value = new Date(value); + }, +}); + const updateEventDateRelatedToTimezone = () => { // update event.value.beginsOn taking care of timezone const dateBeginsOn = new Date(beginsOn.value.getTime()); @@ -1242,13 +1257,6 @@ watch(beginsOn, (newBeginsOn) => { // update event.value.beginsOn taking care of timezone updateEventDateRelatedToTimezone(); - - // Update endsOn to make sure endsOn is later than beginsOn - if (endsOn.value && endsOn.value <= newBeginsOn) { - const newEndsOn = new Date(newBeginsOn); - newEndsOn.setUTCHours(newBeginsOn.getUTCHours() + 1); - endsOn.value = newEndsOn; - } }); watch(endsOn, (newEndsOn) => { @@ -1265,6 +1273,22 @@ watch(endsOn, (newEndsOn) => { updateEventDateRelatedToTimezone(); }); +/* +For endsOn, we need to check consistencyBeginsOnBeforeEndsOn() at blur +because the datetime-local component update itself immediately +Ex : your event start at 10:00 and stops at 12:00 +To type "10" hours, you will first have "1" hours, then "10" hours +So you cannot check consistensy in real time, only onBlur because of the moment we falsely have "1:00" + */ +const consistencyBeginsOnBeforeEndsOn = () => { + // Update endsOn to make sure endsOn is later than beginsOn + if (endsOn.value && endsOn.value <= beginsOn.value) { + const newEndsOn = new Date(beginsOn.value); + newEndsOn.setUTCHours(beginsOn.value.getUTCHours() + 1); + endsOn.value = newEndsOn; + } +}; + const { timezones: rawTimezones, loading: timezoneLoading } = useTimezones(); const timezones = computed((): Record<string, string[]> => { From f236ec3a2de10b1806d1224258d119b9afffcac9 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 20:24:58 +0100 Subject: [PATCH 29/78] Issue #1571 Refactor to a new date component <event-date-picker> --- src/components/Event/EventDatePicker.vue | 46 +++++++++++++++++++++++ src/views/Event/EditView.vue | 47 ++++-------------------- 2 files changed, 54 insertions(+), 39 deletions(-) create mode 100644 src/components/Event/EventDatePicker.vue diff --git a/src/components/Event/EventDatePicker.vue b/src/components/Event/EventDatePicker.vue new file mode 100644 index 000000000..741a62c8d --- /dev/null +++ b/src/components/Event/EventDatePicker.vue @@ -0,0 +1,46 @@ +<template> + <input + type="datetime-local" + class="rounded" + v-model="component" + :min="computeMin" + @blur="$emit('blur')" + /> +</template> +<script lang="ts" setup> +import { computed } from "vue"; + +const props = withDefaults( + defineProps<{ + modelValue: Date; + min?: Date | undefined; + }>(), + { + min: undefined, + } +); + +const emit = defineEmits(["update:modelValue", "blur"]); + +/** Format a Date to 'YYYY-MM-DDTHH:MM' based on local time zone */ +const UTCToLocal = (date: Date) => { + const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + return localDate.toISOString().slice(0, 16); +}; + +const component = computed({ + get() { + return UTCToLocal(props.modelValue); + }, + set(value) { + emit("update:modelValue", new Date(value)); + }, +}); + +const computeMin = computed((): string | undefined => { + if (!props.min) { + return undefined; + } + return UTCToLocal(props.min); +}); +</script> diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index a4b634e68..1dc5f2125 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -66,12 +66,10 @@ class="items-center" label-for="begins-on-field" > - <input - type="datetime-local" - class="rounded" - v-model="beginsOnComponentDateTime" + <event-date-picker + v-model="beginsOn" @blur="consistencyBeginsOnBeforeEndsOn" - /> + ></event-date-picker> <o-switch v-model="eventOptions.showStartTime">{{ t("Show the time when the event begins") @@ -84,13 +82,11 @@ label-for="ends-on-field" class="items-center" > - <input - type="datetime-local" - class="rounded" - v-model="endsOnComponentDateTime" - :min="beginsOnComponentDateTime" + <event-date-picker + v-model="endsOn" @blur="consistencyBeginsOnBeforeEndsOn" - /> + :min="beginsOn" + ></event-date-picker> <o-switch v-model="eventOptions.showEndTime">{{ t("Show the time when the event ends") }}</o-switch> @@ -654,6 +650,7 @@ import { useHead } from "@/utils/head"; import { useOruga } from "@oruga-ui/oruga-next"; import sortBy from "lodash/sortBy"; import { escapeHtml } from "@/utils/html"; +import EventDatePicker from "@/components/Event/EventDatePicker.vue"; const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10; @@ -1205,34 +1202,6 @@ const isEventModified = computed((): boolean => { const beginsOn = ref(new Date()); const endsOn = ref(new Date()); -const beginsOnComponentDateTime = computed({ - get() { - // UTC to local - const localDate = new Date( - beginsOn.value.getTime() - beginsOn.value.getTimezoneOffset() * 60000 - ); - return localDate.toISOString().slice(0, 16); // Format to 'YYYY-MM-DDTHH:MM' - }, - set(value) { - // Local timezone - beginsOn.value = new Date(value); - }, -}); - -const endsOnComponentDateTime = computed({ - get() { - // UTC to local - const localDate = new Date( - endsOn.value.getTime() - endsOn.value.getTimezoneOffset() * 60000 - ); - return localDate.toISOString().slice(0, 16); // Format to 'YYYY-MM-DDTHH:MM' - }, - set(value) { - // Local timezone - endsOn.value = new Date(value); - }, -}); - const updateEventDateRelatedToTimezone = () => { // update event.value.beginsOn taking care of timezone const dateBeginsOn = new Date(beginsOn.value.getTime()); From 9992286e7e23781137da0314dc97523d1dbe02a6 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 20:47:19 +0100 Subject: [PATCH 30/78] Issue #1571 Handle the case where the date is invalid --- src/components/Event/EventDatePicker.vue | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/Event/EventDatePicker.vue b/src/components/Event/EventDatePicker.vue index 741a62c8d..4cbe221b5 100644 --- a/src/components/Event/EventDatePicker.vue +++ b/src/components/Event/EventDatePicker.vue @@ -1,7 +1,7 @@ <template> <input type="datetime-local" - class="rounded" + class="rounded invalid:border-red-500" v-model="component" :min="computeMin" @blur="$emit('blur')" @@ -12,8 +12,8 @@ import { computed } from "vue"; const props = withDefaults( defineProps<{ - modelValue: Date; - min?: Date | undefined; + modelValue: Date | null; + min?: Date | null | undefined; }>(), { min: undefined, @@ -30,10 +30,19 @@ const UTCToLocal = (date: Date) => { const component = computed({ get() { + if (!props.modelValue) { + return null; + } return UTCToLocal(props.modelValue); }, set(value) { - emit("update:modelValue", new Date(value)); + console.log("value" + value); + if (!value) { + emit("update:modelValue", null); + return; + } + const date = new Date(value); + emit("update:modelValue", date); }, }); From 7f43169fae70742dab19086afab6c0f438f3c80b Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 21:25:37 +0100 Subject: [PATCH 31/78] Issue #1571 Hide the time when the user chooses to not display it --- src/components/Event/EventDatePicker.vue | 17 ++++++++++++++--- src/views/Event/EditView.vue | 2 ++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/Event/EventDatePicker.vue b/src/components/Event/EventDatePicker.vue index 4cbe221b5..7c2fe5040 100644 --- a/src/components/Event/EventDatePicker.vue +++ b/src/components/Event/EventDatePicker.vue @@ -1,6 +1,12 @@ <template> + <!-- + :key is required to force rerender when time change + If not used, the input becomes empty + See : https://vuejs.org/api/built-in-special-attributes.html#key + --> <input - type="datetime-local" + :type="time ? 'datetime-local' : 'date'" + :key="time.toString()" class="rounded invalid:border-red-500" v-model="component" :min="computeMin" @@ -13,6 +19,7 @@ import { computed } from "vue"; const props = withDefaults( defineProps<{ modelValue: Date | null; + time: boolean; min?: Date | null | undefined; }>(), { @@ -25,7 +32,7 @@ const emit = defineEmits(["update:modelValue", "blur"]); /** Format a Date to 'YYYY-MM-DDTHH:MM' based on local time zone */ const UTCToLocal = (date: Date) => { const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); - return localDate.toISOString().slice(0, 16); + return localDate.toISOString().slice(0, props.time ? 16 : 10); }; const component = computed({ @@ -36,12 +43,16 @@ const component = computed({ return UTCToLocal(props.modelValue); }, set(value) { - console.log("value" + value); if (!value) { emit("update:modelValue", null); return; } const date = new Date(value); + + if (!props.time) { + date.setHours(0, 0, 0, 0); + } + emit("update:modelValue", date); }, }); diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 1dc5f2125..0896ecf36 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -67,6 +67,7 @@ label-for="begins-on-field" > <event-date-picker + :time="eventOptions.showStartTime" v-model="beginsOn" @blur="consistencyBeginsOnBeforeEndsOn" ></event-date-picker> @@ -83,6 +84,7 @@ class="items-center" > <event-date-picker + :time="eventOptions.showEndTime" v-model="endsOn" @blur="consistencyBeginsOnBeforeEndsOn" :min="beginsOn" From 19ba095e07f07f70f0c4b8f15862304dfebdf49e Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 22:35:01 +0100 Subject: [PATCH 32/78] Issue #1571: Fixes the switches for displaying hours not working. --- src/views/Event/EditView.vue | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 0896ecf36..341664ca2 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -67,12 +67,12 @@ label-for="begins-on-field" > <event-date-picker - :time="eventOptions.showStartTime" + :time="showStartTime" v-model="beginsOn" @blur="consistencyBeginsOnBeforeEndsOn" ></event-date-picker> - <o-switch v-model="eventOptions.showStartTime">{{ + <o-switch v-model="showStartTime">{{ t("Show the time when the event begins") }}</o-switch> </o-field> @@ -84,12 +84,12 @@ class="items-center" > <event-date-picker - :time="eventOptions.showEndTime" + :time="showEndTime" v-model="endsOn" @blur="consistencyBeginsOnBeforeEndsOn" :min="beginsOn" ></event-date-picker> - <o-switch v-model="eventOptions.showEndTime">{{ + <o-switch v-model="showEndTime">{{ t("Show the time when the event ends") }}</o-switch> </o-field> @@ -1201,6 +1201,30 @@ const isEventModified = computed((): boolean => { ); }); +const showStartTime = computed({ + get(): boolean { + return event.value.options.showStartTime; + }, + set(newShowStartTime: boolean) { + event.value.options = { + ...event.value.options, + showStartTime: newShowStartTime, + }; + }, +}); + +const showEndTime = computed({ + get(): boolean { + return event.value.options.showEndTime; + }, + set(newshowEndTime: boolean) { + event.value.options = { + ...event.value.options, + showEndTime: newshowEndTime, + }; + }, +}); + const beginsOn = ref(new Date()); const endsOn = ref(new Date()); From 3d7ffbf5ca7186b8565f3b6e5cfe48a2746eae3f Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 22:49:54 +0100 Subject: [PATCH 33/78] Issue #1571: Design adjustments for mobile --- src/views/Event/EditView.vue | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 341664ca2..3582a3140 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -62,6 +62,7 @@ <o-field grouped + groupMultiline :label="t('Starts on…')" class="items-center" label-for="begins-on-field" @@ -71,14 +72,16 @@ v-model="beginsOn" @blur="consistencyBeginsOnBeforeEndsOn" ></event-date-picker> - - <o-switch v-model="showStartTime">{{ - t("Show the time when the event begins") - }}</o-switch> + <div class="my-2"> + <o-switch v-model="showStartTime">{{ + t("Show the time when the event begins") + }}</o-switch> + </div> </o-field> <o-field grouped + groupMultiline :label="t('Ends on…')" label-for="ends-on-field" class="items-center" @@ -89,9 +92,11 @@ @blur="consistencyBeginsOnBeforeEndsOn" :min="beginsOn" ></event-date-picker> - <o-switch v-model="showEndTime">{{ - t("Show the time when the event ends") - }}</o-switch> + <div class="my-2"> + <o-switch v-model="showEndTime">{{ + t("Show the time when the event ends") + }}</o-switch> + </div> </o-field> <o-button class="block" variant="text" @click="dateSettingsIsOpen = true"> From 42c98be036dff467645d359f28f758ccff8d8e4d Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 22:59:09 +0100 Subject: [PATCH 34/78] Issue #1571: Fix null check before use --- src/views/Event/EditView.vue | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 3582a3140..c1b79002e 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -1235,14 +1235,18 @@ const endsOn = ref(new Date()); const updateEventDateRelatedToTimezone = () => { // update event.value.beginsOn taking care of timezone - const dateBeginsOn = new Date(beginsOn.value.getTime()); - dateBeginsOn.setUTCMinutes(dateBeginsOn.getUTCMinutes() - tzOffset.value); - event.value.beginsOn = dateBeginsOn.toISOString(); + if (beginsOn.value) { + const dateBeginsOn = new Date(beginsOn.value.getTime()); + dateBeginsOn.setUTCMinutes(dateBeginsOn.getUTCMinutes() - tzOffset.value); + event.value.beginsOn = dateBeginsOn.toISOString(); + } - // update event.value.endsOn taking care of timezone - const dateEndsOn = new Date(endsOn.value.getTime()); - dateEndsOn.setUTCMinutes(dateEndsOn.getUTCMinutes() - tzOffset.value); - event.value.endsOn = dateEndsOn.toISOString(); + if (endsOn.value) { + // update event.value.endsOn taking care of timezone + const dateEndsOn = new Date(endsOn.value.getTime()); + dateEndsOn.setUTCMinutes(dateEndsOn.getUTCMinutes() - tzOffset.value); + event.value.endsOn = dateEndsOn.toISOString(); + } }; watch(beginsOn, (newBeginsOn) => { @@ -1282,7 +1286,7 @@ So you cannot check consistensy in real time, only onBlur because of the moment */ const consistencyBeginsOnBeforeEndsOn = () => { // Update endsOn to make sure endsOn is later than beginsOn - if (endsOn.value && endsOn.value <= beginsOn.value) { + if (endsOn.value && beginsOn.value && endsOn.value <= beginsOn.value) { const newEndsOn = new Date(beginsOn.value); newEndsOn.setUTCHours(beginsOn.value.getUTCHours() + 1); endsOn.value = newEndsOn; From 5e201dd28cff1890949d1a295926917024f2dc76 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Mon, 4 Nov 2024 23:28:40 +0100 Subject: [PATCH 35/78] Issue #1571: Fix dark mode for <event-date-picker> --- src/components/Event/EventDatePicker.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Event/EventDatePicker.vue b/src/components/Event/EventDatePicker.vue index 7c2fe5040..fdd37b8f6 100644 --- a/src/components/Event/EventDatePicker.vue +++ b/src/components/Event/EventDatePicker.vue @@ -7,7 +7,7 @@ <input :type="time ? 'datetime-local' : 'date'" :key="time.toString()" - class="rounded invalid:border-red-500" + class="rounded invalid:border-red-500 dark:bg-zinc-600" v-model="component" :min="computeMin" @blur="$emit('blur')" From f2d7da7af7bc39b4e9f1075ddd13a1c191c55527 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 6 Nov 2024 12:58:02 +0100 Subject: [PATCH 36/78] #1492: add distance value in selector + add with filter in search page --- src/components/Home/SearchFields.vue | 129 ++++++++--------------- src/i18n/en_US.json | 1 + src/i18n/fr_FR.json | 1 + src/utils/location.ts | 72 ++++++------- src/views/HomeView.vue | 78 +++++++++++--- src/views/SearchView.vue | 149 +++++---------------------- 6 files changed, 162 insertions(+), 268 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index 58551fd9e..ea3b24eeb 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -21,20 +21,17 @@ /> <full-address-auto-complete :resultType="AddressSearchType.ADMINISTRATIVE" - v-model="location" + v-model="address" :hide-map="true" :hide-selected="true" - :default-text="locationDefaultText" + :default-text="addressDefaultText" labelClass="sr-only" :placeholder="t('e.g. Nantes, Berlin, Cork, …')" v-on:update:modelValue="modelValueUpdate" > <o-dropdown v-model="distance" position="bottom-right" v-if="distance"> <template #trigger> - <o-button - icon-left="map-marker-distance" - :title="t('Select distance')" - /> + <o-button :title="t('Select distance')">{{ distanceText }}</o-button> </template> <o-dropdown-item v-for="distance_item in distanceList" @@ -56,8 +53,8 @@ import { IAddress } from "@/types/address.model"; import { AddressSearchType } from "@/types/enums"; import { addressToLocation, - getLocationFromLocal, - storeLocationInLocal, + getAddressFromLocal, + storeAddressInLocal, } from "@/utils/location"; import { computed, defineAsyncComponent } from "vue"; import { useI18n } from "vue-i18n"; @@ -69,8 +66,8 @@ const FullAddressAutoComplete = defineAsyncComponent( ); const props = defineProps<{ - location: IAddress | null; - locationDefaultText?: string | null; + address: IAddress | null; + addressDefaultText?: string | null; search: string; distance: number | null; fromLocalStorage?: boolean | false; @@ -80,26 +77,27 @@ const router = useRouter(); const route = useRoute(); const emit = defineEmits<{ - (event: "update:location", location: IAddress | null): void; + (event: "update:address", address: IAddress | null): void; (event: "update:search", newSearch: string): void; (event: "update:distance", newDistance: number): void; (event: "submit"): void; }>(); -const location = computed({ +const address = computed({ get(): IAddress | null { - if (props.location) { - return props.location; + console.debug("-- get address --", props); + if (props.address) { + return props.address; } if (props.fromLocalStorage) { - return getLocationFromLocal(); + return getAddressFromLocal(); } return null; }, - set(newLocation: IAddress | null) { - emit("update:location", newLocation); + set(newAddress: IAddress | null) { + emit("update:address", newAddress); if (props.fromLocalStorage) { - storeLocationInLocal(newLocation); + storeAddressInLocal(newAddress); } }, }); @@ -122,75 +120,31 @@ const distance = computed({ }, }); -const distanceList = computed(() => { - return [ - { - distance: 5, - label: t( - "{number} kilometers", - { - number: 5, - }, - 5 - ), - }, - { - distance: 10, - label: t( - "{number} kilometers", - { - number: 10, - }, - 10 - ), - }, - { - distance: 25, - label: t( - "{number} kilometers", - { - number: 25, - }, - 25 - ), - }, - { - distance: 50, - label: t( - "{number} kilometers", - { - number: 50, - }, - 50 - ), - }, - { - distance: 100, - label: t( - "{number} kilometers", - { - number: 100, - }, - 100 - ), - }, - { - distance: 150, - label: t( - "{number} kilometers", - { - number: 150, - }, - 150 - ), - }, - ]; +const distanceText = computed(() => { + return distance.value + " km"; }); -console.debug("initial", distance.value, search.value, location.value); +const distanceList = computed(() => { + const distances = []; + [5, 10, 25, 50, 100, 150].forEach((value) => { + distances.push({ + distance: value, + label: t( + "{number} kilometers", + { + number: value, + }, + value + ), + }); + }); + return distances; +}); -const modelValueUpdate = (newlocation: IAddress | null) => { - emit("update:location", newlocation); +console.debug("initial", distance.value, search.value, address.value); + +const modelValueUpdate = (newaddress: IAddress | null) => { + emit("update:address", newaddress); }; const submit = () => { @@ -205,10 +159,9 @@ const submit = () => { if (search.value != "") { search_query.search = search.value; } - if (location.value) { - const { lat, lon } = addressToLocation(location.value); - search_query.locationName = - location.value.locality ?? location.value.region; + if (address.value) { + const { lat, lon } = addressToLocation(address.value); + search_query.locationName = address.value.locality ?? address.value.region; search_query.lat = lat; search_query.lon = lon; if (distance.value != null) { diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index 3b688f59b..452369e91 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -1360,6 +1360,7 @@ "Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.", "Go!": "Go!", "Explore!": "Explore!", + "Select distance": "Select distance", "Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance", "Open user menu": "Open user menu", "Open main menu": "Open main menu", diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index b6edb8399..0fb9ca01a 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -1028,6 +1028,7 @@ "Select a radius": "Sélectionnez un rayon", "Select a timezone": "Selectionnez un fuseau horaire", "Select all resources": "Sélectionner toutes les ressources", + "Select distance": "Sélectionner la distance", "Select languages": "Choisissez une langue", "Select the activities for which you wish to receive an email or a push notification.": "Sélectionnez les activités pour lesquelles vous souhaitez recevoir un email ou une notification push.", "Select this resource": "Sélectionner cette ressource", diff --git a/src/utils/location.ts b/src/utils/location.ts index 6dc17cf8b..b87707172 100644 --- a/src/utils/location.ts +++ b/src/utils/location.ts @@ -9,9 +9,19 @@ const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway export const addressToLocation = ( address: IAddress ): LocationType | undefined => { - if (!address.geom) return undefined; + if (!address.geom) + return { + lon: undefined, + lat: undefined, + name: undefined, + }; const arr = address.geom.split(";"); - if (arr.length < 2) return undefined; + if (arr.length < 2) + return { + lon: undefined, + lat: undefined, + name: undefined, + }; return { lon: parseFloat(arr[0]), lat: parseFloat(arr[1]), @@ -19,6 +29,17 @@ export const addressToLocation = ( }; }; +export const locationToAddress = (location: LocationType): IAddress | null => { + if (location.lon && location.lat) { + const new_add = new Address(); + new_add.geom = location.lon.toString() + ";" + location.lat.toString(); + new_add.description = location.name || ""; + console.debug("locationToAddress", location, new_add); + return new_add; + } + return null; +}; + export const coordsToGeoHash = ( lat: number | undefined, lon: number | undefined, @@ -38,44 +59,24 @@ export const geoHashToCoords = ( return latitude && longitude ? { latitude, longitude } : undefined; }; -export const storeLocationInLocal = (location: IAddress | null): undefined => { - if (location) { - window.localStorage.setItem("location", JSON.stringify(location)); +export const storeAddressInLocal = (address: IAddress | null): undefined => { + if (address) { + window.localStorage.setItem("address", JSON.stringify(address)); } else { - window.localStorage.removeItem("location"); + window.localStorage.removeItem("address"); } }; -export const getLocationFromLocal = (): IAddress | null => { - const locationString = window.localStorage.getItem("location"); - if (!locationString) { +export const getAddressFromLocal = (): IAddress | null => { + const addressString = window.localStorage.getItem("address"); + if (!addressString) { return null; } - const location = JSON.parse(locationString) as IAddress; - if (!location.description || !location.geom) { + const address = JSON.parse(addressString) as IAddress; + if (!address.description || !address.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; + return address; }; export const storeUserLocationAndRadiusFromUserSettings = ( @@ -84,18 +85,13 @@ export const storeUserLocationAndRadiusFromUserSettings = ( if (location) { const latlon = geoHashToCoords(location.geohash); if (latlon) { - storeLocationInLocal({ + storeAddressInLocal({ ...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 74d46735b..2ce33e3dc 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -28,11 +28,12 @@ <!-- Search fields --> <search-fields v-model:search="search" - v-model:location="location" + v-model:address="userAddress" v-model:distance="distance" - :locationDefaultText="userLocation?.name" - v-on:update:location="updateLocation" + v-on:update:address="updateAddress" :fromLocalStorage="true" + :addressDefaultText="userLocation?.name" + :key="increated" /> <!-- Categories preview --> <categories-preview /> @@ -185,7 +186,13 @@ 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 { addressToLocation, geoHashToCoords } from "@/utils/location"; +import { + addressToLocation, + geoHashToCoords, + getAddressFromLocal, + locationToAddress, + storeAddressInLocal, +} from "@/utils/location"; import { useServerProvidedLocation } from "@/composition/apollo/config"; import { ABOUT } from "@/graphql/config"; import { IConfig } from "@/types/config.model"; @@ -238,13 +245,14 @@ const currentUserParticipations = computed( () => loggedUser.value?.participations.elements ); -const location = ref(null); +const increated = ref(0); +const address = ref(null); const search = ref(""); -const noLocation = ref(false); +const noAddress = ref(false); const current_distance = ref(null); -watch(location, (newLoc, oldLoc) => - console.debug("LOCATION UPDATED from", { ...oldLoc }, " to ", { ...newLoc }) +watch(address, (newAdd, oldAdd) => + console.debug("ADDRESS UPDATED from", { ...oldAdd }, " to ", { ...newAdd }) ); const isToday = (date: string): boolean => { @@ -401,13 +409,13 @@ const { result: reverseGeocodeResult } = useQuery<{ })); const userSettingsLocation = computed(() => { - const address = reverseGeocodeResult.value?.reverseGeocode[0]; - const placeName = address?.locality ?? address?.region ?? address?.country; + const location = reverseGeocodeResult.value?.reverseGeocode[0]; + const placeName = location?.locality ?? location?.region ?? location?.country; return { lat: coords.value?.latitude, lon: coords.value?.longitude, name: placeName, - picture: address?.pictureInfo, + picture: location?.pictureInfo, isIPLocation: coords.value?.isIPLocation, }; }); @@ -433,16 +441,20 @@ const currentUserLocation = computed(() => { const userLocation = computed(() => { console.debug("new userLocation"); - if (noLocation.value) { + if (noAddress.value) { return { lon: null, lat: null, name: null, }; } - if (location.value) { + if (address.value) { console.debug("userLocation is typed location"); - return addressToLocation(location.value); + return addressToLocation(address.value); + } + const local_address = getAddressFromLocal(); + if (local_address) { + return addressToLocation(local_address); } if ( !userSettingsLocation.value || @@ -454,9 +466,36 @@ const userLocation = computed(() => { return userSettingsLocation.value; }); +const userAddress = computed({ + get(): IAddress | null { + if (noAddress.value) { + return null; + } + if (address.value) { + return address.value; + } + const local_address = getAddressFromLocal(); + if (local_address) { + return local_address; + } + if ( + !userSettingsLocation.value || + (userSettingsLocation.value?.isIPLocation && + currentUserLocation.value?.name) + ) { + return locationToAddress(currentUserLocation.value); + } + return locationToAddress(userSettingsLocation.value); + }, + set(newAddress: IAddress | null) { + address.value = newAddress; + noAddress.value = newAddress == null; + }, +}); + const distance = computed({ get(): number | null { - if (noLocation.value || !userLocation.value?.name) { + if (noAddress.value || !userLocation.value?.name) { return null; } else if (current_distance.value == null) { return userLocation.value?.isIPLocation ? 150 : 25; @@ -528,8 +567,13 @@ const performGeoLocation = () => { ); }; -const updateLocation = (newlocation: IAddress | null) => { - noLocation.value = newlocation == null; +const updateAddress = (newAddress: IAddress | null) => { + if (address.value?.geom != newAddress?.geom || newAddress == null) { + increated.value += 1; + storeAddressInLocal(newAddress); + } + address.value = newAddress; + noAddress.value = newAddress == null; }; /** diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 37d242361..5907ba402 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -3,8 +3,9 @@ <search-fields class="md:ml-10 mr-2" v-model:search="search" - v-model:location="location" - :locationDefaultText="locationName" + v-model:address="address" + v-model:distance="radius" + :addressDefaultText="addressName" :fromLocalStorage="true" /> </div> @@ -155,43 +156,6 @@ </template> </filter-section> - <filter-section - v-show="!isOnline" - v-model:opened="searchFilterSectionsOpenStatus.eventDistance" - :title="t('Distance')" - > - <template #options> - <fieldset class="flex flex-col"> - <legend class="sr-only">{{ t("Distance") }}</legend> - <div - v-for="distanceOption in eventDistance" - :key="distanceOption.id" - > - <input - :id="distanceOption.id" - v-model="distance" - type="radio" - name="eventDistance" - :value="distanceOption.id" - class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600" - /> - <label - :for="distanceOption.id" - class="cursor-pointer ml-3 text-sm font-medium text-gray-900 dark:text-gray-300" - >{{ distanceOption.label }}</label - > - </div> - </fieldset> - </template> - <template #preview> - <span - class="bg-blue-100 text-blue-800 text-sm font-semibold p-0.5 rounded dark:bg-blue-200 dark:text-blue-800 grow-0" - > - {{ eventDistance.find(({ id }) => id === distance)?.label }} - </span> - </template> - </filter-section> - <filter-section v-show="contentType !== 'GROUPS'" v-model:opened="searchFilterSectionsOpenStatus.eventCategory" @@ -620,7 +584,7 @@ :contentType="contentType" :latitude="latitude" :longitude="longitude" - :locationName="locationName" + :locationName="addressName" @map-updated="setBounds" :events="searchEvents" :groups="searchGroups" @@ -697,25 +661,25 @@ const EventMarkerMap = defineAsyncComponent( const search = useRouteQuery("search", ""); const searchDebounced = refDebounced(search, 1000); -const locationName = useRouteQuery("locationName", null); -const location = ref<IAddress | null>(null); +const addressName = useRouteQuery("locationName", null); +const address = ref<IAddress | null>(null); -watch(location, (newLocation) => { - console.debug("location change", newLocation); - if (newLocation?.geom) { - latitude.value = parseFloat(newLocation?.geom.split(";")[1]); - longitude.value = parseFloat(newLocation?.geom.split(";")[0]); - locationName.value = newLocation?.description; - console.debug("set location", [ +watch(address, (newAddress: IAddress) => { + console.debug("address change", newAddress); + if (newAddress?.geom) { + latitude.value = parseFloat(newAddress?.geom.split(";")[1]); + longitude.value = parseFloat(newAddress?.geom.split(";")[0]); + addressName.value = newAddress?.description; + console.debug("set address", [ latitude.value, longitude.value, - locationName.value, + addressName.value, ]); } else { - console.debug("location emptied"); + console.debug("address emptied"); latitude.value = undefined; longitude.value = undefined; - locationName.value = null; + addressName.value = null; } }); @@ -759,9 +723,6 @@ 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( @@ -946,75 +907,6 @@ const contentTypeMapping = computed(() => { } }); -const eventDistance = computed(() => { - return [ - { - id: "anywhere", - label: t("Any distance"), - }, - { - id: "5_km", - label: t( - "{number} kilometers", - { - number: 5, - }, - 5 - ), - }, - { - id: "10_km", - label: t( - "{number} kilometers", - { - number: 10, - }, - 10 - ), - }, - { - id: "25_km", - label: t( - "{number} kilometers", - { - number: 25, - }, - 25 - ), - }, - { - id: "50_km", - label: t( - "{number} kilometers", - { - number: 50, - }, - 50 - ), - }, - { - id: "100_km", - label: t( - "{number} kilometers", - { - number: 100, - }, - 100 - ), - }, - { - id: "150_km", - label: t( - "{number} kilometers", - { - number: 150, - }, - 150 - ), - }, - ]; -}); - const eventStatuses = computed(() => { return [ { @@ -1049,7 +941,14 @@ const geoHashLocation = computed(() => coordsToGeoHash(latitude.value, longitude.value) ); -const radius = computed(() => Number.parseInt(distance.value.slice(0, -3))); +const radius = computed({ + get(): number | null { + return Number.parseInt(distance.value.slice(0, -3)); + }, + set(newRadius: number) { + distance.value = newRadius.toString() + "_km"; + }, +}); const longEvents = computed(() => { if (contentType.value === ContentType.EVENTS) { From ea6d1ba83d9c2e20b5b56fa148dc6bc7e11cc009 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 6 Nov 2024 17:36:35 +0100 Subject: [PATCH 37/78] #1492: search page correction + refactoring --- src/components/Local/CloseGroups.vue | 116 -------------------------- src/components/Local/LastEvents.vue | 68 --------------- src/components/Local/OnlineEvents.vue | 75 ----------------- src/components/NavBar.vue | 14 ---- src/views/SearchView.vue | 6 +- 5 files changed, 5 insertions(+), 274 deletions(-) delete mode 100644 src/components/Local/CloseGroups.vue delete mode 100644 src/components/Local/LastEvents.vue delete mode 100644 src/components/Local/OnlineEvents.vue diff --git a/src/components/Local/CloseGroups.vue b/src/components/Local/CloseGroups.vue deleted file mode 100644 index 0f238baef..000000000 --- a/src/components/Local/CloseGroups.vue +++ /dev/null @@ -1,116 +0,0 @@ -<template> - <close-content - class="container mx-auto px-2" - v-show="loading || selectedGroups.length > 0" - @do-geo-loc="emit('doGeoLoc')" - :suggestGeoloc="userLocation.isIPLocation" - :doingGeoloc="doingGeoloc" - > - <template #title> - <template v-if="userLocationName"> - {{ - t("Popular groups nearby {position}", { - position: userLocationName, - }) - }} - </template> - <template v-else> - {{ t("Popular groups close to you") }} - </template> - </template> - <template #content> - <skeleton-group-result - v-for="i in [...Array(6).keys()]" - class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4" - :key="i" - v-show="loading" - /> - <group-card - v-for="group in selectedGroups" - :key="group.id" - :group="group" - :mode="'column'" - :showSummary="false" - /> - - <more-content - v-if="userLocationName" - :to="{ - name: RouteName.SEARCH, - query: { - locationName: userLocationName, - lat: userLocation.lat?.toString(), - lon: userLocation.lon?.toString(), - contentType: 'GROUPS', - distance: `${distance}_km`, - }, - }" - :picture="userLocation.picture" - > - {{ - t("View more groups around {position}", { - position: userLocationName, - }) - }} - </more-content> - </template> - </close-content> -</template> - -<script lang="ts" setup> -import SkeletonGroupResult from "@/components/Group/SkeletonGroupResult.vue"; -import sampleSize from "lodash/sampleSize"; -import { LocationType } from "@/types/user-location.model"; -import MoreContent from "./MoreContent.vue"; -import CloseContent from "./CloseContent.vue"; -import { IGroup } from "@/types/actor"; -import { SEARCH_GROUPS } from "@/graphql/search"; -import { useQuery } from "@vue/apollo-composable"; -import { Paginate } from "@/types/paginate"; -import { computed } from "vue"; -import GroupCard from "@/components/Group/GroupCard.vue"; -import { coordsToGeoHash } from "@/utils/location"; -import { useI18n } from "vue-i18n"; -import RouteName from "@/router/name"; - -const props = defineProps<{ - userLocation: LocationType; - doingGeoloc?: boolean; -}>(); -const emit = defineEmits(["doGeoLoc"]); - -const { t } = useI18n({ useScope: "global" }); - -const userLocation = computed(() => props.userLocation); - -const geoHash = computed(() => - coordsToGeoHash(userLocation.value.lat, userLocation.value.lon) -); - -const distance = computed<number>(() => - userLocation.value?.isIPLocation ? 150 : 25 -); - -const { result: groupsResult, loading: loadingGroups } = useQuery<{ - searchGroups: Paginate<IGroup>; -}>( - SEARCH_GROUPS, - () => ({ - location: geoHash.value, - radius: distance.value, - page: 1, - limit: 12, - }), - () => ({ enabled: geoHash.value !== undefined }) -); - -const groups = computed( - () => groupsResult.value?.searchGroups ?? { total: 0, elements: [] } -); - -const selectedGroups = computed(() => sampleSize(groups.value?.elements, 5)); - -const userLocationName = computed(() => props?.userLocation?.name); - -const loading = computed(() => props.doingGeoloc || loadingGroups.value); -</script> diff --git a/src/components/Local/LastEvents.vue b/src/components/Local/LastEvents.vue deleted file mode 100644 index 959cec8a0..000000000 --- a/src/components/Local/LastEvents.vue +++ /dev/null @@ -1,68 +0,0 @@ -<template> - <close-content - class="container mx-auto px-2" - v-show="loadingEvents || (events && events.total > 0)" - :suggestGeoloc="false" - v-on="attrs" - > - <template #title> - {{ t("Agenda") }} - </template> - <template #content> - <skeleton-event-result - v-for="i in 6" - class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4" - :key="i" - v-show="loadingEvents" - /> - <event-card - v-for="event in events.elements" - :event="event" - :key="event.uuid" - /> - <more-content - :to="{ - name: RouteName.SEARCH, - query: { - contentType: 'EVENTS', - }, - }" - > - {{ t("View more events") }} - </more-content> - </template> - </close-content> -</template> - -<script lang="ts" setup> -import MoreContent from "./MoreContent.vue"; -import CloseContent from "./CloseContent.vue"; -import { computed, useAttrs } from "vue"; -import { IEvent } from "@/types/event.model"; -import { useQuery } from "@vue/apollo-composable"; -import EventCard from "../Event/EventCard.vue"; -import { Paginate } from "@/types/paginate"; -import SkeletonEventResult from "../Event/SkeletonEventResult.vue"; -import { EventSortField, SortDirection } from "@/types/enums"; -import { FETCH_EVENTS } from "@/graphql/event"; -import { useI18n } from "vue-i18n"; -import RouteName from "@/router/name"; - -defineProps<{ - instanceName: string; -}>(); - -const { t } = useI18n({ useScope: "global" }); -const attrs = useAttrs(); - -const { result: resultEvents, loading: loadingEvents } = useQuery<{ - events: Paginate<IEvent>; -}>(FETCH_EVENTS, { - orderBy: EventSortField.BEGINS_ON, - direction: SortDirection.ASC, - longevents: false, -}); -const events = computed( - () => resultEvents.value?.events ?? { total: 0, elements: [] } -); -</script> diff --git a/src/components/Local/OnlineEvents.vue b/src/components/Local/OnlineEvents.vue deleted file mode 100644 index bf4036764..000000000 --- a/src/components/Local/OnlineEvents.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> - <close-content - class="container mx-auto px-2" - :suggest-geoloc="false" - v-show="loadingEvents || (events?.elements && events?.elements.length > 0)" - > - <template #title> - {{ $t("Online upcoming events") }} - </template> - <template #content> - <skeleton-event-result - v-for="i in [...Array(6).keys()]" - class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4" - :key="i" - v-show="loadingEvents" - /> - <event-card - class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]" - v-for="event in events?.elements" - :key="event.id" - :event="event" - mode="column" - /> - <more-content - :to="{ - name: RouteName.SEARCH, - query: { - contentType: 'EVENTS', - isOnline: 'true', - }, - }" - :picture="{ - url: '/img/online-event.webp', - author: { - name: 'Chris Montgomery', - url: 'https://unsplash.com/@cwmonty', - }, - source: { - name: 'Unsplash', - url: 'https://unsplash.com/?utm_source=Mobilizon&utm_medium=referral', - }, - }" - > - {{ $t("View more online events") }} - </more-content> - </template> - </close-content> -</template> - -<script lang="ts" setup> -import { computed } from "vue"; -import SkeletonEventResult from "@/components/Event/SkeletonEventResult.vue"; -import MoreContent from "./MoreContent.vue"; -import CloseContent from "./CloseContent.vue"; -import { SEARCH_EVENTS } from "@/graphql/search"; -import EventCard from "@/components/Event/EventCard.vue"; -import { useQuery } from "@vue/apollo-composable"; -import RouteName from "@/router/name"; -import { Paginate } from "@/types/paginate"; -import { IEvent } from "@/types/event.model"; - -const EVENT_PAGE_LIMIT = 12; - -const { result: searchEventResult, loading: loadingEvents } = useQuery<{ - searchEvents: Paginate<IEvent>; -}>(SEARCH_EVENTS, () => ({ - beginsOn: new Date(), - endsOn: undefined, - eventPage: 1, - limit: EVENT_PAGE_LIMIT, - type: "ONLINE", -})); - -const events = computed(() => searchEventResult.value?.searchEvents); -</script> diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index 31c1e966e..fdcf298ad 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -179,13 +179,6 @@ <ul class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold" > - <search-fields - v-if="showMobileMenu" - class="m-auto w-auto" - v-model:search="search" - v-model:location="location" - /> - <li class="m-auto"> <router-link :to="{ @@ -257,13 +250,6 @@ >{{ t("Register") }}</router-link > </li> - - <search-fields - v-if="!showMobileMenu" - class="m-auto w-auto" - v-model:search="search" - v-model:location="location" - /> </ul> </div> </div> diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 5907ba402..5ffdd8c62 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -943,7 +943,11 @@ const geoHashLocation = computed(() => const radius = computed({ get(): number | null { - return Number.parseInt(distance.value.slice(0, -3)); + if (addressName.value) { + return Number.parseInt(distance.value.slice(0, -3)); + } else { + return null; + } }, set(newRadius: number) { distance.value = newRadius.toString() + "_km"; From 3885c8e62fe9bef4f138d6a499931f6eec54cac7 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 6 Nov 2024 17:44:54 +0100 Subject: [PATCH 38/78] #1492: address distance correction --- src/components/Home/SearchFields.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index ea3b24eeb..34dc3d99b 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -30,8 +30,13 @@ v-on:update:modelValue="modelValueUpdate" > <o-dropdown v-model="distance" position="bottom-right" v-if="distance"> - <template #trigger> - <o-button :title="t('Select distance')">{{ distanceText }}</o-button> + <template #trigger="{ active }"> + <o-button + :title="t('Select distance')" + :icon-right="active ? 'menu-up' : 'menu-down'" + > + {{ distanceText }} + </o-button> </template> <o-dropdown-item v-for="distance_item in distanceList" From 0c56267795a13c11f2da13a9569c5650bb8f9874 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 6 Nov 2024 18:43:24 +0100 Subject: [PATCH 39/78] test elixir correction --- test/graphql/resolvers/config_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/graphql/resolvers/config_test.exs b/test/graphql/resolvers/config_test.exs index 71ecb4cf2..1730ede7e 100644 --- a/test/graphql/resolvers/config_test.exs +++ b/test/graphql/resolvers/config_test.exs @@ -113,9 +113,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ConfigTest do assert res["data"]["config"]["long_description"] == nil assert res["data"]["config"]["slogan"] == nil assert res["data"]["config"]["languages"] == [] - assert length(res["data"]["config"]["timezones"]) == 596 + assert length(res["data"]["config"]["timezones"]) == 594 assert res["data"]["config"]["rules"] == nil - assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.0.0" + assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.0.1" assert res["data"]["config"]["federating"] == true end From 3a3d77b69835e63a49e83484880f8734225d783e Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 6 Nov 2024 15:46:54 +0100 Subject: [PATCH 40/78] Issue #1511: Remove useUserSettings() and always add user settings in useLoggedUser() The user time zone setting is now available in the event display page --- .../Settings/NotificationsOnboarding.vue | 4 ++-- .../Settings/SettingsOnboarding.vue | 4 ++-- src/composition/apollo/user.ts | 20 ++++--------------- src/graphql/user.ts | 19 ++++++++++++++++++ src/views/Event/EditView.vue | 4 ++-- src/views/Settings/PreferencesView.vue | 4 ++-- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/Settings/NotificationsOnboarding.vue b/src/components/Settings/NotificationsOnboarding.vue index cf4acc01c..ad729243e 100644 --- a/src/components/Settings/NotificationsOnboarding.vue +++ b/src/components/Settings/NotificationsOnboarding.vue @@ -41,12 +41,12 @@ </template> <script lang="ts" setup> // import { SnackbarProgrammatic as Snackbar } from "buefy"; -import { doUpdateSetting, useUserSettings } from "@/composition/apollo/user"; +import { doUpdateSetting, useLoggedUser } from "@/composition/apollo/user"; import { onMounted, ref } from "vue"; const notificationOnDay = ref(true); -const { loggedUser } = useUserSettings(); +const { loggedUser } = useLoggedUser(); const updateSetting = async ( variables: Record<string, unknown> diff --git a/src/components/Settings/SettingsOnboarding.vue b/src/components/Settings/SettingsOnboarding.vue index 383d7d25f..795242765 100644 --- a/src/components/Settings/SettingsOnboarding.vue +++ b/src/components/Settings/SettingsOnboarding.vue @@ -55,7 +55,7 @@ import { useTimezones } from "@/composition/apollo/config"; import { doUpdateSetting, updateLocale, - useUserSettings, + useLoggedUser, } from "@/composition/apollo/user"; import { saveLocaleData } from "@/utils/auth"; import { computed, onMounted, watch } from "vue"; @@ -68,7 +68,7 @@ const { t, locale } = useI18n({ useScope: "global" }); const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; -const { loggedUser } = useUserSettings(); +const { loggedUser } = useLoggedUser(); const { mutate: doUpdateLocale } = updateLocale(); diff --git a/src/composition/apollo/user.ts b/src/composition/apollo/user.ts index 288645cd9..c2f5033e9 100644 --- a/src/composition/apollo/user.ts +++ b/src/composition/apollo/user.ts @@ -1,11 +1,10 @@ import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor"; import { CURRENT_USER_CLIENT, - LOGGED_USER, + LOGGED_USER_AND_SETTINGS, LOGGED_USER_LOCATION, SET_USER_SETTINGS, UPDATE_USER_LOCALE, - USER_SETTINGS, } from "@/graphql/user"; import { IPerson } from "@/types/actor"; import { ICurrentUser, IUser } from "@/types/current-user.model"; @@ -31,25 +30,14 @@ export function useCurrentUserClient() { export function useLoggedUser() { const { currentUser } = useCurrentUserClient(); - const { result, error, onError } = useQuery<{ loggedUser: IUser }>( - LOGGED_USER, + const { result, error, onError, loading } = useQuery<{ loggedUser: IUser }>( + LOGGED_USER_AND_SETTINGS, {}, () => ({ enabled: currentUser.value?.id != null }) ); const loggedUser = computed(() => result.value?.loggedUser); - return { loggedUser, error, onError }; -} - -export function useUserSettings() { - const { - result: userSettingsResult, - error, - loading, - } = useQuery<{ loggedUser: IUser }>(USER_SETTINGS); - - const loggedUser = computed(() => userSettingsResult.value?.loggedUser); - return { loggedUser, error, loading }; + return { loggedUser, error, onError, loading }; } export function useUserLocation() { diff --git a/src/graphql/user.ts b/src/graphql/user.ts index 1d21080cd..22e476b1b 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -125,6 +125,25 @@ export const USER_SETTINGS_FRAGMENT = gql` } `; +export const LOGGED_USER_AND_SETTINGS = gql` + query LoggedUserQuery { + loggedUser { + id + email + locale + provider + defaultActor { + ...ActorFragment + } + settings { + ...UserSettingFragment + } + } + } + ${ACTOR_FRAGMENT} + ${USER_SETTINGS_FRAGMENT} +`; + export const USER_SETTINGS = gql` query UserSetting { loggedUser { diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index c1b79002e..4fbe885fb 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -632,7 +632,7 @@ import { useCurrentUserIdentities, usePersonStatusGroup, } from "@/composition/apollo/actor"; -import { useUserSettings } from "@/composition/apollo/user"; +import { useLoggedUser } from "@/composition/apollo/user"; import { computed, inject, @@ -664,7 +664,7 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10; const { eventCategories } = useEventCategories(); const { anonymousParticipationConfig } = useAnonymousParticipationConfig(); const { currentActor } = useCurrentActorClient(); -const { loggedUser } = useUserSettings(); +const { loggedUser } = useLoggedUser(); const { identities } = useCurrentUserIdentities(); const { features } = useFeatures(); diff --git a/src/views/Settings/PreferencesView.vue b/src/views/Settings/PreferencesView.vue index cd3917866..810fd1889 100644 --- a/src/views/Settings/PreferencesView.vue +++ b/src/views/Settings/PreferencesView.vue @@ -147,7 +147,7 @@ import RouteName from "../../router/name"; import { AddressSearchType } from "@/types/enums"; import { Address, IAddress } from "@/types/address.model"; import { useTimezones } from "@/composition/apollo/config"; -import { useUserSettings, updateLocale } from "@/composition/apollo/user"; +import { useLoggedUser, updateLocale } from "@/composition/apollo/user"; import { useHead } from "@/utils/head"; import { computed, defineAsyncComponent, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; @@ -159,7 +159,7 @@ const FullAddressAutoComplete = defineAsyncComponent( const { timezones: serverTimezones, loading: loadingTimezones } = useTimezones(); -const { loggedUser, loading: loadingUserSettings } = useUserSettings(); +const { loggedUser, loading: loadingUserSettings } = useLoggedUser(); const { t } = useI18n({ useScope: "global" }); From 0fa7a204141251cf4b260644577a83136d8de41e Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 6 Nov 2024 16:16:55 +0100 Subject: [PATCH 41/78] Issue #1511: Date display problem : handle case where event start time is hidden but event end time is displayed --- src/components/Event/EventFullDate.vue | 93 +++++++++++++++++--------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/src/components/Event/EventFullDate.vue b/src/components/Event/EventFullDate.vue index 55d2f8756..30354084d 100644 --- a/src/components/Event/EventFullDate.vue +++ b/src/components/Event/EventFullDate.vue @@ -12,35 +12,47 @@ {{ singleTimeZone }} </o-switch> </p> - <p v-else-if="isSameDay() && showStartTime && showEndTime"> - <span>{{ - t("On {date} from {startTime} to {endTime}", { - date: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), - endTime: formatTime(endsOn, timezoneToShow), - }) - }}</span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ singleTimeZone }} - </o-switch> - </p> - <p v-else-if="isSameDay() && showStartTime && !showEndTime"> - {{ - t("On {date} starting at {startTime}", { - date: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), - }) - }} - </p> - <p v-else-if="isSameDay()"> - {{ t("On {date}", { date: formatDate(beginsOn) }) }} - </p> - <p v-else-if="endsOn && showStartTime && showEndTime"> + <!-- endsOn is set and isSameDay() --> + <template v-else-if="isSameDay()"> + <p v-if="showStartTime && showEndTime"> + <span>{{ + t("On {date} from {startTime} to {endTime}", { + date: formatDate(beginsOn), + startTime: formatTime(beginsOn, timezoneToShow), + endTime: formatTime(endsOn, timezoneToShow), + }) + }}</span> + <br /> + <o-switch + size="small" + v-model="showLocalTimezone" + v-if="differentFromUserTimezone" + > + {{ singleTimeZone }} + </o-switch> + </p> + <p v-else-if="showStartTime && !showEndTime"> + {{ + t("On {date} starting at {startTime}", { + date: formatDate(beginsOn), + startTime: formatTime(beginsOn, timezoneToShow), + }) + }} + </p> + <p v-else-if="!showStartTime && showEndTime"> + {{ + t("On {date} ending at {endTime}", { + date: formatDate(beginsOn), + endTime: formatTime(endsOn, timezoneToShow), + }) + }} + </p> + <p v-else> + {{ t("On {date}", { date: formatDate(beginsOn) }) }} + </p> + </template> + <!-- endsOn is set and !isSameDay() --> + <p v-else-if="showStartTime && showEndTime"> <span> {{ t("From the {startDate} at {startTime} to the {endDate} at {endTime}", { @@ -60,7 +72,7 @@ {{ multipleTimeZones }} </o-switch> </p> - <p v-else-if="endsOn && showStartTime"> + <p v-else-if="showStartTime && !showEndTime"> <span> {{ t("From the {startDate} at {startTime} to the {endDate}", { @@ -79,7 +91,26 @@ {{ singleTimeZone }} </o-switch> </p> - <p v-else-if="endsOn"> + <p v-else-if="!showStartTime && showEndTime"> + <span> + {{ + t("From the {startDate} to the {endDate} at {endTime}", { + startDate: formatDate(beginsOn), + endDate: formatDate(endsOn), + endTime: formatTime(endsOn, timezoneToShow), + }) + }} + </span> + <br /> + <o-switch + size="small" + v-model="showLocalTimezone" + v-if="differentFromUserTimezone" + > + {{ singleTimeZone }} + </o-switch> + </p> + <p v-else> {{ t("From the {startDate} to the {endDate}", { startDate: formatDate(beginsOn), From 3c0db7877c94b2ef3abff682bfb62ccc8cf6717d Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 6 Nov 2024 18:22:58 +0100 Subject: [PATCH 42/78] Issue #1511: Date display problem : - the time zone is used to calculate the date - use the offset to know if beginsOn and endsOn are the same day - refactor the <o-switch> Solves, for example, this display problem : The date range "2024-12-31 17:45 to 2025-01-01 01:00" in the Asia/Shanghai time zone (spanning different years and days) is equivalent to "2024-12-31 at 10:45 to 2024-12-31 at 18:00" in the Europe/Paris time zone (same year and same day). --- src/components/Event/EventFullDate.vue | 110 +++++++++---------------- src/filters/datetime.ts | 5 +- 2 files changed, 41 insertions(+), 74 deletions(-) diff --git a/src/components/Event/EventFullDate.vue b/src/components/Event/EventFullDate.vue index 30354084d..74494df8f 100644 --- a/src/components/Event/EventFullDate.vue +++ b/src/components/Event/EventFullDate.vue @@ -3,14 +3,6 @@ <span>{{ formatDateTimeString(beginsOn, timezoneToShow, showStartTime) }}</span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ singleTimeZone }} - </o-switch> </p> <!-- endsOn is set and isSameDay() --> <template v-else-if="isSameDay()"> @@ -18,24 +10,16 @@ <span>{{ t("On {date} from {startTime} to {endTime}", { date: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), - endTime: formatTime(endsOn, timezoneToShow), + startTime: formatTime(beginsOn), + endTime: formatTime(endsOn), }) }}</span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ singleTimeZone }} - </o-switch> </p> <p v-else-if="showStartTime && !showEndTime"> {{ t("On {date} starting at {startTime}", { date: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), + startTime: formatTime(beginsOn), }) }} </p> @@ -43,7 +27,7 @@ {{ t("On {date} ending at {endTime}", { date: formatDate(beginsOn), - endTime: formatTime(endsOn, timezoneToShow), + endTime: formatTime(endsOn), }) }} </p> @@ -57,39 +41,23 @@ {{ t("From the {startDate} at {startTime} to the {endDate} at {endTime}", { startDate: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), + startTime: formatTime(beginsOn), endDate: formatDate(endsOn), - endTime: formatTime(endsOn, timezoneToShow), + endTime: formatTime(endsOn), }) }} </span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ multipleTimeZones }} - </o-switch> </p> <p v-else-if="showStartTime && !showEndTime"> <span> {{ t("From the {startDate} at {startTime} to the {endDate}", { startDate: formatDate(beginsOn), - startTime: formatTime(beginsOn, timezoneToShow), + startTime: formatTime(beginsOn), endDate: formatDate(endsOn), }) }} </span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ singleTimeZone }} - </o-switch> </p> <p v-else-if="!showStartTime && showEndTime"> <span> @@ -97,18 +65,10 @@ t("From the {startDate} to the {endDate} at {endTime}", { startDate: formatDate(beginsOn), endDate: formatDate(endsOn), - endTime: formatTime(endsOn, timezoneToShow), + endTime: formatTime(endsOn), }) }} </span> - <br /> - <o-switch - size="small" - v-model="showLocalTimezone" - v-if="differentFromUserTimezone" - > - {{ singleTimeZone }} - </o-switch> </p> <p v-else> {{ @@ -118,6 +78,13 @@ }) }} </p> + <o-switch + size="small" + v-model="showLocalTimezone" + v-if="differentFromUserTimezone" + > + {{ singleTimeZone }} + </o-switch> </template> <script lang="ts" setup> import { @@ -163,33 +130,43 @@ const userActualTimezone = computed((): string => { }); const formatDate = (value: string): string | undefined => { - return formatDateString(value); + return formatDateString(value, timezoneToShow.value ?? "Etc/UTC"); }; -const formatTime = ( - value: string, - timezone: string | undefined = undefined -): string | undefined => { - return formatTimeString(value, timezone ?? "Etc/UTC"); +const formatTime = (value: string): string | undefined => { + return formatTimeString(value, timezoneToShow.value ?? "Etc/UTC"); }; +// We need to compare date after the offset is applied +// Because some date can be in the same day in a time zone, but different day in another. +// Example : From 2025-11-30 at 23:00 to 2025-12-01 01:00 in Asia/Shanghai (different days) +// It is from 2025-11-30 at 16:00 to 2025-11-30 at 18:00 in Europe/Paris (same day) const isSameDay = (): boolean => { if (!props.endsOn) return false; + + const offset = + getTimezoneOffset(timezoneToShow.value ?? "Etc/UTC", new Date()) / + (60 * 1000); + + const beginsOnOffset = new Date(props.beginsOn); + beginsOnOffset.setUTCMinutes(beginsOnOffset.getUTCMinutes() + offset); + + const endsOnOffset = new Date(props.endsOn); + endsOnOffset.setUTCMinutes(endsOnOffset.getUTCMinutes() + offset); + return ( - beginsOnDate.value.toDateString() === new Date(props.endsOn).toDateString() + beginsOnOffset.getUTCFullYear() === endsOnOffset.getUTCFullYear() && + beginsOnOffset.getUTCMonth() === endsOnOffset.getUTCMonth() && + beginsOnOffset.getUTCDate() === endsOnOffset.getUTCDate() ); }; -const beginsOnDate = computed((): Date => { - return new Date(props.beginsOn); -}); - const differentFromUserTimezone = computed((): boolean => { return ( !!props.timezone && !!userActualTimezone.value && - getTimezoneOffset(props.timezone, beginsOnDate.value) !== - getTimezoneOffset(userActualTimezone.value, beginsOnDate.value) && + getTimezoneOffset(props.timezone, new Date()) !== + getTimezoneOffset(userActualTimezone.value, new Date()) && props.timezone !== userActualTimezone.value ); }); @@ -204,15 +181,4 @@ const singleTimeZone = computed((): string => { timezone: timezoneToShow.value, }); }); - -const multipleTimeZones = computed((): string => { - if (showLocalTimezone.value) { - return t("Local times ({timezone})", { - timezone: timezoneToShow.value, - }); - } - return t("Times in your timezone ({timezone})", { - timezone: timezoneToShow.value, - }); -}); </script> diff --git a/src/filters/datetime.ts b/src/filters/datetime.ts index a11933e21..c1f812739 100644 --- a/src/filters/datetime.ts +++ b/src/filters/datetime.ts @@ -8,12 +8,13 @@ function formatDateISOStringWithoutTime(value: string): string { return parseDateTime(value).toISOString().split("T")[0]; } -function formatDateString(value: string): string { +function formatDateString(value: string, timeZone?: string): string { return parseDateTime(value).toLocaleString(locale(), { weekday: "long", year: "numeric", month: "long", day: "numeric", + timeZone: timeZone, }); } @@ -21,7 +22,7 @@ function formatTimeString(value: string, timeZone?: string): string { return parseDateTime(value).toLocaleTimeString(locale(), { hour: "numeric", minute: "numeric", - timeZone, + timeZone: timeZone, }); } From 29c9ded5b7113c84641f98701db37c289a5ffcaa Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 6 Nov 2024 18:37:59 +0100 Subject: [PATCH 43/78] Issue #1511: Date display problem : If an event do not have a time zone, use the user's setting time zone for display or the browser time zone otherwise --- src/components/Event/EventFullDate.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Event/EventFullDate.vue b/src/components/Event/EventFullDate.vue index 74494df8f..1faacc7d9 100644 --- a/src/components/Event/EventFullDate.vue +++ b/src/components/Event/EventFullDate.vue @@ -117,7 +117,7 @@ const showLocalTimezone = ref(true); const timezoneToShow = computed((): string | undefined => { if (showLocalTimezone.value) { - return props.timezone; + return props.timezone ?? userActualTimezone.value; } return userActualTimezone.value; }); From 63a237b05fba4516bfbc876dc50e99124c91313e Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 6 Nov 2024 19:19:32 +0100 Subject: [PATCH 44/78] Update French translation --- src/i18n/fr_FR.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 0fb9ca01a..d516a272b 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -486,6 +486,7 @@ "From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}", "From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}", "From the {startDate} to the {endDate}": "Du {startDate} au {endDate}", + "From the {startDate} to the {endDate} at {endTime}": "Du {startDate} au {endDate} à {endTime}", "From this instance only": "Depuis cette instance uniquement", "From yourself": "De vous", "Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant", @@ -808,7 +809,7 @@ "On foot": "À pied", "On the Fediverse": "Dans le fediverse", "On {date}": "Le {date}", - "On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}", + "On {date} ending at {endTime}": "Le {date} jusqu'à {endTime}", "On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}", "On {date} starting at {startTime}": "Le {date} à partir de {startTime}", "On {instance} and other federated instances": "Sur {instance} et d'autres instances fédérées", From 67393ba2f0738e3f81192eda3662fef948c02779 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 7 Nov 2024 14:22:48 +0100 Subject: [PATCH 45/78] Issue #1569 : New event times and participants are hidden by default --- src/views/Event/EditView.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 4fbe885fb..091e39bc9 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -716,10 +716,12 @@ const setEventTimezoneToUserTimezoneIfUnset = () => { // usefull if the page is loaded from scratch watch(loggedUser, setEventTimezoneToUserTimezoneIfUnset); -const initializeEvent = () => { +const initializeNewEvent = () => { // usefull if the data is already cached setEventTimezoneToUserTimezoneIfUnset(); + // Default values for beginsOn and endsOn + const roundUpTo15Minutes = (time: Date) => { time.setUTCMilliseconds( Math.round(time.getUTCMilliseconds() / 1000) * 1000 @@ -736,6 +738,13 @@ const initializeEvent = () => { beginsOn.value = now; endsOn.value = end; + + // Default values for showStartTime and showEndTime + showStartTime.value = false; + showEndTime.value = false; + + // Default values for hideParticipants + hideParticipants.value = true; }; const organizerActor = computed({ @@ -793,7 +802,7 @@ onMounted(async () => { pictureFile.value = await buildFileFromIMedia(event.value.picture); limitedPlaces.value = eventOptions.value.maximumAttendeeCapacity > 0; if (!(props.isUpdate || props.isDuplicate)) { - initializeEvent(); + initializeNewEvent(); } else { event.value = new EventModel({ ...event.value, From 7c68c9c0c1d0787e0c0937aa636f184335052515 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 7 Nov 2024 14:47:49 +0100 Subject: [PATCH 46/78] Issue #1308: remove MATCH_DESC, MEMBER_COUNT_ASC and CREATED_AT_ASC options for sorting groups Remove MATCH_DESC for events for consistency. Options stays available in backend and GraphQL. --- src/views/SearchView.vue | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 5ffdd8c62..3b4b9bbfb 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -695,7 +695,6 @@ enum ViewMode { } enum EventSortValues { - MATCH_DESC = "MATCH_DESC", CREATED_AT_ASC = "CREATED_AT_ASC", CREATED_AT_DESC = "CREATED_AT_DESC", START_TIME_ASC = "START_TIME_ASC", @@ -704,10 +703,7 @@ enum EventSortValues { } enum GroupSortValues { - MATCH_DESC = "MATCH_DESC", - CREATED_AT_ASC = "CREATED_AT_ASC", CREATED_AT_DESC = "CREATED_AT_DESC", - MEMBER_COUNT_ASC = "MEMBER_COUNT_ASC", MEMBER_COUNT_DESC = "MEMBER_COUNT_DESC", LAST_EVENT_ACTIVITY = "LAST_EVENT_ACTIVITY", } @@ -747,12 +743,12 @@ const searchTarget = useRouteQuery( const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode)); const sortByEvents = useRouteQuery( "sortByEvents", - EventSortValues.MATCH_DESC, + EventSortValues.START_TIME_ASC, enumTransformer(EventSortValues) ); const sortByGroups = useRouteQuery( "sortByGroups", - GroupSortValues.MATCH_DESC, + GroupSortValues.LAST_EVENT_ACTIVITY, enumTransformer(GroupSortValues) ); const bbox = useRouteQuery("bbox", undefined); @@ -971,29 +967,17 @@ const totalCount = computed(() => { const sortOptionsGroups = computed(() => { const options = [ { - key: GroupSortValues.MATCH_DESC, - label: t("Best match"), - }, - { - key: GroupSortValues.MEMBER_COUNT_ASC, - label: t("Increasing number of members"), + key: GroupSortValues.LAST_EVENT_ACTIVITY, + label: t("Last event activity"), }, { key: GroupSortValues.MEMBER_COUNT_DESC, label: t("Decreasing number of members"), }, - { - key: GroupSortValues.CREATED_AT_ASC, - label: t("Increasing creation date"), - }, { key: GroupSortValues.CREATED_AT_DESC, label: t("Decreasing creation date"), }, - { - key: GroupSortValues.LAST_EVENT_ACTIVITY, - label: t("Last event activity"), - }, ]; return options; @@ -1001,10 +985,6 @@ const sortOptionsGroups = computed(() => { const sortOptionsEvents = computed(() => { const options = [ - { - key: EventSortValues.MATCH_DESC, - label: t("Best match"), - }, { key: EventSortValues.START_TIME_ASC, label: t("Event date"), From a23d8eeaf88aa67759f10192d88c9159efd4e099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Z=C3=B6ller?= <tim.zoeller@lambdaschmiede.com> Date: Sun, 18 Aug 2024 17:33:35 +0200 Subject: [PATCH 47/78] Prevent the user menu from extending the screen The user menu always opened to the right. This made the menu extend over the boundary of the screen, add a horizontal scrollbar and cut off the menu. This fix opens the menu to the left, preventing such behavior --- src/components/NavBar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index fdcf298ad..a86643443 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -29,7 +29,7 @@ > </span> </router-link> - <o-dropdown position="bottom-left"> + <o-dropdown position="bottom-right"> <template #trigger> <button type="button" From 68240c74766e07359324a4b5f6c2936337b99bd5 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 8 Nov 2024 16:34:42 +0100 Subject: [PATCH 48/78] Issue #1580 Update French translation --- src/i18n/fr_FR.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index d516a272b..b136e8ae8 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -732,7 +732,7 @@ "Next page": "Page suivante", "Next week": "La semaine prochaine", "No about content yet":"À propos n'est pas encore renseigné", - "No activities found": "Aucun activité trouvé", + "No activities found": "Aucune activité trouvée", "No address defined": "Aucune adresse définie", "No apps authorized yet": "Aucune application autorisée pour le moment", "No categories with public upcoming events on this instance were found.": "Aucune catégorie avec des événements publics à venir n'a été trouvée.", @@ -765,7 +765,7 @@ "No memberships found": "Aucune adhésion trouvée", "No message": "Pas de message", "No moderation logs yet": "Pas encore de journaux de modération", - "No more activity to display.": "Il n'y a plus d'activités à afficher.", + "No more activity to display.": "Il n'y a plus d'activité à afficher.", "No one is participating|One person participating|{going} people participating": "Personne ne participe|Une personne participe|{going} personnes participent", "No open reports yet": "Aucun signalement ouvert pour le moment", "No organized events found": "Aucun événement organisé trouvé", @@ -1185,7 +1185,7 @@ "This application will be able to access all of your informations and post content. Make sure you only approve applications you trust.": "Cette application sera capable d'accéder à toutes vos informations et poster du contenu. Assurez-vous d'approuver uniquement des applications en lesquelles vous avez confiance.", "This application will be allowed to access all of the groups you're a member of": "Cette application pourra accéder à tous les groupes dont vous êtes membres", "This application will be allowed to access group activities in all of the groups you're a member of": "This application will be allowed to access group activities in all of the groups you're a member of", - "This application will be allowed to access your user activity settings": "Cette application sera autorisée a accéder à vos paramètres utilisateur·ice d'activité", + "This application will be allowed to access your user activity settings": "Cette application sera autorisée à accéder à vos paramètres utilisateur·ice d'activité", "This application will be allowed to access your user settings": "Cette application sera autorisée a accéder à vos paramètres utilisateur·ice", "This application will be allowed to create feed tokens": "This application will be allowed to create feed tokens", "This application will be allowed to create group discussions": "This application will be allowed to create group discussions", From dc5c3f6a520e262b36e3ec40fd95604aa5d149c4 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 8 Nov 2024 16:45:52 +0100 Subject: [PATCH 49/78] Update French translation to always use "e-mail" --- src/i18n/fr_FR.json | 68 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index b136e8ae8..c8866d1a9 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -33,7 +33,7 @@ "A resource has been created or updated": "Une resource a été créée ou mise à jour", "A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement", "A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.", - "A validation email was sent to {email}": "Un email de validation a été envoyé à {email}", + "A validation email was sent to {email}": "Un e-mail de validation a été envoyé à {email}", "API": "API", "Abandon editing": "Abandonner la modification", "About": "À propos", @@ -112,7 +112,7 @@ "An event I'm organizing has a new pending participation": "Un événement que j'organise a une nouvelle participation en attente", "An event from one of my groups has been published": "Un événement d'un de mes groupes a été publié", "An event from one of my groups has been updated or deleted": "Un événement d'un de mes groupes a été mis à jour ou supprimé", - "An instance is an installed version of the Mobilizon software running on a server. An instance can be run by anyone using the {mobilizon_software} or other federated apps, aka the “fediverse”. This instance's name is {instance_name}. Mobilizon is a federated network of multiple instances (just like email servers), users registered on different instances may communicate even though they didn't register on the same instance.": "Une instance est une version du logiciel Mobilizon fonctionnant sur un serveur. Une instance peut être gérée par n'importe qui avec le {mobilizon_software} ou d'autres applications fédérées, correspondant au « fediverse ». Cette instance se nomme {instance_name}. Mobilizon est un réseau fédéré de multiples instances (tout comme des serveurs email), des utilisateur·rice·s inscrites sur différentes instances peuvent communiquer bien qu'il·elle·s ne se soient pas enregistré·e·s sur la même instance.", + "An instance is an installed version of the Mobilizon software running on a server. An instance can be run by anyone using the {mobilizon_software} or other federated apps, aka the “fediverse”. This instance's name is {instance_name}. Mobilizon is a federated network of multiple instances (just like email servers), users registered on different instances may communicate even though they didn't register on the same instance.": "Une instance est une version du logiciel Mobilizon fonctionnant sur un serveur. Une instance peut être gérée par n'importe qui avec le {mobilizon_software} ou d'autres applications fédérées, correspondant au « fediverse ». Cette instance se nomme {instance_name}. Mobilizon est un réseau fédéré de multiples instances (tout comme des serveurs e-mail), des utilisateur·rice·s inscrites sur différentes instances peuvent communiquer bien qu'il·elle·s ne se soient pas enregistré·e·s sur la même instance.", "An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events on your behalf, automatically and remotely.": "Une « interface de programmation d’application » ou « API » est un protocole de communication qui permet aux composants logiciels de communiquer entre eux. L'API Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec les instances Mobilizon pour effectuer certaines actions, telles que la publication d'événements en votre nom, automatiquement et à distance.", "An “application programming interface” or “API” is a communication protocol that allows software components to communicate with each other. The Mobilizon API, for example, can allow third-party software tools to communicate with Mobilizon instances to carry out certain actions, such as posting events, automatically and remotely.": "Une « interface de programmation d'application » ou « API » est un protocole de communication qui permet à des composants logiciels de communiquer entre eux. L'API de Mobilizon, par exemple, peut permettre à des outils logiciels tiers de communiquer avec des instances de Mobilizon pour effectuer certaines actions, comme la publication d'événements, automatiquement et à distance.", "And {number} comments": "Et {number} commentaires", @@ -120,7 +120,7 @@ "Announcements and mentions notifications are always sent straight away.": "Les notifications d'annonces et de mentions sont toujours envoyées directement.", "Announcements for {eventTitle}": "Annonces pour {eventTitle}", "Anonymous participant": "Participant·e anonyme", - "Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.", + "Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par e-mail.", "Anonymous participations": "Participations anonymes", "Any category": "N'importe quelle catégorie", "Any day": "N'importe quand", @@ -187,7 +187,7 @@ "By {group}": "Par {group}", "By {username}": "Par {username}", "Calendar": "Calendrier", - "Can be an email or a link, or just plain text.": "Peut être une adresse email ou bien un lien, ou alors du simple texte brut.", + "Can be an email or a link, or just plain text.": "Peut être une adresse e-mail ou bien un lien, ou alors du simple texte brut.", "Cancel": "Annuler", "Cancel anonymous participation": "Annuler ma participation anonyme", "Cancel creation": "Annuler la création", @@ -205,14 +205,14 @@ "Category illustrations credits": "Crédits des illustrations des catégories", "Category list": "Liste des catégories", "Change": "Modifier", - "Change email": "Changer l'email", + "Change email": "Changer l'e-mail", "Change my email": "Changer mon adresse e-mail", "Change my identity…": "Changer mon identité…", "Change my password": "Modifier mon mot de passe", "Change role": "Changer le role", "Change the filters.": "Changez les filtres.", "Change timezone": "Changer de fuseau horaire", - "Change user email": "Modifier l'email de l'utilisateur·ice", + "Change user email": "Modifier l'e-mail de l'utilisateur·ice", "Change user role": "Changer le role de l'utilisateur", "Check your device to continue. You may now close this window.": "Vérifiez votre appareil pour continuer. Vous pouvez maintenant fermer cette fenêtre.", "Check your inbox (and your junk mail folder).": "Vérifiez votre boîte de réception (et votre dossier des indésirables).", @@ -371,21 +371,21 @@ "Edit": "Modifier", "Edit post": "Éditer le billet", "Edit profile {profile}": "Éditer le profil {profile}", - "Edit user email": "Éditer l'email de l'utilisateur·ice", + "Edit user email": "Éditer l'e-mail de l'utilisateur·ice", "Edited {ago}": "Édité il y a {ago}", "Edited {relative_time} ago": "Édité il y a {relative_time}", "Eg: Stockholm, Dance, Chess…": "Par exemple : Lyon, Danse, Bridge…", "Either on the {instance} instance or on another instance.": "Sur l'instance {instance} ou bien sur une autre instance.", "Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est incorrect.", - "Either the email has already been changed, either the validation token is incorrect.": "Soit l'adresse email a déjà été modifiée, soit le jeton de validation est incorrect.", + "Either the email has already been changed, either the validation token is incorrect.": "Soit l'adresse e-mail a déjà été modifiée, soit le jeton de validation est incorrect.", "Either the participation request has already been validated, either the validation token is incorrect.": "Soit la demande de participation a déjà été validée, soit le jeton de validation est incorrect.", "Either your participation has already been cancelled, either the validation token is incorrect.": "Soit votre participation a déjà été annulée, soit le jeton de validation est incorrect.", "Element title": "Titre de l'élement", "Element value": "Valeur de l'élement", "Email": "Courriel", - "Email address": "Adresse email", - "Email validate": "Validation de l'email", - "Emails usually don't contain capitals, make sure you haven't made a typo.": "Les emails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe.", + "Email address": "Adresse e-mail", + "Email validate": "Validation de l'e-mail", + "Emails usually don't contain capitals, make sure you haven't made a typo.": "Les e-mails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe.", "Enabled": "Activé", "Ends on…": "Se termine le…", "Enter the code displayed on your device": "Saisissez le code affiché sur votre appareil", @@ -399,7 +399,7 @@ "Error stacktrace": "Trace d'appels de l'erreur", "Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}", "Error while cancelling your participation": "Erreur lors de l'annulation de votre participation", - "Error while changing email": "Erreur lors de la modification de l'adresse email", + "Error while changing email": "Erreur lors de la modification de l'adresse e-mail", "Error while loading the preview": "Erreur lors du chargement de l'aperçu", "Error while login with {provider}. Retry or login another way.": "Erreur lors de la connexion avec {provider}. Réessayez ou bien connectez vous autrement.", "Error while login with {provider}. This login provider doesn't exist.": "Erreur lors de la connexion avec {provider}. Cette méthode de connexion n'existe pas.", @@ -559,10 +559,10 @@ "Identity {displayName} deleted": "Identité {displayName} supprimée", "Identity {displayName} updated": "Identité {displayName} mise à jour", "If allowed by organizer": "Si autorisé par l'organisateur·rice", - "If an account with this email exists, we just sent another confirmation email to {email}": "Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à {email}", + "If an account with this email exists, we just sent another confirmation email to {email}": "Si un compte avec un tel e-mail existe, nous venons juste d'envoyer un nouvel e-mail de confirmation à {email}", "If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "Si cette identité est la seule administratrice de certains groupes, vous devez les supprimer avant de pouvoir supprimer cette identité.", "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :", - "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.", + "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un e-mail pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.", "If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur·ice de l'événement ci-dessous.", "Ignore": "Ignorer", "Illustration picture for “{category}” by {author} on {source} ({license})": "Image d'illustration pour “{category}” par {author} sur {source} ({license})", @@ -693,7 +693,7 @@ "Mobilizon software": "logiciel Mobilizon", "Mobilizon uses a system of profiles to compartiment your activities. You will be able to create as many profiles as you want.": "Mobilizon utilise un système de profils pour compartimenter vos activités. Vous pourrez créer autant de profils que vous voulez.", "Mobilizon version": "Version de Mobilizon", - "Mobilizon will send you an email when the events you are attending have important changes: date and time, address, confirmation or cancellation, etc.": "Mobilizon vous enverra un email lors de changements importants pour les événements auxquels vous participez : date et heure, adresse, confirmation ou annulation, etc.", + "Mobilizon will send you an email when the events you are attending have important changes: date and time, address, confirmation or cancellation, etc.": "Mobilizon vous enverra un e-mail lors de changements importants pour les événements auxquels vous participez : date et heure, adresse, confirmation ou annulation, etc.", "Moderate new members": "Modérer les nouvelles et nouveaux membres", "Moderated comments (shown after approval)": "Commentaires modérés (affichés après validation)", "Moderation": "Modération", @@ -860,7 +860,7 @@ "Participants": "Participant·e·s", "Participants to {eventTitle}": "Participant·es à {eventTitle}", "Participate": "Participer", - "Participate using your email address": "Participer en utilisant votre adresse email", + "Participate using your email address": "Participer en utilisant votre adresse e-mail", "Participation approval": "Validation des participations", "Participation confirmation": "Confirmation de votre participation", "Participation notifications": "Notifications de participation", @@ -882,7 +882,7 @@ "Pick an identity": "Choisissez une identité", "Pick an instance": "Choisir une instance", "Please add as many details as possible to help identify the problem.": "Merci d'ajouter un maximum de détails afin d'aider à identifier le problème.", - "Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.", + "Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'e-mail.", "Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur·rice de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.", "Please do not use it in any real way.": "Merci de ne pas en faire une utilisation réelle.", "Please enter your password to confirm this action.": "Merci d'entrer votre mot de passe pour confirmer cette action.", @@ -903,7 +903,7 @@ "Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Propulsé par {mobilizon}. © 2018 - {date} Les contributeur·rice·s Mobilizon - Fait avec le soutien financier de {contributors}.", "Preferences": "Préférences", "Previous": "Précédent", - "Previous email": "Email précédent", + "Previous email": "E-mail précédent", "Previous month": "Mois précédent", "Previous page": "Page précédente", "Price sheet": "Feuille des prix", @@ -998,8 +998,8 @@ "Reports": "Signalements", "Reports list": "Liste des signalements", "Request for participation confirmation sent": "Demande de confirmation de participation envoyée", - "Resend confirmation email": "Envoyer à nouveau l'email de confirmation", - "Resent confirmation email": "Réenvoi de l'email de confirmation", + "Resend confirmation email": "Envoyer à nouveau l'e-mail de confirmation", + "Resent confirmation email": "Réenvoi de l'e-mail de confirmation", "Reset": "Remettre à zéro", "Reset filters": "Réinitialiser les filtres", "Reset my password": "Réinitialiser mon mot de passe", @@ -1031,14 +1031,14 @@ "Select all resources": "Sélectionner toutes les ressources", "Select distance": "Sélectionner la distance", "Select languages": "Choisissez une langue", - "Select the activities for which you wish to receive an email or a push notification.": "Sélectionnez les activités pour lesquelles vous souhaitez recevoir un email ou une notification push.", + "Select the activities for which you wish to receive an email or a push notification.": "Sélectionnez les activités pour lesquelles vous souhaitez recevoir un e-mail ou une notification push.", "Select this resource": "Sélectionner cette ressource", "Send": "Envoyer", - "Send email": "Envoyer un email", + "Send email": "Envoyer un e-mail", "Send feedback": "Envoyer vos remarques", "Send notification e-mails": "Envoyer des e-mails de notification", "Send password reset": "Envoi de la réinitalisation du mot de passe", - "Send the confirmation email again": "Envoyer l'email de confirmation à nouveau", + "Send the confirmation email again": "Envoyer l'e-mail de confirmation à nouveau", "Send the report": "Envoyer le signalement", "Sent to {count} participants": "Envoyé à aucun·e participant·e|Envoyé à une participant·e|Envoyé à {count} participant·es", "Set an URL to a page with your own privacy policy.": "Entrez une URL vers une page web avec votre propre politique de confidentialité.", @@ -1105,7 +1105,7 @@ "The URL where the event can be watched live": "L'URL où l'événement peut être visionné en direct", "The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé", "The Zoom video teleconference URL": "L'URL de visio-conférence Zoom", - "The account's email address was changed. Check your emails to verify it.": "L'adresse email du compte a été modifiée. Vérifiez vos emails pour confirmer le changement.", + "The account's email address was changed. Check your emails to verify it.": "L'adresse e-mail du compte a été modifiée. Vérifiez vos e-mails pour confirmer le changement.", "The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participant·e·s peut être différent, car cet événement provient d'une autre instance.", "The calc will be created on {service}": "Le calc sera créé sur {service}", "The content came from another server. Transfer an anonymous copy of the report?": "Le contenu provient d'une autre instance. Transférer une copie anonyme du signalement ?", @@ -1176,7 +1176,7 @@ "These events may interest you": "Ces événements peuvent vous intéresser", "These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.", "These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.", - "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur·ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par email.", + "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur·ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par e-mail.", "This URL doesn't seem to be valid": "Cette URL ne semble pas être valide", "This URL is not supported": "Cette URL n'est pas supportée", "This announcement will be send to all participants with the statuses selected below. They will not be allowed to reply to your announcement, but they can create a new conversation with you.": "Cette annonce sera envoyée à tous les participant·es ayant le statut sélectionné ci-dessous. Iels ne pourront pas répondre à votre annonce, mais iels peuvent créer une nouvelle conversation avec vous.", @@ -1337,7 +1337,7 @@ "Username": "Identifiant", "Users": "Utilisateur·rice·s", "Validating account": "Validation du compte", - "Validating email": "Validation de l'email", + "Validating email": "Validation de l'e-mail", "Video Conference": "Visio-conférence", "View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses", "View account on {hostname} (in a new window)": "Voir le compte sur {hostname} (dans une nouvelle fenêtre)", @@ -1368,11 +1368,11 @@ "We collect your feedback and the error information in order to improve this service.": "Nous recueillons vos réactions et les informations sur les erreurs afin d'améliorer ce service.", "We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "Nous n'avons pas pu sauvegarder votre participation dans ce navigateur. Aucune inquiétude, vous avez bien confirmé votre participation, nous n'avons juste pas pu enregistrer son statut dans ce navigateur à cause d'un souci technique.", "We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Nous améliorons ce logiciel grâce à vos retours. Pour nous avertir de ce problème, vous avez deux possibilités (les deux requièrent toutefois la création d'un compte) :", - "We just sent an email to {email}": "Nous venons d'envoyer un email à {email}", + "We just sent an email to {email}": "Nous venons d'envoyer un e-mail à {email}", "We use your timezone to make sure you get notifications for an event at the correct time.": "Nous utilisons votre fuseau horaire pour nous assurer que vous recevez les notifications pour un événement au bon moment.", "We will redirect you to your instance in order to interact with this event": "Nous vous redirigerons vers votre instance pour interagir avec cet événement", "We will redirect you to your instance in order to interact with this group": "Nous vous redirigerons vers votre instance afin que vous puissiez interagir avec ce groupe", - "We'll send you an email one hour before the event begins, to be sure you won't forget about it.": "Nous vous enverrons un email une heure avant que l'événement débute, pour être sûr que vous ne l'oubliez pas.", + "We'll send you an email one hour before the event begins, to be sure you won't forget about it.": "Nous vous enverrons un e-mail une heure avant que l'événement débute, pour être sûr que vous ne l'oubliez pas.", "We'll use your timezone settings to send a recap of the morning of the event.": "Nous prendrons en compte votre fuseau horaire pour vous envoyer un récapitulatif de vos événements le matin.", "Website": "Site web", "Website / URL": "Site web / URL", @@ -1424,7 +1424,7 @@ "You can try another search term or drag and drop the marker on the map": "Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte", "You can't change your password because you are registered through {provider}.": "Vous ne pouvez pas changer votre mot de passe car vous vous êtes enregistré via {provider}.", "You can't use push notifications in this browser.": "Vous ne pouvez pas utiliser les notifications push dans ce navigateur.", - "You changed your email or password": "Vous avez modifié votre email ou votre mot de passe", + "You changed your email or password": "Vous avez modifié votre e-mail ou votre mot de passe", "You created the discussion {discussion}.": "Vous avez créé la discussion {discussion}.", "You created the event {event}.": "Vous avez créé l'événement {event}.", "You created the folder {resource}.": "Vous avez créé le dossier {resource}.", @@ -1497,7 +1497,7 @@ "You'll get a weekly recap every Monday for upcoming events, if you have any.": "Vous recevrez un récapitulatif hebdomadaire chaque lundi pour les événements de la semaine, si vous en avez.", "You'll need to change the URLs where there were previously entered.": "Vous devrez changer les URLs là où vous les avez entrées précédemment.", "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "Vous aurez besoin de transmettre l'URL du groupe pour que d'autres personnes accèdent au profil du groupe. Le groupe ne sera pas trouvable dans la recherche de Mobilizon ni dans les moteurs de recherche habituels.", - "You'll receive a confirmation email.": "Vous recevrez un email de confirmation.", + "You'll receive a confirmation email.": "Vous recevrez un e-mail de confirmation.", "YouTube live": "Direct sur YouTube", "YouTube replay": "Replay sur YouTube", "Your account has been successfully deleted": "Votre compte a été supprimé avec succès", @@ -1508,10 +1508,10 @@ "Your city or region and the radius will only be used to suggest you events nearby. The event radius will consider the administrative center of the area.": "Votre ville ou région et le rayon seront uniquement utilisés pour vous suggérer des événements proches. Le rayon des événements proches sera calculé par rapport au centre administratif de la zone.", "Your current email is {email}. You use it to log in.": "Votre adresse e-mail actuelle est {email}. Vous l'utilisez pour vous connecter.", "Your email": "Votre adresse e-mail", - "Your email address was automatically set based on your {provider} account.": "Votre adresse email a été définie automatiquement en se basant sur votre compte {provider}.", - "Your email has been changed": "Votre adresse email a bien été modifiée", - "Your email is being changed": "Votre adresse email est en train d'être modifiée", - "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Votre email sera uniquement utilisé pour confirmer que vous êtes bien une personne réelle et vous envoyer des éventuelles mises à jour pour cet événement. Il ne sera PAS transmis à d'autres instances ou à l'organisateur de l'événement.", + "Your email address was automatically set based on your {provider} account.": "Votre adresse e-mail a été définie automatiquement en se basant sur votre compte {provider}.", + "Your email has been changed": "Votre adresse e-mail a bien été modifiée", + "Your email is being changed": "Votre adresse e-mail est en train d'être modifiée", + "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Votre e-mail sera uniquement utilisé pour confirmer que vous êtes bien une personne réelle et vous envoyer des éventuelles mises à jour pour cet événement. Il ne sera PAS transmis à d'autres instances ou à l'organisateur de l'événement.", "Your federated identity": "Votre identité fédérée", "Your membership is pending approval": "Votre adhésion est en attente d'approbation", "Your membership was approved by {profile}.": "Votre demande d'adhésion a été approuvée par {profile}.", From 800a4e0d1e507d5ac1837d20dee6a444d36e94f6 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 8 Nov 2024 16:56:51 +0100 Subject: [PATCH 50/78] Issue #1573 : Update French translation --- src/i18n/fr_FR.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index c8866d1a9..df33c992f 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -305,8 +305,8 @@ "Deactivate notifications": "Désactiver les notifications", "Decline": "Refuser", "Decrease": "Baisser", - "Decreasing creation date": "Date de création décroissante", - "Decreasing number of members": "Nombre décroissant de membres", + "Decreasing creation date": "Nouveaux groupes", + "Decreasing number of members": "Le plus de membres", "Default": "Défaut", "Default Mobilizon privacy policy": "Politique de confidentialité par défaut de Mobilizon", "Default Mobilizon terms": "Conditions d'utilisation par défaut de Mobilizon", @@ -613,7 +613,7 @@ "Keyword, event title, group name, etc.": "Mot clé, titre d'un événement, nom d'un groupe, etc.", "Language": "Langue", "Languages": "Langues", - "Last event activity": "Dernière activité événementielle", + "Last event activity": "Événements récents", "Last IP adress": "Dernière adresse IP", "Last group created": "Dernier groupe créé", "Last published event": "Dernier événement publié", From e7a42f08a0b4daa9e235ca736ae184dfb6df2bd7 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 8 Nov 2024 18:28:28 +0100 Subject: [PATCH 51/78] Issue #1579 : The event menu is now fully clickable --- src/components/Event/EventActionSection.vue | 73 ++++++++-------- .../Event/EventParticipationCard.vue | 87 +++++++++---------- 2 files changed, 76 insertions(+), 84 deletions(-) diff --git a/src/components/Event/EventActionSection.vue b/src/components/Event/EventActionSection.vue index eb8e3d41b..a7b855ff4 100644 --- a/src/components/Event/EventActionSection.vue +++ b/src/components/Event/EventActionSection.vue @@ -110,67 +110,66 @@ {{ t("Actions") }} </o-button> </template> - <o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent"> - <router-link - class="flex gap-1" - :to="{ + <o-dropdown-item + aria-role="listitem" + has-link + v-if="canManageEvent" + @click=" + router.push({ name: RouteName.PARTICIPATIONS, params: { eventId: event?.uuid }, - }" - > - <AccountMultiple /> - {{ t("Participations") }} - </router-link> + }) + " + > + <AccountMultiple /> + {{ t("Participations") }} </o-dropdown-item> - <o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent"> - <router-link - class="flex gap-1" - :to="{ + <o-dropdown-item + aria-role="listitem" + has-link + v-if="canManageEvent" + @click=" + router.push({ name: RouteName.ANNOUNCEMENTS, params: { eventId: event?.uuid }, - }" - > - <Bullhorn /> - {{ t("Announcements") }} - </router-link> + }) + " + > + <Bullhorn /> + {{ t("Announcements") }} </o-dropdown-item> <o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent || event?.draft" - > - <router-link - class="flex gap-1" - :to="{ + @click=" + router.push({ name: RouteName.EDIT_EVENT, params: { eventId: event?.uuid }, - }" - > - <Pencil /> - {{ t("Edit") }} - </router-link> + }) + " + > + <Pencil /> + {{ t("Edit") }} </o-dropdown-item> <o-dropdown-item aria-role="listitem" has-link v-if="canManageEvent || event?.draft" - > - <router-link - class="flex gap-1" - :to="{ + @click=" + router.push({ name: RouteName.DUPLICATE_EVENT, params: { eventId: event?.uuid }, - }" - > - <ContentDuplicate /> - {{ t("Duplicate") }} - </router-link> + }) + " + > + <ContentDuplicate /> + {{ t("Duplicate") }} </o-dropdown-item> <o-dropdown-item aria-role="listitem" v-if="canManageEvent || event?.draft" @click="openDeleteEventModal" - @keyup.enter="openDeleteEventModal" ><span class="flex gap-1"> <Delete /> {{ t("Delete") }} diff --git a/src/components/Event/EventParticipationCard.vue b/src/components/Event/EventParticipationCard.vue index f16348da1..2356a9888 100644 --- a/src/components/Event/EventParticipationCard.vue +++ b/src/components/Event/EventParticipationCard.vue @@ -206,16 +206,14 @@ ParticipantRole.NOT_APPROVED, ].includes(participation.role) " + @click=" + gotToWithCheck(participation, { + name: RouteName.EDIT_EVENT, + params: { eventId: participation.event.uuid }, + }) + " > - <div - class="flex gap-1" - @click=" - gotToWithCheck(participation, { - name: RouteName.EDIT_EVENT, - params: { eventId: participation.event.uuid }, - }) - " - > + <div class="flex gap-1"> <Pencil /> {{ t("Edit") }} </div> @@ -224,16 +222,14 @@ <o-dropdown-item aria-role="listitem" v-if="participation.role === ParticipantRole.CREATOR" + @click=" + gotToWithCheck(participation, { + name: RouteName.DUPLICATE_EVENT, + params: { eventId: participation.event.uuid }, + }) + " > - <div - class="flex gap-1" - @click=" - gotToWithCheck(participation, { - name: RouteName.DUPLICATE_EVENT, - params: { eventId: participation.event.uuid }, - }) - " - > + <div class="flex gap-1"> <ContentDuplicate /> {{ t("Duplicate") }} </div> @@ -247,8 +243,9 @@ ParticipantRole.NOT_APPROVED, ].includes(participation.role) " + @click="openDeleteEventModalWrapper" > - <div @click="openDeleteEventModalWrapper" class="flex gap-1"> + <div class="flex gap-1"> <Delete /> {{ t("Delete") }} </div> @@ -262,16 +259,14 @@ ParticipantRole.NOT_APPROVED, ].includes(participation.role) " + @click=" + gotToWithCheck(participation, { + name: RouteName.PARTICIPATIONS, + params: { eventId: participation.event.uuid }, + }) + " > - <div - class="flex gap-1" - @click=" - gotToWithCheck(participation, { - name: RouteName.PARTICIPATIONS, - params: { eventId: participation.event.uuid }, - }) - " - > + <div class="flex gap-1"> <AccountMultiplePlus /> {{ t("Manage participations") }} </div> @@ -286,30 +281,28 @@ ParticipantRole.NOT_APPROVED, ].includes(participation.role) " - > - <router-link - class="flex gap-1" - :to="{ + @click=" + router.push({ name: RouteName.ANNOUNCEMENTS, params: { eventId: participation.event?.uuid }, - }" - > - <Bullhorn /> - {{ t("Announcements") }} - </router-link> + }) + " + > + <Bullhorn /> + {{ t("Announcements") }} </o-dropdown-item> - <o-dropdown-item aria-role="listitem"> - <router-link - class="flex gap-1" - :to="{ + <o-dropdown-item + aria-role="listitem" + @click=" + router.push({ name: RouteName.EVENT, - params: { uuid: participation.event.uuid }, - }" - > - <ViewCompact /> - {{ t("View event page") }} - </router-link> + params: { eventId: participation.event.uuid }, + }) + " + > + <ViewCompact /> + {{ t("View event page") }} </o-dropdown-item> </o-dropdown> </div> From 1265b3f53348e7fd4589e2de08c673f9a5c15e7a Mon Sep 17 00:00:00 2001 From: Mark Andrew Jaroski <mark.jaroski@gmail.com> Date: Fri, 26 Jul 2024 18:36:03 +0200 Subject: [PATCH 52/78] Added an index to make geo-based event search faster --- ...0240725130410_add_event_physical_address_index.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 priv/repo/migrations/20240725130410_add_event_physical_address_index.exs diff --git a/priv/repo/migrations/20240725130410_add_event_physical_address_index.exs b/priv/repo/migrations/20240725130410_add_event_physical_address_index.exs new file mode 100644 index 000000000..b9201c901 --- /dev/null +++ b/priv/repo/migrations/20240725130410_add_event_physical_address_index.exs @@ -0,0 +1,11 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddEventPhysicalAddressIndex do + use Ecto.Migration + + def up do + create(index("events", [:physical_address_id], name: :events_phys_addr_id)) + end + + def down do + drop(index("events", [:physical_address_id], name: :events_phys_addr_id)) + end +end From abeeef6aa310b4834adea08bf1584954eee4183c Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Sat, 23 Nov 2024 12:03:09 +0100 Subject: [PATCH 53/78] fix(tests): prepare for next version --- mix.exs | 2 +- package-lock.json | 4 ++-- package.json | 2 +- test/fixtures/nodeinfo/data.json | 2 +- test/graphql/api/search_test.exs | 3 ++- test/graphql/resolvers/config_test.exs | 6 ++++-- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/mix.exs b/mix.exs index 8c413b104..8338abf89 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Mobilizon.Mixfile do use Mix.Project - @version "5.0.0-beta.1" + @version "5.1.0" def project do [ diff --git a/package-lock.json b/package-lock.json index 47e154224..8eca88a05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mobilizon", - "version": "5.0.0-beta.1", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mobilizon", - "version": "5.0.0-beta.1", + "version": "5.1.0", "hasInstallScript": true, "dependencies": { "@apollo/client": "^3.3.16", diff --git a/package.json b/package.json index d8eb571b7..106a1b0be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobilizon", - "version": "5.0.0-beta.1", + "version": "5.1.0", "private": true, "scripts": { "dev": "vite", diff --git a/test/fixtures/nodeinfo/data.json b/test/fixtures/nodeinfo/data.json index 34cad60db..8fcb78e7b 100644 --- a/test/fixtures/nodeinfo/data.json +++ b/test/fixtures/nodeinfo/data.json @@ -22,7 +22,7 @@ }, "software": { "name": "Mobilizon", - "version": "5.0.0-beta.1", + "version": "5.1.0", "repository": "https://framagit.org/framasoft/mobilizon" }, "openRegistrations": true diff --git a/test/graphql/api/search_test.exs b/test/graphql/api/search_test.exs index c719cb4d8..e6511887b 100644 --- a/test/graphql/api/search_test.exs +++ b/test/graphql/api/search_test.exs @@ -56,7 +56,8 @@ defmodule Mobilizon.GraphQL.API.SearchTest do current_actor_id: nil, exclude_my_groups: false, exclude_stale_actors: true, - local_only: false + local_only: false, + sort_by: nil ], 1, 10 diff --git a/test/graphql/resolvers/config_test.exs b/test/graphql/resolvers/config_test.exs index 1730ede7e..f477701c2 100644 --- a/test/graphql/resolvers/config_test.exs +++ b/test/graphql/resolvers/config_test.exs @@ -113,9 +113,11 @@ defmodule Mobilizon.GraphQL.Resolvers.ConfigTest do assert res["data"]["config"]["long_description"] == nil assert res["data"]["config"]["slogan"] == nil assert res["data"]["config"]["languages"] == [] - assert length(res["data"]["config"]["timezones"]) == 594 + assert length(res["data"]["config"]["timezones"]) > 500 assert res["data"]["config"]["rules"] == nil - assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.0.1" + # there is no real way to make this test work when bumping instance version + # as there is no tag with this new version number in the git history, yet + # assert String.slice(res["data"]["config"]["version"], 0, 5) == "5.1.0" assert res["data"]["config"]["federating"] == true end From 2a1f620121f63e0b6b12f6e003a42bcca234fdd0 Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Sat, 23 Nov 2024 21:36:35 +0100 Subject: [PATCH 54/78] fix(test): fix some tests --- test/graphql/resolvers/admin_test.exs | 3 ++- test/service/metadata/instance_test.exs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/graphql/resolvers/admin_test.exs b/test/graphql/resolvers/admin_test.exs index 94c2c897a..c41dae263 100644 --- a/test/graphql/resolvers/admin_test.exs +++ b/test/graphql/resolvers/admin_test.exs @@ -4,6 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do import Swoosh.TestAssertions alias Mobilizon.Actors.Actor + alias Mobilizon.Config alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Reports.{Note, Report} @@ -568,7 +569,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do assert_email_sent( to: user.email, subject: - "An administrator manually changed the email attached to your account on Test instance" + "An administrator manually changed the email attached to your account on #{Config.instance_name()}" ) # # Swoosh.TestAssertions can't test multiple emails sent diff --git a/test/service/metadata/instance_test.exs b/test/service/metadata/instance_test.exs index 97b67d99c..50fecaf86 100644 --- a/test/service/metadata/instance_test.exs +++ b/test/service/metadata/instance_test.exs @@ -11,7 +11,7 @@ defmodule Mobilizon.Service.Metadata.InstanceTest do assert Instance.build_tags() |> Utils.stringify_tags() == """ - <title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>\n<link href=\"#{Endpoint.url()}/feed/instance/atom\" rel=\"alternate\" title=\"Test instance's feed\" type=\"application/atom+xml\"><link href=\"#{Endpoint.url()}/feed/instance/ics\" rel=\"alternate\" title=\"Test instance's feed\" type=\"text/calendar\">\ + <title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>\n<link href=\"#{Endpoint.url()}/feed/instance/atom\" rel=\"alternate\" title=\"#{Config.instance_name()}'s feed\" type=\"application/atom+xml\"><link href=\"#{Endpoint.url()}/feed/instance/ics\" rel=\"alternate\" title=\"#{Config.instance_name()}'s feed\" type=\"text/calendar\">\ """ end end From 70ae23b82c22ebe6f5f9921e8a6a3bd32eb8e858 Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Fri, 29 Nov 2024 18:26:51 +0100 Subject: [PATCH 55/78] chore(build): remove sentry step ; remove e2e tests stop from packages --- .gitlab-ci.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1883f8ef0..797504dec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,6 @@ stages: - install - check - build-js - - sentry - test - build - upload @@ -93,21 +92,6 @@ build-frontend: needs: - lint-front -sentry-commit: - stage: sentry - image: getsentry/sentry-cli - script: - - echo "Create a new release $CI_COMMIT_TAG" - - sentry-cli releases new $CI_COMMIT_TAG - - sentry-cli releases set-commits $CI_COMMIT_TAG --auto - - sentry-cli releases files $CI_COMMIT_TAG upload-sourcemaps priv/static/assets/ - - sentry-cli releases finalize $CI_COMMIT_TAG - - echo "Finalized release for $CI_COMMIT_TAG" - needs: - - build-frontend - only: - - tags@framasoft/mobilizon - deps: stage: check before_script: @@ -162,6 +146,8 @@ vitest: e2e: stage: test + except: + - tags@framasoft/mobilizon services: - name: postgis/postgis:16-3.4 alias: postgres From 09119026184f2292788232aa96704cdc97c5bf22 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 13 Nov 2024 12:02:15 +0100 Subject: [PATCH 56/78] #1574 - homepage : remove text search + add buttons for each content type --- src/components/Home/SearchFields.vue | 46 +++++++++++++++++++++++++--- src/i18n/en_US.json | 3 ++ src/i18n/fr_FR.json | 3 ++ src/views/HomeView.vue | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index 34dc3d99b..9e9bcf006 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -9,6 +9,7 @@ t("Keyword, event title, group name, etc.") }}</label> <o-input + v-if="search != null" v-model="search" :placeholder="t('Keyword, event title, group name, etc.')" id="search_field_input" @@ -46,16 +47,40 @@ /> </o-dropdown> </full-address-auto-complete> - <o-button native-type="submit" icon-left="magnify"> + <o-button native-type="submit" icon-left="magnify" v-if="search != null"> <template v-if="search">{{ t("Go!") }}</template> <template v-else>{{ t("Explore!") }}</template> </o-button> + <o-button + class="search-Event min-w-60 mr-1 mb-1" + native-type="submit" + icon-left="calendar" + v-if="search == null" + > + {{ t("Event filter") }} + </o-button> + <o-button + class="search-Activity min-w-60 mr-1 mb-1" + native-type="submit" + icon-left="calendar-star" + v-if="search == null && islongEvents" + > + {{ t("Find other activities") }} + </o-button> + <o-button + class="search-Group min-w-60 mr-1 mb-1" + native-type="submit" + icon-left="account-multiple" + v-if="search == null" + > + {{ t("Find groups") }} + </o-button> </form> </template> <script lang="ts" setup> import { IAddress } from "@/types/address.model"; -import { AddressSearchType } from "@/types/enums"; +import { AddressSearchType, ContentType } from "@/types/enums"; import { addressToLocation, getAddressFromLocal, @@ -65,6 +90,7 @@ import { computed, defineAsyncComponent } from "vue"; import { useI18n } from "vue-i18n"; import { useRouter, useRoute } from "vue-router"; import RouteName from "@/router/name"; +import { useIsLongEvents } from "@/composition/apollo/config"; const FullAddressAutoComplete = defineAsyncComponent( () => import("@/components/Event/FullAddressAutoComplete.vue") @@ -73,13 +99,14 @@ const FullAddressAutoComplete = defineAsyncComponent( const props = defineProps<{ address: IAddress | null; addressDefaultText?: string | null; - search: string; + search: string | null; distance: number | null; fromLocalStorage?: boolean | false; }>(); const router = useRouter(); const route = useRoute(); +const { islongEvents } = useIsLongEvents(); const emit = defineEmits<{ (event: "update:address", address: IAddress | null): void; @@ -152,14 +179,16 @@ const modelValueUpdate = (newaddress: IAddress | null) => { emit("update:address", newaddress); }; -const submit = () => { +const submit = (event) => { emit("submit"); + const btn_classes = event.submitter.getAttribute("class").split(" "); const search_query = { locationName: undefined, lat: undefined, lon: undefined, search: undefined, distance: undefined, + contentType: undefined, }; if (search.value != "") { search_query.search = search.value; @@ -173,6 +202,15 @@ const submit = () => { search_query.distance = distance.value.toString() + "_km"; } } + if (btn_classes.includes("search-Event")) { + search_query.contentType = ContentType.EVENTS; + } + if (btn_classes.includes("search-Activity")) { + search_query.contentType = ContentType.LONGEVENTS; + } + if (btn_classes.includes("search-Group")) { + search_query.contentType = ContentType.GROUPS; + } router.push({ name: RouteName.SEARCH, query: { diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index 452369e91..f81440b8f 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -1360,6 +1360,9 @@ "Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.", "Go!": "Go!", "Explore!": "Explore!", + "Event filter": "Event filter", + "Find other activities": "Find other activities", + "Find groups": "Find groups", "Select distance": "Select distance", "Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance", "Open user menu": "Open user menu", diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index df33c992f..81c1f15ce 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -422,6 +422,7 @@ "Event deleted and report resolved": "Événement supprimé et signalement résolu", "Event description body": "Corps de la description de l'événement", "Event edition": "Modification d'événement", + "Event filter": "Filtrer les événements", "Event list": "Liste d'événements", "Event metadata": "Métadonnées de l'événement", "Event page settings": "Paramètres de la page de l'événement", @@ -459,7 +460,9 @@ "Find an address": "Trouver une adresse", "Find an instance": "Trouver une instance", "Find another instance": "Trouver une autre instance", + "Find groups": "Trouver des groupes", "Find or add an element": "Trouver ou ajouter un élément", + "Find other activities": "Trouver d'autres activités", "First steps": "Premiers pas", "Follow": "Suivre", "Follow a new instance": "Suivre une nouvelle instance", diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 2ce33e3dc..1821651b0 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -247,7 +247,7 @@ const currentUserParticipations = computed( const increated = ref(0); const address = ref(null); -const search = ref(""); +const search = ref(null); const noAddress = ref(false); const current_distance = ref(null); From eb60da8ec9d3d5779e01ed695a5be855dfc03edb Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 27 Nov 2024 12:22:57 +0100 Subject: [PATCH 57/78] #1574 - home page & search page : 3 buttons for search --- src/components/Home/SearchFields.vue | 20 +++----- src/components/NavBar.vue | 40 +-------------- src/i18n/en_US.json | 3 -- src/i18n/fr_FR.json | 3 -- src/views/SearchView.vue | 75 ---------------------------- 5 files changed, 8 insertions(+), 133 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index 9e9bcf006..b6c87e62a 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -47,33 +47,27 @@ /> </o-dropdown> </full-address-auto-complete> - <o-button native-type="submit" icon-left="magnify" v-if="search != null"> - <template v-if="search">{{ t("Go!") }}</template> - <template v-else>{{ t("Explore!") }}</template> - </o-button> <o-button - class="search-Event min-w-60 mr-1 mb-1" + class="search-Event min-w-40 mr-1 mb-1" native-type="submit" icon-left="calendar" - v-if="search == null" > - {{ t("Event filter") }} + {{ t("Events") }} </o-button> <o-button - class="search-Activity min-w-60 mr-1 mb-1" + class="search-Activity min-w-40 mr-1 mb-1" native-type="submit" icon-left="calendar-star" - v-if="search == null && islongEvents" + v-if="islongEvents" > - {{ t("Find other activities") }} + {{ t("Activities") }} </o-button> <o-button - class="search-Group min-w-60 mr-1 mb-1" + class="search-Group min-w-40 mr-1 mb-1" native-type="submit" icon-left="account-multiple" - v-if="search == null" > - {{ t("Find groups") }} + {{ t("Groups") }} </o-button> </form> </template> diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index a86643443..40dbc0417 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -179,39 +179,6 @@ <ul class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold" > - <li class="m-auto"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'EVENTS' }, - }" - class="block 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("Events") }}</router-link - > - </li> - <li class="m-auto" v-if="islongEvents"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'LONGEVENTS' }, - }" - class="block 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("Activities") }}</router-link - > - </li> - <li class="m-auto"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'GROUPS' }, - }" - class="block 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("Groups") }}</router-link - > - </li> <li class="m-auto"> <router-link :to="{ name: RouteName.EVENT_CALENDAR }" @@ -275,10 +242,7 @@ import { import { useLazyQuery, useMutation } from "@vue/apollo-composable"; import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor"; import { changeIdentity } from "@/utils/identity"; -import { - useRegistrationConfig, - useIsLongEvents, -} from "@/composition/apollo/config"; +import { useRegistrationConfig } from "@/composition/apollo/config"; import { useOruga } from "@oruga-ui/oruga-next"; import { UNREAD_ACTOR_CONVERSATIONS, @@ -286,8 +250,6 @@ import { } from "@/graphql/user"; import { ICurrentUser } from "@/types/current-user.model"; -const { islongEvents } = useIsLongEvents(); - const { currentUser } = useCurrentUserClient(); const { currentActor } = useCurrentActorClient(); diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json index f81440b8f..452369e91 100644 --- a/src/i18n/en_US.json +++ b/src/i18n/en_US.json @@ -1360,9 +1360,6 @@ "Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.", "Go!": "Go!", "Explore!": "Explore!", - "Event filter": "Event filter", - "Find other activities": "Find other activities", - "Find groups": "Find groups", "Select distance": "Select distance", "Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance", "Open user menu": "Open user menu", diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index 81c1f15ce..df33c992f 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -422,7 +422,6 @@ "Event deleted and report resolved": "Événement supprimé et signalement résolu", "Event description body": "Corps de la description de l'événement", "Event edition": "Modification d'événement", - "Event filter": "Filtrer les événements", "Event list": "Liste d'événements", "Event metadata": "Métadonnées de l'événement", "Event page settings": "Paramètres de la page de l'événement", @@ -460,9 +459,7 @@ "Find an address": "Trouver une adresse", "Find an instance": "Trouver une instance", "Find another instance": "Trouver une autre instance", - "Find groups": "Trouver des groupes", "Find or add an element": "Trouver ou ajouter un élément", - "Find other activities": "Trouver d'autres activités", "First steps": "Premiers pas", "Follow": "Suivre", "Follow a new instance": "Suivre une nouvelle instance", diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 3b4b9bbfb..955e5bc49 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -28,46 +28,6 @@ :class="{ hidden: filtersPanelOpened }" class="lg:block mt-4 px-2" > - <p class="sr-only">{{ t("Type") }}</p> - <ul - class="font-medium text-gray-900 dark:text-slate-100 space-y-4 pb-4 border-b border-gray-200 dark:border-gray-500" - > - <li - v-for="content in contentTypeMapping" - :key="content.contentType" - class="flex gap-1 items-center" - > - <input - :id="'contentType' + content.contentType" - v-model="contentType" - type="radio" - name="contentType" - :value="content.contentType" - class="w-4 h-4 border-gray-300 focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-gray-700 dark:border-gray-600" - /> - - <label - :for="'contentType' + content.contentType" - class="cursor-pointer w-full font-medium text-gray-900 dark:text-gray-300 flex gap-1" - > - <Calendar - v-if="content.contentType === ContentType.EVENTS" - :size="24" - /> - - <CalendarStar - v-if="content.contentType === ContentType.LONGEVENTS" - :size="24" - /> - - <AccountMultiple - v-if="content.contentType === ContentType.GROUPS" - :size="24" - /><span>{{ content.label }}</span></label - > - </li> - </ul> - <div class="py-4 border-b border-gray-200 dark:border-gray-500" v-show="globalSearchEnabled" @@ -627,9 +587,6 @@ import { enumTransformer, booleanTransformer, } from "vue-use-route-query"; -import Calendar from "vue-material-design-icons/Calendar.vue"; -import CalendarStar from "vue-material-design-icons/CalendarStar.vue"; -import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue"; import { useHead } from "@/utils/head"; import type { Locale } from "date-fns"; @@ -639,7 +596,6 @@ import langs from "@/i18n/langs.json"; import { useEventCategories, useFeatures, - useIsLongEvents, useSearchConfig, } from "@/composition/apollo/config"; import { coordsToGeoHash } from "@/utils/location"; @@ -760,7 +716,6 @@ const GROUP_PAGE_LIMIT = 16; const { features } = useFeatures(); const { eventCategories } = useEventCategories(); -const { islongEvents } = useIsLongEvents(); const orderedCategories = computed(() => { if (!eventCategories.value) return []; @@ -873,36 +828,6 @@ const searchIsUrl = computed((): boolean => { return url.protocol === "http:" || url.protocol === "https:"; }); -const contentTypeMapping = computed(() => { - if (islongEvents.value) { - return [ - { - contentType: ContentType.EVENTS, - label: t("Events"), - }, - { - contentType: ContentType.LONGEVENTS, - label: t("Activities"), - }, - { - contentType: ContentType.GROUPS, - label: t("Groups"), - }, - ]; - } else { - return [ - { - contentType: ContentType.EVENTS, - label: t("Events"), - }, - { - contentType: ContentType.GROUPS, - label: t("Groups"), - }, - ]; - } -}); - const eventStatuses = computed(() => { return [ { From 4545a8c8e346e886482d1cba0142bb44dfd3e345 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:05:35 +0100 Subject: [PATCH 58/78] Remove confusing background color for unchecked <o-checkbox> --- src/assets/oruga-tailwindcss.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/oruga-tailwindcss.css b/src/assets/oruga-tailwindcss.css index 8f33ffe8b..755163eca 100644 --- a/src/assets/oruga-tailwindcss.css +++ b/src/assets/oruga-tailwindcss.css @@ -210,7 +210,7 @@ body { } .checkbox-check { - @apply appearance-none bg-primary border-primary; + @apply appearance-none border-primary; } .checkbox-checked { From 2dfb5a5bdf1794980b53a9e81a9a9621c584f774 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:13:31 +0100 Subject: [PATCH 59/78] Add the border-primary color to <o-radio> for consistency with the <o-checkbox> design --- src/assets/oruga-tailwindcss.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/oruga-tailwindcss.css b/src/assets/oruga-tailwindcss.css index 755163eca..0d5941ebe 100644 --- a/src/assets/oruga-tailwindcss.css +++ b/src/assets/oruga-tailwindcss.css @@ -250,7 +250,7 @@ body { @apply mr-2; } .form-radio { - @apply bg-none text-primary accent-primary; + @apply bg-none text-primary border-primary accent-primary; } .radio-label { @apply pl-2; From 539c0f2216558d927b6cfbd9d4efbbe9da07fee2 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:15:58 +0100 Subject: [PATCH 60/78] Issue #1066 Add a loading state on <o-button> in the create and edit event page --- src/views/Event/EditView.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/Event/EditView.vue b/src/views/Event/EditView.vue index 091e39bc9..d46e36961 100644 --- a/src/views/Event/EditView.vue +++ b/src/views/Event/EditView.vue @@ -548,12 +548,14 @@ outlined @click="createOrUpdateDraft" :disabled="saving" + :loading="saving" >{{ t("Save draft") }}</o-button > <o-button expanded variant="primary" :disabled="saving" + :loading="saving" @click="createOrUpdatePublish" @keyup.enter="createOrUpdatePublish" > From b96c476eaa072d5a8de8e77dd091d10d7a4dc2b4 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:24:22 +0100 Subject: [PATCH 61/78] Issue #1066 Add a loading state on <o-button> in the create group page --- src/views/Group/CreateView.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/views/Group/CreateView.vue b/src/views/Group/CreateView.vue index f230e8863..66ffa9b3c 100644 --- a/src/views/Group/CreateView.vue +++ b/src/views/Group/CreateView.vue @@ -199,7 +199,13 @@ </o-checkbox> </fieldset> - <o-button variant="primary" native-type="submit" class="mt-3"> + <o-button + variant="primary" + :disabled="loading" + :loading="loading" + native-type="submit" + class="mt-3" + > {{ t("Create my group") }} </o-button> </form> @@ -370,7 +376,7 @@ const preferredUsernameErrors = computed(() => { return [message, type]; }); -const { onDone, onError, mutate } = useCreateGroup(); +const { onDone, onError, mutate, loading } = useCreateGroup(); onDone(() => { notifier?.success( From 27da46829dc8f61450d5603e5899557ba0d05c9b Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:52:56 +0100 Subject: [PATCH 62/78] Issue #1571 Replace <o-datepicker> by <event-date-picker> --- src/views/Event/MyEventsView.vue | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/views/Event/MyEventsView.vue b/src/views/Event/MyEventsView.vue index 4045a33e5..0832cb3e2 100644 --- a/src/views/Event/MyEventsView.vue +++ b/src/views/Event/MyEventsView.vue @@ -33,11 +33,11 @@ " labelFor="events-start-datepicker" > - <o-datepicker - v-model="datePick" - :first-day-of-week="firstDayOfWeek" + <event-date-picker id="events-start-datepicker" - /> + :time="false" + v-model="datePick" + ></event-date-picker> <o-button @click="datePick = new Date()" class="reset-area !h-auto" @@ -221,17 +221,17 @@ import { LOGGED_USER_UPCOMING_EVENTS, } from "@/graphql/participant"; import { useApolloClient, useQuery } from "@vue/apollo-composable"; -import { computed, inject, ref, defineAsyncComponent } from "vue"; +import { computed, ref, defineAsyncComponent } from "vue"; import { IUser } from "@/types/current-user.model"; import { booleanTransformer, integerTransformer, useRouteQuery, } from "vue-use-route-query"; -import { Locale } from "date-fns"; import { useI18n } from "vue-i18n"; import { useRestrictions } from "@/composition/apollo/config"; import { useHead } from "@/utils/head"; +import EventDatePicker from "@/components/Event/EventDatePicker.vue"; const EventParticipationCard = defineAsyncComponent( () => import("@/components/Event/EventParticipationCard.vue") @@ -490,12 +490,6 @@ const hideCreateEventButton = computed((): boolean => { return restrictions.value?.onlyGroupsCanCreateEvents === true; }); -const dateFnsLocale = inject<Locale>("dateFnsLocale"); - -const firstDayOfWeek = computed((): number => { - return dateFnsLocale?.options?.weekStartsOn ?? 0; -}); - useHead({ title: computed(() => t("My events")), }); From fba0eb7cc8fc430b3e2b071511ca1bfb4ad375da Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 18:53:36 +0100 Subject: [PATCH 63/78] fix small errors --- src/views/Event/MyEventsView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/Event/MyEventsView.vue b/src/views/Event/MyEventsView.vue index 0832cb3e2..1dd92933c 100644 --- a/src/views/Event/MyEventsView.vue +++ b/src/views/Event/MyEventsView.vue @@ -246,7 +246,7 @@ const pastPage = ref(1); const limit = ref(10); function startOfDay(d: Date): string { - const pad = (n: int): string => { + const pad = (n: number): string => { return (n > 9 ? "" : "0") + n.toString(); }; return ( @@ -376,7 +376,7 @@ const monthlyFutureEvents = computed((): Map<string, Eventable[]> => { }); const monthlyPastParticipations = computed((): Map<string, Eventable[]> => { - return monthlyEvents(pastParticipations.value.elements, true); + return monthlyEvents(pastParticipations.value.elements); }); const monthParticipationsIds = (elements: Eventable[]): string[] => { From 227c9034ae1cb16751d32184d4e4e5890a7a58e5 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Wed, 13 Nov 2024 19:21:34 +0100 Subject: [PATCH 64/78] Issue #1066 Add a loading text when "my events" are loading Instead of showing the no event found text --- src/views/Event/MyEventsView.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/views/Event/MyEventsView.vue b/src/views/Event/MyEventsView.vue index 1dd92933c..5dab69baa 100644 --- a/src/views/Event/MyEventsView.vue +++ b/src/views/Event/MyEventsView.vue @@ -18,7 +18,6 @@ >{{ t("Create event") }}</o-button > </div> - <!-- <o-loading v-model:active="$apollo.loading"></o-loading> --> <div class="flex flex-wrap gap-4 items-start"> <div class="rounded p-3 flex-auto md:flex-none bg-zinc-300 dark:bg-zinc-700" @@ -137,13 +136,18 @@ > </div> </section> + <section v-if="loading"> + <div class="text-center prose dark:prose-invert max-w-full"> + <p>{{ t("Loading…") }}</p> + </div> + </section> <section class="text-center not-found" v-if=" showUpcoming && monthlyFutureEvents && monthlyFutureEvents.size === 0 && - true // !$apollo.loading + !loading " > <div class="text-center prose dark:prose-invert max-w-full"> @@ -291,6 +295,7 @@ const hasMorePastParticipations = ref(true); const { result: loggedUserUpcomingEventsResult, fetchMore: fetchMoreUpcomingEvents, + loading, } = useQuery<{ loggedUser: IUser; }>(LOGGED_USER_UPCOMING_EVENTS, () => ({ From 844642aff53a3f9429418f8807318b358301571f Mon Sep 17 00:00:00 2001 From: Laurent Gay <laurent@sleto.fr> Date: Thu, 21 Nov 2024 15:47:29 +0100 Subject: [PATCH 65/78] #1564 : correct homepage default filtering + add debug log for testing --- src/components/Local/CloseEvents.vue | 2 +- src/views/HomeView.vue | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/Local/CloseEvents.vue b/src/components/Local/CloseEvents.vue index 03437d43b..ae1d303c9 100644 --- a/src/components/Local/CloseEvents.vue +++ b/src/components/Local/CloseEvents.vue @@ -123,7 +123,7 @@ const eventsQuery = useQuery<{ orderBy: EventSortField.BEGINS_ON, direction: SortDirection.ASC, longevents: false, - location: geoHash.value, + location: geoHash.value ?? "", radius: distance.value, limit: 93, })); diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 1821651b0..a84d720ff 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -411,6 +411,11 @@ const { result: reverseGeocodeResult } = useQuery<{ const userSettingsLocation = computed(() => { const location = reverseGeocodeResult.value?.reverseGeocode[0]; const placeName = location?.locality ?? location?.region ?? location?.country; + console.debug( + "userSettingsLocation from reverseGeocode", + location, + placeName + ); return { lat: coords.value?.latitude, lon: coords.value?.longitude, @@ -426,6 +431,10 @@ const { result: currentUserLocationResult } = useQuery<{ // The user's location currently in the Apollo cache const currentUserLocation = computed(() => { + console.debug( + "currentUserLocation from LocationType", + currentUserLocationResult.value + ); return { ...(currentUserLocationResult.value?.currentUserLocation ?? { lat: undefined, From 368cdb9592f4d46f4f10d301d8560c1be6a708c2 Mon Sep 17 00:00:00 2001 From: Laurent Gay <laurent@sleto.fr> Date: Thu, 21 Nov 2024 16:55:14 +0100 Subject: [PATCH 66/78] add debug log for testing --- src/views/HomeView.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index a84d720ff..2c585b5eb 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -413,7 +413,8 @@ const userSettingsLocation = computed(() => { const placeName = location?.locality ?? location?.region ?? location?.country; console.debug( "userSettingsLocation from reverseGeocode", - location, + reverseGeocodeResult.value, + coords.value, placeName ); return { From 9796304a9a16ff5346122478a298a6bc894d6136 Mon Sep 17 00:00:00 2001 From: Laurent Gay <laurent@sleto.fr> Date: Thu, 21 Nov 2024 17:47:09 +0100 Subject: [PATCH 67/78] correct if no place find --- src/views/HomeView.vue | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 2c585b5eb..31c216720 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -417,13 +417,17 @@ const userSettingsLocation = computed(() => { coords.value, placeName ); - return { - lat: coords.value?.latitude, - lon: coords.value?.longitude, - name: placeName, - picture: location?.pictureInfo, - isIPLocation: coords.value?.isIPLocation, - }; + if (placeName) { + return { + lat: coords.value?.latitude, + lon: coords.value?.longitude, + name: placeName, + picture: location?.pictureInfo, + isIPLocation: coords.value?.isIPLocation, + }; + } else { + return {}; + } }); const { result: currentUserLocationResult } = useQuery<{ From 752da9e641ab9268a31a751befef023072200981 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 19:52:57 +0100 Subject: [PATCH 68/78] Issue #1567 : Add a `long_event` computed field to an event The actual long_event implementation is only done for search and long_event is a parameter of the search request. This change is needed for the the front-end to know if an event is a long_event everywhere an event is received. The computed field (Ecto virtual field) is set after the Ecto request with the function with_virtual_fields(). with_virtual_fields() handles cases where there is an event, a list of events and a paginated list of events returned. --- lib/graphql/schema/event.ex | 5 +++ lib/graphql/schema/search.ex | 10 ++++++ lib/mobilizon/events/event.ex | 5 ++- lib/mobilizon/events/events.ex | 57 ++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index 4ecd682fe..e76528cf2 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -32,6 +32,11 @@ defmodule Mobilizon.GraphQL.Schema.EventType do field(:description, :string, description: "The event's description") field(:begins_on, :datetime, description: "Datetime for when the event begins") field(:ends_on, :datetime, description: "Datetime for when the event ends") + + field(:long_event, :boolean, + description: "Whether the event is a long event (activity) or not" + ) + field(:status, :event_status, description: "Status of the event") field(:visibility, :event_visibility, description: "The event's visibility") field(:join_options, :event_join_options, description: "The event's visibility") diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index e7742e9fe..5325566be 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -17,6 +17,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do field(:title, :string, description: "The event's title") field(:begins_on, :datetime, description: "Datetime for when the event begins") field(:ends_on, :datetime, description: "Datetime for when the event ends") + + field(:long_event, :boolean, + description: "Whether the event is a long event (activity) or not" + ) + field(:status, :event_status, description: "Status of the event") field(:picture, :media, description: "The event's picture") field(:physical_address, :address, description: "The event's physical address") @@ -52,6 +57,11 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do field(:title, :string, description: "The event's title") field(:begins_on, :datetime, description: "Datetime for when the event begins") field(:ends_on, :datetime, description: "Datetime for when the event ends") + + field(:long_event, :boolean, + description: "Whether the event is a long event (activity) or not" + ) + field(:status, :event_status, description: "Status of the event") field(:picture, :media, description: "The event's picture") field(:physical_address, :address, description: "The event's physical address") diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex index ff76568ae..8e39f5e70 100644 --- a/lib/mobilizon/events/event.ex +++ b/lib/mobilizon/events/event.ex @@ -66,6 +66,7 @@ defmodule Mobilizon.Events.Event do participants: [Actor.t()], contacts: [Actor.t()], language: String.t(), + long_event: boolean, metadata: [EventMetadata.t()] } @@ -89,7 +90,8 @@ defmodule Mobilizon.Events.Event do :picture_id, :physical_address_id, :attributed_to_id, - :language + :language, + :long_event ] @attrs @required_attrs ++ @optional_attrs @@ -102,6 +104,7 @@ defmodule Mobilizon.Events.Event do field(:slug, :string) field(:description, :string) field(:ends_on, :utc_datetime) + field(:long_event, :boolean, virtual: true, default: nil) field(:title, :string) field(:status, EventStatus, default: :confirmed) field(:draft, :boolean, default: false) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 7bf770723..f05bd0844 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -12,6 +12,8 @@ defmodule Mobilizon.Events do import Mobilizon.Storage.Ecto import Mobilizon.Events.Utils, only: [calculate_notification_time: 1] + require Logger + alias Ecto.{Changeset, Multi} alias Mobilizon.Actors.{Actor, Follower} @@ -141,6 +143,7 @@ defmodule Mobilizon.Events do url |> event_by_url_query() |> Repo.one() + |> with_virtual_fields() end @doc """ @@ -153,6 +156,7 @@ defmodule Mobilizon.Events do |> event_by_url_query() |> preload_for_event() |> Repo.one!() + |> with_virtual_fields() end @doc """ @@ -167,6 +171,7 @@ defmodule Mobilizon.Events do |> filter_draft() |> preload_for_event() |> Repo.one!() + |> with_virtual_fields() end @doc """ @@ -180,6 +185,7 @@ defmodule Mobilizon.Events do |> filter_draft() |> preload_for_event() |> Repo.one() + |> with_virtual_fields() end @spec check_if_event_has_instance_follow(String.t(), integer()) :: boolean() @@ -199,6 +205,7 @@ defmodule Mobilizon.Events do |> event_by_uuid_query() |> preload_for_event() |> Repo.one() + |> with_virtual_fields() end @doc """ @@ -212,6 +219,7 @@ defmodule Mobilizon.Events do |> filter_not_event_uuid(not_event_uuid) |> filter_draft() |> Repo.one() + |> with_virtual_fields() end @doc """ @@ -392,6 +400,7 @@ defmodule Mobilizon.Events do |> filter_cancelled_events() |> filter_local_or_from_followed_instances_events() |> Page.build_page(page, limit) + |> with_virtual_fields() end @spec stream_events_for_sitemap :: Enum.t() @@ -413,6 +422,7 @@ defmodule Mobilizon.Events do |> preload_for_event() |> event_order_by(sort, direction) |> Page.build_page(page, limit) + |> with_virtual_fields() end @doc """ @@ -425,6 +435,7 @@ defmodule Mobilizon.Events do |> events_by_tags_query(limit) |> filter_draft() |> Repo.all() + |> with_virtual_fields() end @doc """ @@ -440,6 +451,7 @@ defmodule Mobilizon.Events do actor_id |> do_list_public_events_for_actor() |> Page.build_page(page, limit) + |> with_virtual_fields() end @doc """ @@ -467,6 +479,7 @@ defmodule Mobilizon.Events do |> do_list_public_events_for_actor() |> event_filter_begins_on(DateTime.utc_now(), nil) |> Page.build_page(page, limit) + |> with_virtual_fields() end @spec do_list_public_events_for_actor(integer()) :: Ecto.Query.t() @@ -485,6 +498,7 @@ defmodule Mobilizon.Events do |> event_for_actor_query(desc: :begins_on) |> preload_for_event() |> Page.build_page(page, limit) + |> with_virtual_fields() end @spec list_simple_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: @@ -520,6 +534,7 @@ defmodule Mobilizon.Events do |> event_order_by(order_by, order_direction) |> preload_for_event() |> Page.build_page(page, limit) + |> with_virtual_fields() end @spec list_drafts_for_user(integer, integer | nil, integer | nil) :: Page.t(Event.t()) @@ -529,6 +544,7 @@ defmodule Mobilizon.Events do |> filter_draft(true) |> order_by(desc: :updated_at) |> Page.build_page(page, limit) + |> with_virtual_fields() end @spec user_moderator_for_event?(integer | String.t(), integer | String.t()) :: boolean @@ -552,6 +568,7 @@ defmodule Mobilizon.Events do |> close_events_query(radius) |> filter_draft() |> Repo.all() + |> with_virtual_fields() end @doc """ @@ -603,6 +620,7 @@ defmodule Mobilizon.Events do |> filter_public_visibility() |> event_order(Map.get(args, :sort_by, :match_desc), search_string) |> Page.build_page(page, limit) + |> with_virtual_fields() end @doc """ @@ -978,6 +996,7 @@ defmodule Mobilizon.Events do actor_id |> event_participations_for_actor_query() |> Page.build_page(page, limit) + |> with_virtual_fields() end @doc """ @@ -993,6 +1012,7 @@ defmodule Mobilizon.Events do actor_id |> event_participations_for_actor_query(DateTime.utc_now()) |> Page.build_page(page, limit) + |> with_virtual_fields() end @doc """ @@ -1871,6 +1891,7 @@ defmodule Mobilizon.Events do ) ) |> Repo.all() + |> with_virtual_fields() end @spec list_participations_for_user_query(integer()) :: Ecto.Query.t() @@ -2098,4 +2119,40 @@ defmodule Mobilizon.Events do |> preload_for_event() |> Page.chunk(chunk_size) end + + # Handling the case where Repo.XXXX() return nil + def with_virtual_fields(nil), do: nil + + # Handling the case where there is an event + # Using Repo.one(), for example + def with_virtual_fields(%Event{} = event) do + duration = Config.get([:instance, :duration_of_long_event], 0) + + event_duration = DateTime.diff(event.ends_on, event.begins_on, :day) + + # duration need to be > 0 for long event to be activated + long_event = duration > 0 && event_duration > duration + + %{event | long_event: long_event} + end + + # Handling the case where there is a list of events + # Using Repo.all(), for example + def with_virtual_fields(events) when is_list(events) do + Enum.map(events, &with_virtual_fields/1) + end + + # Handling the case of a paginated list of events + def with_virtual_fields(%Page{total: _total, elements: elements} = page) do + elements_with_virtual_fields = Enum.map(elements, &with_virtual_fields/1) + %{page | elements: elements_with_virtual_fields} + end + + # In case the function is called on an element without virtual_fields + def with_virtual_fields(invalid) do + Logger.warning("with_virtual_fields called on invalid element : #{inspect(invalid)}") + + # Return the element without modification + invalid + end end From 91b0e7d265f68b38afaa2b6113b5e0cafadacc87 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 19:55:31 +0100 Subject: [PATCH 69/78] remove an obsolete comment --- lib/mobilizon/storage/page.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/mobilizon/storage/page.ex b/lib/mobilizon/storage/page.ex index 9a8537774..cc1ff6de6 100644 --- a/lib/mobilizon/storage/page.ex +++ b/lib/mobilizon/storage/page.ex @@ -19,9 +19,6 @@ defmodule Mobilizon.Storage.Page do @doc """ Returns a Page struct for a query. - - `field` is use to define the field that will be used for the count aggregate, which should be the same as the field used for order_by - See https://stackoverflow.com/q/12693089/10204399 """ @spec build_page(Ecto.Queryable.t(), integer | nil, integer) :: t(any) def build_page(query, page, limit) do From 71c0ce37a956755da04216267df7102938cba5a4 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 20:12:20 +0100 Subject: [PATCH 70/78] case harmonization with other fields - longevents -> long_events (back-end) - longevents -> longEvents (front-end) --- lib/graphql/resolvers/event.ex | 4 ++-- lib/graphql/schema/event.ex | 2 +- lib/graphql/schema/search.ex | 2 +- lib/mobilizon/events/events.ex | 12 ++++++------ schema.graphql | 2 +- src/components/Local/CloseEvents.vue | 2 +- src/graphql/event.ts | 4 ++-- src/graphql/search.ts | 10 +++++----- src/views/SearchView.vue | 2 +- test/graphql/resolvers/search_test.exs | 16 ++++++++-------- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/graphql/resolvers/event.ex b/lib/graphql/resolvers/event.ex index fe7aaec69..04ab9b8c5 100644 --- a/lib/graphql/resolvers/event.ex +++ b/lib/graphql/resolvers/event.ex @@ -76,7 +76,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do limit: limit, order_by: order_by, direction: direction, - longevents: longevents, + long_events: long_events, location: location, radius: radius }, @@ -84,7 +84,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do ) when limit < @event_max_limit do {:ok, - Events.list_events(page, limit, order_by, direction, true, longevents, location, radius)} + Events.list_events(page, limit, order_by, direction, true, long_events, location, radius)} end def list_events( diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index e76528cf2..253756319 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -400,7 +400,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do description: "Direction for the sort" ) - arg(:longevents, :boolean, + arg(:long_events, :boolean, default_value: nil, description: "if mention filter in or out long events" ) diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index 5325566be..7d8448102 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -287,7 +287,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do description: "Radius around the location to search in" ) - arg(:longevents, :boolean, description: "if mention filter in or out long events") + arg(:long_events, :boolean, description: "if mention filter in or out long events") arg(:bbox, :string, description: "The bbox to search events into") arg(:zoom, :integer, description: "The zoom level for searching events") diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index f05bd0844..b3786952f 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -383,7 +383,7 @@ defmodule Mobilizon.Events do sort \\ :begins_on, direction \\ :asc, is_future \\ true, - longevents \\ nil, + long_events \\ nil, location \\ nil, radius \\ nil ) do @@ -394,7 +394,7 @@ defmodule Mobilizon.Events do |> maybe_join_address(%{location: location, radius: radius}) |> events_for_location(%{location: location, radius: radius}) |> filter_future_events(is_future) - |> events_for_longevents(longevents) + |> events_for_long_events(long_events) |> filter_public_visibility() |> filter_draft() |> filter_cancelled_events() @@ -604,7 +604,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(Map.get(args, :longevents)) + |> events_for_long_events(Map.get(args, :long_events)) |> events_for_category(args) |> events_for_categories(args) |> events_for_languages(args) @@ -1414,14 +1414,14 @@ defmodule Mobilizon.Events do end end - @spec events_for_longevents(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t() - defp events_for_longevents(query, longevents) do + @spec events_for_long_events(Ecto.Queryable.t(), Boolean.t() | nil) :: Ecto.Query.t() + defp events_for_long_events(query, long_events) do duration = Config.get([:instance, :duration_of_long_event], 0) if duration <= 0 do query else - case longevents do + case long_events do nil -> query diff --git a/schema.graphql b/schema.graphql index 372868580..1e5d1e707 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2268,7 +2268,7 @@ type RootQueryType { endsOn: DateTime "Filter for long events in function of configuration parameter 'duration_of_long_event'" - longevents: Boolean + longEvents: Boolean ): Events "Interact with an URI" diff --git a/src/components/Local/CloseEvents.vue b/src/components/Local/CloseEvents.vue index ae1d303c9..7029f1748 100644 --- a/src/components/Local/CloseEvents.vue +++ b/src/components/Local/CloseEvents.vue @@ -122,7 +122,7 @@ const eventsQuery = useQuery<{ }>(FETCH_EVENTS, () => ({ orderBy: EventSortField.BEGINS_ON, direction: SortDirection.ASC, - longevents: false, + longEvents: false, location: geoHash.value ?? "", radius: distance.value, limit: 93, diff --git a/src/graphql/event.ts b/src/graphql/event.ts index b2bdabaea..8bf036c27 100644 --- a/src/graphql/event.ts +++ b/src/graphql/event.ts @@ -109,7 +109,7 @@ export const FETCH_EVENTS = gql` $direction: SortDirection $page: Int $limit: Int - $longevents: Boolean + $longEvents: Boolean ) { events( location: $location @@ -118,7 +118,7 @@ export const FETCH_EVENTS = gql` direction: $direction page: $page limit: $limit - longevents: $longevents + longEvents: $longEvents ) { total elements { diff --git a/src/graphql/search.ts b/src/graphql/search.ts index 2eff07b5a..fe13db545 100644 --- a/src/graphql/search.ts +++ b/src/graphql/search.ts @@ -33,7 +33,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` $searchTarget: SearchTarget $beginsOn: DateTime $endsOn: DateTime - $longevents: Boolean + $longEvents: Boolean $bbox: String $zoom: Int $eventPage: Int @@ -55,7 +55,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` searchTarget: $searchTarget beginsOn: $beginsOn endsOn: $endsOn - longevents: $longevents + longEvents: $longEvents bbox: $bbox zoom: $zoom page: $eventPage @@ -155,7 +155,7 @@ export const SEARCH_EVENTS = gql` $endsOn: DateTime $eventPage: Int $limit: Int - $longevents: Boolean + $longEvents: Boolean ) { searchEvents( location: $location @@ -168,7 +168,7 @@ export const SEARCH_EVENTS = gql` endsOn: $endsOn page: $eventPage limit: $limit - longevents: $longevents + longEvents: $longEvents ) { total elements { @@ -218,7 +218,7 @@ export const SEARCH_CALENDAR_EVENTS = gql` endsOn: $endsOn page: $eventPage limit: $limit - longevents: false + longEvents: false ) { total elements { diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 955e5bc49..2424306ff 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -1049,7 +1049,7 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{ location: geoHashLocation.value, beginsOn: start.value, endsOn: end.value, - longevents: longEvents.value, + longEvents: longEvents.value, radius: geoHashLocation.value ? radius.value : undefined, eventPage: eventPage.value, groupPage: groupPage.value, diff --git a/test/graphql/resolvers/search_test.exs b/test/graphql/resolvers/search_test.exs index 8af96b043..27352e4e2 100644 --- a/test/graphql/resolvers/search_test.exs +++ b/test/graphql/resolvers/search_test.exs @@ -18,8 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do describe "search events/3" do @search_events_query """ - query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $longevents:Boolean, $searchTarget: SearchTarget) { - searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, longevents: $longevents, searchTarget: $searchTarget) { + query SearchEvents($location: String, $radius: Float, $tags: String, $term: String, $beginsOn: DateTime, $endsOn: DateTime, $longEvents:Boolean, $searchTarget: SearchTarget) { + searchEvents(location: $location, radius: $radius, tags: $tags, term: $term, beginsOn: $beginsOn, endsOn: $endsOn, longEvents: $longEvents, searchTarget: $searchTarget) { total, elements { id @@ -224,7 +224,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: false} + variables: %{longEvents: false} ) assert res["errors"] == nil @@ -241,7 +241,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: true} + variables: %{longEvents: true} ) assert res["errors"] == nil @@ -296,7 +296,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: false} + variables: %{longEvents: false} ) assert res["errors"] == nil @@ -311,7 +311,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: true} + variables: %{longEvents: true} ) assert res["errors"] == nil @@ -364,7 +364,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: false} + variables: %{longEvents: false} ) assert res["errors"] == nil @@ -378,7 +378,7 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do res = AbsintheHelpers.graphql_query(conn, query: @search_events_query, - variables: %{longevents: true} + variables: %{longEvents: true} ) assert res["errors"] == nil From f6c273f6f312ca0dd2df79709681643a20551d2f Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 20:23:40 +0100 Subject: [PATCH 71/78] islongEvents -> isLongEvents --- src/components/Home/SearchFields.vue | 4 ++-- src/components/NavBar.vue | 35 ++++++++++++++++++++++++++++ src/composition/apollo/config.ts | 4 ++-- src/views/SearchView.vue | 31 ++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index b6c87e62a..7a04715fc 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -58,7 +58,7 @@ class="search-Activity min-w-40 mr-1 mb-1" native-type="submit" icon-left="calendar-star" - v-if="islongEvents" + v-if="search == null && isLongEvents" > {{ t("Activities") }} </o-button> @@ -100,7 +100,7 @@ const props = defineProps<{ const router = useRouter(); const route = useRoute(); -const { islongEvents } = useIsLongEvents(); +const { isLongEvents } = useIsLongEvents(); const emit = defineEmits<{ (event: "update:address", address: IAddress | null): void; diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index 40dbc0417..af5192e63 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -179,6 +179,39 @@ <ul class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold" > + <li class="m-auto"> + <router-link + :to="{ + ...$route, + name: RouteName.SEARCH, + query: { ...$route.query, contentType: 'EVENTS' }, + }" + class="block 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("Events") }}</router-link + > + </li> + <li class="m-auto" v-if="isLongEvents"> + <router-link + :to="{ + ...$route, + name: RouteName.SEARCH, + query: { ...$route.query, contentType: 'LONGEVENTS' }, + }" + class="block 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("Activities") }}</router-link + > + </li> + <li class="m-auto"> + <router-link + :to="{ + ...$route, + name: RouteName.SEARCH, + query: { ...$route.query, contentType: 'GROUPS' }, + }" + class="block 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("Groups") }}</router-link + > + </li> <li class="m-auto"> <router-link :to="{ name: RouteName.EVENT_CALENDAR }" @@ -250,6 +283,8 @@ import { } from "@/graphql/user"; import { ICurrentUser } from "@/types/current-user.model"; +const { isLongEvents } = useIsLongEvents(); + const { currentUser } = useCurrentUserClient(); const { currentActor } = useCurrentActorClient(); diff --git a/src/composition/apollo/config.ts b/src/composition/apollo/config.ts index be0361e66..4ca1f5dfb 100644 --- a/src/composition/apollo/config.ts +++ b/src/composition/apollo/config.ts @@ -227,8 +227,8 @@ export function useIsLongEvents() { config: Pick<IConfig, "longEvents">; }>(LONG_EVENTS); - const islongEvents = computed(() => result.value?.config.longEvents); - return { islongEvents, error, loading }; + const isLongEvents = computed(() => result.value?.config.longEvents); + return { isLongEvents, error, loading }; } export function useAnalytics() { diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index 2424306ff..f3fb69005 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -716,6 +716,7 @@ const GROUP_PAGE_LIMIT = 16; const { features } = useFeatures(); const { eventCategories } = useEventCategories(); +const { isLongEvents } = useIsLongEvents(); const orderedCategories = computed(() => { if (!eventCategories.value) return []; @@ -828,6 +829,36 @@ const searchIsUrl = computed((): boolean => { return url.protocol === "http:" || url.protocol === "https:"; }); +const contentTypeMapping = computed(() => { + if (isLongEvents.value) { + return [ + { + contentType: ContentType.EVENTS, + label: t("Events"), + }, + { + contentType: ContentType.LONGEVENTS, + label: t("Activities"), + }, + { + contentType: ContentType.GROUPS, + label: t("Groups"), + }, + ]; + } else { + return [ + { + contentType: ContentType.EVENTS, + label: t("Events"), + }, + { + contentType: ContentType.GROUPS, + label: t("Groups"), + }, + ]; + } +}); + const eventStatuses = computed(() => { return [ { From 9bfbc09f3f73b6d8131883805e0aae5fdd2bd455 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 20:49:44 +0100 Subject: [PATCH 72/78] update French translation --- src/i18n/fr_FR.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index df33c992f..dc142b943 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -69,6 +69,7 @@ "Activate browser push notifications": "Activer les notifications push du navigateur", "Activate notifications": "Activer les notifications", "Activated": "Activé", + "Activities": "Activités", "Active": "Actif·ive", "Activity": "Activité", "Actor": "Acteur", From 04fe44f72b6ed1b3e31ff1fc2c2dc5a2e5544148 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Thu, 21 Nov 2024 21:19:55 +0100 Subject: [PATCH 73/78] Issue #1567 : distinction between activities (long_event) and normal events on group page --- .../Group/Sections/EventsSection.vue | 28 ++++++++++++++----- src/graphql/group.ts | 1 + src/graphql/search.ts | 1 + src/i18n/fr_FR.json | 6 ++++ src/types/event.model.ts | 3 ++ src/views/Group/GroupView.vue | 22 +++++++++++++-- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/components/Group/Sections/EventsSection.vue b/src/components/Group/Sections/EventsSection.vue index a1572677c..0931f711e 100644 --- a/src/components/Group/Sections/EventsSection.vue +++ b/src/components/Group/Sections/EventsSection.vue @@ -1,6 +1,6 @@ <template> <group-section - :title="t('Events')" + :title="longEvent ? t('Activities') : t('Events')" icon="calendar" :route="{ name: RouteName.GROUP_EVENTS, @@ -13,13 +13,19 @@ v-if="group && group.organizedEvents.total > 0" > <event-minimalist-card - v-for="event in group.organizedEvents.elements.slice(0, 3)" + v-for="event in group.organizedEvents.elements + .filter((event) => (longEvent ? event.longEvent : !event.longEvent)) + .slice(0, 3)" :event="event" :key="event.uuid" /> </div> - <empty-content v-else-if="group" icon="calendar" :inline="true"> - {{ t("No public upcoming events") }} + <empty-content v-else-if="group" icon="calendar" :inline="true" + >{{ + longEvent + ? t("No public upcoming activities") + : t("No public upcoming events") + }} </empty-content> <!-- <o-skeleton animated v-else></o-skeleton> --> </template> @@ -33,7 +39,9 @@ params: { preferredUsername: usernameWithDomain(group) }, query: { showPassedEvents: true }, }" - >{{ t("View past events") }}</o-button + >{{ + longEvent ? t("+ View past activities") : t("+ View past events") + }}</o-button > <o-button tag="router-link" @@ -43,7 +51,9 @@ query: { actorId: group?.id }, }" class="button is-primary" - >{{ t("+ Create an event") }}</o-button + >{{ + longEvent ? t("+ Create an activity") : t("+ Create an event") + }}</o-button > </template> </group-section> @@ -60,5 +70,9 @@ import GroupSection from "@/components/Group/GroupSection.vue"; const { t } = useI18n({ useScope: "global" }); -defineProps<{ group: IGroup; isModerator: boolean }>(); +defineProps<{ + group: IGroup; + isModerator: boolean; + longEvent: boolean; +}>(); </script> diff --git a/src/graphql/group.ts b/src/graphql/group.ts index b53ac350a..d09959cbb 100644 --- a/src/graphql/group.ts +++ b/src/graphql/group.ts @@ -150,6 +150,7 @@ export const GROUP_BASIC_FIELDS_FRAGMENTS = gql` beginsOn status draft + longEvent language options { maximumAttendeeCapacity diff --git a/src/graphql/search.ts b/src/graphql/search.ts index fe13db545..d3e80b6aa 100644 --- a/src/graphql/search.ts +++ b/src/graphql/search.ts @@ -70,6 +70,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` uuid beginsOn endsOn + longEvent picture { id url diff --git a/src/i18n/fr_FR.json b/src/i18n/fr_FR.json index dc142b943..a4bf30b31 100644 --- a/src/i18n/fr_FR.json +++ b/src/i18n/fr_FR.json @@ -6,7 +6,10 @@ "+ Add a resource": "+ Ajouter une ressource", "+ Create a post": "+ Créer un billet", "+ Create an event": "+ Créer un événement", + "+ Create an activity": "+ Créer une activité", "+ Start a discussion": "+ Lancer une discussion", + "+ View past activities": "+ Voir les activités passées", + "+ View past events": "+ Voir les événements passés", "0 Bytes": "0 octets", "<b>{contact}</b> will be displayed as contact.": "<b>{contact}</b> sera affiché·e comme contact.|<b>{contact}</b> seront affiché·e·s comme contacts.", "@{group}": "@{group}", @@ -778,6 +781,7 @@ "No posts found": "Aucun billet trouvé", "No posts yet": "Pas encore de billets", "No profile matches the filters": "Aucun profil ne correspond aux filtres", + "No public upcoming activities": "Aucune activité publique à venir", "No public upcoming events": "Aucun événement public à venir", "No resolved reports yet": "Aucun signalement résolu pour le moment", "No resources in this folder": "Aucune ressource dans ce dossier", @@ -1356,6 +1360,7 @@ "View more groups around {position}": "Voir plus de groupes près de {position}", "View more online events": "Voir plus d'événements en ligne", "View page on {hostname} (in a new window)": "Voir la page sur {hostname} (dans une nouvelle fenêtre)", + "View past activities": "Voir les activités passées", "View past events": "Voir les événements passés", "View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine", "Visibility was set to an unknown value.": "La visibilité a été définie à une valeur inconnue.", @@ -1548,6 +1553,7 @@ "as {identity}": "en tant que {identity}", "contact uninformed": "contact non renseigné", "create a group": "créer un groupe", + "create an activity": "créer une activité", "create an event": "créer un événement", "default Mobilizon privacy policy": "politique de confidentialité par défaut de Mobilizon", "default Mobilizon terms": "conditions d'utilisation par défaut de Mobilizon", diff --git a/src/types/event.model.ts b/src/types/event.model.ts index 77b545e4f..b71fdc702 100644 --- a/src/types/event.model.ts +++ b/src/types/event.model.ts @@ -71,6 +71,7 @@ export interface IEvent { beginsOn: string; endsOn: string | null; publishAt: string; + longEvent: boolean; status: EventStatus; visibility: EventVisibility; joinOptions: EventJoinOptions; @@ -144,6 +145,8 @@ export class EventModel implements IEvent { publishAt = new Date().toISOString(); + longEvent = false; + language = "und"; participantStats = { diff --git a/src/views/Group/GroupView.vue b/src/views/Group/GroupView.vue index e7d01dba2..9934d7576 100644 --- a/src/views/Group/GroupView.vue +++ b/src/views/Group/GroupView.vue @@ -373,7 +373,7 @@ </div> </header> </div> - <div class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2"> + <div v-if="group" class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2"> <!-- Public thing: Members --> <group-section :title="t('Members')" icon="account-group"> <template #default> @@ -526,11 +526,24 @@ </group-section> </div> <div v-if="group"> - <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2"> + <div + :class="[ + 'grid grid-cols-1 gap-2 mb-2', + { 'xl:grid-cols-3': isLongEvents, 'md:grid-cols-2': !isLongEvents }, + ]" + > + <!-- Public thing: Long Events --> + <Events + v-if="isLongEvents" + :group="group" + :isModerator="isCurrentActorAGroupModerator && !previewPublic" + :longEvent="true" + /> <!-- Public thing: Events --> <Events :group="group" :isModerator="isCurrentActorAGroupModerator && !previewPublic" + :longEvent="false" /> <!-- Public thing: Posts --> <Posts @@ -538,6 +551,8 @@ :isModerator="isCurrentActorAGroupModerator && !previewPublic" :isMember="isCurrentActorAGroupMember && !previewPublic" /> + </div> + <div class="grid grid-cols-1 gap-2 mb-2 md:grid-cols-2"> <!-- Private thing: Group discussions --> <Discussions v-if="isCurrentActorAGroupMember && !previewPublic" @@ -656,6 +671,7 @@ import { Notifier } from "@/plugins/notifier"; import { useGroupResourcesList } from "@/composition/apollo/resources"; import { useGroupMembers } from "@/composition/apollo/members"; import GroupSection from "@/components/Group/GroupSection.vue"; +import { useIsLongEvents } from "@/composition/apollo/config"; const props = defineProps<{ preferredUsername: string; @@ -680,6 +696,8 @@ const { group: resourcesGroup } = useGroupResourcesList(preferredUsername, { const { t } = useI18n({ useScope: "global" }); +const { isLongEvents } = useIsLongEvents(); + // const { person } = usePersonStatusGroup(group); const { result, subscribeToMore } = useQuery<{ From f67bd900f7636a016e65d93f755595a329284d8b Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Fri, 22 Nov 2024 18:45:11 +0100 Subject: [PATCH 74/78] Issue #1567 : use ends_on for comparaisons to get future and ongoing events --- lib/mobilizon/events/events.ex | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index b3786952f..0b48fb02a 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -530,7 +530,9 @@ defmodule Mobilizon.Events do group_id |> event_for_group_query() |> event_filter_visibility(visibility) - |> event_filter_begins_on(after_datetime, before_datetime) + # We want future and ongoing events, so we use ends_on + # See issue #1567 + |> event_filter_ends_on(after_datetime, before_datetime) |> event_order_by(order_by, order_direction) |> preload_for_event() |> Page.build_page(page, limit) @@ -2024,6 +2026,26 @@ defmodule Mobilizon.Events do |> where([e], e.begins_on > ^after_datetime) end + defp event_filter_ends_on(query, nil, nil), do: query + + defp event_filter_ends_on(query, %DateTime{} = after_datetime, nil) do + where(query, [e], e.ends_on > ^after_datetime) + end + + defp event_filter_ends_on(query, nil, %DateTime{} = before_datetime) do + where(query, [e], e.ends_on < ^before_datetime) + end + + defp event_filter_ends_on( + query, + %DateTime{} = after_datetime, + %DateTime{} = before_datetime + ) do + query + |> where([e], e.ends_on < ^before_datetime) + |> where([e], e.ends_on > ^after_datetime) + end + defp event_order_by(query, order_by, direction) when order_by in [:begins_on, :inserted_at, :updated_at] and direction in [:asc, :desc] do order_by_instruction = Keyword.new([{direction, order_by}]) From 15850cc72cd76aab7ff3d8e201a495ee27834e41 Mon Sep 17 00:00:00 2001 From: Massedil <massedil-framagit.org@msd.im> Date: Sat, 23 Nov 2024 16:10:27 +0100 Subject: [PATCH 75/78] Issue #1066 : The "Update group" button indicates that a background update is in progress --- src/views/Group/GroupSettings.vue | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/views/Group/GroupSettings.vue b/src/views/Group/GroupSettings.vue index d8ecb53cc..ae9f0de0d 100644 --- a/src/views/Group/GroupSettings.vue +++ b/src/views/Group/GroupSettings.vue @@ -165,9 +165,12 @@ /> <div class="flex flex-wrap gap-2 my-2"> - <o-button native-type="submit" variant="primary">{{ - t("Update group") - }}</o-button> + <o-button + :loading="loadingUpdateGroup" + native-type="submit" + variant="primary" + >{{ t("Update group") }}</o-button + > <o-button @click="confirmDeleteGroup" variant="danger">{{ t("Delete group") }}</o-button> @@ -243,7 +246,12 @@ const showCopiedTooltip = ref(false); const editableGroup = ref<IGroup>(); -const { onDone, onError, mutate: updateGroup } = useUpdateGroup(); +const { + onDone, + onError, + mutate: updateGroup, + loading: loadingUpdateGroup, +} = useUpdateGroup(); onDone(() => { notifier?.success(t("Group settings saved")); From 71cc09dd7a41c88e564c76dbf4b9412751c38c73 Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 27 Nov 2024 15:35:20 +0100 Subject: [PATCH 76/78] #1587 : remove categories card in home page --- src/views/HomeView.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 31c216720..2c777dbb3 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -35,8 +35,6 @@ :addressDefaultText="userLocation?.name" :key="increated" /> - <!-- Categories preview --> - <categories-preview /> <!-- Welcome back --> <section class="container mx-auto" @@ -182,7 +180,6 @@ import { UPDATE_CURRENT_USER_LOCATION_CLIENT, } from "@/graphql/location"; import { LocationType } from "@/types/user-location.model"; -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"; From 448974385346f28881179a517214e79de2cffd4a Mon Sep 17 00:00:00 2001 From: Laurent GAY <l.gay@sd-libre.fr> Date: Wed, 27 Nov 2024 16:16:15 +0100 Subject: [PATCH 77/78] issue #1574 - home page & search page : 3 buttons for search --- src/components/Home/SearchFields.vue | 2 +- src/components/NavBar.vue | 35 ---------------------------- src/views/SearchView.vue | 31 ------------------------ 3 files changed, 1 insertion(+), 67 deletions(-) diff --git a/src/components/Home/SearchFields.vue b/src/components/Home/SearchFields.vue index 7a04715fc..6d4f331c2 100644 --- a/src/components/Home/SearchFields.vue +++ b/src/components/Home/SearchFields.vue @@ -58,7 +58,7 @@ class="search-Activity min-w-40 mr-1 mb-1" native-type="submit" icon-left="calendar-star" - v-if="search == null && isLongEvents" + v-if="isLongEvents" > {{ t("Activities") }} </o-button> diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index af5192e63..40dbc0417 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -179,39 +179,6 @@ <ul class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold" > - <li class="m-auto"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'EVENTS' }, - }" - class="block 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("Events") }}</router-link - > - </li> - <li class="m-auto" v-if="isLongEvents"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'LONGEVENTS' }, - }" - class="block 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("Activities") }}</router-link - > - </li> - <li class="m-auto"> - <router-link - :to="{ - ...$route, - name: RouteName.SEARCH, - query: { ...$route.query, contentType: 'GROUPS' }, - }" - class="block 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("Groups") }}</router-link - > - </li> <li class="m-auto"> <router-link :to="{ name: RouteName.EVENT_CALENDAR }" @@ -283,8 +250,6 @@ import { } from "@/graphql/user"; import { ICurrentUser } from "@/types/current-user.model"; -const { isLongEvents } = useIsLongEvents(); - const { currentUser } = useCurrentUserClient(); const { currentActor } = useCurrentActorClient(); diff --git a/src/views/SearchView.vue b/src/views/SearchView.vue index f3fb69005..2424306ff 100644 --- a/src/views/SearchView.vue +++ b/src/views/SearchView.vue @@ -716,7 +716,6 @@ const GROUP_PAGE_LIMIT = 16; const { features } = useFeatures(); const { eventCategories } = useEventCategories(); -const { isLongEvents } = useIsLongEvents(); const orderedCategories = computed(() => { if (!eventCategories.value) return []; @@ -829,36 +828,6 @@ const searchIsUrl = computed((): boolean => { return url.protocol === "http:" || url.protocol === "https:"; }); -const contentTypeMapping = computed(() => { - if (isLongEvents.value) { - return [ - { - contentType: ContentType.EVENTS, - label: t("Events"), - }, - { - contentType: ContentType.LONGEVENTS, - label: t("Activities"), - }, - { - contentType: ContentType.GROUPS, - label: t("Groups"), - }, - ]; - } else { - return [ - { - contentType: ContentType.EVENTS, - label: t("Events"), - }, - { - contentType: ContentType.GROUPS, - label: t("Groups"), - }, - ]; - } -}); - const eventStatuses = computed(() => { return [ { From 255c8fda67f3cf2f17ab4102b39ccd8d58d38834 Mon Sep 17 00:00:00 2001 From: setop <setop@zoocoop.com> Date: Fri, 6 Dec 2024 12:49:08 +0100 Subject: [PATCH 78/78] fix(back): event.ends_on can be nil --- lib/mobilizon/events/events.ex | 23 +++++++++++++++---- test/web/controllers/feed_controller_test.exs | 5 ++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 0b48fb02a..f741354c3 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -374,7 +374,7 @@ defmodule Mobilizon.Events do atom, boolean, boolean | nil, - string | nil, + String.t() | nil, float | nil ) :: Page.t(Event.t()) def list_events( @@ -2029,11 +2029,20 @@ defmodule Mobilizon.Events do defp event_filter_ends_on(query, nil, nil), do: query defp event_filter_ends_on(query, %DateTime{} = after_datetime, nil) do - where(query, [e], e.ends_on > ^after_datetime) + where( + query, + [e], + (is_nil(e.ends_on) and e.begins_on >= ^after_datetime) or + e.ends_on >= ^after_datetime + ) end defp event_filter_ends_on(query, nil, %DateTime{} = before_datetime) do - where(query, [e], e.ends_on < ^before_datetime) + where( + query, + [e], + (is_nil(e.ends_on) and e.begins_on <= ^before_datetime) or e.ends_on <= ^before_datetime + ) end defp event_filter_ends_on( @@ -2042,8 +2051,8 @@ defmodule Mobilizon.Events do %DateTime{} = before_datetime ) do query - |> where([e], e.ends_on < ^before_datetime) - |> where([e], e.ends_on > ^after_datetime) + |> event_filter_ends_on(after_datetime, nil) + |> event_filter_ends_on(nil, before_datetime) end defp event_order_by(query, order_by, direction) @@ -2145,6 +2154,10 @@ defmodule Mobilizon.Events do # Handling the case where Repo.XXXX() return nil def with_virtual_fields(nil), do: nil + # if envent has no end date, it can not be a long events + def with_virtual_fields(%Event{} = event) when is_nil(event.ends_on), + do: %{event | long_event: false} + # Handling the case where there is an event # Using Repo.one(), for example def with_virtual_fields(%Event{} = event) do diff --git a/test/web/controllers/feed_controller_test.exs b/test/web/controllers/feed_controller_test.exs index 0cf3155d6..b37cdafcf 100644 --- a/test/web/controllers/feed_controller_test.exs +++ b/test/web/controllers/feed_controller_test.exs @@ -115,11 +115,12 @@ defmodule Mobilizon.Web.FeedControllerTest do begins_on: DateTime.add(DateTime.utc_now(), 4, :day) ) - event3 = + event_in_the_past = insert(:event, organizer_actor: actor, attributed_to: group, title: "Event Three", + ends_on: DateTime.add(DateTime.utc_now(), -3, :day), begins_on: DateTime.add(DateTime.utc_now(), -2, :day) ) @@ -135,7 +136,7 @@ defmodule Mobilizon.Web.FeedControllerTest do Enum.each(entries, fn entry -> assert entry.summary in [event1.title, event2.title] - refute entry.summary == event3.title + refute entry.summary == event_in_the_past.title end) assert entry1.categories == [tag1.title]