diff --git a/js/src/components/Activity/EventActivityItem.vue b/js/src/components/Activity/EventActivityItem.vue
index 4c9f1a359..cdb13264e 100644
--- a/js/src/components/Activity/EventActivityItem.vue
+++ b/js/src/components/Activity/EventActivityItem.vue
@@ -42,6 +42,7 @@ import { usernameWithDomain } from "@/types/actor";
 import { formatTimeString } from "@/filters/datetime";
 import {
   ActivityEventCommentSubject,
+  ActivityEventParticipantSubject,
   ActivityEventSubject,
 } from "@/types/enums";
 import { computed } from "vue";
@@ -90,6 +91,14 @@ const translation = computed((): string | undefined => {
         return "You posted a comment on the event {event}.";
       }
       return "{profile} posted a comment on the event {event}.";
+    case ActivityEventParticipantSubject.EVENT_NEW_PARTICIPATION:
+      if (isAuthorCurrentActor.value) {
+        return "You joined the event {event}.";
+      }
+      if (props.activity.author.preferredUsername === "anonymous") {
+        return "An anonymous profile joined the event {event}.";
+      }
+      return "{profile} joined the the event {event}.";
     default:
       return undefined;
   }
diff --git a/js/src/components/Participation/ConfirmParticipation.vue b/js/src/components/Participation/ConfirmParticipation.vue
index e433a581c..223da0cc1 100644
--- a/js/src/components/Participation/ConfirmParticipation.vue
+++ b/js/src/components/Participation/ConfirmParticipation.vue
@@ -67,7 +67,7 @@ import { EventJoinOptions } from "@/types/enums";
 import { IParticipant } from "../../types/participant.model";
 import RouteName from "../../router/name";
 import { CONFIRM_PARTICIPATION } from "../../graphql/event";
-import { computed, ref } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import { useMutation } from "@vue/apollo-composable";
 import { useI18n } from "vue-i18n";
 import { useHead } from "@vueuse/head";
@@ -90,9 +90,15 @@ const { onDone, onError, mutate } = useMutation<{
   confirmParticipation: IParticipant;
 }>(CONFIRM_PARTICIPATION);
 
-mutate(() => ({
-  token: props.token,
-}));
+const participationToken = computed(() => props.token);
+
+watchEffect(() => {
+  if (participationToken.value) {
+    mutate({
+      token: participationToken.value,
+    });
+  }
+});
 
 onDone(async ({ data }) => {
   participation.value = data?.confirmParticipation;
diff --git a/js/src/components/Settings/SettingsOnboarding.vue b/js/src/components/Settings/SettingsOnboarding.vue
index e9a489b7c..383d7d25f 100644
--- a/js/src/components/Settings/SettingsOnboarding.vue
+++ b/js/src/components/Settings/SettingsOnboarding.vue
@@ -70,14 +70,16 @@ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
 
 const { loggedUser } = useUserSettings();
 
+const { mutate: doUpdateLocale } = updateLocale();
+
 onMounted(() => {
-  updateLocale(locale as unknown as string);
+  doUpdateLocale({ locale: locale as unknown as string });
   doUpdateSetting({ timezone });
 });
 
 watch(locale, () => {
   if (locale.value) {
-    updateLocale(locale.value as string);
+    doUpdateLocale({ locale: locale as unknown as string });
     saveLocaleData(locale.value as string);
   }
 });
diff --git a/js/src/composition/apollo/user.ts b/js/src/composition/apollo/user.ts
index 4e8128eb4..b1459ff2b 100644
--- a/js/src/composition/apollo/user.ts
+++ b/js/src/composition/apollo/user.ts
@@ -59,12 +59,8 @@ export async function doUpdateSetting(
   }));
 }
 
