From 03824b898ce81bb3f67dd70274c7f0d2e3d73e8e Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 5 Mar 2021 11:23:17 +0100 Subject: [PATCH] Get membership status only for the current group Closes #575 Signed-off-by: Thomas Citharel --- js/src/graphql/actor.ts | 64 ++++++++++++++++++++ js/src/graphql/event.ts | 32 ---------- js/src/mixins/group.ts | 31 ++++++---- js/src/views/Discussions/DiscussionsList.vue | 21 +++++-- js/src/views/Group/Group.vue | 11 ++-- js/src/views/Group/GroupFollowers.vue | 3 +- lib/federation/activity_pub/activity_pub.ex | 5 +- lib/federation/activity_pub/types/actors.ex | 4 +- lib/federation/activity_pub/types/members.ex | 11 +++- lib/graphql/resolvers/person.ex | 33 ++++++++-- lib/graphql/schema/actors/person.ex | 13 +++- 11 files changed, 161 insertions(+), 67 deletions(-) diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 1243f601e..19f7d1bc4 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -446,6 +446,70 @@ export const PERSON_MEMBERSHIPS_WITH_MEMBERS = gql` } `; +export const PERSON_MEMBERSHIP_GROUP = gql` + query PersonMembershipGroup($id: ID!, $group: String!) { + person(id: $id) { + id + memberships(group: $group) { + total + elements { + id + role + parent { + id + preferredUsername + name + domain + avatar { + id + url + } + } + invitedBy { + id + preferredUsername + name + } + insertedAt + updatedAt + } + } + } + } +`; + +export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql` + subscription($actorId: ID!, $group: String!) { + groupMembershipChanged(personId: $actorId, group: $group) { + id + memberships { + total + elements { + id + role + parent { + id + preferredUsername + name + domain + avatar { + id + url + } + } + invitedBy { + id + preferredUsername + name + } + insertedAt + updatedAt + } + } + } + } +`; + export const CREATE_PERSON = gql` mutation CreatePerson( $preferredUsername: String! diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 1012798c2..6e532fd86 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -592,38 +592,6 @@ export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql` } `; -export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql` - subscription($actorId: ID!) { - groupMembershipChanged(personId: $actorId) { - id - memberships { - total - elements { - id - role - parent { - id - preferredUsername - name - domain - avatar { - id - url - } - } - invitedBy { - id - preferredUsername - name - } - insertedAt - updatedAt - } - } - } - } -`; - export const FETCH_GROUP_EVENTS = gql` query( $name: String! diff --git a/js/src/mixins/group.ts b/js/src/mixins/group.ts index 43d0d3f1d..86ec8bbdf 100644 --- a/js/src/mixins/group.ts +++ b/js/src/mixins/group.ts @@ -1,5 +1,8 @@ -import { PERSON_MEMBERSHIPS, CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; -import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event"; +import { + CURRENT_ACTOR_CLIENT, + GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, + PERSON_MEMBERSHIP_GROUP, +} from "@/graphql/actor"; import { FETCH_GROUP } from "@/graphql/group"; import RouteName from "@/router/name"; import { Group, IActor, IGroup, IPerson } from "@/types/actor"; @@ -26,11 +29,12 @@ import { Component, Vue } from "vue-property-decorator"; }, }, person: { - query: PERSON_MEMBERSHIPS, + query: PERSON_MEMBERSHIP_GROUP, fetchPolicy: "cache-and-network", variables() { return { id: this.currentActor.id, + group: this.$route.params.preferredUsername, }; }, subscribeToMore: { @@ -38,14 +42,23 @@ import { Component, Vue } from "vue-property-decorator"; variables() { return { actorId: this.currentActor.id, + group: this.$route.params.preferredUsername, }; }, skip() { - return !this.currentActor || !this.currentActor.id; + return ( + !this.currentActor || + !this.currentActor.id || + !this.$route.params.preferredUsername + ); }, }, skip() { - return !this.currentActor || !this.currentActor.id; + return ( + !this.currentActor || + !this.currentActor.id || + !this.$route.params.preferredUsername + ); }, }, currentActor: CURRENT_ACTOR_CLIENT, @@ -71,13 +84,7 @@ export default class GroupMixin extends Vue { hasCurrentActorThisRole(givenRole: string | string[]): boolean { const roles = Array.isArray(givenRole) ? givenRole : [givenRole]; - return ( - this.person && - this.person.memberships.elements.some( - ({ parent: { id }, role }) => - id === this.group.id && roles.includes(role) - ) - ); + return roles.includes(this.person?.memberships?.elements[0].role); } handleErrors(errors: any[]): void { diff --git a/js/src/views/Discussions/DiscussionsList.vue b/js/src/views/Discussions/DiscussionsList.vue index e548fe230..07dbfc00a 100644 --- a/js/src/views/Discussions/DiscussionsList.vue +++ b/js/src/views/Discussions/DiscussionsList.vue @@ -75,8 +75,11 @@ import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor"; import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue"; import RouteName from "../../router/name"; import { MemberRole } from "@/types/enums"; -import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor"; -import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event"; +import { + CURRENT_ACTOR_CLIENT, + GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, + PERSON_MEMBERSHIP_GROUP, +} from "@/graphql/actor"; import { IMember } from "@/types/actor/member.model"; import EmptyContent from "@/components/Utils/EmptyContent.vue"; @@ -96,11 +99,12 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue"; }, }, person: { - query: PERSON_MEMBERSHIPS, + query: PERSON_MEMBERSHIP_GROUP, fetchPolicy: "cache-and-network", variables() { return { id: this.currentActor.id, + group: this.preferredUsername, }; }, subscribeToMore: { @@ -108,14 +112,21 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue"; variables() { return { actorId: this.currentActor.id, + group: this.preferredUsername, }; }, skip() { - return !this.currentActor || !this.currentActor.id; + return ( + !this.currentActor || + !this.currentActor.id || + !this.preferredUsername + ); }, }, skip() { - return !this.currentActor || !this.currentActor.id; + return ( + !this.currentActor || !this.currentActor.id || !this.preferredUsername + ); }, }, currentActor: CURRENT_ACTOR_CLIENT, diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index 1c1e0e6e7..e4d566943 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -628,15 +628,14 @@ export default class Group extends mixins(GroupMixin) { } get groupMember(): IMember | undefined { - if (!this.person || !this.person.id) return undefined; - return this.person.memberships.elements.find( - ({ parent: { id } }) => id === this.group.id - ); + if (this.person?.memberships?.total > 0) { + return this.person?.memberships?.elements[0]; + } + return undefined; } get groupMemberships(): (string | undefined)[] { - if (!this.person || !this.person.id) return []; - return this.person.memberships.elements + return this.person?.memberships?.elements .filter( (membership: IMember) => ![ diff --git a/js/src/views/Group/GroupFollowers.vue b/js/src/views/Group/GroupFollowers.vue index 47f24c97b..069e98da2 100644 --- a/js/src/views/Group/GroupFollowers.vue +++ b/js/src/views/Group/GroupFollowers.vue @@ -33,14 +33,13 @@

