From 757d2cabec8625ff472f3181f9a9137633b6da1e Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Thu, 26 Sep 2019 16:38:58 +0200
Subject: [PATCH] Add a dropdown on participate menu, disallow listing
 participations

Now requires quering the person endpoint to know if an actor
participates in an event, organizers can make authenticated requests to
event { participants { } } to see the pending / approved participants.

Also closes #174

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/App.vue                                |  43 +---
 js/src/components/Event/DateTimePicker.vue    |   2 +-
 js/src/components/Event/EventListCard.vue     | 118 +++++------
 .../components/Event/ParticipationButton.vue  | 111 ++++++++++
 .../components/Event/ParticipationModal.vue   |  92 ---------
 js/src/graphql/event.ts                       |  31 ++-
 js/src/i18n/en_US.json                        |  16 ++
 js/src/i18n/fr_FR.json                        |  16 ++
 js/src/types/actor/person.model.ts            |   4 +-
 js/src/views/Account/IdentityPicker.vue       |  50 ++---
 .../views/Account/IdentityPickerWrapper.vue   |  39 ++++
 js/src/views/Account/Register.vue             |   1 +
 js/src/views/Event/Edit.vue                   |   7 +-
 js/src/views/Event/Event.vue                  | 194 +++++++++++-------
 js/src/views/Event/Participants.vue           |   3 +
 js/src/views/Home.vue                         |  13 +-
 js/src/views/Moderation/Report.vue            |   4 +-
 lib/mobilizon/events/events.ex                |  20 +-
 lib/mobilizon_web/resolvers/event.ex          |  50 +++--
 lib/mobilizon_web/resolvers/person.ex         |  27 ++-
 lib/mobilizon_web/resolvers/user.ex           |   7 +-
 lib/mobilizon_web/schema.ex                   |   1 -
 lib/mobilizon_web/schema/actors/person.ex     |   7 +-
 lib/mobilizon_web/schema/event.ex             |   1 +
 .../schema/events/participant.ex              |  10 -
 lib/mobilizon_web/schema/user.ex              |   2 +-
 lib/service/export/feed.ex                    |  16 +-
 lib/service/export/icalendar.ex               |  17 +-
 schema.graphql                                |  15 +-
 test/mobilizon/events/events_test.exs         |   4 +-
 .../activity_pub/transmogrifier_test.exs      |   4 +-
 .../controllers/feed_controller_test.exs      |   7 +-
 .../resolvers/participant_resolver_test.exs   |  91 +++++---
 .../resolvers/person_resolver_test.exs        |  71 +++++--
 34 files changed, 655 insertions(+), 439 deletions(-)
 create mode 100644 js/src/components/Event/ParticipationButton.vue
 delete mode 100644 js/src/components/Event/ParticipationModal.vue
 create mode 100644 js/src/views/Account/IdentityPickerWrapper.vue

diff --git a/js/src/App.vue b/js/src/App.vue
index e165fdbd5..38e2d4c5c 100644
--- a/js/src/App.vue
+++ b/js/src/App.vue
@@ -71,49 +71,10 @@ export default class App extends Vue {
 @import "variables";
 
 /* Bulma imports */
-@import "~bulma/sass/utilities/_all";
-@import "~bulma/sass/base/_all.sass";
-@import "~bulma/sass/components/card.sass";
-@import "~bulma/sass/components/media.sass";
-@import "~bulma/sass/components/message.sass";
-@import "~bulma/sass/components/modal.sass";
-@import "~bulma/sass/components/navbar.sass";
-@import "~bulma/sass/components/pagination.sass";
-@import "~bulma/sass/components/dropdown.sass";
-@import "~bulma/sass/components/breadcrumb.sass";
-@import "~bulma/sass/components/list.sass";
-@import "~bulma/sass/components/tabs";
-@import "~bulma/sass/elements/box.sass";
-@import "~bulma/sass/elements/button.sass";
-@import "~bulma/sass/elements/container.sass";
-@import "~bulma/sass/form/_all";
-@import "~bulma/sass/elements/icon.sass";
-@import "~bulma/sass/elements/image.sass";
-@import "~bulma/sass/elements/other.sass";
-@import "~bulma/sass/elements/progress.sass";
-@import "~bulma/sass/elements/tag.sass";
-@import "~bulma/sass/elements/title.sass";
-@import "~bulma/sass/elements/notification";
-@import "~bulma/sass/elements/table";
-@import "~bulma/sass/grid/_all.sass";
-@import "~bulma/sass/layout/_all.sass";
+@import "~bulma/bulma";
 
 /* Buefy imports */
-@import "~buefy/src/scss/utils/_all";
-@import "~buefy/src/scss/components/datepicker";
-@import "~buefy/src/scss/components/notices";
-@import "~buefy/src/scss/components/dropdown";
-@import "~buefy/src/scss/components/autocomplete";
-@import "~buefy/src/scss/components/form";
-@import "~buefy/src/scss/components/modal";
-@import "~buefy/src/scss/components/progress";
-@import "~buefy/src/scss/components/tag";
-@import "~buefy/src/scss/components/taginput";
-@import "~buefy/src/scss/components/upload";
-@import "~buefy/src/scss/components/radio";
-@import "~buefy/src/scss/components/switch";
-@import "~buefy/src/scss/components/table";
-@import "~buefy/src/scss/components/tabs";
+@import "~buefy/src/scss/buefy";
 
 .router-enter-active,
 .router-leave-active {
diff --git a/js/src/components/Event/DateTimePicker.vue b/js/src/components/Event/DateTimePicker.vue
index 2a0b91f4e..40d2d070a 100644
--- a/js/src/components/Event/DateTimePicker.vue
+++ b/js/src/components/Event/DateTimePicker.vue
@@ -6,7 +6,7 @@
 </template>
 <script lang="ts">
 import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
-@Component({})
+@Component
 export default class DateTimePicker extends Vue {
   @Prop({ required: true, type: Date }) value!: Date;
   @Prop({ required: false, type: String, default: 'Datetime' }) label!: string;
diff --git a/js/src/components/Event/EventListCard.vue b/js/src/components/Event/EventListCard.vue
index 4bcb5c842..ee29a9584 100644
--- a/js/src/components/Event/EventListCard.vue
+++ b/js/src/components/Event/EventListCard.vue
@@ -1,64 +1,66 @@
 <template>
-  <article class="box columns">
-    <div class="content column">
-      <div class="title-wrapper">
-        <div class="date-component" v-if="!mergedOptions.hideDate">
-          <date-calendar-icon :date="participation.event.beginsOn" />
-        </div>
-        <h2 class="title" ref="title">{{ participation.event.title }}</h2>
-      </div>
-      <div>
-        <span v-if="participation.event.physicalAddress && participation.event.physicalAddress.locality">{{ participation.event.physicalAddress.locality }} - </span>
-        <span v-if="participation.actor.id === participation.event.organizerActor.id">{{ $t("You're organizing this event") }}</span>
-        <span v-else>
-          <span v-if="participation.event.beginsOn < new Date()">{{ $t('Organized by {name}', { name: participation.event.organizerActor.displayName() } ) }}</span>
-          |
-          <span>{{ $t('Going as {name}', { name: participation.actor.displayName() }) }}</span>
-        </span>
-      </div>
-      <div class="columns">
-        <span class="column is-narrow">
-          <b-icon icon="earth" v-if=" participation.event.visibility === EventVisibility.PUBLIC" />
-          <b-icon icon="lock_opened" v-if=" participation.event.visibility === EventVisibility.RESTRICTED" />
-          <b-icon icon="lock" v-if=" participation.event.visibility === EventVisibility.PRIVATE" />
-        </span>
-        <span class="column">
-          <span v-if="!participation.event.options.maximumAttendeeCapacity">
-            {{ $tc('{count} participants', participation.event.participantStats.approved, { count: participation.event.participantStats.approved })}}
-          </span>
-          <b-progress
-                  v-if="participation.event.options.maximumAttendeeCapacity > 0"
-                  type="is-primary"
-                  size="is-medium"
-                  :value="participation.event.participantStats.approved * 100 / participation.event.options.maximumAttendeeCapacity" show-value>
-            {{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.approved, total: participation.event.options.maximumAttendeeCapacity }) }}
-          </b-progress>
-          <span
-            v-if="participation.event.participantStats.unapproved > 0">
-            {{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}}
-          </span>
-        </span>
+  <article class="box">
+    <div class="title-wrapper">
+      <div class="date-component" v-if="!mergedOptions.hideDate">
+        <date-calendar-icon :date="participation.event.beginsOn" />
       </div>
+      <h2 class="title" ref="title">{{ participation.event.title }}</h2>
     </div>
-    <div class="actions column is-narrow">
-      <ul>
-        <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
-          <router-link :to="{ name: EventRouteName.EDIT_EVENT, params: { eventId: participation.event.uuid }  }">
-            <b-icon icon="pencil" /> {{ $t('Edit') }}
-          </router-link>
-        </li>
-        <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
-          <a @click="openDeleteEventModalWrapper"><b-icon icon="delete" /> {{ $t('Delete') }}</a>
-        </li>
-        <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
-          <router-link :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } }">
-            <b-icon icon="account-multiple-plus" /> {{ $t('Manage participations') }}
-          </router-link>
-        </li>
-        <li>
-          <router-link :to="{ name: EventRouteName.EVENT, params: { uuid: participation.event.uuid } }"><b-icon icon="view-compact" /> {{ $t('View event page') }}</router-link>
-        </li>
-      </ul>
+    <div class="columns">
+      <div class="content column">
+        <div>
+          <span v-if="participation.event.physicalAddress && participation.event.physicalAddress.locality">{{ participation.event.physicalAddress.locality }} - </span>
+          <span v-if="participation.actor.id === participation.event.organizerActor.id">{{ $t("You're organizing this event") }}</span>
+          <span v-else>
+            <span v-if="participation.event.beginsOn < new Date()">{{ $t('Organized by {name}', { name: participation.event.organizerActor.displayName() } ) }}</span>
+            |
+            <span>{{ $t('Going as {name}', { name: participation.actor.displayName() }) }}</span>
+          </span>
+        </div>
+        <div class="columns">
+          <span class="column is-narrow">
+            <b-icon icon="earth" v-if=" participation.event.visibility === EventVisibility.PUBLIC" />
+            <b-icon icon="lock_opened" v-if=" participation.event.visibility === EventVisibility.RESTRICTED" />
+            <b-icon icon="lock" v-if=" participation.event.visibility === EventVisibility.PRIVATE" />
+          </span>
+          <span class="column">
+            <span v-if="!participation.event.options.maximumAttendeeCapacity">
+              {{ $tc('{count} participants', participation.event.participantStats.approved, { count: participation.event.participantStats.approved })}}
+            </span>
+            <b-progress
+                    v-if="participation.event.options.maximumAttendeeCapacity > 0"
+                    type="is-primary"
+                    size="is-medium"
+                    :value="participation.event.participantStats.approved * 100 / participation.event.options.maximumAttendeeCapacity" show-value>
+              {{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.approved, total: participation.event.options.maximumAttendeeCapacity }) }}
+            </b-progress>
+            <span
+              v-if="participation.event.participantStats.unapproved > 0">
+              {{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}}
+            </span>
+          </span>
+        </div>
+      </div>
+      <div class="actions column is-narrow">
+        <ul>
+          <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
+            <router-link :to="{ name: EventRouteName.EDIT_EVENT, params: { eventId: participation.event.uuid }  }">
+              <b-icon icon="pencil" /> {{ $t('Edit') }}
+            </router-link>
+          </li>
+          <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
+            <a @click="openDeleteEventModalWrapper"><b-icon icon="delete" /> {{ $t('Delete') }}</a>
+          </li>
+          <li v-if="!([ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(participation.role))">
+            <router-link :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } }">
+              <b-icon icon="account-multiple-plus" /> {{ $t('Manage participations') }}
+            </router-link>
+          </li>
+          <li>
+            <router-link :to="{ name: EventRouteName.EVENT, params: { uuid: participation.event.uuid } }"><b-icon icon="view-compact" /> {{ $t('View event page') }}</router-link>
+          </li>
+        </ul>
+      </div>
     </div>
     </article>
 </template>