-export async function updateLocale(locale: string) {
-  useMutation<{ id: string; locale: string }>(UPDATE_USER_LOCALE, () => ({
-    variables: {
-      locale,
-    },
-  }));
+export function updateLocale() {
+  return useMutation<{ id: string; locale: string }>(UPDATE_USER_LOCALE);
 }
 
 export function registerAccount() {
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index dc52d2ec0..58d487f1f 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -1579,5 +1579,8 @@
   "Access drafts events": "Access drafts events",
   "This application will be allowed to list and view your draft events": "This application will be allowed to list and view your draft events",
   "Access group suggested events": "Access group suggested events",
-  "This application will be allowed to list your suggested group events": "This application will be allowed to list your suggested group events"
+  "This application will be allowed to list your suggested group events": "This application will be allowed to list your suggested group events",
+  "{profile} joined the the event {event}.": "{profile} joined the the event {event}.",
+  "You joined the event {event}.": "You joined the event {event}.",
+  "An anonymous profile joined the event {event}.": "An anonymous profile joined the event {event}."
 }
\ No newline at end of file
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 9552c8370..32fee266e 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -1575,5 +1575,8 @@
   "Access drafts events": "Accéder aux événements brouillons",
   "This application will be allowed to list and view your draft events": "Cetta application sera autorisée à lister et accéder à vos événements brouillons",
   "Access group suggested events": "Accéder aux événements des groupes suggérés",
-  "This application will be allowed to list your suggested group events": "Cetta application sera autorisée à lister les événements de vos groupes qui vous sont suggérés"
+  "This application will be allowed to list your suggested group events": "Cetta application sera autorisée à lister les événements de vos groupes qui vous sont suggérés",
+  "{profile} joined the the event {event}.": "{profile} a rejoint l'événement {event}.",
+  "You joined the event {event}.": "Vous avez rejoint l'événement {event}.",
+  "An anonymous profile joined the event {event}.": "Un profil anonyme a rejoint l'événement {event}."
 }