{{ $t("Group Followers") }} ({{ followers.total }})

{{ $t("Pending") }} "Join", "id" => member.url, diff --git a/lib/federation/activity_pub/types/members.ex b/lib/federation/activity_pub/types/members.ex index a8151432b..8c55fa72a 100644 --- a/lib/federation/activity_pub/types/members.ex +++ b/lib/federation/activity_pub/types/members.ex @@ -5,11 +5,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Service.Activity.Member, as: MemberActivity + alias Mobilizon.Web.Endpoint require Logger import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] def update( - %Member{parent: %Actor{id: group_id}, id: member_id, role: current_role} = old_member, + %Member{ + parent: %Actor{id: group_id} = group, + id: member_id, + role: current_role, + actor: %Actor{id: actor_id} = actor + } = old_member, %{role: updated_role} = args, %{moderator: %Actor{url: moderator_url, id: moderator_id} = moderator} = additional ) do @@ -27,6 +33,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do moderator: moderator, subject: "member_updated" ), + Absinthe.Subscription.publish(Endpoint, actor, + group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id] + ), {:ok, true} <- Cachex.del(:activity_pub, "member_#{member_id}"), member_as_data <- Convertible.model_to_as(member), diff --git a/lib/graphql/resolvers/person.ex b/lib/graphql/resolvers/person.ex index abf032fdc..09f0aab1a 100644 --- a/lib/graphql/resolvers/person.ex +++ b/lib/graphql/resolvers/person.ex @@ -6,9 +6,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do import Mobilizon.Users.Guards alias Mobilizon.{Actors, Events, Users} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Events.Participant - alias Mobilizon.Storage.Page + alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User import Mobilizon.Web.Gettext @@ -306,10 +306,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do @doc """ Returns the list of events this person is going to """ - def person_memberships(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do + @spec person_memberships(Actor.t(), map(), map()) :: {:ok, Page.t()} | {:error, String.t()} + def person_memberships(%Actor{id: actor_id}, %{group: group}, %{ + context: %{current_user: user} + }) do + with {:is_owned, %Actor{id: actor_id}} <- User.owns_actor(user, actor_id), + %Actor{id: group_id} <- Actors.get_actor_by_name(group, :Group), + {:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id), + memberships <- %Page{ + total: 1, + elements: [Repo.preload(membership, [:actor, :parent, :invited_by])] + } do + {:ok, memberships} + else + {:error, :member_not_found} -> + {:ok, %Page{total: 0, elements: []}} + + {:is_owned, nil} -> + {:error, dgettext("errors", "Profile is not owned by authenticated user")} + end + end + + def person_memberships(%Actor{id: actor_id}, %{page: page, limit: limit}, %{ + context: %{current_user: user} + }) do with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id), - participations <- Actors.list_members_for_actor(actor) do - {:ok, participations} + memberships <- Actors.list_members_for_actor(actor, page, limit) do + {:ok, memberships} else {:is_owned, nil} -> {:error, dgettext("errors", "Profile is not owned by authenticated user")} diff --git a/lib/graphql/schema/actors/person.ex b/lib/graphql/schema/actors/person.ex index 1ed2ce262..fd8aedf03 100644 --- a/lib/graphql/schema/actors/person.ex +++ b/lib/graphql/schema/actors/person.ex @@ -70,7 +70,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do field(:participations, :paginated_participant_list, description: "The list of events this person goes to" ) do - arg(:event_id, :id) + arg(:event_id, :id, description: "Filter by event ID") arg(:page, :integer, default_value: 1, @@ -86,6 +86,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do field(:memberships, :paginated_member_list, description: "The list of group this person is member of" ) do + arg(:group, :string, description: "Filter by group federated username") + + arg(:page, :integer, + default_value: 1, + description: "The page in the paginated memberships list" + ) + + arg(:limit, :integer, default_value: 10, description: "The limit of memberships per page") resolve(&Person.person_memberships/3) end end @@ -225,9 +233,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do @desc "Notify when a person's membership's status changed for a group" field :group_membership_changed, :person do arg(:person_id, non_null(:id), description: "The person's ID") + arg(:group, non_null(:string), description: "The group's federated username") config(fn args, _ -> - {:ok, topic: args.person_id} + {:ok, topic: [args.group, args.person_id]} end) end end