diff --git a/js/src/components/Event/ParticipationButton.vue b/js/src/components/Event/ParticipationButton.vue
new file mode 100644
index 000000000..e86698af5
--- /dev/null
+++ b/js/src/components/Event/ParticipationButton.vue
@@ -0,0 +1,111 @@
+<template>
+    <div class="participation-button">
+        <b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
+            <button class="button is-success" type="button" slot="trigger">
+                <template>
+                    <span>{{ $t('I participate') }}</span>
+                </template>
+                <b-icon icon="menu-down"></b-icon>
+            </button>
+
+            <!--                <b-dropdown-item :value="false" aria-role="listitem">-->
+            <!--                  {{ $t('Change my identity…')}}-->
+            <!--                </b-dropdown-item>-->
+
+            <b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave">
+                {{ $t('Cancel my participation…')}}
+            </b-dropdown-item>
+        </b-dropdown>
+
+        <div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
+            <b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
+                <button class="button is-success" type="button" slot="trigger">
+                    <template>
+                        <span>{{ $t('I participate') }}</span>
+                    </template>
+                    <b-icon icon="menu-down"></b-icon>
+                </button>
+
+                <!--                <b-dropdown-item :value="false" aria-role="listitem">-->
+                <!--                  {{ $t('Change my identity…')}}-->
+                <!--                </b-dropdown-item>-->
+
+                <b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave">
+                    {{ $t('Cancel my participation request…')}}
+                </b-dropdown-item>
+            </b-dropdown>
+            <small>{{ $t('Participation requested!')}}</small><br />
+            <small>{{ $t('Waiting for organization team approval.')}}</small>
+        </div>
+
+        <b-dropdown aria-role="list" position="is-bottom-left" v-if="!participation">
+            <button class="button is-primary" type="button" slot="trigger">
+                <template>
+                    <span>{{ $t('Participate') }}</span>
+                </template>
+                <b-icon icon="menu-down"></b-icon>
+            </button>
+
+            <b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
+                <div class="media">
+                    <div class="media-left">
+                        <figure class="image is-32x32" v-if="currentActor.avatar">
+                            <img class="is-rounded" :src="currentActor.avatar.url" alt="" />
+                        </figure>
+                    </div>
+                    <div class="media-content">
+                        <span>{{ $t('with {identity}', {identity: currentActor.preferredUsername }) }}</span>
+                    </div>
+                </div>
+            </b-dropdown-item>
+
+            <b-dropdown-item :value="false" aria-role="listitem" @click="joinModal">
+                {{ $t('with another identity…')}}
+            </b-dropdown-item>
+        </b-dropdown>
+    </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator';
+import { IParticipant, ParticipantRole } from '@/types/event.model';
+import { IPerson } from '@/types/actor';
+
+@Component
+export default class ParticipationButton extends Vue {
+  @Prop({ required: true }) participation!: IParticipant;
+  @Prop({ required: true }) currentActor!: IPerson;
+
+  ParticipantRole = ParticipantRole;
+
+  joinEvent(actor: IPerson) {
+    this.$emit('joinEvent', actor);
+  }
+
+  joinModal() {
+    this.$emit('joinModal');
+  }
+
+  confirmLeave() {
+    this.$emit('confirmLeave');
+  }
+
+}
+</script>
+
+<style lang="scss">
+    .participation-button {
+        .dropdown {
+            display: flex;
+            justify-content: flex-end;
+
+            &.dropdown-disabled button {
+                opacity: 0.5;
+            }
+        }
+
+        button {
+            font-size: 1.5rem;
+        }
+    }
+</style>
\ No newline at end of file
diff --git a/js/src/components/Event/ParticipationModal.vue b/js/src/components/Event/ParticipationModal.vue
deleted file mode 100644
index 0ad1dda27..000000000
--- a/js/src/components/Event/ParticipationModal.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-    <div class="modal-card">
-        <header class="modal-card-head">
-            <p class="modal-card-title">{{ $t('Join event {title}', {title: event.title}) }}</p>
-        </header>
-
-        <section class="modal-card-body is-flex">
-            <div class="media">
-                <div
-                        class="media-left">
-                    <b-icon
-                            icon="alert"
-                            type="is-warning"
-                            size="is-large"/>
-                </div>
-                <div class="media-content">
-                    <p>{{ $t('Do you want to participate in {title}?', {title: event.title}) }}?</p>
-
-                    <b-field :label="$t('Identity')">
-                        <identity-picker v-model="identity"></identity-picker>
-                    </b-field>
-
-                    <p v-if="event.joinOptions === EventJoinOptions.RESTRICTED">
-                        {{ $t('The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved')}}
-                    </p>
-
-                    <p v-if="!event.local">
-                        {{ $t('The event came from another instance. Your participation will be confirmed after we confirm it with the other instance.') }}
-                    </p>
-                </div>
-            </div>
-        </section>
-
-        <footer class="modal-card-foot">
-            <button
-                    class="button"
-                    ref="cancelButton"
-                    @click="close">
-                {{ $t('Cancel') }}
-            </button>
-            <button
-                    class="button is-primary"
-                    ref="confirmButton"
-                    @click="confirm">
-                {{ $t('Confirm my particpation') }}
-            </button>
-        </footer>
-    </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator';
-import { IEvent, EventJoinOptions } from '@/types/event.model';
-import IdentityPicker from '@/views/Account/IdentityPicker.vue';
-import { IPerson } from '@/types/actor';
-
-@Component({
-  components: {
-    IdentityPicker,
-  },
-  mounted() {
-    this.$data.isActive = true;
-  },
-})
-export default class ReportModal extends Vue {
-  @Prop({ type: Function, default: () => {} }) onConfirm;
-  @Prop({ type: Object }) event! : IEvent;
-  @Prop({ type: Object }) defaultIdentity!: IPerson;
-
-  isActive: boolean = false;
-  identity: IPerson = this.defaultIdentity;
-
-  EventJoinOptions = EventJoinOptions;
-
-  confirm() {
-    this.onConfirm(this.identity);
-  }
-
-    /**
-     * Close the Dialog.
-     */
-  close() {
-    this.isActive = false;
-    this.$emit('close');
-  }
-}
-</script>
-<style lang="scss">
-    .modal-card .modal-card-foot {
-        justify-content: flex-end;
-    }
-</style>
\ No newline at end of file
diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index e9bccbda0..6d53b8964 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -10,6 +10,9 @@ const participantQuery = `
     },
     name,
     id
+  },
+  event {
+    id
   }
 `;
 