diff --git a/js/src/services/push-subscription.ts b/js/src/services/push-subscription.ts
index 0abb0d3c4..437a5867d 100644
--- a/js/src/services/push-subscription.ts
+++ b/js/src/services/push-subscription.ts
@@ -32,9 +32,8 @@ export async function subscribeUserToPush(): Promise<PushSubscription | null> {
         };
         const registration = await navigator.serviceWorker.ready;
         try {
-          const pushSubscription = await registration.pushManager.subscribe(
-            subscribeOptions
-          );
+          const pushSubscription =
+            await registration.pushManager.subscribe(subscribeOptions);
           console.debug("Received PushSubscription: ", pushSubscription);
           resolve(pushSubscription);
         } catch (e) {
diff --git a/js/src/types/activity.model.ts b/js/src/types/activity.model.ts
index c5c218757..f3ecb913e 100644
--- a/js/src/types/activity.model.ts
+++ b/js/src/types/activity.model.ts
@@ -3,6 +3,7 @@ import { IMember } from "./actor/member.model";
 import {
   ActivityDiscussionSubject,
   ActivityEventCommentSubject,
+  ActivityEventParticipantSubject,
   ActivityEventSubject,
   ActivityGroupSubject,
   ActivityMemberSubject,
@@ -21,7 +22,8 @@ export type ActivitySubject =
   | ActivityResourceSubject
   | ActivityDiscussionSubject
   | ActivityGroupSubject
-  | ActivityEventCommentSubject;
+  | ActivityEventCommentSubject
+  | ActivityEventParticipantSubject;
 
 export interface IActivity {
   id: string;
diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts
index 0eb3841bb..dc551f3f9 100644
--- a/js/src/types/enums.ts
+++ b/js/src/types/enums.ts
@@ -200,6 +200,10 @@ export enum ActivityEventCommentSubject {
   COMMENT_POSTED = "comment_posted",
 }
 
+export enum ActivityEventParticipantSubject {
+  EVENT_NEW_PARTICIPATION = "event_new_participation",
+}
+
 export enum ActivityPostSubject {
   POST_CREATED = "post_created",
   POST_UPDATED = "post_updated",
diff --git a/js/src/views/Event/ParticipantsView.vue b/js/src/views/Event/ParticipantsView.vue
index 3b859bfa3..f89562787 100644
--- a/js/src/views/Event/ParticipantsView.vue
+++ b/js/src/views/Event/ParticipantsView.vue
@@ -21,7 +21,7 @@
         <div class="">
           <o-field :label="t('Status')" horizontal label-for="role-select">
             <o-select v-model="role" id="role-select">
-              <option :value="null">
+              <option value="EVERYTHING">
                 {{ t("Everything") }}
               </option>
               <option :value="ParticipantRole.CREATOR">
@@ -303,17 +303,15 @@ const participantsExportFormats = useParticipantsExportFormats();
 const ellipsize = (text?: string) =>
   text && text.substring(0, MESSAGE_ELLIPSIS_LENGTH).concat("…");
 
-// metaInfo() {
-//   return {
-//     title: this.t("Participants") as string,
-//   };
-// },
+const eventId = computed(() => props.eventId);
+
+const ParticipantAllRoles = { ...ParticipantRole, EVERYTHING: "EVERYTHING" };
 
 const page = useRouteQuery("page", 1, integerTransformer);
 const role = useRouteQuery(
   "role",
-  ParticipantRole.PARTICIPANT,
-  enumTransformer(ParticipantRole)
+  "EVERYTHING",
+  enumTransformer(ParticipantAllRoles)
 );
 
 const checkedRows = ref<IParticipant[]>([]);
@@ -325,10 +323,10 @@ const { result: participantsResult, loading: participantsLoading } = useQuery<{
 }>(
   PARTICIPANTS,
   () => ({
-    uuid: props.eventId,
+    uuid: eventId.value,
     page: page.value,
     limit: PARTICIPANTS_PER_PAGE,
-    roles: role.value,
+    roles: role.value === "EVERYTHING" ? undefined : role.value,
   }),
   () => ({
     enabled:
diff --git a/js/src/views/Settings/PreferencesView.vue b/js/src/views/Settings/PreferencesView.vue
index 919131972..2be472a9e 100644
--- a/js/src/views/Settings/PreferencesView.vue
+++ b/js/src/views/Settings/PreferencesView.vue
@@ -47,6 +47,7 @@
         <o-select
           :loading="loadingTimezones || loadingUserSettings"
           v-model="$i18n.locale"
+          @update:modelValue="updateLanguage"
           :placeholder="t('Select a language')"
           id="setting-language"
         >
@@ -147,7 +148,7 @@ import RouteName from "../../router/name";
 import { AddressSearchType } from "@/types/enums";
 import { Address, IAddress } from "@/types/address.model";
 import { useTimezones } from "@/composition/apollo/config";
-import { useUserSettings } from "@/composition/apollo/user";
+import { useUserSettings, updateLocale } from "@/composition/apollo/user";
 import { useHead } from "@vueuse/head";
 import { computed, defineAsyncComponent, ref, watch } from "vue";
 import { useI18n } from "vue-i18n";
@@ -172,6 +173,12 @@ useHead({
 const theme = ref(localStorage.getItem("theme"));
 const systemTheme = ref(!("theme" in localStorage));
 
+const { mutate: doUpdateLocale } = updateLocale();
+
+const updateLanguage = (newLocale: string) => {
+  doUpdateLocale({ locale: newLocale });
+};
+
 watch(systemTheme, (newSystemTheme) => {
   console.debug("changing system theme", newSystemTheme);
   if (newSystemTheme) {
diff --git a/lib/federation/activity_pub/actions/accept.ex b/lib/federation/activity_pub/actions/accept.ex
index 87912de97..1480a262f 100644
--- a/lib/federation/activity_pub/actions/accept.ex
+++ b/lib/federation/activity_pub/actions/accept.ex
@@ -82,6 +82,11 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
       )
 
       Scheduler.trigger_notifications_for_participant(participant)
+
+      Mobilizon.Service.Activity.Participant.insert_activity(participant,
+        subject: "event_new_participation"
+      )
+
       participant_as_data = Convertible.model_to_as(participant)
       audience = Audience.get_audience(participant)
 
diff --git a/lib/federation/activity_pub/types/events.ex b/lib/federation/activity_pub/types/events.ex
index 13b3e2f12..94cd9d665 100644
--- a/lib/federation/activity_pub/types/events.ex
+++ b/lib/federation/activity_pub/types/events.ex
@@ -224,6 +224,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
     cond do
       Mobilizon.Events.get_default_participant_role(event) == :participant &&
           role == :participant ->
+        Mobilizon.Service.Activity.Participant.insert_activity(participant,
+          subject: "event_new_participation"
+        )
+
         {:accept,
          Actions.Accept.accept(
            :join,
diff --git a/lib/graphql/resolvers/push_subscription.ex b/lib/graphql/resolvers/push_subscription.ex
index 5501fa8b2..5be49a7b5 100644
--- a/lib/graphql/resolvers/push_subscription.ex
+++ b/lib/graphql/resolvers/push_subscription.ex
@@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
   alias Mobilizon.Storage.Page
   alias Mobilizon.Users
   alias Mobilizon.Users.{PushSubscription, User}
+  import Mobilizon.Web.Gettext
 
   @doc """
   List all of an user's registered push subscriptions
@@ -33,6 +34,19 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
       {:ok, %PushSubscription{}} ->
         {:ok, "OK"}
 
+      {:error,
+       %Ecto.Changeset{
+         errors: [
+           digest:
+             {"has already been taken",
+              [
+                constraint: :unique,
+                constraint_name: "user_push_subscriptions_user_id_digest_index"
+              ]}
+         ]
+       }} ->
+        {:error, dgettext("errors", "The same push subscription has already been registered")}
+
       {:error, err} ->
         require Logger
         Logger.error(inspect(err))
diff --git a/lib/graphql/schema/users/push_subscription.ex b/lib/graphql/schema/users/push_subscription.ex
index 9565199de..6824ea59d 100644
--- a/lib/graphql/schema/users/push_subscription.ex
+++ b/lib/graphql/schema/users/push_subscription.ex
@@ -4,6 +4,7 @@ defmodule Mobilizon.GraphQL.Schema.Users.PushSubscription do
   """
   use Absinthe.Schema.Notation
   alias Mobilizon.GraphQL.Resolvers.PushSubscription
+  alias Mobilizon.Users.User
 
   # object :push_subscription do
   #   field(:id, :id)
@@ -29,8 +30,9 @@ defmodule Mobilizon.GraphQL.Schema.Users.PushSubscription do
 
       middleware(Rajska.QueryAuthorization,
         permit: :user,
-        scope: false,
-        rule: :"write:user:setting:push"
+        scope: User,
+        rule: :"write:user:setting:push",
+        args: %{}
       )
 
       resolve(&PushSubscription.register_push_subscription/3)
@@ -41,8 +43,9 @@ defmodule Mobilizon.GraphQL.Schema.Users.PushSubscription do
 
       middleware(Rajska.QueryAuthorization,
         permit: :user,
-        scope: false,
-        rule: :"write:user:setting:push"
+        scope: User,
+        rule: :"write:user:setting:push",
+        args: %{}
       )
 
       resolve(&PushSubscription.unregister_push_subscription/3)
diff --git a/lib/mobilizon/activities/activities.ex b/lib/mobilizon/activities/activities.ex
index 52b7683ed..d851f19fb 100644
--- a/lib/mobilizon/activities/activities.ex
+++ b/lib/mobilizon/activities/activities.ex
@@ -19,6 +19,7 @@ defmodule Mobilizon.Activities do
 
   @activity_types ["event", "post", "discussion", "resource", "group", "member", "comment"]
   @event_activity_subjects ["event_created", "event_updated", "event_deleted", "comment_posted"]
+  @participant_activity_subjects ["event_new_participation"]
   @post_activity_subjects ["post_created", "post_updated", "post_deleted"]
   @discussion_activity_subjects [
     "discussion_created",
@@ -48,12 +49,23 @@ defmodule Mobilizon.Activities do
   @settings_activity_subjects ["group_created", "group_updated"]
 
   @subjects @event_activity_subjects ++
+              @participant_activity_subjects ++
               @post_activity_subjects ++
               @discussion_activity_subjects ++
               @resource_activity_subjects ++
               @member_activity_subjects ++ @settings_activity_subjects
 
-  @object_type ["event", "actor", "post", "discussion", "resource", "member", "group", "comment"]
+  @object_type [
+    "event",
+    "participant",
+    "actor",
+    "post",
+    "discussion",
+    "resource",
+    "member",
+    "group",
+    "comment"
+  ]
 
   defenum(Type, @activity_types)
   defenum(Subject, @subjects)
diff --git a/lib/service/activity/activity.ex b/lib/service/activity/activity.ex
index 9c436fecf..1262102c6 100644
--- a/lib/service/activity/activity.ex
+++ b/lib/service/activity/activity.ex
@@ -4,7 +4,17 @@ defmodule Mobilizon.Service.Activity do
   """
 
   alias Mobilizon.Activities.Activity
-  alias Mobilizon.Service.Activity.{Comment, Discussion, Event, Group, Member, Post, Resource}
+
+  alias Mobilizon.Service.Activity.{
+    Comment,
+    Discussion,
+    Event,
+    Group,
+    Member,
+    Participant,
+    Post,
+    Resource
+  }
 
   @callback insert_activity(entity :: struct(), options :: Keyword.t()) ::
               {:ok, Oban.Job.t()} | {:ok, any()} | {:error, Ecto.Changeset.t()}
@@ -45,4 +55,8 @@ defmodule Mobilizon.Service.Activity do
   defp do_get_object(:comment, comment_id) do
     Comment.get_object(comment_id)
   end
+
+  defp do_get_object(:participant, participant_id) do
+    Participant.get_object(participant_id)
+  end
 end
diff --git a/lib/service/activity/participant.ex b/lib/service/activity/participant.ex
new file mode 100644
index 000000000..791e67fff
--- /dev/null
+++ b/lib/service/activity/participant.ex
@@ -0,0 +1,48 @@
+defmodule Mobilizon.Service.Activity.Participant do
+  @moduledoc """
+  Insert an event activity
+  """
+  alias Mobilizon.{Actors, Events}
+  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Events.Participant
+  alias Mobilizon.Service.Activity
+  alias Mobilizon.Service.Workers.ActivityBuilder
+
+  @behaviour Activity
+
+  @impl Activity
+  def insert_activity(event, options \\ [])
+
+  def insert_activity(
+        %Participant{event_id: event_id, actor_id: actor_id, id: participant_id} =
+          _participant,
+        options
+      ) do
+    actor = Actors.get_actor(actor_id)
+    event = Events.get_event!(event_id)
+    subject = Keyword.fetch!(options, :subject)
+
+    ActivityBuilder.enqueue(:build_activity, %{
+      "type" => "event",
+      "subject" => subject,
+      "subject_params" => %{
+        actor_name: Actor.display_name(actor),
+        event_title: event.title,
+        event_uuid: event.uuid
+      },
+      "group_id" => event.attributed_to_id,
+      "author_id" => actor.id,
+      "object_type" => "participant",
+      "object_id" => participant_id,
+      "inserted_at" => DateTime.utc_now()
+    })
+  end
+
+  @impl Activity
+  def insert_activity(_, _), do: {:ok, nil}
+
+  @impl Activity
+  def get_object(participant_id) do
+    Events.get_participant(participant_id)
+  end
+end
diff --git a/lib/service/activity/renderer/event.ex b/lib/service/activity/renderer/event.ex
index 809583bb0..3bf07232f 100644
--- a/lib/service/activity/renderer/event.ex
+++ b/lib/service/activity/renderer/event.ex
@@ -1,6 +1,6 @@
 defmodule Mobilizon.Service.Activity.Renderer.Event do
   @moduledoc """
-  Insert a comment activity
+  Insert an event activity
   """
   alias Mobilizon.Activities.Activity
   alias Mobilizon.Actors.Actor
@@ -67,6 +67,16 @@ defmodule Mobilizon.Service.Activity.Renderer.Event do
             url: event_url(activity)
           }
         end
+
+      :event_new_participation ->
+        %{
+          body:
+            dgettext("activity", "%{profile} joined your event %{event}.", %{
+              profile: profile(activity),
+              event: title(activity)
+            }),
+          url: event_url(activity)
+        }
     end
   end
 
diff --git a/lib/service/date_time/date_time.ex b/lib/service/date_time/date_time.ex
index bccf9e7d0..dbceb7cc4 100644
--- a/lib/service/date_time/date_time.ex
+++ b/lib/service/date_time/date_time.ex
@@ -3,6 +3,8 @@ defmodule Mobilizon.Service.DateTime do
   Module to represent a datetime in a given locale
   """
   alias Cldr.DateTime.Relative
+  alias Mobilizon.Cldr, as: MobilizonCldr
+  import Mobilizon.Cldr, only: [locale_or_default: 1]
 
   @typep to_string_format :: :short | :medium | :long | :full
 
@@ -10,25 +12,25 @@ defmodule Mobilizon.Service.DateTime do
 
   @spec datetime_to_string(DateTime.t(), String.t(), to_string_format()) :: String.t()
   def datetime_to_string(%DateTime{} = datetime, locale \\ "en", format \\ :medium) do
-    Mobilizon.Cldr.DateTime.to_string!(datetime,
+    MobilizonCldr.DateTime.to_string!(datetime,
       format: format,
-      locale: Mobilizon.Cldr.locale_or_default(locale)
+      locale: locale_or_default(locale)
     )
   end
 
   @spec datetime_to_time_string(DateTime.t(), String.t(), to_string_format()) :: String.t()
   def datetime_to_time_string(%DateTime{} = datetime, locale \\ "en", format \\ :short) do
-    Mobilizon.Cldr.Time.to_string!(datetime,
+    MobilizonCldr.Time.to_string!(datetime,
       format: format,
-      locale: Mobilizon.Cldr.locale_or_default(locale)
+      locale: locale_or_default(locale)
     )
   end
 
   @spec datetime_to_date_string(DateTime.t(), String.t(), to_string_format()) :: String.t()
   def datetime_to_date_string(%DateTime{} = datetime, locale \\ "en", format \\ :short) do
-    Mobilizon.Cldr.Date.to_string!(datetime,
+    MobilizonCldr.Date.to_string!(datetime,
       format: format,
-      locale: Mobilizon.Cldr.locale_or_default(locale)
+      locale: locale_or_default(locale)
     )
   end
 
@@ -47,9 +49,9 @@ defmodule Mobilizon.Service.DateTime do
 
   @spec datetime_relative(DateTime.t(), String.t()) :: String.t()
   def datetime_relative(%DateTime{} = datetime, locale \\ "en") do
-    Relative.to_string!(datetime, Mobilizon.Cldr,
+    Relative.to_string!(datetime, MobilizonCldr,
       relative_to: DateTime.utc_now(),
-      locale: Mobilizon.Cldr.locale_or_default(locale)
+      locale: locale_or_default(locale)
     )
   end
 
diff --git a/lib/service/export/participants/common.ex b/lib/service/export/participants/common.ex
index afc0b9d88..84c848e1e 100644
--- a/lib/service/export/participants/common.ex
+++ b/lib/service/export/participants/common.ex
@@ -9,6 +9,7 @@ defmodule Mobilizon.Service.Export.Participants.Common do
   alias Mobilizon.Events.Participant.Metadata
   alias Mobilizon.Storage.Repo
   import Mobilizon.Web.Gettext, only: [gettext: 1]
+  import Mobilizon.Service.DateTime, only: [datetime_to_string: 2]
 
   @spec save_upload(String.t(), String.t(), String.t(), String.t(), String.t()) ::
           {:ok, Export.t()} | {:error, atom() | Ecto.Changeset.t()}
@@ -58,7 +59,12 @@ defmodule Mobilizon.Service.Export.Participants.Common do
 
   @spec columns :: list(String.t())
   def columns do
-    [gettext("Participant name"), gettext("Participant status"), gettext("Participant message")]
+    [
+      gettext("Participant name"),
+      gettext("Participant status"),
+      gettext("Participant registration date"),
+      gettext("Participant message")
+    ]
   end
 
   # One hour
@@ -82,14 +88,26 @@ defmodule Mobilizon.Service.Export.Participants.Common do
 
   @spec to_list({Participant.t(), Actor.t()}) :: list(String.t())
   def to_list(
-        {%Participant{role: role, metadata: metadata},
+        {%Participant{role: role, metadata: metadata, inserted_at: inserted_at},
          %Actor{domain: nil, preferred_username: "anonymous"}}
       ) do
-    [gettext("Anonymous participant"), translate_role(role), convert_metadata(metadata)]
+    [
+      gettext("Anonymous participant"),
+      translate_role(role),
+      datetime_to_string(inserted_at, Gettext.get_locale()),
+      convert_metadata(metadata)
+    ]
   end
 
-  def to_list({%Participant{role: role, metadata: metadata}, %Actor{} = actor}) do
-    [Actor.display_name_and_username(actor), translate_role(role), convert_metadata(metadata)]
+  def to_list(
+        {%Participant{role: role, metadata: metadata, inserted_at: inserted_at}, %Actor{} = actor}
+      ) do
+    [
+      Actor.display_name_and_username(actor),
+      translate_role(role),
+      datetime_to_string(inserted_at, Gettext.get_locale()),
+      convert_metadata(metadata)
+    ]
   end
 
   @spec convert_metadata(Metadata.t() | nil) :: String.t()
diff --git a/lib/service/notifier/email.ex b/lib/service/notifier/email.ex
index acd213d2e..750c70706 100644
--- a/lib/service/notifier/email.ex
+++ b/lib/service/notifier/email.ex
@@ -96,7 +96,7 @@ defmodule Mobilizon.Service.Notifier.Email do
 
   defp can_send_activity?(activity, user, options) do
     Logger.warning(
-      "Can't check if user #{inspect(user)} can be sent an activity (#{inspect(activity)}) (#{inspect(options)})"
+      "Can't check if user #{inspect(user.email)} can be sent an activity (#{inspect(activity)}) (#{inspect(options)})"
     )
 
     false
diff --git a/lib/web/templates/email/activity/_event_activity_item.html.heex b/lib/web/templates/email/activity/_event_activity_item.html.heex
index 9af392741..0497d658d 100644
--- a/lib/web/templates/email/activity/_event_activity_item.html.heex
+++ b/lib/web/templates/email/activity/_event_activity_item.html.heex
@@ -51,4 +51,13 @@
       })
       |> raw %>
     <% end %>
+  <% :event_new_participation -> %>
+    <%= dgettext("activity", "%{profile} joined your event %{event}.", %{
+      profile: "<b>#{escaped_display_name_and_username(@activity.author)}</b>",
+      event:
+        "<a href=\"#{Routes.page_url(Mobilizon.Web.Endpoint,
+        :event,
+        @activity.subject_params["event_uuid"]) |> URI.decode()}\">#{escape_html(@activity.subject_params["event_title"])}</a>"
+    })
+    |> raw %>
 <% end %>
diff --git a/lib/web/templates/email/activity/_event_activity_item.text.eex b/lib/web/templates/email/activity/_event_activity_item.text.eex
index 390bd2611..30b05abc0 100644
--- a/lib/web/templates/email/activity/_event_activity_item.text.eex
+++ b/lib/web/templates/email/activity/_event_activity_item.text.eex
@@ -1,31 +1,37 @@
 <%= case @activity.subject do %><% :event_created -> %><%= dgettext("activity", "The event %{event} was created by %{profile}.", 
     %{
-        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        profile: display_name_and_username(@activity.author),
         event: @activity.subject_params["event_title"]
     }
 ) %>
 <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_updated -> %><%= dgettext("activity", "The event %{event} was updated by %{profile}.", 
     %{
-        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        profile: display_name_and_username(@activity.author),
         event: @activity.subject_params["event_title"]
     }
 ) %>
 <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_deleted -> %><%= dgettext("activity", "The event %{event} was deleted by %{profile}.", 
     %{
-        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        profile: display_name_and_username(@activity.author),
         event: @activity.subject_params["event_title"]
     }
 ) %>
 <% :comment_posted -> %><%= if @activity.subject_params["comment_reply_to"] do %><%= dgettext("activity", "%{profile} replied to a comment on the event %{event}.", 
     %{
-        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        profile: display_name_and_username(@activity.author),
         event: @activity.subject_params["event_title"]
     }
 ) %>
 <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} posted a comment on the event %{event}.", 
     %{
-        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        profile: display_name_and_username(@activity.author),
         event: @activity.subject_params["event_title"]
     }
 ) %>
-<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% end %>
\ No newline at end of file
+<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% :event_new_participation -> %><%= dgettext("activity", "%{profile} joined your event %{event}.",
+    %{
+        profile: display_name_and_username(@activity.author),
+        event: @activity.subject_params["event_title"]
+    }
+) %>
+<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %>
\ No newline at end of file
diff --git a/lib/web/views/email_view.ex b/lib/web/views/email_view.ex
index c103e4fcf..82b0e74d9 100644
--- a/lib/web/views/email_view.ex
+++ b/lib/web/views/email_view.ex
@@ -25,7 +25,6 @@ defmodule Mobilizon.Web.EmailView do
   defdelegate datetime_relative(datetime, locale \\ "en"), to: DateTimeRenderer
   defdelegate render_address(address), to: Address
   defdelegate is_same_day?(one, two), to: DateTimeRenderer
-  defdelegate display_name_and_username(actor), to: Actor
   defdelegate display_name(actor), to: Actor
   defdelegate preferred_username_and_domain(actor), to: Actor
 
@@ -38,7 +37,13 @@ defmodule Mobilizon.Web.EmailView do
 
   def escaped_display_name_and_username(actor) do
     actor
-    |> Actor.display_name_and_username()
+    |> display_name_and_username()
     |> escape_html()
   end
+
+  def display_name_and_username(%Actor{preferred_username: "anonymous"}) do
+    dgettext("activity", "An anonymous profile")
+  end
+
+  def display_name_and_username(actor), do: Actor.display_name_and_username(actor)
 end
diff --git a/test/graphql/resolvers/push_subscription_test.exs b/test/graphql/resolvers/push_subscription_test.exs
new file mode 100644
index 000000000..456aea443
--- /dev/null
+++ b/test/graphql/resolvers/push_subscription_test.exs
@@ -0,0 +1,149 @@
+defmodule Mobilizon.GraphQL.Resolvers.PushSubscriptionTest do
+  use Mobilizon.Web.ConnCase
+
+  import Mobilizon.Factory
+
+  alias Mobilizon.GraphQL.AbsintheHelpers
+
+  describe "create a new push subscription" do
+    @register_push_mutation """
+    mutation RegisterPush($endpoint: String!, $auth: String!, $p256dh: String!) {
+      registerPush(endpoint: $endpoint, auth: $auth, p256dh: $p256dh)
+    }
+    """
+
+    test "without auth", %{conn: conn} do
+      res =
+        AbsintheHelpers.graphql_query(conn,
+          query: @register_push_mutation,
+          variables: %{endpoint: "https://yolo.com/gfjgfd", auth: "gjrigf", p256dh: "gbgof"}
+        )
+
+      assert hd(res["errors"])["status_code"] == 401
+      assert hd(res["errors"])["message"] == "You need to be logged in"
+    end
+
+    test "succeeds", %{conn: conn} do
+      user = insert(:user)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @register_push_mutation,
+          variables: %{endpoint: "https://yolo.com/gfjgfd", auth: "gjrigf", p256dh: "gbgof"}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["registerPush"] == "OK"
+    end
+
+    test "fails on duplicate", %{conn: conn} do
+      user = insert(:user)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @register_push_mutation,
+          variables: %{
+            endpoint: "https://yolo.com/duplicate",
+            auth: "duplicate",
+            p256dh: "duplicate"
+          }
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["registerPush"] == "OK"
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @register_push_mutation,
+          variables: %{
+            endpoint: "https://yolo.com/duplicate",
+            auth: "duplicate",
+            p256dh: "duplicate"
+          }
+        )
+
+      assert hd(res["errors"])["message"] ==
+               "The same push subscription has already been registered"
+
+      refute res["data"]["registerPush"] == "OK"
+    end
+  end
+
+  describe "unregister a push subscription" do
+    @unregister_push_mutation """
+    mutation UnRegisterPush($endpoint: String!) {
+      unregisterPush(endpoint: $endpoint)
+    }
+    """
+
+    test "without auth", %{conn: conn} do
+      res =
+        AbsintheHelpers.graphql_query(conn,
+          query: @unregister_push_mutation,
+          variables: %{endpoint: "https://yolo.com/gfjgfd"}
+        )
+
+      assert hd(res["errors"])["status_code"] == 401
+      assert hd(res["errors"])["message"] == "You need to be logged in"
+    end
+
+    test "fails when not existing", %{conn: conn} do
+      user = insert(:user)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @unregister_push_mutation,
+          variables: %{
+            endpoint: "https://yolo.com/duplicate",
+            auth: "duplicate",
+            p256dh: "duplicate"
+          }
+        )
+
+      assert hd(res["errors"])["status_code"] == 404
+      assert hd(res["errors"])["message"] == "Resource not found"
+      refute res["data"]["registerPush"] == "OK"
+    end
+
+    test "fails when wrong user", %{conn: conn} do
+      user = insert(:user)
+      push_subscription = insert(:push_subscription)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @unregister_push_mutation,
+          variables: %{endpoint: push_subscription.endpoint}
+        )
+
+      assert hd(res["errors"])["status_code"] == 403
+      assert hd(res["errors"])["message"] == "You don't have permission to do this"
+      refute res["data"]["registerPush"] == "OK"
+    end
+
+    test "succeeds", %{conn: conn} do
+      user = insert(:user)
+      push_subscription = insert(:push_subscription, user: user)
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> AbsintheHelpers.graphql_query(
+          query: @unregister_push_mutation,
+          variables: %{endpoint: push_subscription.endpoint}
+        )
+
+      assert res["errors"] == nil
+      assert res["data"]["unregisterPush"] == "OK"
+    end
+  end
+end
diff --git a/test/service/export/participants/common_test.exs b/test/service/export/participants/common_test.exs
index 830772866..df0bb0581 100644
--- a/test/service/export/participants/common_test.exs
+++ b/test/service/export/participants/common_test.exs
@@ -5,24 +5,28 @@ defmodule Mobilizon.Service.Export.Participants.CommonTest do
 
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Service.Export.Participants.Common
+  import Mobilizon.Service.DateTime, only: [datetime_to_string: 1]
 
   test "convert participants to list items" do
     participant = insert(:participant)
     actor = insert(:actor)
     name = Actor.display_name_and_username(actor)
-    assert [^name, _, ""] = Common.to_list({participant, actor})
+    date = datetime_to_string(participant.inserted_at)
+    assert [^name, _, ^date, ""] = Common.to_list({participant, actor})
   end
 
   test "convert participants with metadata to list items" do
     participant = insert(:participant, metadata: %{message: "a message"})
     actor = insert(:actor)
     name = Actor.display_name_and_username(actor)
-    assert [^name, _, "a message"] = Common.to_list({participant, actor})
+    date = datetime_to_string(participant.inserted_at)
+    assert [^name, _, ^date, "a message"] = Common.to_list({participant, actor})
   end
 
   test "convert anonymous participants to list items" do
     participant = insert(:participant)
     actor = insert(:actor, domain: nil, preferred_username: "anonymous")
-    assert ["Anonymous participant", _, ""] = Common.to_list({participant, actor})
+    date = datetime_to_string(participant.inserted_at)
+    assert ["Anonymous participant", _, ^date, ""] = Common.to_list({participant, actor})
   end
 end