From 03824b898ce81bb3f67dd70274c7f0d2e3d73e8e Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
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 <tcit@tcit.fr>
---
 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 @@
     </nav>
     <section
       class="container section"
-      v-if="group && isCurrentActorAGroupAdmin"
+      v-if="group && isCurrentActorAGroupAdmin && followers"
     >
       <h1>{{ $t("Group Followers") }} ({{ followers.total }})</h1>
       <b-field :label="$t('Status')" horizontal>
         <b-switch v-model="pending">{{ $t("Pending") }}</b-switch>
       </b-field>
       <b-table
-        v-if="followers"
         :data="followers.elements"
         ref="queueTable"
         :loading="this.$apollo.loading"
diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex
index dd74f25e9..ead2d141c 100644
--- a/lib/federation/activity_pub/activity_pub.ex
+++ b/lib/federation/activity_pub/activity_pub.ex
@@ -934,7 +934,10 @@ defmodule Mobilizon.Federation.ActivityPub do
              do: Refresher.fetch_group(member.parent.url, member.actor)
            ),
          Absinthe.Subscription.publish(Endpoint, member.actor,
-           group_membership_changed: member.actor.id
+           group_membership_changed: [
+             Actor.preferred_username_and_domain(member.parent),
+             member.actor.id
+           ]
          ),
          member_as_data <- Convertible.model_to_as(member),
          audience <-
diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex
index 7496920da..e02ddffab 100644
--- a/lib/federation/activity_pub/types/actors.ex
+++ b/lib/federation/activity_pub/types/actors.ex
@@ -126,7 +126,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
            }),
          {:ok, _} <-
            Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined"),
-         Absinthe.Subscription.publish(Endpoint, actor, group_membership_changed: actor.id),
+         Absinthe.Subscription.publish(Endpoint, actor,
+           group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
+         ),
          join_data <- %{
            "type" => "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