@@ -52,7 +55,7 @@ const optionsQuery = `
 `;
 
 export const FETCH_EVENT = gql`
-  query($uuid:UUID!, $roles: String) {
+  query($uuid:UUID!) {
     event(uuid: $uuid) {
       id,
       uuid,
@@ -95,9 +98,6 @@ export const FETCH_EVENT = gql`
       #     preferredUsername,
       #     name,
       # },
-      participants (roles: $roles) {
-        ${participantQuery}
-      },
       participantStats {
         approved,
         unapproved
@@ -363,9 +363,10 @@ export const DELETE_EVENT = gql`
 `;
 
 export const PARTICIPANTS = gql`
-  query($uuid: UUID!, $page: Int, $limit: Int, $roles: String) {
+  query($uuid: UUID!, $page: Int, $limit: Int, $roles: String, $actorId: ID!) {
     event(uuid: $uuid) {
-      participants(page: $page, limit: $limit, roles: $roles) {
+      id,
+      participants(page: $page, limit: $limit, roles: $roles, actorId: $actorId) {
         ${participantQuery}
       },
       participantStats {
@@ -375,3 +376,21 @@ export const PARTICIPANTS = gql`
     }
   }
 `;
+
+export const EVENT_PERSON_PARTICIPATION = gql`
+  query($name: String!, $eventId: ID!) {
+    person(preferredUsername: $name) {
+      id,
+      participations(eventId: $eventId) {
+        id,
+        role,
+        actor {
+          id
+        },
+        event {
+          id
+        }
+      }
+    }
+  }
+`;
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index 45d66a819..2f42b26ab 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -17,8 +17,13 @@
     "Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.",
     "Before you can login, you need to click on the link inside it to validate your account": "Before you can login, you need to click on the link inside it to validate your account",
     "By {name}": "By {name}",
+    "Cancel my participation request…": "Cancel my participation request…",
+    "Cancel my participation…": "Cancel my participation…",
     "Cancel": "Cancel",
     "Category": "Category",
+    "Change my identity…": "Change my identity…",
+    "Change my password": "Change my password",
+    "Change password": "Change password",
     "Change": "Change",
     "Clear": "Clear",
     "Click to select": "Click to select",
@@ -82,6 +87,7 @@
     "Group": "Group",
     "Groups": "Groups",
     "I create an identity": "I create an identity",
+    "I participate": "I participate",
     "I want to approve every participation request": "I want to approve every participation request",
     "Identities": "Identities",
     "Identity {displayName} created": "Identity {displayName} created",
@@ -116,6 +122,7 @@
     "My events": "My events",
     "My identities": "My identities",
     "Name": "Name",
+    "New password": "New password",
     "No address defined": "No address defined",
     "No events found": "No events found",
     "No group found": "No group found",
@@ -123,6 +130,7 @@
     "No participants yet.": "No participants yet.",
     "No results for \"{queryText}\"": "No results for \"{queryText}\"",
     "Number of places": "Number of places",
+    "Old password": "Old password",
     "One person is going": "No one is going | One person is going | {approved} persons are going",
     "Only accessible through link and search (private)": "Only accessible through link and search (private)",
     "Opened reports": "Opened reports",
@@ -133,8 +141,11 @@
     "Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.",
     "Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)",
     "Participants": "Participants",
+    "Participate": "Participate",
     "Participation approval": "Participation approval",
+    "Participation requested!": "Participation requested!",
     "Password (confirmation)": "Password (confirmation)",
+    "Password change": "Password change",
     "Password reset": "Password reset",
     "Password": "Password",
     "Past events": "Passed events",
@@ -187,6 +198,7 @@
     "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved",
     "The event title will be ellipsed.": "The event title will be ellipsed.",
     "The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.",
+    "The password was successfully changed": "The password was successfully changed",
     "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.",
     "The {date} at {time}": "The {date} at {time}",
     "The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}",
@@ -208,6 +220,7 @@
     "View event page": "View event page",
     "View everything": "View everything",
     "Visible everywhere on the web (public)": "Visible everywhere on the web (public)",
+    "Waiting for organization team approval.": "Waiting for organization team approval.",
     "Waiting list": "Waiting list",
     "We just sent an email to {email}": "We just sent an email to {email}",
     "Website / URL": "Website / URL",
@@ -220,6 +233,7 @@
     "You announced that you're going to this event.": "You announced that you're going to this event.",
     "You are already logged-in.": "You are already logged-in.",
     "You are an organizer.": "You are an organizer.",
+    "You have been disconnected": "You have been disconnected",
     "You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days",
     "You have one event today.": "You have no events today | You have one event today. | You have {count} events today",
     "You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow",
@@ -233,6 +247,8 @@
     "e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot",
     "iCal Feed": "iCal Feed",
     "meditate a bit": "meditate a bit",
+    "with another identity…": "with another identity…",
+    "with {identity}": "with {identity}",
     "{actor}'s avatar": "{actor}'s avatar",
     "{approved} / {total} seats": "{approved} / {total} seats",
     "{count} participants": "{count} participants",
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index b1093854b..96b018aa4 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -17,8 +17,13 @@
     "Are you sure you want to delete this event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet événement ? Cette action ne peut être annulée.",
     "Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
     "By {name}": "Par {name}",
+    "Cancel my participation request…": "Cancel my participation request…",
+    "Cancel my participation…": "Annuler ma participation…",
     "Cancel": "Annuler",
     "Category": "Catégorie",
+    "Change my identity…": "Changer mon identité…",
+    "Change my password": "Modifier mon mot de passe",
+    "Change password": "Modifier mot de passe",
     "Change": "Modifier",
     "Clear": "Effacer",
     "Click to select": "Cliquez pour sélectionner",
@@ -82,6 +87,7 @@
     "Group": "Groupe",
     "Groups": "Groupes",
     "I create an identity": "Je crée une identité",
+    "I participate": "Je participe",
     "I want to approve every participation request": "Je veux approuver chaque demande de participation",
     "Identities": "Identités",
     "Identity {displayName} created": "Identité {displayName} créée",
@@ -116,6 +122,7 @@
     "My events": "Mes événements",
     "My identities": "Mes identités",
     "Name": "Nom",
+    "New password": "Nouveau mot de passe",
     "No address defined": "Aucune adresse définie",
     "No events found": "Aucun événement trouvé",
     "No group found": "Aucun groupe trouvé",
@@ -123,6 +130,7 @@
     "No participants yet.": "Pas de participants pour le moment.",
     "No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
     "Number of places": "Nombre de places",
+    "Old password": "Ancien mot de passe",
     "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
     "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
     "Opened reports": "Signalements ouverts",
@@ -133,8 +141,11 @@
     "Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
     "Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
     "Participants": "Participants",
+    "Participate": "Participer",
     "Participation approval": "Validation des participations",
+    "Participation requested!": "Participation demandée !",
     "Password (confirmation)": "Mot de passe (confirmation)",
+    "Password change": "Changement de mot de passe",
     "Password reset": "Réinitialisation du mot de passe",
     "Password": "Mot de passe",
     "Past events": "Événements passés",
@@ -187,6 +198,7 @@
     "The event organizer has chosen to approve manually the participations to this event. You will receive a notification when your participation has been approved": "L'organisateur⋅ice de l'événement a choisi d'approuver manuellement les participations à cet événement. Vous recevrez une notification lorsque votre participation sera approuvée",
     "The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
     "The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
+    "The password was successfully changed": "Le mot de passe a été changé avec succès",
     "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
     "The {date} at {time}": "Le {date} à {time}",
     "The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
@@ -208,6 +220,7 @@
     "View event page": "Voir la page de l'événement",
     "View everything": "Voir tout",
     "Visible everywhere on the web (public)": "Visible partout sur le web (public)",
+    "Waiting for organization team approval.": "En attente d'approbation par l'organisation.",
     "Waiting list": "Liste d'attente",
     "We just sent an email to {email}": "Nous venons d'envoyer un email à {email}",
     "Website / URL": "Site web / URL",
@@ -220,6 +233,7 @@
     "You announced that you're going to this event.": "Vous avez annoncé vous rendre à cet événement.",
     "You are already logged-in.": "Vous êtes déjà connecté.",
     "You are an organizer.": "Vous êtes un organisateur.",
+    "You have been disconnected": "Vous avez été déconnecté⋅e",
     "You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
     "You have one event today.": "Vous n'avez pas d'évenement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
     "You have one event tomorrow.": "Vous n'avez pas d'événement demain | Vous avez un événement demain. | Vous avez {count} événements demain",
@@ -233,6 +247,8 @@
     "e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot",
     "iCal Feed": "Flux iCal",
     "meditate a bit": "méditez un peu",
+    "with another identity…": "avec une autre identité…",
+    "with {identity}": "avec {identity}",
     "{actor}'s avatar": "Avatar de {actor}",
     "{approved} / {total} seats": "{approved} / {total} places",
     "{count} participants": "Un⋅e participant⋅e|{count} participant⋅e⋅s",
diff --git a/js/src/types/actor/person.model.ts b/js/src/types/actor/person.model.ts
index 3a3abcee8..f346b68ab 100644
--- a/js/src/types/actor/person.model.ts
+++ b/js/src/types/actor/person.model.ts
@@ -1,5 +1,5 @@
 import { ICurrentUser } from '@/types/current-user.model';
-import { IEvent } from '@/types/event.model';
+import { IEvent, IParticipant } from '@/types/event.model';
 import { Actor, IActor } from '@/types/actor/actor.model';
 
 export interface IFeedToken {
@@ -11,11 +11,13 @@ export interface IFeedToken {
 export interface IPerson extends IActor {
   feedTokens: IFeedToken[];
   goingToEvents: IEvent[];
+  participations: IParticipant[];
 }
 
 export class Person extends Actor implements IPerson {
   feedTokens: IFeedToken[] = [];
   goingToEvents: IEvent[] = [];
+  participations: IParticipant[] = [];
 
   constructor(hash: IPerson | {} = {}) {
     super(hash);
diff --git a/js/src/views/Account/IdentityPicker.vue b/js/src/views/Account/IdentityPicker.vue
index 89a182ab2..126b8a777 100644
--- a/js/src/views/Account/IdentityPicker.vue
+++ b/js/src/views/Account/IdentityPicker.vue
@@ -1,29 +1,22 @@
 <template>
-    <div class="identity-picker">
-        <img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url"  :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
-        <b-button type="is-text" @click="isComponentModalActive = true">
-            {{ $t('Change') }}
-        </b-button>
-        <b-modal :active.sync="isComponentModalActive" has-modal-card>
-            <div class="modal-card">
-                <header class="modal-card-head">
-                    <p class="modal-card-title">{{ $t('Pick an identity') }}</p>
-                </header>
-                <section class="modal-card-body">
-                    <div class="list is-hoverable">
-                        <a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
-                            <div class="media">
-                                <img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" />
-                                <div class="media-content">
-                                    <h3>@{{ identity.preferredUsername }}</h3>
-                                    <small>{{ identity.name }}</small>
-                                </div>
-                            </div>
-                        </a>
+    <div class="modal-card">
+        <header class="modal-card-head">
+            <p class="modal-card-title">{{ $t('Pick an identity') }}</p>
+        </header>
+        <section class="modal-card-body">
+            <div class="list is-hoverable">
+                <a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
+                    <div class="media">
+                        <img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" />
+                        <div class="media-content">
+                            <h3>@{{ identity.preferredUsername }}</h3>
+                            <small>{{ identity.name }}</small>
+                        </div>
                     </div>
-                </section>
+                </a>
             </div>
-        </b-modal>
+        </section>
+        <slot name="footer"></slot>
     </div>
 </template>
 <script lang="ts">
@@ -40,22 +33,13 @@ import { IDENTITIES } from '@/graphql/actor';
 })
 export default class IdentityPicker extends Vue {
   @Prop() value!: IActor;
-  isComponentModalActive: boolean = false;
   identities: IActor[] = [];
   currentIdentity: IActor = this.value;
 
   changeCurrentIdentity(identity: IActor) {
     this.currentIdentity = identity;
     this.$emit('input', identity);
-    this.isComponentModalActive = false;
   }
 
 }
-</script>
-<style lang="scss">
-    .identity-picker img.image {
-        display: inline;
-        height: 1.5em;
-        vertical-align: text-bottom;
-    }
-</style>
\ No newline at end of file
+</script>
\ No newline at end of file
diff --git a/js/src/views/Account/IdentityPickerWrapper.vue b/js/src/views/Account/IdentityPickerWrapper.vue
new file mode 100644
index 000000000..028c29802
--- /dev/null
+++ b/js/src/views/Account/IdentityPickerWrapper.vue
@@ -0,0 +1,39 @@
+<template>
+    <div class="identity-picker">
+        <img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url"  :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
+        <b-button type="is-text" @click="isComponentModalActive = true">
+            {{ $t('Change') }}
+        </b-button>
+        <b-modal :active.sync="isComponentModalActive" has-modal-card>
+            <identity-picker :currentIdentity="currentIdentity" @input="relay" />
+        </b-modal>
+    </div>
+</template>
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator';
+import { IActor } from '@/types/actor';
+import IdentityPicker from './IdentityPicker.vue';
+
+@Component({
+  components: { IdentityPicker },
+})
+export default class IdentityPickerWrapper extends Vue {
+  @Prop() value!: IActor;
+  isComponentModalActive: boolean = false;
+  currentIdentity: IActor = this.value;
+
+  relay(identity: IActor) {
+    this.currentIdentity = identity;
+    this.$emit('input', identity);
+    this.isComponentModalActive = false;
+  }
+
+}
+</script>
+<style lang="scss">
+    .identity-picker img.image {
+        display: inline;
+        height: 1.5em;
+        vertical-align: text-bottom;
+    }
+</style>
\ No newline at end of file
diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue
index b11454078..07236d154 100644
--- a/js/src/views/Account/Register.vue
+++ b/js/src/views/Account/Register.vue
@@ -92,6 +92,7 @@ export default class Register extends Vue {
     domain: null,
     feedTokens: [],
     goingToEvents: [],
+    participations: [],
   };
   errors: object = {};
   validationSent: boolean = false;
diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue
index 3ed81b536..fd5651c7c 100644
--- a/js/src/views/Event/Edit.vue
+++ b/js/src/views/Event/Edit.vue
@@ -29,7 +29,7 @@ import {EventJoinOptions} from "@/types/event.model";
         <address-auto-complete v-model="event.physicalAddress" />
 
         <b-field :label="$t('Organizer')">
-          <identity-picker v-model="event.organizerActor"></identity-picker>
+          <identity-picker-wrapper v-model="event.organizerActor"></identity-picker-wrapper>
         </b-field>
 
         <div class="field">
@@ -188,7 +188,6 @@ import {
     EventModel,
     EventStatus,
     EventVisibility,
-    EventVisibilityJoinOptions,
   } from '@/types/event.model';
 import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
 import { Person } from '@/types/actor';
@@ -200,10 +199,10 @@ import { TAGS } from '@/graphql/tags';
 import { ITag } from '@/types/tag.model';
 import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
 import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
-import IdentityPicker from '@/views/Account/IdentityPicker.vue';
+import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
 
 @Component({
-  components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor, IdentityPicker },
+  components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
   apollo: {
     currentActor: {
       query: CURRENT_ACTOR_CLIENT,
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index 34bb6fd20..a1f4b93af 100644
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -1,3 +1,6 @@
+import {ParticipantRole} from "@/types/event.model";
+import {ParticipantRole} from "@/types/event.model";
+import {ParticipantRole} from "@/types/event.model";
 <template>
   <div>
     <b-loading :active.sync="$apollo.loading"></b-loading>
@@ -10,7 +13,7 @@
           <img src="https://picsum.photos/600/200/">
         </figure>
       </div>
-        <section class="container">
+        <section>
           <div class="title-and-participate-button">
             <div class="title-wrapper">
               <div class="date-component">
@@ -18,21 +21,21 @@
               </div>
               <h1 class="title">{{ event.title }}</h1>
             </div>
-            <span v-if="event.participantStats.approved > 0 && !actorIsParticipant()">
-                {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
-            </span>
-            <span v-else>
-              {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }}
-            </span>
-            <div v-if="!actorIsOrganizer()" class="participate-button has-text-centered">
-              <a v-if="!actorIsParticipant()" @click="isJoinModalActive = true" class="button is-large is-primary is-rounded">
-                <b-icon icon="circle-outline"></b-icon>
-                {{ $t('Join') }}
-              </a>
-              <a v-if="actorIsParticipant()" @click="confirmLeave()" class="button is-large is-primary is-rounded">
-                <b-icon icon="check-circle"></b-icon>
-                {{ $t('Leave') }}
-              </a>
+            <div class="has-text-right">
+              <small v-if="event.participantStats.approved > 0 && !actorIsParticipant">
+                  {{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
+              </small>
+              <small v-else>
+                {{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }}
+              </small>
+              <participation-button
+                      v-if="currentActor.id && !actorIsOrganizer"
+                      :participation="participations[0]"
+                      :current-actor="currentActor"
+                      @joinEvent="joinEvent"
+                      @joinModal="isJoinModalActive = true"
+                      @confirmLeave="confirmLeave"
+              />
             </div>
           </div>
           <div class="metadata columns">
@@ -60,8 +63,8 @@
               </p>
             </div>
             <div class="column sidebar">
-              <div class="field has-addons">
-                <p class="control" v-if="actorIsOrganizer()">
+              <div class="field has-addons" v-if="currentActor.id">
+                <p class="control" v-if="actorIsOrganizer">
                   <router-link
                           class="button"
                           :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
@@ -69,7 +72,7 @@
                     {{ $t('Edit') }}
                   </router-link>
                 </p>
-                <p class="control" v-if="actorIsOrganizer()">
+                <p class="control" v-if="actorIsOrganizer">
                   <a class="button is-danger" @click="openDeleteEventModalWrapper">
                     {{ $t('Delete') }}
                   </a>
@@ -133,26 +136,6 @@
             </div>
           </div>
         </div>
-      <section class="container">
-        <h3 class="title">{{ $t('Participants') }}</h3>
-        <router-link v-if="currentActor.id === event.organizerActor.id" :to="{ name: EventRouteName.PARTICIPATIONS, params: { eventId: event.uuid } }">
-          {{ $t('Manage participants') }}
-        </router-link>
-        <span v-if="event.participants.length === 0">{{ $t('No participants yet.') }}</span>
-        <div class="columns">
-          <div
-            class="column"
-            v-for="participant in event.participants"
-            :key="participant.id"
-          >
-              <figure class="image is-48x48">
-                <img v-if="!participant.actor.avatar.url" src="https://picsum.photos/48/48/" class="is-rounded">
-                <img v-else :src="participant.actor.avatar.url" class="is-rounded">
-              </figure>
-              <span>{{ participant.actor.preferredUsername }}</span>
-          </div>
-        </div>
-      </section>
       <section class="share">
         <div class="container">
           <div class="columns">
@@ -188,19 +171,35 @@
         <report-modal :on-confirm="reportEvent" :title="$t('Report this event')" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" />
       </b-modal>
       <b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal">
-        <participation-modal :on-confirm="joinEvent" :event="event" :defaultIdentity="currentActor" @close="$refs.participationModal.close()" />
+            <identity-picker v-model="identity">
+              <template v-slot:footer>
+                <footer class="modal-card-foot">
+                  <button
+                          class="button"
+                          ref="cancelButton"
+                          @click="isJoinModalActive = false">
+                    {{ $t('Cancel') }}
+                  </button>
+                  <button
+                          class="button is-primary"
+                          ref="confirmButton"
+                          @click="joinEvent(identity)">
+                    {{ $t('Confirm my particpation') }}
+                  </button>
+                </footer>
+              </template>
+            </identity-picker>
       </b-modal>
       </div>
     </div>
 </template>
 
 <script lang="ts">
-import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
-import { Component, Prop, Vue } from 'vue-property-decorator';
+import { EVENT_PERSON_PARTICIPATION, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
+import { Component, Prop } from 'vue-property-decorator';
 import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
 import { EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
-import { IPerson } from '@/types/actor';
-import { RouteName } from '@/router';
+import { IPerson, Person } from '@/types/actor';
 import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';
 import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
 import BIcon from 'buefy/src/components/icon/Icon.vue';
@@ -208,11 +207,11 @@ import EventCard from '@/components/Event/EventCard.vue';
 import EventFullDate from '@/components/Event/EventFullDate.vue';
 import ActorLink from '@/components/Account/ActorLink.vue';
 import ReportModal from '@/components/Report/ReportModal.vue';
-import ParticipationModal from '@/components/Event/ParticipationModal.vue';
 import { IReport } from '@/types/report.model';
 import { CREATE_REPORT } from '@/graphql/report';
 import EventMixin from '@/mixins/event';
-import { EventRouteName } from '@/router/event';
+import IdentityPicker from '@/views/Account/IdentityPicker.vue';
+import ParticipationButton from '@/components/Event/ParticipationButton.vue';
 
 @Component({
   components: {
@@ -222,7 +221,8 @@ import { EventRouteName } from '@/router/event';
     BIcon,
     DateCalendarIcon,
     ReportModal,
-    ParticipationModal,
+    IdentityPicker,
+    ParticipationButton,
     // tslint:disable:space-in-parens
     'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
     // tslint:enable
@@ -233,13 +233,25 @@ import { EventRouteName } from '@/router/event';
       variables() {
         return {
           uuid: this.uuid,
-          roles: [ParticipantRole.CREATOR, ParticipantRole.MODERATOR, ParticipantRole.MODERATOR, ParticipantRole.PARTICIPANT].join(),
         };
       },
     },
     currentActor: {
       query: CURRENT_ACTOR_CLIENT,
     },
+    participations: {
+      query: EVENT_PERSON_PARTICIPATION,
+      variables() {
+        return {
+          eventId: this.event.id,
+          name: this.currentActor.preferredUsername,
+        };
+      },
+      update: (data) => {
+        if (data && data.person) return data.person.participations;
+        return [];
+      },
+    },
   },
 })
 export default class Event extends EventMixin {
@@ -247,13 +259,17 @@ export default class Event extends EventMixin {
 
   event!: IEvent;
   currentActor!: IPerson;
-  validationSent: boolean = false;
+  identity: IPerson = new Person();
+  participations: IParticipant[] = [];
   showMap: boolean = false;
   isReportModalActive: boolean = false;
   isJoinModalActive: boolean = false;
 
   EventVisibility = EventVisibility;
-  EventRouteName = EventRouteName;
+
+  mounted() {
+    this.identity = this.currentActor;
+  }
 
   /**
    * Delete the event, then redirect to home.
@@ -298,6 +314,24 @@ export default class Event extends EventMixin {
         },
         update: (store, { data }) => {
           if (data == null) return;
+
+          const participationCachedData = store.readQuery<{ person: IPerson }>({
+            query: EVENT_PERSON_PARTICIPATION,
+            variables: { eventId: this.event.id, name: identity.preferredUsername },
+          });
+          if (participationCachedData == null) return;
+          const { person } = participationCachedData;
+          if (person === null) {
+            console.error('Cannot update participation cache, because of null value.');
+            return;
+          }
+          person.participations.push(data.joinEvent);
+          store.writeQuery({
+            query: EVENT_PERSON_PARTICIPATION,
+            variables: { eventId: this.event.id, name: identity.preferredUsername },
+            data: { person },
+          });
+
           const cachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
           if (cachedData == null) return;
           const { event } = cachedData;
@@ -306,9 +340,13 @@ export default class Event extends EventMixin {
             return;
           }
 
-          event.participants = event.participants.concat([data.joinEvent]);
+          if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
+            event.participantStats.unapproved = event.participantStats.unapproved + 1;
+          } else {
+            event.participantStats.approved = event.participantStats.approved + 1;
+          }
 
-          store.writeQuery({ query: FETCH_EVENT, data: { event } });
+          store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
         },
       });
     } catch (error) {
@@ -338,19 +376,38 @@ export default class Event extends EventMixin {
         },
         update: (store, { data }) => {
           if (data == null) return;
-          const cachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
-          if (cachedData == null) return;
-          const { event } = cachedData;
-          if (event === null) {
-            console.error('Cannot update event participant cache, because of null value.');
+
+          const participationCachedData = store.readQuery<{ person: IPerson }>({
+            query: EVENT_PERSON_PARTICIPATION,
+            variables: { eventId: this.event.id, name: this.currentActor.preferredUsername },
+          });
+          if (participationCachedData == null) return;
+          const { person } = participationCachedData;
+          if (person === null) {
+            console.error('Cannot update participation cache, because of null value.');
             return;
           }
+          const participation = person.participations[0];
+          person.participations = [];
+          store.writeQuery({
+            query: EVENT_PERSON_PARTICIPATION,
+            variables: { eventId: this.event.id, name: this.currentActor.preferredUsername },
+            data: { person },
+          });
 
-          event.participants = event.participants
-            .filter(p => p.actor.id !== data.leaveEvent.actor.id);
-          event.participantStats.approved = event.participantStats.approved - 1;
-
-          store.writeQuery({ query: FETCH_EVENT, data: { event } });
+          const eventCachedData = store.readQuery<{ event: IEvent }>({ query: FETCH_EVENT, variables: { uuid: this.event.uuid } });
+          if (eventCachedData == null) return;
+          const { event } = eventCachedData;
+          if (event === null) {
+            console.error('Cannot update event cache, because of null value.');
+            return;
+          }
+          if (participation.role === ParticipantRole.NOT_APPROVED) {
+            event.participantStats.unapproved = event.participantStats.unapproved - 1;
+          } else {
+            event.participantStats.approved = event.participantStats.approved - 1;
+          }
+          store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
         },
       });
     } catch (error) {
@@ -369,17 +426,14 @@ export default class Event extends EventMixin {
     document.body.removeChild(link);
   }
 
-  actorIsParticipant() {
-    if (this.actorIsOrganizer()) return true;
+  get actorIsParticipant() {
+    if (this.actorIsOrganizer) return true;
 
-    return this.currentActor &&
-      this.event.participants
-          .some(participant => participant.actor.id === this.currentActor.id);
+    return this.participations.length > 0 && this.participations[0].role === ParticipantRole.PARTICIPANT;
   }
 
-  actorIsOrganizer() {
-    return this.currentActor && this.event.organizerActor &&
-      this.currentActor.id === this.event.organizerActor.id;
+  get actorIsOrganizer() {
+    return this.participations.length > 0 && this.participations[0].role === ParticipantRole.CREATOR;
   }
 
   get twitterShareUrl(): string {
diff --git a/js/src/views/Event/Participants.vue b/js/src/views/Event/Participants.vue
index 5731c9cdb..afd0b404b 100644
--- a/js/src/views/Event/Participants.vue
+++ b/js/src/views/Event/Participants.vue
@@ -68,6 +68,7 @@ import { IPerson } from '@/types/actor';
           page: 1,
           limit: 10,
           roles: [ParticipantRole.PARTICIPANT].join(),
+          actorId: this.currentActor.id,
         };
       },
     },
@@ -79,6 +80,7 @@ import { IPerson } from '@/types/actor';
           page: 1,
           limit: 20,
           roles: [ParticipantRole.CREATOR].join(),
+          actorId: this.currentActor.id,
         };
       },
       update: data => data.event.participants.map(participation => new Participant(participation)),
@@ -91,6 +93,7 @@ import { IPerson } from '@/types/actor';
           page: 1,
           limit: 20,
           roles: [ParticipantRole.NOT_APPROVED].join(),
+          actorId: this.currentActor.id,
         };
       },
       update: data => data.event.participants.map(participation => new Participant(participation)),
diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue
index 880ba76c0..a9688c6a4 100644
--- a/js/src/views/Home.vue
+++ b/js/src/views/Home.vue
@@ -35,7 +35,6 @@
       <h3 class="title">
         {{ $t("Upcoming") }}
       </h3>
-      <pre>{{ Array.from(goingToEvents.entries()) }}</pre>
       <b-loading :active.sync="$apollo.loading"></b-loading>
       <div v-for="row in goingToEvents" class="upcoming-events">
         <span class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
@@ -53,13 +52,12 @@
               {{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }}
           </h3>
         </span>
-        <div class="level">
+        <div>
           <EventListCard
                   v-for="participation in row[1]"
                   v-if="isInLessThanSevenDays(row[0])"
                   :key="participation[1].event.uuid"
                   :participation="participation[1]"
-                  class="level-item"
           />
         </div>
       </div>
@@ -72,12 +70,11 @@
         {{ $t("Last week") }}
       </h3>
       <b-loading :active.sync="$apollo.loading"></b-loading>
-      <div class="level">
+      <div>
           <EventListCard
                   v-for="participation in lastWeekEvents"
                   :key="participation.id"
                   :participation="participation"
-                  class="level-item"
                   :options="{ hideDate: false }"
           />
       </div>
@@ -295,12 +292,6 @@ export default class Home extends Vue {
   }
 }
 
-  .upcoming-events {
-    .level {
-      margin-left: 4rem;
-    }
-  }
-
     section.container {
         margin: auto auto 3rem;
     }
diff --git a/js/src/views/Moderation/Report.vue b/js/src/views/Moderation/Report.vue
index d03dfc2ee..ef2869ffd 100644
--- a/js/src/views/Moderation/Report.vue
+++ b/js/src/views/Moderation/Report.vue
@@ -169,7 +169,7 @@ export default class Report extends Vue {
 
           report.notes = report.notes.concat([note]);
 
-          store.writeQuery({ query: REPORT, data: { report } });
+          store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
         },
       });
 
@@ -235,7 +235,7 @@ export default class Report extends Vue {
           const updatedReport = data.updateReportStatus;
           report.status = updatedReport.status;
 
-          store.writeQuery({ query: REPORT, data: { report } });
+          store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
         },
       });
     } catch (error) {
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index c6c47b72a..117169f83 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -593,12 +593,12 @@ defmodule Mobilizon.Events do
   @spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
           [Participant.t()]
   def list_participants_for_event(
-        uuid,
+        id,
         roles \\ @default_participant_roles,
         page \\ nil,
         limit \\ nil
       ) do
-    uuid
+    id
     |> list_participants_for_event_query()
     |> filter_role(roles)
     |> Page.paginate(page, limit)
@@ -688,7 +688,7 @@ defmodule Mobilizon.Events do
   Returns the list of participations for an actor.
   """
   @spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) ::
-          [Event.t()]
+          [Participant.t()]
   def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
     actor_id
     |> event_participations_for_actor_query()
@@ -1241,13 +1241,11 @@ defmodule Mobilizon.Events do
   @spec event_participations_for_actor_query(integer) :: Ecto.Query.t()
   def event_participations_for_actor_query(actor_id) do
     from(
-      e in Event,
-      join: p in Participant,
-      join: a in Actor,
-      on: p.actor_id == a.id,
+      p in Participant,
+      join: e in Event,
       on: p.event_id == e.id,
-      where: a.id == ^actor_id and p.role != ^:not_approved,
-      preload: [:picture, :tags]
+      where: p.actor_id == ^actor_id and p.role != ^:not_approved,
+      preload: [:event]
     )
   end
 
@@ -1281,12 +1279,12 @@ defmodule Mobilizon.Events do
   end
 
   @spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
-  defp list_participants_for_event_query(event_uuid) do
+  defp list_participants_for_event_query(event_id) do
     from(
       p in Participant,
       join: e in Event,
       on: p.event_id == e.id,
-      where: e.uuid == ^event_uuid,
+      where: e.id == ^event_id,
       preload: [:actor]
     )
   end
diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex
index 96783f3c2..29f058ac5 100644
--- a/lib/mobilizon_web/resolvers/event.ex
+++ b/lib/mobilizon_web/resolvers/event.ex
@@ -38,34 +38,42 @@ defmodule MobilizonWeb.Resolvers.Event do
     end
   end
 
-  @doc """
-  List participant for event (separate request)
-  """
-  def list_participants_for_event(_parent, %{uuid: uuid, page: page, limit: limit}, _resolution) do
-    {:ok, Mobilizon.Events.list_participants_for_event(uuid, [], page, limit)}
-  end
-
   @doc """
   List participants for event (through an event request)
   """
   def list_participants_for_event(
-        %Event{uuid: uuid},
-        %{page: page, limit: limit, roles: roles},
-        _resolution
+        %Event{id: event_id},
+        %{page: page, limit: limit, roles: roles, actor_id: actor_id},
+        %{context: %{current_user: %User{} = user}} = _resolution
       ) do
-    roles =
-      case roles do
-        "" ->
-          []
+    with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
+         # Check that moderator has right
+         {:actor_approve_permission, true} <-
+           {:actor_approve_permission, Mobilizon.Events.moderator_for_event?(event_id, actor_id)} do
+      roles =
+        case roles do
+          "" ->
+            []
 
-        roles ->
-          roles
-          |> String.split(",")
-          |> Enum.map(&String.downcase/1)
-          |> Enum.map(&String.to_existing_atom/1)
-      end
+          roles ->
+            roles
+            |> String.split(",")
+            |> Enum.map(&String.downcase/1)
+            |> Enum.map(&String.to_existing_atom/1)
+        end
 
-    {:ok, Mobilizon.Events.list_participants_for_event(uuid, roles, page, limit)}
+      {:ok, Mobilizon.Events.list_participants_for_event(event_id, roles, page, limit)}
+    else
+      {:is_owned, nil} ->
+        {:error, "Moderator Actor ID is not owned by authenticated user"}
+
+      {:actor_approve_permission, _} ->
+        {:error, "Provided moderator actor ID doesn't have permission on this event"}
+    end
+  end
+
+  def list_participants_for_event(_, _args, _resolution) do
+    {:ok, []}
   end
 
   def stats_participants_for_event(%Event{id: id}, _args, _resolution) do
diff --git a/lib/mobilizon_web/resolvers/person.ex b/lib/mobilizon_web/resolvers/person.ex
index 7c39d1250..219449d80 100644
--- a/lib/mobilizon_web/resolvers/person.ex
+++ b/lib/mobilizon_web/resolvers/person.ex
@@ -6,6 +6,7 @@ defmodule MobilizonWeb.Resolvers.Person do
   alias Mobilizon.Actors
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Events
+  alias Mobilizon.Events.Participant
   alias Mobilizon.Service.ActivityPub
   alias Mobilizon.Users
   alias Mobilizon.Users.User
@@ -173,27 +174,33 @@ defmodule MobilizonWeb.Resolvers.Person do
   end
 
   @doc """
-  Returns the list of events this person is going to
+  Returns the participation for a specific event
   """
-  def person_going_to_events(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
-    with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
-         events <- Events.list_event_participations_for_actor(actor) do
-      {:ok, events}
+  def person_participations(%Actor{id: actor_id}, %{event_id: event_id}, %{
+        context: %{current_user: user}
+      }) do
+    with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
+         {:no_participant, {:ok, %Participant{} = participant}} <-
+           {:no_participant, Events.get_participant(event_id, actor_id)} do
+      {:ok, [participant]}
     else
       {:is_owned, nil} ->
         {:error, "Actor id is not owned by authenticated user"}
+
+      {:no_participant, _} ->
+        {:ok, []}
     end
   end
 
   @doc """
   Returns the list of events this person is going to
   """
-  def person_going_to_events(_parent, %{}, %{context: %{current_user: user}}) do
-    with %Actor{} = actor <- Users.get_actor_for_user(user),
-         events <- Events.list_event_participations_for_actor(actor) do
-      {:ok, events}
+  def person_participations(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
+    with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
+         participations <- Events.list_event_participations_for_actor(actor) do
+      {:ok, participations}
     else
-      {:is_owned, false} ->
+      {:is_owned, nil} ->
         {:error, "Actor id is not owned by authenticated user"}
     end
   end
diff --git a/lib/mobilizon_web/resolvers/user.ex b/lib/mobilizon_web/resolvers/user.ex
index 82deae829..fbefd49c5 100644
--- a/lib/mobilizon_web/resolvers/user.ex
+++ b/lib/mobilizon_web/resolvers/user.ex
@@ -225,10 +225,11 @@ defmodule MobilizonWeb.Resolvers.User do
   @doc """
   Returns the list of events for all of this user's identities are going to
   """
-  def user_participations(_parent, args, %{
-        context: %{current_user: %User{id: user_id}}
+  def user_participations(%User{id: user_id}, args, %{
+        context: %{current_user: %User{id: logged_user_id}}
       }) do
-    with participations <-
+    with true <- user_id == logged_user_id,
+         participations <-
            Events.list_participations_for_user(
              user_id,
              Map.get(args, :after_datetime),
diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex
index 6403e8360..9eaa26411 100644
--- a/lib/mobilizon_web/schema.ex
+++ b/lib/mobilizon_web/schema.ex
@@ -116,7 +116,6 @@ defmodule MobilizonWeb.Schema do
     import_fields(:person_queries)
     import_fields(:group_queries)
     import_fields(:event_queries)
-    import_fields(:participant_queries)
     import_fields(:tag_queries)
     import_fields(:address_queries)
     import_fields(:config_queries)
diff --git a/lib/mobilizon_web/schema/actors/person.ex b/lib/mobilizon_web/schema/actors/person.ex
index ef7f8b4b7..71826e9b4 100644
--- a/lib/mobilizon_web/schema/actors/person.ex
+++ b/lib/mobilizon_web/schema/actors/person.ex
@@ -56,8 +56,11 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
     )
 
     @desc "The list of events this person goes to"
-    field :going_to_events, list_of(:event) do
-      resolve(&Person.person_going_to_events/3)
+    field(:participations, list_of(:participant),
+      description: "The list of events this person goes to"
+    ) do
+      arg(:event_id, :id)
+      resolve(&Person.person_participations/3)
     end
   end
 
diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex
index 5584a9d35..913023e5f 100644
--- a/lib/mobilizon_web/schema/event.ex
+++ b/lib/mobilizon_web/schema/event.ex
@@ -61,6 +61,7 @@ defmodule MobilizonWeb.Schema.EventType do
       arg(:page, :integer, default_value: 1)
       arg(:limit, :integer, default_value: 10)
       arg(:roles, :string, default_value: "")
+      arg(:actor_id, :id)
       resolve(&Event.list_participants_for_event/3)
     end
 
diff --git a/lib/mobilizon_web/schema/events/participant.ex b/lib/mobilizon_web/schema/events/participant.ex
index 7f80cef40..3dab24067 100644
--- a/lib/mobilizon_web/schema/events/participant.ex
+++ b/lib/mobilizon_web/schema/events/participant.ex
@@ -44,16 +44,6 @@ defmodule MobilizonWeb.Schema.Events.ParticipantType do
     field(:actor, :deleted_object)
   end
 
-  object :participant_queries do
-    @desc "Get all participants for an event uuid"
-    field :participants, list_of(:participant) do
-      arg(:uuid, non_null(:uuid))
-      arg(:page, :integer, default_value: 1)
-      arg(:limit, :integer, default_value: 10)
-      resolve(&Resolvers.Event.list_participants_for_event/3)
-    end
-  end
-
   object :participant_mutations do
     @desc "Join an event"
     field :join_event, :participant do
diff --git a/lib/mobilizon_web/schema/user.ex b/lib/mobilizon_web/schema/user.ex
index 9a2595138..d02aa6457 100644
--- a/lib/mobilizon_web/schema/user.ex
+++ b/lib/mobilizon_web/schema/user.ex
@@ -47,7 +47,7 @@ defmodule MobilizonWeb.Schema.UserType do
     field(:role, :user_role, description: "The role for the user")
 
     field(:participations, list_of(:participant),
-      description: "The list of events this person goes to"
+      description: "The list of events this user goes to"
     ) do
       arg(:after_datetime, :datetime)
       arg(:before_datetime, :datetime)
diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex
index ce33e73bb..26446c379 100644
--- a/lib/service/export/feed.ex
+++ b/lib/service/export/feed.ex
@@ -128,14 +128,18 @@ defmodule Mobilizon.Service.Export.Feed do
          %FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
       case actor do
         %Actor{} = actor ->
-          events = fetch_identity_going_to_events(actor)
+          events = actor |> fetch_identity_participations() |> participations_to_events()
           {:ok, build_actor_feed(actor, events, false)}
 
         nil ->
           with actors <- Users.get_actors_for_user(user),
                events <-
                  actors
-                 |> Enum.map(&Events.list_event_participations_for_actor/1)
+                 |> Enum.map(fn actor ->
+                   actor
+                   |> Events.list_event_participations_for_actor()
+                   |> participations_to_events()
+                 end)
                  |> Enum.concat() do
             {:ok, build_user_feed(events, user, token)}
           end
@@ -143,12 +147,18 @@ defmodule Mobilizon.Service.Export.Feed do
     end
   end
 
-  defp fetch_identity_going_to_events(%Actor{} = actor) do
+  defp fetch_identity_participations(%Actor{} = actor) do
     with events <- Events.list_event_participations_for_actor(actor) do
       events
     end
   end
 
+  defp participations_to_events(participations) do
+    participations
+    |> Enum.map(& &1.event_id)
+    |> Enum.map(&Events.get_event_with_preload!/1)
+  end
+
   # Build an atom feed from actor and it's public events
   @spec build_user_feed(list(), User.t(), String.t()) :: String.t()
   defp build_user_feed(events, %User{email: email}, token) do
diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex
index 70d865f48..fe053d451 100644
--- a/lib/service/export/icalendar.ex
+++ b/lib/service/export/icalendar.ex
@@ -33,7 +33,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
       dtend: event.ends_on,
       description: event.description,
       uid: event.uuid,
-      categories: [event.category] ++ (event.tags |> Enum.map(& &1.slug))
+      categories: event.tags |> Enum.map(& &1.slug)
     }
   end
 
@@ -52,7 +52,8 @@ defmodule Mobilizon.Service.Export.ICalendar do
 
   @spec export_private_actor(Actor.t()) :: String.t()
   def export_private_actor(%Actor{} = actor) do
-    with events <- Events.list_event_participations_for_actor(actor) do
+    with events <-
+           actor |> Events.list_event_participations_for_actor() |> participations_to_events() do
       {:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
     end
   end
@@ -107,7 +108,11 @@ defmodule Mobilizon.Service.Export.ICalendar do
           with actors <- Users.get_actors_for_user(user),
                events <-
                  actors
-                 |> Enum.map(&Events.list_event_participations_for_actor/1)
+                 |> Enum.map(fn actor ->
+                   actor
+                   |> Events.list_event_participations_for_actor()
+                   |> participations_to_events()
+                 end)
                  |> Enum.concat() do
             {:ok,
              %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
@@ -115,4 +120,10 @@ defmodule Mobilizon.Service.Export.ICalendar do
       end
     end
   end
+
+  defp participations_to_events(participations) do
+    participations
+    |> Enum.map(& &1.event_id)
+    |> Enum.map(&Events.get_event_with_preload!/1)
+  end
 end
diff --git a/schema.graphql b/schema.graphql
index 51749d01c..716dc6c27 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1,5 +1,5 @@
 # source: http://localhost:4000/api
-# timestamp: Tue Sep 24 2019 18:20:05 GMT+0200 (GMT+02:00)
+# timestamp: Wed Sep 25 2019 16:41:05 GMT+0200 (GMT+02:00)
 
 schema {
   query: RootQueryType
@@ -287,7 +287,7 @@ type Event implements ActionLogObject {
   participantStats: ParticipantStats
 
   """The event's participants"""
-  participants(limit: Int = 10, page: Int = 1, roles: String = ""): [Participant]
+  participants(actorId: ID, limit: Int = 10, page: Int = 1, roles: String = ""): [Participant]
 
   """Phone address for the event"""
   phoneAddress: String
@@ -710,9 +710,6 @@ type Person implements Actor {
   """Number of actors following this actor"""
   followingCount: Int
 
-  """The list of events this person goes to"""
-  goingToEvents: [Event]
-
   """Internal ID for this person"""
   id: ID
 
@@ -734,6 +731,9 @@ type Person implements Actor {
   """A list of the events this actor has organized"""
   organizedEvents: [Event]
 
+  """The list of events this person goes to"""
+  participations(eventId: ID): [Participant]
+
   """The actor's preferred username"""
   preferredUsername: String
 
@@ -1128,9 +1128,6 @@ type RootQueryType {
   """Get the current user"""
   loggedUser: User
 
-  """Get all participants for an event uuid"""
-  participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant]
-
   """Get a person by it's preferred username"""
   person(preferredUsername: String!): Person
 
@@ -1223,7 +1220,7 @@ type User {
   """The user's ID"""
   id: ID!
 
-  """The list of events this person goes to"""
+  """The list of events this user goes to"""
   participations(afterDatetime: DateTime, beforeDatetime: DateTime, limit: Int = 10, page: Int = 1): [Participant]
 
   """The user's list of profiles (identities)"""
diff --git a/test/mobilizon/events/events_test.exs b/test/mobilizon/events/events_test.exs
index 645b89e17..5564c4d0b 100644
--- a/test/mobilizon/events/events_test.exs
+++ b/test/mobilizon/events/events_test.exs
@@ -99,8 +99,8 @@ defmodule Mobilizon.EventsTest do
       assert event.ends_on == DateTime.from_naive!(~N[2010-04-17 14:00:00Z], "Etc/UTC")
       assert event.title == "some title"
 
-      assert hd(Events.list_participants_for_event(event.uuid)).actor.id == actor.id
-      assert hd(Events.list_participants_for_event(event.uuid)).role == :creator
+      assert hd(Events.list_participants_for_event(event.id)).actor.id == actor.id
+      assert hd(Events.list_participants_for_event(event.id)).role == :creator
     end
 
     test "create_event/1 with invalid data returns error changeset" do
diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs
index 431e7e958..d3749954d 100644
--- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs
+++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs
@@ -784,7 +784,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
       assert :error == Transmogrifier.handle_incoming(reject_data)
 
       # Organiser is not present since we use factories directly
-      assert Events.list_participants_for_event(event.uuid) |> Enum.map(& &1.id) ==
+      assert Events.list_participants_for_event(event.id) |> Enum.map(& &1.id) ==
                []
     end
 
@@ -812,7 +812,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
       assert activity.data["actor"] == participant_url
 
       # The only participant left is the organizer
-      assert Events.list_participants_for_event(event.uuid) |> Enum.map(& &1.id) == [
+      assert Events.list_participants_for_event(event.id) |> Enum.map(& &1.id) == [
                organizer_participation.id
              ]
     end
diff --git a/test/mobilizon_web/controllers/feed_controller_test.exs b/test/mobilizon_web/controllers/feed_controller_test.exs
index 05aeeb031..5ea664169 100644
--- a/test/mobilizon_web/controllers/feed_controller_test.exs
+++ b/test/mobilizon_web/controllers/feed_controller_test.exs
@@ -106,8 +106,8 @@ defmodule MobilizonWeb.FeedControllerTest do
         assert entry.summary in [event1.title, event2.title]
       end)
 
-      assert entry1.categories == [event1.category, tag1.slug]
-      assert entry2.categories == [event2.category, tag1.slug, tag2.slug]
+      assert entry1.categories == [tag1.slug]
+      assert entry2.categories == [tag1.slug, tag2.slug]
     end
 
     test "it returns a 404 page for the actor's public events iCal feed with an actor not publicly visible",
@@ -174,7 +174,7 @@ defmodule MobilizonWeb.FeedControllerTest do
 
       assert entry1.summary == event1.title
 
-      assert entry1.categories == [event1.category, tag1.slug, tag2.slug]
+      assert entry1.categories == [tag1.slug, tag2.slug]
     end
   end
 
@@ -311,6 +311,7 @@ defmodule MobilizonWeb.FeedControllerTest do
 
       [entry1] = ExIcal.parse(conn.resp_body)
       assert entry1.summary == event1.title
+      assert entry1.categories == event1.tags |> Enum.map(& &1.slug)
     end
 
     test "it returns 404 for an not existing feed", %{conn: conn} do
diff --git a/test/mobilizon_web/resolvers/participant_resolver_test.exs b/test/mobilizon_web/resolvers/participant_resolver_test.exs
index 88472f5be..d6f6c160f 100644
--- a/test/mobilizon_web/resolvers/participant_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/participant_resolver_test.exs
@@ -129,13 +129,15 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       actor: actor
     } do
       event = insert(:event, %{organizer_actor: actor})
-      participant = insert(:participant, %{actor: actor, event: event})
-      participant2 = insert(:participant, %{event: event})
+      insert(:participant, %{actor: actor, event: event, role: :creator})
+      user2 = insert(:user)
+      actor2 = insert(:actor, user: user2)
+      participant2 = insert(:participant, %{event: event, actor: actor2, role: :participant})
 
       mutation = """
           mutation {
             leaveEvent(
-              actor_id: #{participant.actor.id},
+              actor_id: #{participant2.actor.id},
               event_id: #{event.id}
             ) {
                 actor {
@@ -150,40 +152,64 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
 
       res =
         conn
-        |> auth_conn(user)
+        |> auth_conn(user2)
         |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
 
       assert json_response(res, 200)["errors"] == nil
       assert json_response(res, 200)["data"]["leaveEvent"]["event"]["id"] == to_string(event.id)
 
       assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] ==
-               to_string(participant.actor.id)
+               to_string(participant2.actor.id)
 
       query = """
       {
-        event(uuid: "#{event.uuid}") {
-          participants {
-            role,
-            actor {
-                preferredUsername
+        person(preferredUsername: "#{actor.preferred_username}") {
+            participations(eventId: "#{event.id}") {
+              event {
+                uuid,
+                title
+              },
+              role
             }
-          }
         }
       }
       """
 
       res =
         conn
-        |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
+        |> auth_conn(user)
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
 
-      assert json_response(res, 200)["data"]["event"]["participants"] == [
+      assert json_response(res, 200)["data"]["person"]["participations"] == [
                %{
-                 "actor" => %{
-                   "preferredUsername" => participant2.actor.preferred_username
+                 "event" => %{
+                   "uuid" => event.uuid,
+                   "title" => event.title
                  },
                  "role" => "CREATOR"
                }
              ]
+
+      query = """
+      {
+        person(preferredUsername: "#{actor2.preferred_username}") {
+            participations(eventId: "#{event.id}") {
+              event {
+                uuid,
+                title
+              },
+              role
+            }
+        }
+      }
+      """
+
+      res =
+        conn
+        |> auth_conn(user2)
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
+
+      assert json_response(res, 200)["data"]["person"]["participations"] == []
     end
 
     test "actor_leave_event/3 should check if the participant is the only creator", %{
@@ -324,17 +350,23 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       assert hd(json_response(res, 200)["errors"])["message"] =~ "Participant not found"
     end
 
-    test "list_participants_for_event/3 returns participants for an event", context do
+    test "list_participants_for_event/3 returns participants for an event", %{
+      conn: conn,
+      actor: actor,
+      user: user
+    } do
       event =
         @event
-        |> Map.put(:organizer_actor_id, context.actor.id)
+        |> Map.put(:organizer_actor_id, actor.id)
 
       {:ok, event} = Events.create_event(event)
 
       query = """
       {
         event(uuid: "#{event.uuid}") {
-          participants(roles: "participant,moderator,administrator,creator") {
+          participants(roles: "participant,moderator,administrator,creator", actor_id: "#{
+        actor.id
+      }") {
             role,
             actor {
                 preferredUsername
@@ -345,13 +377,16 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       """
 
       res =
-        context.conn
+        conn
+        |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
 
+      assert json_response(res, 200)["errors"] == nil
+
       assert json_response(res, 200)["data"]["event"]["participants"] == [
                %{
                  "actor" => %{
-                   "preferredUsername" => context.actor.preferred_username
+                   "preferredUsername" => actor.preferred_username
                  },
                  "role" => "CREATOR"
                }
@@ -368,7 +403,9 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       query = """
       {
         event(uuid: "#{event.uuid}") {
-          participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator") {
+          participants(page: 1, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
+        actor.id
+      }") {
             role,
             actor {
                 preferredUsername
@@ -379,7 +416,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       """
 
       res =
-        context.conn
+        conn
+        |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
 
       sorted_participants =
@@ -402,7 +440,9 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       query = """
       {
         event(uuid: "#{event.uuid}") {
-          participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator") {
+          participants(page: 2, limit: 1, roles: "participant,moderator,administrator,creator", actorId: "#{
+        actor.id
+      }") {
             role,
             actor {
                 preferredUsername
@@ -413,7 +453,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       """
 
       res =
-        context.conn
+        conn
+        |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
 
       sorted_participants =
@@ -427,7 +468,7 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       assert sorted_participants == [
                %{
                  "actor" => %{
-                   "preferredUsername" => context.actor.preferred_username
+                   "preferredUsername" => actor.preferred_username
                  },
                  "role" => "CREATOR"
                }
diff --git a/test/mobilizon_web/resolvers/person_resolver_test.exs b/test/mobilizon_web/resolvers/person_resolver_test.exs
index e1ea66d19..07a4863f2 100644
--- a/test/mobilizon_web/resolvers/person_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/person_resolver_test.exs
@@ -473,7 +473,9 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
 
       assert hd(json_response(res, 200)["errors"])["message"] == "Person with name riri not found"
     end
+  end
 
+  describe "get_current_person/3" do
     test "get_current_person/3 can return the events the person is going to", context do
       user = insert(:user)
       actor = insert(:actor, user: user)
@@ -481,9 +483,11 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
       query = """
       {
           loggedPerson {
-            goingToEvents {
-              uuid,
-              title
+            participations {
+              event {
+                uuid,
+                title
+              }
             }
           }
         }
@@ -494,7 +498,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
         |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
 
-      assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == []
+      assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == []
 
       event = insert(:event, %{organizer_actor: actor})
       insert(:participant, %{actor: actor, event: event})
@@ -504,8 +508,8 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
         |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
 
-      assert json_response(res, 200)["data"]["loggedPerson"]["goingToEvents"] == [
-               %{"title" => event.title, "uuid" => event.uuid}
+      assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == [
+               %{"event" => %{"title" => event.title, "uuid" => event.uuid}}
              ]
     end
 
@@ -519,9 +523,11 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
       query = """
       {
         person(preferredUsername: "#{actor.preferred_username}") {
-            goingToEvents {
-              uuid,
-              title
+            participations {
+              event {
+                uuid,
+                title
+              }
             }
         }
       }
@@ -532,14 +538,16 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
         |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
 
-      assert json_response(res, 200)["data"]["person"]["goingToEvents"] == []
+      assert json_response(res, 200)["data"]["person"]["participations"] == []
 
       query = """
       {
         person(preferredUsername: "#{actor_from_other_user.preferred_username}") {
-            goingToEvents {
-              uuid,
-              title
+            participations {
+              event {
+                uuid,
+                title
+              }
             }
         }
       }
@@ -550,10 +558,45 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
         |> auth_conn(user)
         |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
 
-      assert json_response(res, 200)["data"]["person"]["goingToEvents"] == nil
+      assert json_response(res, 200)["data"]["person"]["participations"] == nil
 
       assert hd(json_response(res, 200)["errors"])["message"] ==
                "Actor id is not owned by authenticated user"
     end
+
+    test "find_person/3 can return the participation for an identity on a specific event",
+         context do
+      user = insert(:user)
+      actor = insert(:actor, user: user)
+      event = insert(:event, organizer_actor: actor)
+      insert(:participant, event: event, actor: actor)
+
+      query = """
+      {
+        person(preferredUsername: "#{actor.preferred_username}") {
+            participations(eventId: "#{event.id}") {
+              event {
+                uuid,
+                title
+              }
+            }
+        }
+      }
+      """
+
+      res =
+        context.conn
+        |> auth_conn(user)
+        |> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
+
+      assert json_response(res, 200)["data"]["person"]["participations"] == [
+               %{
+                 "event" => %{
+                   "uuid" => event.uuid,
+                   "title" => event.title
+                 }
+               }
+             ]
+    end
   end
 end