diff --git a/js/src/components/Group/Invitations.vue b/js/src/components/Group/Invitations.vue
index f11916eca..d999a737b 100644
--- a/js/src/components/Group/Invitations.vue
+++ b/js/src/components/Group/Invitations.vue
@@ -14,6 +14,7 @@ import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
 import { IMember } from "@/types/actor";
 import { Component, Prop, Vue } from "vue-property-decorator";
 import InvitationCard from "@/components/Group/InvitationCard.vue";
+import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
 
 @Component({
   components: {
@@ -30,6 +31,7 @@ export default class Invitations extends Vue {
         variables: {
           id,
         },
+        refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
       });
       if (data) {
         this.$emit("accept-invitation", data.acceptInvitation);
@@ -49,6 +51,7 @@ export default class Invitations extends Vue {
         variables: {
           id,
         },
+        refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
       });
       if (data) {
         this.$emit("reject-invitation", data.rejectInvitation);
diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts
index e09d34a1d..a3afb9f55 100644
--- a/js/src/graphql/group.ts
+++ b/js/src/graphql/group.ts
@@ -125,6 +125,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
     }
     members {
       elements {
+        id
         role
         actor {
           id
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index 44a48d423..8e5569753 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -803,5 +803,7 @@
   "Please read the {fullRules} published by {instance}'s administrators.": "Please read the {fullRules} published by {instance}'s administrators.",
   "Instances following you": "Instances following you",
   "Instances you follow": "Instances you follow",
-  "Last group created": "Last group created"
+  "Last group created": "Last group created",
+  "{username} was invited to {group}": "{username} was invited to {group}",
+  "The member was removed from the group {group}": "The member was removed from the group {group}"
 }
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 5ae9009cc..8fc756106 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -853,5 +853,7 @@
   "Please read the {fullRules} published by {instance}'s administrators.": "Merci de lire les {fullRules} publiées par les administrateur·ices de {instance}.",
   "Instances following you": "Instances vous suivant",
   "Instances you follow": "Instances que vous suivez",
-  "Last group created": "Dernier groupe créé"
+  "Last group created": "Dernier groupe créé",
+  "{username} was invited to {group}": "{username} a été invité à {group}",
+  "The member was removed from the group {group}": "Le ou la membre a été supprimé·e du groupe {group}"
 }
diff --git a/js/src/mixins/group.ts b/js/src/mixins/group.ts
index d5fd8ccdb..0781d06be 100644
--- a/js/src/mixins/group.ts
+++ b/js/src/mixins/group.ts
@@ -38,6 +38,7 @@ import { Component, Vue } from "vue-property-decorator";
 })
 export default class GroupMixin extends Vue {
   group: IGroup = new Group();
+
   currentActor!: IActor;
 
   person!: IPerson;
@@ -51,7 +52,7 @@ export default class GroupMixin extends Vue {
     );
   }
 
-  handleErrors(errors: any[]) {
+  handleErrors(errors: any[]): void {
     if (
       errors.some((error) => error.status_code === 404) ||
       errors.some(({ message }) => message.includes("has invalid value $uuid"))
diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue
index cbc85e31e..05ae51bf8 100644
--- a/js/src/views/Group/Group.vue
+++ b/js/src/views/Group/Group.vue
@@ -23,6 +23,7 @@
         v-if="isCurrentActorAnInvitedGroupMember"
         :invitations="[groupMember]"
         @acceptInvitation="acceptInvitation"
+        @reject-invitation="rejectInvitation"
       />
       <b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger">
         {{ $t("You have been removed from this group's members.") }}
@@ -431,6 +432,16 @@ export default class Group extends mixins(GroupMixin) {
     }
   }
 
+  rejectInvitation({ id: memberId }: { id: string }): void {
+    const index = this.person.memberships.elements.findIndex(
+      (membership) => membership.role === MemberRole.INVITED && membership.id === memberId
+    );
+    if (index > -1) {
+      this.person.memberships.elements.splice(index, 1);
+      this.person.memberships.total -= 1;
+    }
+  }
+
   async reportGroup(content: string, forward: boolean): Promise<void> {
     this.isReportModalActive = false;
     // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -611,6 +622,8 @@ div.container {
     div.address {
       flex: 1;
       text-align: right;
+      justify-content: flex-end;
+      display: flex;
 
       .map-show-button {
         cursor: pointer;
diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue
index 7aaaeb4a7..e39f6f915 100644
--- a/js/src/views/Group/GroupMembers.vue
+++ b/js/src/views/Group/GroupMembers.vue
@@ -181,12 +181,28 @@
 import { Component, Watch } from "vue-property-decorator";
 import GroupMixin from "@/mixins/group";
 import { mixins } from "vue-class-component";
+import { FETCH_GROUP } from "@/graphql/group";
 import RouteName from "../../router/name";
 import { INVITE_MEMBER, GROUP_MEMBERS, REMOVE_MEMBER, UPDATE_MEMBER } from "../../graphql/member";
 import { IGroup, usernameWithDomain } from "../../types/actor";
 import { IMember, MemberRole } from "../../types/actor/group.model";
 
-@Component
+@Component({
+  apollo: {
+    members: {
+      query: GROUP_MEMBERS,
+      variables() {
+        return {
+          name: this.$route.params.preferredUsername,
+          page: 1,
+          limit: this.MEMBERS_PER_PAGE,
+          roles: this.roles,
+        };
+      },
+      update: (data) => data.group.members,
+    },
+  },
+})
 export default class GroupMembers extends mixins(GroupMixin) {
   loading = true;
 
@@ -221,31 +237,16 @@ export default class GroupMembers extends mixins(GroupMixin) {
           groupId: this.group.id,
           targetActorUsername: this.newMemberUsername,
         },
-        update: (store, { data }) => {
-          if (data == null) return;
-          const query = {
-            query: GROUP_MEMBERS,
-            variables: {
-              name: this.$route.params.preferredUsername,
-              page: 1,
-              limit: this.MEMBERS_PER_PAGE,
-              roles: this.roles,
-            },
-          };
-          const memberData: IMember = data.inviteMember;
-          const groupData = store.readQuery<{ group: IGroup }>(query);
-          if (!groupData) return;
-          const { group } = groupData;
-          const index = group.members.elements.findIndex((m) => m.actor.id === memberData.actor.id);
-          if (index === -1) {
-            group.members.elements.push(memberData);
-            group.members.total += 1;
-          } else {
-            group.members.elements.splice(index, 1, memberData);
-          }
-          store.writeQuery({ ...query, data: { group } });
-        },
+        refetchQueries: [
+          { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } },
+        ],
       });
+      this.$notifier.success(
+        this.$t("{username} was invited to {group}", {
+          username: this.newMemberUsername,
+          group: this.group.name || usernameWithDomain(this.group),
+        }) as string
+      );
       this.newMemberUsername = "";
     } catch (error) {
       console.error(error);
@@ -283,34 +284,30 @@ export default class GroupMembers extends mixins(GroupMixin) {
   }
 
   async removeMember(memberId: string): Promise<void> {
-    await this.$apollo.mutate<{ removeMember: IMember }>({
-      mutation: REMOVE_MEMBER,
-      variables: {
-        groupId: this.group.id,
-        memberId,
-      },
-      update: (store, { data }) => {
-        if (data == null) return;
-        const query = {
-          query: GROUP_MEMBERS,
-          variables: {
-            name: this.$route.params.preferredUsername,
-            page: 1,
-            limit: this.MEMBERS_PER_PAGE,
-            roles: this.roles,
-          },
-        };
-        const groupData = store.readQuery<{ group: IGroup }>(query);
-        if (!groupData) return;
-        const { group } = groupData;
-        const index = group.members.elements.findIndex((m) => m.id === memberId);
-        if (index !== -1) {
-          group.members.elements.splice(index, 1);
-          group.members.total -= 1;
-          store.writeQuery({ ...query, data: { group } });
-        }
-      },
-    });
+    console.log("removeMember", memberId);
+    try {
+      await this.$apollo.mutate<{ removeMember: IMember }>({
+        mutation: REMOVE_MEMBER,
+        variables: {
+          groupId: this.group.id,
+          memberId,
+        },
+        refetchQueries: [
+          { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } },
+        ],
+      });
+      this.$notifier.success(
+        this.$t("The member was removed from the group {group}", {
+          username: this.newMemberUsername,
+          group: this.group.name || usernameWithDomain(this.group),
+        }) as string
+      );
+    } catch (error) {
+      console.error(error);
+      if (error.graphQLErrors && error.graphQLErrors.length > 0) {
+        this.$notifier.error(error.graphQLErrors[0].message);
+      }
+    }
   }
 
   promoteMember(member: IMember): void {
@@ -341,7 +338,23 @@ export default class GroupMembers extends mixins(GroupMixin) {
           memberId,
           role,
         },
+        refetchQueries: [
+          { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } },
+        ],
       });
+      let successMessage;
+      switch (role) {
+        case MemberRole.MODERATOR:
+          successMessage = "The member role was updated to moderator";
+          break;
+        case MemberRole.ADMINISTRATOR:
+          successMessage = "The member role was updated to administrator";
+          break;
+        case MemberRole.MEMBER:
+        default:
+          successMessage = "The member role was updated to simple member";
+      }
+      this.$notifier.success(this.$t(successMessage) as string);
     } catch (error) {
       console.error(error);
       if (error.graphQLErrors && error.graphQLErrors.length > 0) {
diff --git a/js/src/views/Group/MyGroups.vue b/js/src/views/Group/MyGroups.vue
index ae9ee9494..98fc3ee0f 100644
--- a/js/src/views/Group/MyGroups.vue
+++ b/js/src/views/Group/MyGroups.vue
@@ -71,7 +71,7 @@ import RouteName from "../../router/name";
     };
   },
 })
