diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 63cfd896..c45fd44d 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -158,8 +158,6 @@ export const FETCH_EVENTS = gql` url } publishAt - # online_address, - # phone_address, physicalAddress { ...AdressFragment } diff --git a/js/src/graphql/search.ts b/js/src/graphql/search.ts index a79d318a..c0124840 100644 --- a/js/src/graphql/search.ts +++ b/js/src/graphql/search.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; import { ADDRESS_FRAGMENT } from "./address"; +import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; import { TAG_FRAGMENT } from "./tags"; export const SEARCH_EVENTS_AND_GROUPS = gql` @@ -9,9 +10,11 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` $radius: Float $tags: String $term: String + $type: EventType $beginsOn: DateTime $endsOn: DateTime - $page: Int + $eventPage: Int + $groupPage: Int $limit: Int ) { searchEvents( @@ -19,9 +22,10 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` radius: $radius tags: $tags term: $term + type: $type beginsOn: $beginsOn endsOn: $endsOn - page: $page + page: $eventPage limit: $limit ) { total @@ -46,6 +50,9 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` attributedTo { ...ActorFragment } + options { + ...EventOptions + } __typename } } @@ -53,7 +60,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` term: $term location: $location radius: $radius - page: $page + page: $groupPage limit: $limit ) { total @@ -75,6 +82,7 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` } } } + ${EVENT_OPTIONS_FRAGMENT} ${TAG_FRAGMENT} ${ADDRESS_FRAGMENT} ${ACTOR_FRAGMENT} diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index c7462900..c7459ab0 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1230,5 +1230,7 @@ "Clear date filter field": "Clear date filter field", "{count} members or followers": "No members or followers|One member or follower|{count} members or followers", "This profile is from another instance, the informations shown here may be incomplete.": "This profile is from another instance, the informations shown here may be incomplete.", - "View full profile": "View full profile" + "View full profile": "View full profile", + "Any type": "Any type", + "In person": "In person" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index d4d2eefa..1b92dcdf 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1334,5 +1334,7 @@ "Clear date filter field": "Vider le champ de filtre de la date", "{count} members or followers": "Aucun⋅e membre ou abonné⋅e|Un⋅e membre ou abonné⋅e|{count} membres ou abonné⋅es", "This profile is from another instance, the informations shown here may be incomplete.": "Ce profil provient d'une autre instance, les informations montrées ici peuvent être incomplètes.", - "View full profile": "Voir le profil complet" + "View full profile": "Voir le profil complet", + "Any type": "N'importe quel type", + "In person": "En personne" } diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 6f66e085..35899be1 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -31,6 +31,8 @@ export interface IEventParticipantStats { going: number; } +export type EventType = "IN_PERSON" | "ONLINE" | null; + interface IEventEditJSON { id?: string; title: string; diff --git a/js/src/views/Search.vue b/js/src/views/Search.vue index e51e26a7..e0521cca 100644 --- a/js/src/views/Search.vue +++ b/js/src/views/Search.vue @@ -9,56 +9,84 @@
- + - - - - - - - - - - - + + + - - + {{ radiusString(radiusOption) }} + + + + + + + + + + + + + +
@@ -171,16 +199,17 @@ import { import { SearchTabs } from "@/types/enums"; import MultiCard from "../components/Event/MultiCard.vue"; import { FETCH_EVENTS } from "../graphql/event"; -import { IEvent } from "../types/event.model"; +import { EventType, IEvent } from "../types/event.model"; import RouteName from "../router/name"; import { IAddress, Address } from "../types/address.model"; -import AddressAutoComplete from "../components/Event/AddressAutoComplete.vue"; +import FullAddressAutoComplete from "../components/Event/FullAddressAutoComplete.vue"; import { SEARCH_EVENTS_AND_GROUPS } from "../graphql/search"; import { Paginate } from "../types/paginate"; import { IGroup } from "../types/actor"; import MultiGroupCard from "../components/Group/MultiGroupCard.vue"; import { CONFIG } from "../graphql/config"; import { REVERSE_GEOCODE } from "../graphql/address"; +import debounce from "lodash/debounce"; interface ISearchTimeOption { label: string; @@ -198,12 +227,10 @@ const DEFAULT_ZOOM = 11; // zoom on a city const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway -const THROTTLE = 2000; // minimum interval in ms between two requests - @Component({ components: { MultiCard, - AddressAutoComplete, + FullAddressAutoComplete, MultiGroupCard, }, apollo: { @@ -217,7 +244,7 @@ const THROTTLE = 2000; // minimum interval in ms between two requests }; }, }, - search: { + searchElements: { query: SEARCH_EVENTS_AND_GROUPS, fetchPolicy: "cache-and-network", variables() { @@ -228,15 +255,16 @@ const THROTTLE = 2000; // minimum interval in ms between two requests beginsOn: this.start, endsOn: this.end, radius: this.radius, - page: this.eventPage, + eventPage: this.eventPage, + groupPage: this.groupPage, limit: EVENT_PAGE_LIMIT, + type: this.type, }; }, update(data) { this.searchEvents = data.searchEvents; this.searchGroups = data.searchGroups; }, - throttle: THROTTLE, }, }, metaInfo() { @@ -261,11 +289,9 @@ export default class Search extends Vue { searchGroups: Paginate = { total: 0, elements: [] }; - groupPage = 1; - location: IAddress = new Address(); - options: Record = { + dateOptions: Record = { today: { label: this.$t("Today") as string, start: new Date(), @@ -315,9 +341,15 @@ export default class Search extends Vue { GROUP_PAGE_LIMIT = GROUP_PAGE_LIMIT; $refs!: { - aac: AddressAutoComplete; + aac: FullAddressAutoComplete; }; + data(): Record { + return { + debouncedUpdateSearchQuery: debounce(this.updateSearchQuery, 200), + }; + } + mounted(): void { this.prepareLocation(this.$route.query.geohash as string); } @@ -335,6 +367,10 @@ export default class Search extends Vue { this.$apollo.queries.searchEvents.refetch(); } + updateSearchQuery(searchQuery: string): void { + this.search = searchQuery; + } + get eventPage(): number { return parseInt(this.$route.query.eventPage as string, 10) || 1; } @@ -346,6 +382,17 @@ export default class Search extends Vue { }); } + get groupPage(): number { + return parseInt(this.$route.query.groupPage as string, 10) || 1; + } + + set groupPage(page: number) { + this.$router.push({ + name: this.$route.name || RouteName.SEARCH, + query: { ...this.$route.query, groupPage: page.toString() }, + }); + } + get search(): string | undefined { return this.$route.query.term as string; } @@ -411,6 +458,23 @@ export default class Search extends Vue { }); } + get type(): EventType { + return this.$route.query.type as EventType; + } + + set type(type: EventType) { + const query = { ...this.$route.query, type }; + if (type == null) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete query.type; + } + this.$router.replace({ + name: RouteName.SEARCH, + query, + }); + } + get weekend(): { start: Date; end: Date } { const now = new Date(); const endOfWeekDate = endOfWeek(now, { locale: this.$dateFnsLocale }); @@ -453,22 +517,24 @@ export default class Search extends Vue { if (this.radius === undefined || this.radius === null) { this.radius = DEFAULT_RADIUS; } - if (e.geom) { + if (e?.geom) { const [lon, lat] = e.geom.split(";"); this.geohash = ngeohash.encode(lat, lon, GEOHASH_DEPTH); + } else { + this.geohash = undefined; } }; get start(): Date | undefined { - if (this.options[this.when]) { - return this.options[this.when].start; + if (this.dateOptions[this.when]) { + return this.dateOptions[this.when].start; } return undefined; } get end(): Date | undefined | null { - if (this.options[this.when]) { - return this.options[this.when].end; + if (this.dateOptions[this.when]) { + return this.dateOptions[this.when].end; } return undefined; } @@ -484,6 +550,7 @@ export default class Search extends Vue { return ( this.stringExists(this.search) || this.stringExists(this.tag) || + this.stringExists(this.type) || (this.stringExists(this.geohash) && this.valueExists(this.radius)) || this.valueExists(this.end) ); @@ -494,13 +561,14 @@ export default class Search extends Vue { return value !== undefined && value !== null; } - private stringExists(value: string | undefined): boolean { + private stringExists(value: string | null | undefined): boolean { return this.valueExists(value) && (value as string).length > 0; } } diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index c1185614..db0c7be5 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -44,6 +44,15 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do end) end + enum :event_type do + value(:in_person, + description: + "The event will happen in person. It can also be livestreamed, but has a physical address" + ) + + value(:online, description: "The event will only happen online. It has no physical address") + end + object :search_queries do @desc "Search persons" field :search_persons, :persons do @@ -83,6 +92,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do arg(:term, :string, default_value: "") arg(:tags, :string, description: "A comma-separated string listing the tags") arg(:location, :string, description: "A geohash for coordinates") + arg(:type, :event_type, description: "Whether the event is online or in person") arg(:radius, :float, default_value: 50, diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 6960acc9..2b851017 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -506,6 +506,7 @@ defmodule Mobilizon.Events do |> events_for_ends_on(args) |> events_for_tags(args) |> events_for_location(args) + |> filter_online(args) |> filter_draft() |> filter_local_or_from_followed_instances_events() |> filter_public_visibility() @@ -1307,6 +1308,18 @@ defmodule Mobilizon.Events do defp events_for_location(query, _args), do: query + @spec filter_online(Ecto.Query.t(), map()) :: Ecto.Query.t() + defp filter_online(query, %{type: :online}), do: is_online_fragment(query, true) + + defp filter_online(query, %{type: :in_person}), do: is_online_fragment(query, false) + + defp filter_online(query, _), do: query + + @spec is_online_fragment(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + defp is_online_fragment(query, value) do + where(query, [q], fragment("(?->>'is_online')::bool = ?", q.options, ^value)) + end + @spec normalize_search_string(String.t()) :: String.t() defp normalize_search_string(search_string) do search_string