From b0e8a32d2aab9c816dc3750391e57b9f0d8d7d9a Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Wed, 2 Sep 2020 17:42:17 +0200 Subject: [PATCH] Improvements to group page Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- js/src/components/Account/ActorCard.vue | 2 +- js/src/components/Editor.vue | 39 ++++++++---- js/src/components/Event/EventListViewCard.vue | 6 +- .../components/Event/EventMinimalistCard.vue | 41 ++++++++++++- js/src/graphql/group.ts | 36 ++++++++++- js/src/views/Event/GroupEvents.vue | 33 +++++++--- js/src/views/Group/Group.vue | 49 +++++++++------ js/src/views/Group/GroupSettings.vue | 16 ++--- lib/federation/activity_pub/types/actors.ex | 29 ++++++--- lib/graphql/resolvers/group.ex | 37 ++++++++++-- lib/graphql/schema/actors/group.ex | 4 ++ lib/mobilizon/actors/actor.ex | 4 +- lib/mobilizon/events/events.ex | 60 ++++++++++++++++++- lib/service/export/feed.ex | 2 +- lib/service/export/icalendar.ex | 2 +- lib/web/controllers/feed_controller.ex | 2 +- lib/web/views/json_ld/object_view.ex | 18 +++--- test/support/factory.ex | 3 +- test/web/controllers/feed_controller_test.exs | 2 +- 19 files changed, 298 insertions(+), 87 deletions(-) diff --git a/js/src/components/Account/ActorCard.vue b/js/src/components/Account/ActorCard.vue index b5fbe5ee9..4f910e534 100644 --- a/js/src/components/Account/ActorCard.vue +++ b/js/src/components/Account/ActorCard.vue @@ -13,7 +13,7 @@ {{ actor.name || `@${usernameWithDomain(actor)}` }} </p> <p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p> - <p v-if="full" class="summary" :class="{ limit: limit }">{{ actor.summary }}</p> + <div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" /> </div> </div> </div> diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index d6a077998..68982a291 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -40,6 +40,7 @@ </button> <button + v-if="!isBasicMode" class="menubar__button" :class="{ 'is-active': isActive.heading({ level: 1 }) }" @click="commands.heading({ level: 1 })" @@ -49,6 +50,7 @@ </button> <button + v-if="!isBasicMode" class="menubar__button" :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })" @@ -58,6 +60,7 @@ </button> <button + v-if="!isBasicMode" class="menubar__button" :class="{ 'is-active': isActive.heading({ level: 3 }) }" @click="commands.heading({ level: 3 })" @@ -75,12 +78,18 @@ <b-icon icon="link" /> </button> - <button class="menubar__button" @click="showImagePrompt(commands.image)" type="button"> + <button + class="menubar__button" + v-if="!isBasicMode" + @click="showImagePrompt(commands.image)" + type="button" + > <b-icon icon="image" /> </button> <button class="menubar__button" + v-if="!isBasicMode" :class="{ 'is-active': isActive.bullet_list() }" @click="commands.bullet_list" type="button" @@ -89,6 +98,7 @@ </button> <button + v-if="!isBasicMode" class="menubar__button" :class="{ 'is-active': isActive.ordered_list() }" @click="commands.ordered_list" @@ -98,6 +108,7 @@ </button> <button + v-if="!isBasicMode" class="menubar__button" :class="{ 'is-active': isActive.blockquote() }" @click="commands.blockquote" @@ -106,11 +117,11 @@ <b-icon icon="format-quote-close" /> </button> - <button class="menubar__button" @click="commands.undo" type="button"> + <button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button"> <b-icon icon="undo" /> </button> - <button class="menubar__button" @click="commands.redo" type="button"> + <button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button"> <b-icon icon="redo" /> </button> </div> @@ -229,26 +240,30 @@ export default class EditorComponent extends Vue { filteredActors: IActor[] = []; - suggestionRange!: object | null; + suggestionRange!: Record<string, unknown> | null; navigatedActorIndex = 0; popup!: Instance[] | null; - get isDescriptionMode() { - return this.mode === "description"; + get isDescriptionMode(): boolean { + return this.mode === "description" || this.isBasicMode; } - get isCommentMode() { + get isCommentMode(): boolean { return this.mode === "comment"; } - get hasResults() { - return this.filteredActors.length; + get hasResults(): boolean { + return this.filteredActors.length > 0; } - get showSuggestions() { - return this.query || this.hasResults; + get showSuggestions(): boolean { + return (this.query || this.hasResults) as boolean; + } + + get isBasicMode(): boolean { + return this.mode === "basic"; } // eslint-disable-next-line @@ -258,7 +273,7 @@ export default class EditorComponent extends Vue { observer!: MutationObserver | null; - mounted() { + mounted(): void { this.editor = new Editor({ extensions: [ new Blockquote(), diff --git a/js/src/components/Event/EventListViewCard.vue b/js/src/components/Event/EventListViewCard.vue index 8e68e2f20..aed279d58 100644 --- a/js/src/components/Event/EventListViewCard.vue +++ b/js/src/components/Event/EventListViewCard.vue @@ -16,7 +16,7 @@ </span> <span> <span> - {{ $t("Organized by {name}", { name: event.organizerActor.displayName() }) }} + {{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }} </span> </span> </div> @@ -53,7 +53,7 @@ import { ParticipantRole, EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model"; import { Component, Prop } from "vue-property-decorator"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; -import { IPerson } from "@/types/actor"; +import { IPerson, usernameWithDomain } from "@/types/actor"; import { mixins } from "vue-class-component"; import ActorMixin from "@/mixins/actor"; import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; @@ -96,6 +96,8 @@ export default class EventListViewCard extends mixins(ActorMixin, EventMixin) { EventVisibility = EventVisibility; RouteName = RouteName; + + usernameWithDomain = usernameWithDomain; } </script> diff --git a/js/src/components/Event/EventMinimalistCard.vue b/js/src/components/Event/EventMinimalistCard.vue index ae3a312b2..7ede0d852 100644 --- a/js/src/components/Event/EventMinimalistCard.vue +++ b/js/src/components/Event/EventMinimalistCard.vue @@ -9,7 +9,46 @@ <p v-if="event.physicalAddress" class="has-text-grey"> {{ event.physicalAddress.description }} </p> - <p v-else>3 demandes de participation à traiter</p> + <p v-else> + <span v-if="event.options.maximumAttendeeCapacity !== 0"> + {{ + $tc( + "{available}/{capacity} available places", + event.options.maximumAttendeeCapacity - event.participantStats.participant, + { + available: + event.options.maximumAttendeeCapacity - event.participantStats.participant, + capacity: event.options.maximumAttendeeCapacity, + } + ) + }} + </span> + <span v-else> + {{ + $tc("{count} participants", event.participantStats.participant, { + count: event.participantStats.participant, + }) + }} + </span> + <span v-if="event.participantStats.notApproved > 0"> + <b-button + type="is-text" + @click=" + gotToWithCheck(participation, { + name: RouteName.PARTICIPATIONS, + query: { role: ParticipantRole.NOT_APPROVED }, + params: { eventId: event.uuid }, + }) + " + > + {{ + $tc("{count} requests waiting", event.participantStats.notApproved, { + count: event.participantStats.notApproved, + }) + }} + </b-button> + </span> + </p> </div> </router-link> </template> diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index 1dddfe9dd..a87f30127 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -78,12 +78,30 @@ export const GROUP_FIELDS_FRAGMENTS = gql` banner { url } - organizedEvents { + organizedEvents( + afterDatetime: $afterDateTime + beforeDatetime: $beforeDateTime + page: $organisedEventsPage + limit: $organisedEventslimit + ) { elements { id uuid title beginsOn + options { + maximumAttendeeCapacity + } + participantStats { + participant + notApproved + } + organizerActor { + id + preferredUsername + name + domain + } } total } @@ -154,7 +172,13 @@ export const GROUP_FIELDS_FRAGMENTS = gql` `; export const FETCH_GROUP = gql` - query($name: String!) { + query( + $name: String! + $afterDateTime: DateTime + $beforeDateTime: DateTime + $organisedEventsPage: Int + $organisedEventslimit: Int + ) { group(preferredUsername: $name) { ...GroupFullFields } @@ -166,7 +190,13 @@ export const FETCH_GROUP = gql` `; export const GET_GROUP = gql` - query($id: ID!) { + query( + $id: ID! + $afterDateTime: DateTime + $beforeDateTime: DateTime + $organisedEventsPage: Int + $organisedEventslimit: Int + ) { getGroup(id: $id) { ...GroupFullFields } diff --git a/js/src/views/Event/GroupEvents.vue b/js/src/views/Event/GroupEvents.vue index 0d63a32a7..e7edd6e23 100644 --- a/js/src/views/Event/GroupEvents.vue +++ b/js/src/views/Event/GroupEvents.vue @@ -34,20 +34,25 @@ }} </p> <b-loading :active.sync="$apollo.loading"></b-loading> - <section v-if="group && group.organizedEvents.total > 0"> + <section v-if="group"> <subtitle> - {{ $t("Past events") }} + {{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }} </subtitle> + <b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch> <transition-group name="list" tag="p"> - <EventListViewCard v-for="event in group.organizedEvents.elements" :key="event.id" /> + <EventListViewCard + v-for="event in group.organizedEvents.elements" + :key="event.id" + :event="event" + /> </transition-group> + <b-message + v-if="group.organizedEvents.elements.length === 0 && $apollo.loading === false" + type="is-danger" + > + {{ $t("No events found") }} + </b-message> </section> - <b-message - v-if="group.organizedEvents.elements.length === 0 && $apollo.loading === false" - type="is-danger" - > - {{ $t("No events found") }} - </b-message> </section> </div> </template> @@ -55,6 +60,8 @@ import { Component, Vue } from "vue-property-decorator"; import { FETCH_GROUP } from "@/graphql/group"; import RouteName from "@/router/name"; +import Subtitle from "@/components/Utils/Subtitle.vue"; +import EventListViewCard from "@/components/Event/EventListViewCard.vue"; import { IGroup, usernameWithDomain } from "../../types/actor"; @Component({ @@ -64,10 +71,16 @@ import { IGroup, usernameWithDomain } from "../../types/actor"; variables() { return { name: this.$route.params.preferredUsername, + beforeDateTime: this.showPassedEvents ? new Date() : null, + afterDateTime: this.showPassedEvents ? null : new Date(), }; }, }, }, + components: { + Subtitle, + EventListViewCard, + }, }) export default class GroupEvents extends Vue { group!: IGroup; @@ -75,5 +88,7 @@ export default class GroupEvents extends Vue { usernameWithDomain = usernameWithDomain; RouteName = RouteName; + + showPassedEvents = false; } </script> diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index db93fa014..70e3a739b 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -302,6 +302,10 @@ {{ $t("No group found") }} </b-message> <div v-else class="public-container"> + <section> + <subtitle>{{ $t("About") }}</subtitle> + <div v-html="group.summary" /> + </section> <section> <subtitle>{{ $t("Upcoming events") }}</subtitle> <div class="organized-events-wrapper" v-if="group && group.organizedEvents.total > 0"> @@ -318,16 +322,12 @@ </section> <section> <subtitle>{{ $t("Latest posts") }}</subtitle> - <div v-if="group && group.posts.total > 0"> - <router-link - v-for="post in group.posts.elements" - :key="post.id" - :to="{ name: RouteName.POST, params: { slug: post.slug } }" - > - {{ post.title }} - </router-link> + <div v-if="group.posts.total > 0" class="posts-wrapper"> + <post-list-item v-for="post in group.posts.elements" :key="post.id" :post="post" /> + </div> + <div v-else-if="group" class="content has-text-grey has-text-centered"> + <p>{{ $t("No posts yet") }}</p> </div> - <span v-else-if="group">{{ $t("No public posts") }}</span> <b-skeleton animated v-else></b-skeleton> </section> <b-modal v-if="physicalAddress && physicalAddress.geom" :active.sync="showMap"> @@ -369,6 +369,7 @@ import FolderItem from "@/components/Resource/FolderItem.vue"; import { Address } from "@/types/address.model"; import Invitations from "@/components/Group/Invitations.vue"; import addMinutes from "date-fns/addMinutes"; +import { Route } from "vue-router"; import GroupSection from "../../components/Group/GroupSection.vue"; import RouteName from "../../router/name"; @@ -413,11 +414,13 @@ import RouteName from "../../router/name"; metaInfo() { return { // if no subcomponents specify a metaInfo.title, this title will be used + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore title: this.groupTitle, // all titles will be injected into this template titleTemplate: "%s | Mobilizon", meta: [ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore { name: "description", content: this.groupSummary }, ], @@ -442,14 +445,14 @@ export default class Group extends Vue { showMap = false; @Watch("currentActor") - watchCurrentActor(currentActor: IActor, oldActor: IActor) { + watchCurrentActor(currentActor: IActor, oldActor: IActor): void { if (currentActor.id && oldActor && currentActor.id !== oldActor.id) { this.$apollo.queries.group.refetch(); } } - async leaveGroup() { - const { data } = await this.$apollo.mutate({ + async leaveGroup(): Promise<Route> { + await this.$apollo.mutate({ mutation: LEAVE_GROUP, variables: { groupId: this.group.id, @@ -458,9 +461,10 @@ export default class Group extends Vue { return this.$router.push({ name: RouteName.MY_GROUPS }); } - acceptInvitation() { + acceptInvitation(): void { if (this.groupMember) { const index = this.person.memberships.elements.findIndex( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ({ id }: IMember) => id === this.groupMember.id ); @@ -471,12 +475,12 @@ export default class Group extends Vue { } } - get groupTitle() { + get groupTitle(): undefined | string { if (!this.group) return undefined; return this.group.preferredUsername; } - get groupSummary() { + get groupSummary(): undefined | string { if (!this.group) return undefined; return this.group.summary; } @@ -486,8 +490,8 @@ export default class Group extends Vue { return this.person.memberships.elements.find(({ parent: { id } }) => id === this.group.id); } - get groupMemberships() { - if (!this.person || !this.person.id) return undefined; + get groupMemberships(): (string | undefined)[] { + if (!this.person || !this.person.id) return []; return this.person.memberships.elements .filter( (membership: IMember) => @@ -499,7 +503,7 @@ export default class Group extends Vue { } get isCurrentActorAGroupMember(): boolean { - return this.groupMemberships != undefined && this.groupMemberships.includes(this.group.id); + return this.groupMemberships !== undefined && this.groupMemberships.includes(this.group.id); } get isCurrentActorARejectedGroupMember(): boolean { @@ -532,7 +536,8 @@ export default class Group extends Vue { } /** - * New members, if on a different server, can take a while to refresh the group and fetch all private data + * New members, if on a different server, + * can take a while to refresh the group and fetch all private data */ get isCurrentActorARecentMember(): boolean { return ( @@ -673,5 +678,11 @@ div.container { } } } + + .public-container { + section { + margin-top: 2rem; + } + } } </style> diff --git a/js/src/views/Group/GroupSettings.vue b/js/src/views/Group/GroupSettings.vue index 084557e43..0ad4ea423 100644 --- a/js/src/views/Group/GroupSettings.vue +++ b/js/src/views/Group/GroupSettings.vue @@ -37,7 +37,7 @@ <b-input v-model="group.name" /> </b-field> <b-field :label="$t('Group short description')"> - <b-input type="textarea" v-model="group.summary" + <editor mode="basic" v-model="group.summary" /></b-field> <p class="label">{{ $t("Group visibility") }}</p> <div class="field"> @@ -105,12 +105,12 @@ <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue"; +import { Route } from "vue-router"; import RouteName from "../../router/name"; import { FETCH_GROUP, UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group"; import { IGroup, usernameWithDomain } from "../../types/actor"; import { Address, IAddress } from "../../types/address.model"; -import { IMember, Group } from "../../types/actor/group.model"; -import { Paginate } from "../../types/paginate"; +import { Group } from "../../types/actor/group.model"; @Component({ apollo: { @@ -129,6 +129,7 @@ import { Paginate } from "../../types/paginate"; }, components: { FullAddressAutoComplete, + editor: () => import("../../components/Editor.vue"), }, }) export default class GroupSettings extends Vue { @@ -149,7 +150,7 @@ export default class GroupSettings extends Vue { showCopiedTooltip = false; - async updateGroup() { + async updateGroup(): Promise<void> { const variables = { ...this.group }; // eslint-disable-next-line // @ts-ignore @@ -165,7 +166,7 @@ export default class GroupSettings extends Vue { }); } - confirmDeleteGroup() { + confirmDeleteGroup(): void { this.$buefy.dialog.confirm({ title: this.$t("Delete group") as string, message: this.$t( @@ -179,7 +180,7 @@ export default class GroupSettings extends Vue { }); } - async deleteGroup() { + async deleteGroup(): Promise<Route> { await this.$apollo.mutate<{ deleteGroup: IGroup }>({ mutation: DELETE_GROUP, variables: { @@ -189,7 +190,7 @@ export default class GroupSettings extends Vue { return this.$router.push({ name: RouteName.MY_GROUPS }); } - async copyURL() { + async copyURL(): Promise<void> { await window.navigator.clipboard.writeText(this.group.url); this.showCopiedTooltip = true; setTimeout(() => { @@ -197,6 +198,7 @@ export default class GroupSettings extends Vue { }, 2000); } + // eslint-disable-next-line class-methods-use-this get canShowCopyButton(): boolean { return window.isSecureContext; } diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index 824bb3b3d..77dfc8871 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -89,12 +89,27 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do def group_actor(%Actor{} = actor), do: actor defp prepare_args_for_actor(args) do - with preferred_username <- - args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), - summary <- args |> Map.get(:summary, "") |> String.trim(), - {summary, _mentions, _tags} <- - summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do - %{args | preferred_username: preferred_username, summary: summary} - end + args + |> maybe_sanitize_username() + |> maybe_sanitize_summary() end + + @spec maybe_sanitize_username(map()) :: map() + defp maybe_sanitize_username(%{preferred_username: preferred_username} = args) do + Map.put(args, :preferred_username, preferred_username |> HTML.strip_tags() |> String.trim()) + end + + defp maybe_sanitize_username(args), do: args + + @spec maybe_sanitize_summary(map()) :: map() + defp maybe_sanitize_summary(%{summary: summary} = args) do + {summary, _mentions, _tags} = + summary + |> String.trim() + |> APIUtils.make_content_html([], "text/html") + + Map.put(args, :summary, summary) + end + + defp maybe_sanitize_summary(args), do: args end diff --git a/lib/graphql/resolvers/group.ex b/lib/graphql/resolvers/group.ex index 6bbf276f6..3b33b404a 100644 --- a/lib/graphql/resolvers/group.ex +++ b/lib/graphql/resolvers/group.ex @@ -9,7 +9,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do alias Mobilizon.Federation.ActivityPub alias Mobilizon.GraphQL.API alias Mobilizon.GraphQL.Resolvers.Person - alias Mobilizon.Storage.Page alias Mobilizon.Users.User require Logger @@ -271,7 +270,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do def find_events_for_group( %Actor{id: group_id} = group, - _args, + %{ + page: page, + limit: limit + } = args, %{ context: %{ current_user: %User{role: user_role} = user @@ -282,15 +284,38 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do # TODO : Handle public / restricted to group members events - {:ok, Events.list_organized_events_for_group(group)} + {:ok, + Events.list_organized_events_for_group( + group, + :all, + Map.get(args, :after_datetime), + Map.get(args, :before_datetime), + page, + limit + )} else {:member, false} -> - {:ok, %Page{total: 0, elements: []}} + find_events_for_group(group, args, nil) end end - def find_events_for_group(_parent, _args, _resolution) do - {:ok, %Page{total: 0, elements: []}} + def find_events_for_group( + %Actor{} = group, + %{ + page: page, + limit: limit + } = args, + _resolution + ) do + {:ok, + Events.list_organized_events_for_group( + group, + :public, + Map.get(args, :after_datetime), + Map.get(args, :before_datetime), + page, + limit + )} end defp restrict_fields_for_non_member_request(%Actor{} = group) do diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index 3a357d46c..395a25e48 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -54,6 +54,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do # This one should have a privacy setting field :organized_events, :paginated_event_list do + arg(:after_datetime, :datetime, default_value: nil) + arg(:before_datetime, :datetime, default_value: nil) + arg(:page, :integer, default_value: 1) + arg(:limit, :integer, default_value: 10) resolve(&Group.find_events_for_group/3) description("A list of the events this actor has organized") end diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index d59154fa6..9d49973f8 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -179,8 +179,8 @@ defmodule Mobilizon.Actors.Actor do @doc """ Checks whether actor visibility is public. """ - @spec is_public_visibility(t) :: boolean - def is_public_visibility(%__MODULE__{visibility: visibility}) do + @spec is_public_visibility?(t) :: boolean + def is_public_visibility?(%__MODULE__{visibility: visibility}) do visibility in [:public, :unlisted] end diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index 7fd6e996c..a492de2d5 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -405,7 +405,7 @@ defmodule Mobilizon.Events do def list_public_events_for_actor(actor, page \\ nil, limit \\ nil) def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit), - do: list_organized_events_for_group(group, page, limit) + do: list_organized_events_for_group(group, :public, nil, page, limit) def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do actor_id @@ -424,10 +424,25 @@ defmodule Mobilizon.Events do |> Page.build_page(page, limit) end - @spec list_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t() - def list_organized_events_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do + @spec list_organized_events_for_group( + Actor.t(), + DateTime.t() | nil, + DateTime.t() | nil, + integer | nil, + integer | nil + ) :: Page.t() + def list_organized_events_for_group( + %Actor{id: group_id}, + visibility \\ :public, + after_datetime \\ nil, + before_datetime \\ nil, + page \\ nil, + limit \\ nil + ) do group_id |> event_for_group_query() + |> event_filter_visibility(visibility) + |> event_filter_begins_on(after_datetime, before_datetime) |> preload_for_event() |> Page.build_page(page, limit) end @@ -1643,6 +1658,45 @@ defmodule Mobilizon.Events do from(p in query, where: p.role == ^role) end + defp event_filter_visibility(query, :all), do: query + + defp event_filter_visibility(query, :public) do + query + |> where(visibility: ^:public) + end + + defp event_filter_begins_on(query, nil, nil), + do: event_order_begins_on_desc(query) + + defp event_filter_begins_on(query, %DateTime{} = after_datetime, nil) do + query + |> where([e], e.begins_on > ^after_datetime) + |> event_order_begins_on_asc() + end + + defp event_filter_begins_on(query, nil, %DateTime{} = before_datetime) do + query + |> where([e], e.begins_on < ^before_datetime) + |> event_order_begins_on_desc() + end + + defp event_filter_begins_on( + query, + %DateTime{} = after_datetime, + %DateTime{} = before_datetime + ) do + query + |> where([e], e.begins_on < ^before_datetime) + |> where([e], e.begins_on > ^after_datetime) + |> event_order_begins_on_asc() + end + + defp event_order_begins_on_asc(query), + do: order_by(query, [e], asc: e.begins_on) + + defp event_order_begins_on_desc(query), + do: order_by(query, [e], desc: e.begins_on) + defp participation_filter_begins_on(query, nil, nil), do: participation_order_begins_on_desc(query) diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index ec077d141..92f3a9d3a 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -46,7 +46,7 @@ defmodule Mobilizon.Service.Export.Feed do @spec fetch_actor_event_feed(String.t()) :: String.t() defp fetch_actor_event_feed(name) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name), - {:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)}, + {:visibility, true} <- {:visibility, Actor.is_public_visibility?(actor)}, %Page{elements: events} <- Events.list_public_events_for_actor(actor) do {:ok, build_actor_feed(actor, events)} else diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index f4b0d047a..eb6e02f24 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -48,7 +48,7 @@ defmodule Mobilizon.Service.Export.ICalendar do """ @spec export_public_actor(Actor.t()) :: String.t() def export_public_actor(%Actor{} = actor) do - with true <- Actor.is_public_visibility(actor), + with {:visibility, true} <- {:visibility, Actor.is_public_visibility?(actor)}, %Page{elements: events} <- Events.list_public_events_for_actor(actor) do {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()} diff --git a/lib/web/controllers/feed_controller.ex b/lib/web/controllers/feed_controller.ex index 129563098..56a522da6 100644 --- a/lib/web/controllers/feed_controller.ex +++ b/lib/web/controllers/feed_controller.ex @@ -25,7 +25,7 @@ defmodule Mobilizon.Web.FeedController do |> put_resp_content_type("text/calendar") |> send_resp(200, data) - _ -> + _err -> {:error, :not_found} end end diff --git a/lib/web/views/json_ld/object_view.ex b/lib/web/views/json_ld/object_view.ex index 7db4a393f..5c241e609 100644 --- a/lib/web/views/json_ld/object_view.ex +++ b/lib/web/views/json_ld/object_view.ex @@ -5,8 +5,8 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do alias Mobilizon.Addresses.Address alias Mobilizon.Events.Event alias Mobilizon.Posts.Post + alias Mobilizon.Web.{Endpoint, MediaProxy} alias Mobilizon.Web.JsonLD.ObjectView - alias Mobilizon.Web.MediaProxy def render("group.json", %{group: %Actor{} = group}) do %{ @@ -37,18 +37,16 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do if(event.status == :cancelled, do: "https://schema.org/EventCancelled", else: "https://schema.org/EventScheduled" + ), + "image" => + if(event.picture, + do: [ + event.picture.file.url |> MediaProxy.url() + ], + else: ["#{Endpoint.url()}/img/mobilizon_default_card.png"] ) } - json_ld = - if event.picture do - Map.put(json_ld, "image", [ - event.picture.file.url |> MediaProxy.url() - ]) - else - json_ld - end - json_ld = if event.begins_on, do: Map.put(json_ld, "startDate", DateTime.to_iso8601(event.begins_on)), diff --git a/test/support/factory.ex b/test/support/factory.ex index a53112992..0f8d555f8 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -53,7 +53,8 @@ defmodule Mobilizon.Factory do outbox_url: Actor.build_url(preferred_username, :outbox), shared_inbox_url: "#{Endpoint.url()}/inbox", last_refreshed_at: DateTime.utc_now(), - user: build(:user) + user: build(:user), + visibility: :public } end diff --git a/test/web/controllers/feed_controller_test.exs b/test/web/controllers/feed_controller_test.exs index e6343a2ea..7d141b263 100644 --- a/test/web/controllers/feed_controller_test.exs +++ b/test/web/controllers/feed_controller_test.exs @@ -44,7 +44,7 @@ defmodule Mobilizon.Web.FeedControllerTest do test "it returns a 404 for the actor's public events Atom feed if the actor is not publicly visible", %{conn: conn} do - actor = insert(:actor) + actor = insert(:actor, visibility: :private) tag1 = insert(:tag, title: "RSS", slug: "rss") tag2 = insert(:tag, title: "ATOM", slug: "atom") insert(:event, organizer_actor: actor, tags: [tag1])