-export default class MyEvents extends Vue {
+export default class MyGroups extends Vue {
   membershipsPages!: Paginate<IMember>;
 
   RouteName = RouteName;
diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex
index 222269492..19bfdb5b9 100644
--- a/lib/graphql/resolvers/member.ex
+++ b/lib/graphql/resolvers/member.ex
@@ -66,10 +66,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
          {:has_rights_to_invite, {:ok, %Member{role: role}}}
          when role in [:moderator, :administrator, :creator] <-
            {:has_rights_to_invite, Actors.get_member(actor_id, group_id)},
+         target_actor_username <-
+           target_actor_username |> String.trim() |> String.trim_leading("@"),
          {:target_actor_username, {:ok, %Actor{id: target_actor_id} = target_actor}} <-
            {:target_actor_username,
             ActivityPub.find_or_make_actor_from_nickname(target_actor_username)},
-         true <- check_member_not_existant_or_rejected(target_actor_id, group.id),
+         {:existant, true} <-
+           {:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)},
          {:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do
       {:ok, member}
     else
@@ -88,6 +91,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
       {:has_rights_to_invite, _} ->
         {:error, dgettext("errors", "You cannot invite to this group")}
 
+      {:existant, _} ->
+        {:error, dgettext("errors", "Profile is already a member of this group")}
+
+      # Remove me ?
       {:ok, %Member{}} ->
         {:error, dgettext("errors", "Profile is already a member of this group")}
     end
@@ -115,7 +122,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
 
   def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do
     with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
-         %Member{actor: %Actor{id: member_actor_id}} = member <- Actors.get_member(member_id),
+         {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <-
+           {:invitation_exists, Actors.get_member(member_id)},
          {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
          {:ok, _activity, %Member{} = member} <-
            ActivityPub.reject(
@@ -127,6 +135,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
     else
       {:is_same_actor, false} ->
         {:error, dgettext("errors", "You can't reject this invitation with this profile.")}
+
+      {:invitation_exists, _} ->
+        {:error, dgettext("errors", "This invitation doesn't exist.")}
     end
   end
 
@@ -158,13 +169,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
         context: %{current_user: %User{} = user}
       }) do
     with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user),
-         %Member{} = member <- Actors.get_member(member_id),
+         %Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
          %Actor{type: :Group} = group <- Actors.get_actor(group_id),
-         {:has_rights_to_invite, {:ok, %Member{role: role}}}
+         {:has_rights_to_remove, {:ok, %Member{role: role}}}
          when role in [:moderator, :administrator, :creator] <-
-           {:has_rights_to_invite, Actors.get_member(moderator_id, group_id)},
+           {:has_rights_to_remove, Actors.get_member(moderator_id, group_id)},
          {:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do
       {:ok, member}
+    else
+      %Member{role: :rejected} ->
+        {:error,
+         dgettext(
+           "errors",
+           "This member already has been rejected."
+         )}
+
+      {:has_rights_to_remove, _} ->
+        {:error,
+         dgettext(
+           "errors",
+           "You don't have the right to remove this member."
+         )}
     end
   end
 
diff --git a/priv/gettext/fr/LC_MESSAGES/errors.po b/priv/gettext/fr/LC_MESSAGES/errors.po
index 110283942..7160ab367 100644
--- a/priv/gettext/fr/LC_MESSAGES/errors.po
+++ b/priv/gettext/fr/LC_MESSAGES/errors.po
@@ -479,7 +479,7 @@ msgstr "Le profil invité n'existe pas"
 #, elixir-format
 #: lib/graphql/resolvers/member.ex:92
 msgid "Profile is already a member of this group"
-msgstr "Vous êtes déjà membre de ce groupe"
+msgstr "Ce profil est déjà membre de ce groupe"
 
 #, elixir-format
 #: lib/graphql/resolvers/post.ex:131 lib/graphql/resolvers/post.ex:171
@@ -549,12 +549,12 @@ msgstr "Membre non trouvé"
 #, elixir-format
 #: lib/graphql/resolvers/person.ex:235
 msgid "You already have a profile for this user"
-msgstr "Vous êtes déjà membre de ce groupe"
+msgstr "Vous avez déjà un profil pour cet utilisateur"
 
 #, elixir-format
 #: lib/graphql/resolvers/participant.ex:134
 msgid "You are already a participant of this event"
-msgstr "Vous êtes déjà membre de ce groupe"
+msgstr "Vous êtes déjà un·e participant·e à cet événement"
 
 #, elixir-format
 #: lib/graphql/resolvers/discussion.ex:185
@@ -564,7 +564,7 @@ msgstr "Vous n'êtes pas un membre du groupe dans lequel se fait la discussion"
 #, elixir-format
 #: lib/graphql/resolvers/member.ex:86
 msgid "You are not a member of this group"
-msgstr "Vous êtes déjà membre de ce groupe"
+msgstr "Vous n'êtes pas membre de ce groupe"
 
 #, elixir-format
 #: lib/graphql/resolvers/member.ex:143