diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue
index 63cfd50aa..ea904c129 100644
--- a/js/src/components/Comment/Comment.vue
+++ b/js/src/components/Comment/Comment.vue
@@ -6,32 +6,26 @@
       :id="commentId"
     >
       <popover-actor-card
-        class="media-left"
         :actor="comment.actor"
         :inline="true"
         v-if="comment.actor"
       >
         <figure
-          class="image is-48x48"
+          class="image is-32x32 media-left"
           v-if="!comment.deletedAt && comment.actor.avatar"
         >
           <img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
         </figure>
-        <b-icon
-          class="media-left"
-          v-else
-          size="is-large"
-          icon="account-circle"
-        />
+        <b-icon class="media-left" v-else icon="account-circle" />
       </popover-actor-card>
       <div v-else class="media-left">
         <figure
-          class="image is-48x48"
+          class="image is-32x32"
           v-if="!comment.deletedAt && comment.actor.avatar"
         >
           <img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
         </figure>
-        <b-icon v-else size="is-large" icon="account-circle" />
+        <b-icon v-else icon="account-circle" />
       </div>
       <div class="media-content">
         <div class="content">
@@ -39,19 +33,21 @@
             <strong :class="{ organizer: commentFromOrganizer }">{{
               comment.actor.name
             }}</strong>
-            <small>@{{ usernameWithDomain(comment.actor) }}</small>
-            <a class="comment-link has-text-grey" :href="commentURL">
-              <small>{{
-                formatDistanceToNow(new Date(comment.updatedAt), {
-                  locale: $dateFnsLocale,
-                  addSuffix: true,
-                })
-              }}</small>
-            </a>
+            <small class="has-text-grey">{{
+              usernameWithDomain(comment.actor)
+            }}</small>
           </span>
           <a v-else class="comment-link has-text-grey" :href="commentURL">
             <span>{{ $t("[deleted]") }}</span>
           </a>
+          <a class="comment-link has-text-grey" :href="commentURL">
+            <small>{{
+              formatDistanceToNow(new Date(comment.updatedAt), {
+                locale: $dateFnsLocale,
+                addSuffix: true,
+              })
+            }}</small>
+          </a>
           <span class="icons" v-if="!comment.deletedAt">
             <button
               v-if="comment.actor.id === currentActor.id"
@@ -369,8 +365,17 @@ form.reply {
   }
 }
 
-.comment-link small:hover {
-  color: hsl(0, 0%, 21%);
+a.comment-link {
+  text-decoration: none;
+  margin-left: 5px;
+  &:hover {
+    text-decoration: underline;
+  }
+  small {
+    &:hover {
+      color: hsl(0, 0%, 21%);
+    }
+  }
 }
 
 .root-comment .replies {
diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue
index 01fca3375..547674867 100644
--- a/js/src/components/Comment/CommentTree.vue
+++ b/js/src/components/Comment/CommentTree.vue
@@ -17,26 +17,34 @@
         </figure>
         <div class="media-content">
           <div class="field">
-            <p class="control">
-              <editor
-                ref="commenteditor"
-                mode="comment"
-                v-model="newComment.text"
-              />
-            </p>
-            <p class="help is-danger" v-if="emptyCommentError">
-              {{ $t("Comment text can't be empty") }}
-            </p>
-          </div>
-          <div class="send-comment">
-            <b-button
-              native-type="submit"
-              type="is-primary"
-              class="comment-button-submit"
-              >{{ $t("Post a comment") }}</b-button
-            >
+            <div class="field">
+              <p class="control">
+                <editor
+                  ref="commenteditor"
+                  mode="comment"
+                  v-model="newComment.text"
+                />
+              </p>
+              <p class="help is-danger" v-if="emptyCommentError">
+                {{ $t("Comment text can't be empty") }}
+              </p>
+            </div>
+            <div class="field notify-participants" v-if="isEventOrganiser">
+              <b-switch v-model="newComment.isAnnouncement">{{
+                $t("Notify participants")
+              }}</b-switch>
+            </div>
           </div>
         </div>
+        <div class="send-comment">
+          <b-button
+            native-type="submit"
+            type="is-primary"
+            class="comment-button-submit"
+            icon-left="send"
+            :aria-label="$t('Post a comment')"
+          />
+        </div>
       </article>
     </form>
     <b-notification v-else-if="isConnected" :closable="false">{{
@@ -157,6 +165,7 @@ export default class CommentTree extends Vue {
           inReplyToCommentId: comment.inReplyToComment
             ? comment.inReplyToComment.id
             : null,
+          isAnnouncement: comment.isAnnouncement,
         },
         update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
           if (data == null) return;
@@ -359,6 +368,10 @@ form.new-comment {
       flex: 1;
       padding-right: 10px;
       margin-bottom: 0;
+
+      &.notify-participants {
+        margin-top: 0.5rem;
+      }
     }
   }
 }
diff --git a/js/src/graphql/comment.ts b/js/src/graphql/comment.ts
index 3da23909b..dd6b6df4c 100644
--- a/js/src/graphql/comment.ts
+++ b/js/src/graphql/comment.ts
@@ -24,6 +24,7 @@ export const COMMENT_FIELDS_FRAGMENT = gql`
     insertedAt
     updatedAt
     deletedAt
+    isAnnouncement
   }
 `;
 
@@ -92,11 +93,13 @@ export const CREATE_COMMENT_FROM_EVENT = gql`
     $eventId: ID!
     $text: String!
     $inReplyToCommentId: ID
+    $isAnnouncement: Boolean
   ) {
     createComment(
       eventId: $eventId
       text: $text
       inReplyToCommentId: $inReplyToCommentId
+      isAnnouncement: $isAnnouncement
     ) {
       ...CommentRecursive
     }
diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts
index 80a74fd58..0d35778fd 100644
--- a/js/src/graphql/user.ts
+++ b/js/src/graphql/user.ts
@@ -171,6 +171,38 @@ export const SET_USER_SETTINGS = gql`
   ${USER_SETTINGS_FRAGMENT}
 `;
 
+export const USER_NOTIFICATIONS = gql`
+  query UserNotifications {
+    loggedUser {
+      id
+      locale
+      settings {
+        ...UserSettingFragment
+      }
+      activitySettings {
+        key
+        method
+        enabled
+      }
+    }
+  }
+  ${USER_SETTINGS_FRAGMENT}
+`;
+
+export const UPDATE_ACTIVITY_SETTING = gql`
+  mutation UpdateActivitySetting(
+    $key: String!
+    $method: String!
+    $enabled: Boolean!
+  ) {
+    updateActivitySetting(key: $key, method: $method, enabled: $enabled) {
+      key
+      method
+      enabled
+    }
+  }
+`;
+
 export const LIST_USERS = gql`
   query ListUsers($email: String, $page: Int, $limit: Int) {
     users(email: $email, page: $page, limit: $limit) {
diff --git a/js/src/registerServiceWorker.ts b/js/src/registerServiceWorker.ts
index 0d1a5b0db..141c1daa4 100644
--- a/js/src/registerServiceWorker.ts
+++ b/js/src/registerServiceWorker.ts
@@ -34,6 +34,6 @@ if ("serviceWorker" in navigator && isProduction()) {
 }
 
 function isProduction(): boolean {
-  // return true;
-  return process.env.NODE_ENV === "production";
+  return true;
+  // return process.env.NODE_ENV === "production";
 }
diff --git a/js/src/types/comment.model.ts b/js/src/types/comment.model.ts
index 95de6f3af..5776bb9bc 100644
--- a/js/src/types/comment.model.ts
+++ b/js/src/types/comment.model.ts
@@ -19,6 +19,7 @@ export interface IComment {
   totalReplies: number;
   insertedAt?: Date | string;
   publishedAt?: Date | string;
+  isAnnouncement: boolean;
 }
 
 export class CommentModel implements IComment {
@@ -50,6 +51,8 @@ export class CommentModel implements IComment {
 
   totalReplies = 0;
 
+  isAnnouncement = false;
+
   constructor(hash?: IComment) {
     if (!hash) return;
 
@@ -66,5 +69,6 @@ export class CommentModel implements IComment {
     this.deletedAt = hash.deletedAt;
     this.insertedAt = new Date(hash.insertedAt as string);
     this.totalReplies = hash.totalReplies;
+    this.isAnnouncement = hash.isAnnouncement;
   }
 }
diff --git a/js/src/types/current-user.model.ts b/js/src/types/current-user.model.ts
index 352b2f2ae..d8f17b038 100644
--- a/js/src/types/current-user.model.ts
+++ b/js/src/types/current-user.model.ts
@@ -28,6 +28,12 @@ export interface IUserSettings {
   location?: IUserPreferredLocation;
 }
 
+export interface IActivitySetting {
+  key: string;
+  method: string;
+  enabled: boolean;
+}
+
 export interface IUser extends ICurrentUser {
   confirmedAt: Date;
   confirmationSendAt: Date;
@@ -37,6 +43,7 @@ export interface IUser extends ICurrentUser {
   mediaSize: number;
   drafts: IEvent[];
   settings: IUserSettings;
+  activitySettings: IActivitySetting[];
   locale: string;
   provider?: string;
   lastSignInAt: string;
diff --git a/js/src/views/Settings/Notifications.vue b/js/src/views/Settings/Notifications.vue
index 81e7a6ef2..99711c055 100644
--- a/js/src/views/Settings/Notifications.vue
+++ b/js/src/views/Settings/Notifications.vue
@@ -16,18 +16,63 @@
     </nav>
     <section>
       <div class="setting-title">
-        <h2>{{ $t("Participation notifications") }}</h2>
+        <h2>{{ $t("Browser notifications") }}</h2>
       </div>
       <b-button v-if="subscribed" @click="unsubscribeToWebPush()">{{
-        $t("Unsubscribe to WebPush")
+        $t("Unsubscribe to browser notifications")
       }}</b-button>
       <b-button
         icon-left="rss"
         @click="subscribeToWebPush"
         v-else-if="canShowWebPush()"
-        >{{ $t("WebPush") }}</b-button
+        >{{ $t("Activate browser notification") }}</b-button
       >
-      <span v-else>{{ $t("You can't use webpush in this browser.") }}</span>
+      <span v-else>{{
+        $t("You can't use notifications in this browser.")
+      }}</span>
+    </section>
+    <section>
+      <div class="setting-title">
+        <h2>{{ $t("Notification settings") }}</h2>
+      </div>
+      <p>
+        {{
+          $t(
+            "Select the activities for which you wish to receive an email or a push notification."
+          )
+        }}
+      </p>
+      <table class="table">
+        <tbody>
+          <template v-for="notificationType in notificationTypes">
+            <tr :key="`${notificationType.label}-title`">
+              <th colspan="3">
+                {{ notificationType.label }}
+              </th>
+            </tr>
+            <tr :key="`${notificationType.label}-subtitle`">
+              <th v-for="(method, key) in notificationMethods" :key="key">
+                {{ method }}
+              </th>
+              <th></th>
+            </tr>
+            <tr v-for="subType in notificationType.subtypes" :key="subType.id">
+              <td v-for="(method, key) in notificationMethods" :key="key">
+                <b-checkbox
+                  :value="notificationValues[subType.id][key]"
+                  @input="(e) => updateNotificationValue(subType.id, key, e)"
+                  :disabled="notificationValues[subType.id].disabled"
+                />
+              </td>
+              <td>
+                {{ subType.label }}
+              </td>
+            </tr>
+          </template>
+        </tbody>
+      </table>
+    </section>
+    <section>
       <div class="setting-title">
         <h2>{{ $t("Participation notifications") }}</h2>
       </div>
@@ -207,9 +252,10 @@
 import { Component, Vue, Watch } from "vue-property-decorator";
 import { INotificationPendingEnum } from "@/types/enums";
 import {
-  USER_SETTINGS,
   SET_USER_SETTINGS,
   FEED_TOKENS_LOGGED_USER,
+  USER_NOTIFICATIONS,
+  UPDATE_ACTIVITY_SETTING,
 } from "../../graphql/user";
 import { IUser } from "../../types/current-user.model";
 import RouteName from "../../router/name";
@@ -223,10 +269,14 @@ import {
   REGISTER_PUSH_MUTATION,
   UNREGISTER_PUSH_MUTATION,
 } from "@/graphql/webPush";
+import { merge } from "lodash";
+
+type NotificationSubType = { label: string; id: string };
+type NotificationType = { label: string; subtypes: NotificationSubType[] };
 
 @Component({
   apollo: {
-    loggedUser: USER_SETTINGS,
+    loggedUser: USER_NOTIFICATIONS,
     feedTokens: {
       query: FEED_TOKENS_LOGGED_USER,
       update: (data) =>
@@ -263,6 +313,201 @@ export default class Notifications extends Vue {
 
   subscribed = false;
 
+  notificationMethods = {
+    email: this.$t("Email") as string,
+    push: this.$t("Push") as string,
+  };
+
+  defaultNotificationValues = {
+    participation_event_updated: {
+      email: true,
+      push: true,
+      disabled: true,
+    },
+    participation_event_comment: {
+      email: true,
+      push: true,
+    },
+    event_new_pending_participation: {
+      email: true,
+      push: true,
+    },
+    event_new_participation: {
+      email: false,
+      push: false,
+    },
+    event_created: {
+      email: false,
+      push: false,
+    },
+    event_updated: {
+      email: false,
+      push: false,
+    },
+    discussion_updated: {
+      email: false,
+      push: false,
+    },
+    post_published: {
+      email: false,
+      push: false,
+    },
+    post_updated: {
+      email: false,
+      push: false,
+    },
+    resource_updated: {
+      email: false,
+      push: false,
+    },
+    member_request: {
+      email: true,
+      push: true,
+    },
+    member_updated: {
+      email: false,
+      push: false,
+    },
+    user_email_password_updated: {
+      email: true,
+      push: false,
+      disabled: true,
+    },
+    event_comment_mention: {
+      email: true,
+      push: true,
+    },
+    discussion_mention: {
+      email: true,
+      push: false,
+    },
+    event_new_comment: {
+      email: true,
+      push: false,
+    },
+  };
+
+  notificationTypes: NotificationType[] = [
+    {
+      label: this.$t("Mentions") as string,
+      subtypes: [
+        {
+          id: "event_comment_mention",
+          label: this.$t(
+            "I've been mentionned in a comment under an event"
+          ) as string,
+        },
+        {
+          id: "discussion_mention",
+          label: this.$t(
+            "I've been mentionned in a group discussion"
+          ) as string,
+        },
+      ],
+    },
+    {
+      label: this.$t("Participations") as string,
+      subtypes: [
+        {
+          id: "participation_event_updated",
+          label: this.$t("An event I'm going to has been updated") as string,
+        },
+        {
+          id: "participation_event_comment",
+          label: this.$t(
+            "An event I'm going to has posted an announcement"
+          ) as string,
+        },
+      ],
+    },
+    {
+      label: this.$t("Organizers") as string,
+      subtypes: [
+        {
+          id: "event_new_pending_participation",
+          label: this.$t(
+            "An event I'm organizing has a new pending participation"
+          ) as string,
+        },
+        {
+          id: "event_new_participation",
+          label: this.$t(
+            "An event I'm organizing has a new participation"
+          ) as string,
+        },
+        {
+          id: "event_new_comment",
+          label: this.$t("An event I'm organizing has a new comment") as string,
+        },
+      ],
+    },
+    {
+      label: this.$t("Group activity") as string,
+      subtypes: [
+        {
+          id: "event_created",
+          label: this.$t(
+            "An event from one of my groups has been published"
+          ) as string,
+        },
+        {
+          id: "event_updated",
+          label: this.$t(
+            "An event from one of my groups has been updated or deleted"
+          ) as string,
+        },
+        {
+          id: "discussion_updated",
+          label: this.$t("A discussion has been created or updated") as string,
+        },
+        {
+          id: "post_published",
+          label: this.$t("A post has been published") as string,
+        },
+        {
+          id: "post_updated",
+          label: this.$t("A post has been updated") as string,
+        },
+        {
+          id: "resource_updated",
+          label: this.$t("A resource has been created or updated") as string,
+        },
+        {
+          id: "member_request",
+          label: this.$t(
+            "A member requested to join one of my groups"
+          ) as string,
+        },
+        {
+          id: "member_updated",
+          label: this.$t("A member has been updated") as string,
+        },
+      ],
+    },
+    {
+      label: this.$t("User settings") as string,
+      subtypes: [
+        {
+          id: "user_email_password_updated",
+          label: this.$t("You changed your email or password") as string,
+        },
+      ],
+    },
+  ];
+
+  get userNotificationValues(): Record<string, Record<string, boolean>> {
+    return this.loggedUser.activitySettings.reduce((acc, activitySetting) => {
+      acc[activitySetting.key] = acc[activitySetting.key] || {};
+      acc[activitySetting.key][activitySetting.method] =
+        activitySetting.enabled;
+      return acc;
+    }, {} as Record<string, Record<string, boolean>>);
+  }
+
+  get notificationValues(): Record<string, Record<string, boolean>> {
+    return merge(this.defaultNotificationValues, this.userNotificationValues);
+  }
+
   mounted(): void {
     this.notificationPendingParticipationValues = {
       [INotificationPendingEnum.NONE]: this.$t("Do not receive any mail"),
@@ -290,7 +535,7 @@ export default class Notifications extends Vue {
     await this.$apollo.mutate<{ setUserSettings: string }>({
       mutation: SET_USER_SETTINGS,
       variables,
-      refetchQueries: [{ query: USER_SETTINGS }],
+      refetchQueries: [{ query: USER_NOTIFICATIONS }],
     });
   }
 
@@ -387,6 +632,22 @@ export default class Notifications extends Vue {
     this.subscribed = await this.isSubscribed();
   }
 
+  async updateNotificationValue(
+    key: string,
+    method: string,
+    enabled: boolean
+  ): Promise<void> {
+    await this.$apollo.mutate({
+      mutation: UPDATE_ACTIVITY_SETTING,
+      variables: {
+        key,
+        method,
+        enabled,
+        userId: this.loggedUser.id,
+      },
+    });
+  }
+
   private async isSubscribed(): Promise<boolean> {
     if (!("serviceWorker" in navigator)) return Promise.resolve(false);
     const registration = await navigator.serviceWorker.getRegistration();
diff --git a/lib/federation/activity_stream/converter/comment.ex b/lib/federation/activity_stream/converter/comment.ex
index 16156c1af..d4ec756d9 100644
--- a/lib/federation/activity_stream/converter/comment.ex
+++ b/lib/federation/activity_stream/converter/comment.ex
@@ -69,7 +69,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
         mentions: mentions,
         local: is_nil(actor_domain),
         visibility: if(Visibility.is_public?(object), do: :public, else: :private),
-        published_at: object["published"]
+        published_at: object["published"],
+        is_announcement: Map.get(object, "isAnnouncement", false)
       }
 
       Logger.debug("Converted object before fetching parents")
@@ -109,7 +110,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
       "uuid" => comment.uuid,
       "id" => comment.url,
       "tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags),
-      "published" => comment.published_at |> DateTime.to_iso8601()
+      "published" => comment.published_at |> DateTime.to_iso8601(),
+      "isAnnouncement" => comment.is_announcement
     }
 
     object =
diff --git a/lib/graphql/resolvers/users/activity_settings.ex b/lib/graphql/resolvers/users/activity_settings.ex
new file mode 100644
index 000000000..f3a76e2e3
--- /dev/null
+++ b/lib/graphql/resolvers/users/activity_settings.ex
@@ -0,0 +1,26 @@
+defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
+  @moduledoc """
+  Handles the user activity settings-related GraphQL calls.
+  """
+
+  alias Mobilizon.Users
+  alias Mobilizon.Users.User
+
+  require Logger
+
+  def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do
+    {:ok, Users.activity_settings_for_user(user)}
+  end
+
+  def user_activity_settings(_parent, _args, _context) do
+    {:error, :unauthenticated}
+  end
+
+  def upsert_user_activity_setting(_parent, args, %{context: %{current_user: %User{id: user_id}}}) do
+    Users.create_activity_setting(Map.put(args, :user_id, user_id))
+  end
+
+  def upsert_user_activity_setting(_parent, _args, _resolution) do
+    {:error, :unauthenticated}
+  end
+end
diff --git a/lib/graphql/schema.ex b/lib/graphql/schema.ex
index 0c620b6ae..74f3f9a38 100644
--- a/lib/graphql/schema.ex
+++ b/lib/graphql/schema.ex
@@ -48,6 +48,7 @@ defmodule Mobilizon.GraphQL.Schema do
   import_types(Schema.AdminType)
   import_types(Schema.StatisticsType)
   import_types(Schema.Users.PushSubscription)
+  import_types(Schema.Users.ActivitySetting)
 
   @desc "A struct containing the id of the deleted object"
   object :deleted_object do
@@ -182,6 +183,7 @@ defmodule Mobilizon.GraphQL.Schema do
     import_fields(:actor_mutations)
     import_fields(:follower_mutations)
     import_fields(:push_mutations)
+    import_fields(:activity_setting_mutations)
   end
 
   @desc """
diff --git a/lib/graphql/schema/discussions/comment.ex b/lib/graphql/schema/discussions/comment.ex
index 779b8e1d1..cef5f5556 100644
--- a/lib/graphql/schema/discussions/comment.ex
+++ b/lib/graphql/schema/discussions/comment.ex
@@ -50,6 +50,10 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
     field(:updated_at, :datetime, description: "When was the comment updated")
     field(:deleted_at, :datetime, description: "When was the comment deleted")
     field(:published_at, :datetime, description: "When was the comment published")
+
+    field(:is_announcement, non_null(:boolean),
+      description: "Whether this comment needs to be announced to participants"
+    )
   end
 
   @desc "The list of visibility options for a comment"
@@ -86,6 +90,8 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
       arg(:event_id, non_null(:id), description: "The event under which this comment is")
       arg(:in_reply_to_comment_id, :id, description: "The comment ID this one replies to")
 
+      arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
+
       resolve(&Comment.create_comment/3)
     end
 
@@ -94,6 +100,8 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
       arg(:text, non_null(:string), description: "The comment updated body")
       arg(:comment_id, non_null(:id), description: "The comment ID")
 
+      arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
+
       resolve(&Comment.update_comment/3)
     end
 
diff --git a/lib/graphql/schema/user.ex b/lib/graphql/schema/user.ex
index 8d713464d..8a5a97aff 100644
--- a/lib/graphql/schema/user.ex
+++ b/lib/graphql/schema/user.ex
@@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
 
   alias Mobilizon.Events
   alias Mobilizon.GraphQL.Resolvers.{Media, User}
+  alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
   alias Mobilizon.GraphQL.Schema
 
   import_types(Schema.SortType)
@@ -131,6 +132,11 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
       resolve: &Media.user_size/3,
       description: "The total size of all the media from this user (from all their actors)"
     )
+
+    field(:activity_settings, list_of(:activity_setting),
+      resolve: &ActivitySettings.user_activity_settings/3,
+      description: "The user's activity settings"
+    )
   end
 
   @desc "The list of roles an user can have"
diff --git a/lib/graphql/schema/users/activity_setting.ex b/lib/graphql/schema/users/activity_setting.ex
new file mode 100644
index 000000000..497b7b19b
--- /dev/null
+++ b/lib/graphql/schema/users/activity_setting.ex
@@ -0,0 +1,23 @@
+defmodule Mobilizon.GraphQL.Schema.Users.ActivitySetting do
+  @moduledoc """
+  Schema representation for PushSubscription
+  """
+  use Absinthe.Schema.Notation
+  alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
+
+  object :activity_setting do
+    field(:key, :string)
+    field(:method, :string)
+    field(:enabled, :boolean)
+    field(:user, :user)
+  end
+
+  object :activity_setting_mutations do
+    field :update_activity_setting, :activity_setting do
+      arg(:key, non_null(:string))
+      arg(:method, non_null(:string))
+      arg(:enabled, non_null(:boolean))
+      resolve(&ActivitySettings.upsert_user_activity_setting/3)
+    end
+  end
+end
diff --git a/lib/mobilizon/activities/activities.ex b/lib/mobilizon/activities/activities.ex
index e38fbc15b..71887f2e2 100644
--- a/lib/mobilizon/activities/activities.ex
+++ b/lib/mobilizon/activities/activities.ex
@@ -17,7 +17,7 @@ defmodule Mobilizon.Activities do
     very_high: 50
   )
 
-  @activity_types ["event", "post", "discussion", "resource", "group", "member"]
+  @activity_types ["event", "post", "discussion", "resource", "group", "member", "comment"]
   @event_activity_subjects ["event_created", "event_updated", "event_deleted", "comment_posted"]
   @post_activity_subjects ["post_created", "post_updated", "post_deleted"]
   @discussion_activity_subjects [
diff --git a/lib/mobilizon/discussions/comment.ex b/lib/mobilizon/discussions/comment.ex
index 391ffb4f5..bba59586c 100644
--- a/lib/mobilizon/discussions/comment.ex
+++ b/lib/mobilizon/discussions/comment.ex
@@ -45,6 +45,7 @@ defmodule Mobilizon.Discussions.Comment do
     :attributed_to_id,
     :deleted_at,
     :local,
+    :is_announcement,
     :discussion_id
   ]
   @attrs @required_attrs ++ @optional_attrs
@@ -58,6 +59,7 @@ defmodule Mobilizon.Discussions.Comment do
     field(:total_replies, :integer, virtual: true, default: 0)
     field(:deleted_at, :utc_datetime)
     field(:published_at, :utc_datetime)
+    field(:is_announcement, :boolean, default: false)
 
     belongs_to(:actor, Actor, foreign_key: :actor_id)
     belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
diff --git a/lib/mobilizon/users/activity_setting.ex b/lib/mobilizon/users/activity_setting.ex
new file mode 100644
index 000000000..12f956c50
--- /dev/null
+++ b/lib/mobilizon/users/activity_setting.ex
@@ -0,0 +1,34 @@
+defmodule Mobilizon.Users.ActivitySetting do
+  @moduledoc """
+  Module to manage users settings
+  """
+
+  use Ecto.Schema
+  import Ecto.Changeset
+  alias Mobilizon.Users.User
+
+  @type t :: %__MODULE__{
+          key: String.t(),
+          method: String.t(),
+          enabled: boolean()
+        }
+
+  @attrs [:key, :method, :enabled, :user_id]
+
+  @primary_key {:user_id, :id, autogenerate: false}
+  schema "user_activity_settings" do
+    field(:key, :string)
+    field(:method, :string)
+    field(:enabled, :boolean)
+
+    belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false)
+  end
+
+  @doc false
+  def changeset(activity_setting, attrs) do
+    activity_setting
+    |> cast(attrs, @attrs)
+    |> validate_required(@attrs)
+    |> unique_constraint([:key, :method], name: :user_activity_settings_user_id_key_method_index)
+  end
+end
diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex
index b20166aef..d747c06d8 100644
--- a/lib/mobilizon/users/users.ex
+++ b/lib/mobilizon/users/users.ex
@@ -13,7 +13,7 @@ defmodule Mobilizon.Users do
   alias Mobilizon.{Crypto, Events}
   alias Mobilizon.Events.FeedToken
   alias Mobilizon.Storage.{Page, Repo}
-  alias Mobilizon.Users.{PushSubscription, Setting, User}
+  alias Mobilizon.Users.{ActivitySetting, PushSubscription, Setting, User}
 
   defenum(UserRole, :user_role, [:administrator, :moderator, :user])
 
@@ -478,6 +478,48 @@ defmodule Mobilizon.Users do
     Repo.delete(push_subscription)
   end
 
+  @doc """
+  Lists the activity settings for an user
+
+  ## Examples
+
+      iex> activity_settings_for_user(user)
+      [%ActivitySetting{}]
+
+      iex> activity_settings_for_user(user)
+      []
+
+  """
+  def activity_settings_for_user(%User{id: user_id}) do
+    ActivitySetting
+    |> where([a], a.user_id == ^user_id)
+    |> Repo.all()
+  end
+
+  def activity_setting(%User{id: user_id}, key, method) do
+    ActivitySetting
+    |> where([a], a.user_id == ^user_id and a.key == ^key and a.method == ^method)
+    |> Repo.one()
+  end
+
+  @doc """
+  Creates an activity setting. Overrides existing values if present
+
+  ## Examples
+
+      iex> create_activity_setting(%{field: value})
+      {:ok, %ActivitySetting{}}
+
+      iex> create_activity_setting(%{field: bad_value})
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def create_activity_setting(attrs \\ %{}) do
+    %ActivitySetting{}
+    |> ActivitySetting.changeset(attrs)
+    |> Repo.insert(on_conflict: :replace_all, conflict_target: [:user_id, :key, :method])
+  end
+
   @spec user_by_email_query(String.t(), boolean | nil, boolean()) :: Ecto.Query.t()
   defp user_by_email_query(email, activated, unconfirmed) do
     User
diff --git a/lib/service/activity/comment.ex b/lib/service/activity/comment.ex
index 85c7a596c..af0cd7a13 100644
--- a/lib/service/activity/comment.ex
+++ b/lib/service/activity/comment.ex
@@ -2,12 +2,12 @@ defmodule Mobilizon.Service.Activity.Comment do
   @moduledoc """
   Insert a comment activity
   """
-  alias Mobilizon.{Actors, Discussions, Events}
+  alias Mobilizon.{Discussions, Events}
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Discussions.Comment
   alias Mobilizon.Events.Event
   alias Mobilizon.Service.Activity
-  alias Mobilizon.Service.Workers.ActivityBuilder
+  alias Mobilizon.Service.Workers.{ActivityBuilder, LegacyNotifierBuilder}
 
   @behaviour Activity
 
@@ -17,33 +17,21 @@ defmodule Mobilizon.Service.Activity.Comment do
   def insert_activity(
         %Comment{
           actor_id: actor_id,
-          event_id: event_id,
-          in_reply_to_comment_id: in_reply_to_comment_id
+          event_id: event_id
         } = comment,
         options
       )
       when not is_nil(actor_id) and not is_nil(event_id) do
-    with {:ok, %Event{attributed_to: %Actor{type: :Group} = group} = event} <-
-           Events.get_event_with_preload(event_id),
-         %Actor{id: actor_id} <- Actors.get_actor(actor_id),
-         subject <- Keyword.fetch!(options, :subject) do
-      ActivityBuilder.enqueue(:build_activity, %{
-        "type" => "event",
-        "subject" => subject,
-        "subject_params" => %{
-          event_title: event.title,
-          event_uuid: event.uuid,
-          comment_reply_to: !is_nil(in_reply_to_comment_id)
-        },
-        "group_id" => group.id,
-        "author_id" => actor_id,
-        "object_type" => "comment",
-        "object_id" => to_string(comment.id),
-        "inserted_at" => DateTime.utc_now()
-      })
-    else
-      # Event not from group
-      {:ok, %Event{}} -> {:ok, nil}
+    with {:ok, %Event{} = event} <-
+           Events.get_event_with_preload(event_id) do
+      # Notify the actors mentionned
+      notify_mentionned(comment, event)
+
+      # Notify participants if there's a new announcement
+      notify_announcement(comment, event)
+
+      # Notify event organizer or group that there's new comments
+      notify_organizer(comment, event, options)
     end
   end
 
@@ -53,4 +41,116 @@ defmodule Mobilizon.Service.Activity.Comment do
   def get_object(comment_id) do
     Discussions.get_comment(comment_id)
   end
+
+  defp notify_mentionned(%Comment{actor_id: actor_id, id: comment_id, mentions: mentions}, %Event{
+         uuid: uuid,
+         title: title
+       })
+       when length(mentions) > 0 do
+    LegacyNotifierBuilder.enqueue(:legacy_notify, %{
+      "type" => :comment,
+      "subject" => :event_comment_mention,
+      "subject_params" => %{
+        event_uuid: uuid,
+        event_title: title
+      },
+      "author_id" => actor_id,
+      "object_type" => :comment,
+      "object_id" => to_string(comment_id),
+      "inserted_at" => DateTime.utc_now(),
+      "mentions" => Enum.map(mentions, & &1.actor_id)
+    })
+  end
+
+  defp notify_mentionned(_, _), do: {:ok, :skipped}
+
+  defp notify_announcement(
+         %Comment{actor_id: actor_id, is_announcement: true, id: comment_id},
+         %Event{
+           id: event_id,
+           uuid: uuid,
+           title: title
+         }
+       ) do
+    LegacyNotifierBuilder.enqueue(:legacy_notify, %{
+      "type" => :comment,
+      "subject" => :participation_event_comment,
+      "subject_params" => %{
+        event_id: event_id,
+        event_uuid: uuid,
+        event_title: title
+      },
+      "author_id" => actor_id,
+      "object_type" => :comment,
+      "object_id" => to_string(comment_id),
+      "inserted_at" => DateTime.utc_now()
+    })
+  end
+
+  defp notify_announcement(_, _), do: {:ok, :skipped}
+
+  @spec notify_organizer(Comment.t(), Event.t(), Keyword.t()) ::
+          {:ok, Oban.Job.t()} | {:ok, :skipped}
+  defp notify_organizer(
+         %Comment{
+           actor_id: actor_id,
+           is_announcement: true,
+           in_reply_to_comment_id: in_reply_to_comment_id,
+           id: comment_id
+         },
+         %Event{
+           uuid: uuid,
+           title: title,
+           attributed_to: %Actor{type: :Group, id: group_id}
+         },
+         options
+       ) do
+    ActivityBuilder.enqueue(:build_activity, %{
+      "type" => "event",
+      "subject" => Keyword.fetch!(options, :subject),
+      "subject_params" => %{
+        event_title: title,
+        event_uuid: uuid,
+        comment_reply_to: !is_nil(in_reply_to_comment_id)
+      },
+      "group_id" => group_id,
+      "author_id" => actor_id,
+      "object_type" => "comment",
+      "object_id" => to_string(comment_id),
+      "inserted_at" => DateTime.utc_now()
+    })
+  end
+
+  defp notify_organizer(
+         %Comment{
+           actor_id: actor_id,
+           is_announcement: true,
+           in_reply_to_comment_id: in_reply_to_comment_id,
+           id: comment_id
+         },
+         %Event{
+           uuid: uuid,
+           title: title,
+           attributed_to: nil,
+           organizer_actor_id: organizer_actor_id
+         },
+         _options
+       )
+       when actor_id !== organizer_actor_id do
+    LegacyNotifierBuilder.enqueue(:legacy_notify, %{
+      "type" => :comment,
+      "subject" => :event_new_comment,
+      "subject_params" => %{
+        event_title: title,
+        event_uuid: uuid,
+        comment_reply_to: !is_nil(in_reply_to_comment_id)
+      },
+      "author_id" => actor_id,
+      "object_type" => :comment,
+      "object_id" => to_string(comment_id),
+      "inserted_at" => DateTime.utc_now()
+    })
+  end
+
+  defp notify_organizer(_, _, _), do: {:ok, :skipped}
 end
diff --git a/lib/service/activity/renderer/comment.ex b/lib/service/activity/renderer/comment.ex
new file mode 100644
index 000000000..31febd367
--- /dev/null
+++ b/lib/service/activity/renderer/comment.ex
@@ -0,0 +1,111 @@
+defmodule Mobilizon.Service.Activity.Renderer.Comment do
+  @moduledoc """
+  Insert a comment activity
+  """
+  alias Mobilizon.Activities.Activity
+  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Service.Activity.Renderer
+  alias Mobilizon.Web.{Endpoint, Gettext}
+  alias Mobilizon.Web.Router.Helpers, as: Routes
+  import Mobilizon.Web.Gettext, only: [dgettext: 3]
+
+  @behaviour Renderer
+
+  @impl Renderer
+  def render(%Activity{} = activity, options) do
+    locale = Keyword.get(options, :locale, "en")
+    Gettext.put_locale(locale)
+    profile = profile(activity)
+
+    case activity.subject do
+      :event_comment_mention ->
+        %{
+          body:
+            dgettext(
+              "activity",
+              "%{profile} mentionned you in a comment under event %{event}.",
+              %{
+                profile: profile,
+                event: event_title(activity)
+              }
+            ),
+          url: event_url(activity)
+        }
+
+      :participation_event_comment ->
+        %{
+          body:
+            dgettext(
+              "activity",
+              "%{profile} has posted an announcement under event %{event}.",
+              %{
+                profile: profile,
+                event: event_title(activity)
+              }
+            ),
+          url: event_url(activity)
+        }
+
+      :discussion_mention ->
+        %{
+          body:
+            dgettext("activity", "%{profile} mentionned you in the discussion %{discussion}.", %{
+              profile: profile,
+              discussion: title(activity)
+            }),
+          url: discussion_url(activity)
+        }
+
+      :discussion_renamed ->
+        %{
+          body:
+            dgettext("activity", "%{profile} renamed the discussion %{discussion}.", %{
+              profile: profile,
+              discussion: title(activity)
+            }),
+          url: discussion_url(activity)
+        }
+
+      :discussion_archived ->
+        %{
+          body:
+            dgettext("activity", "%{profile} archived the discussion %{discussion}.", %{
+              profile: profile,
+              discussion: title(activity)
+            }),
+          url: discussion_url(activity)
+        }
+
+      :discussion_deleted ->
+        %{
+          body:
+            dgettext("activity", "%{profile} deleted the discussion %{discussion}.", %{
+              profile: profile,
+              discussion: title(activity)
+            }),
+          url: nil
+        }
+    end
+  end
+
+  defp discussion_url(activity) do
+    Routes.page_url(
+      Endpoint,
+      :discussion,
+      Actor.preferred_username_and_domain(activity.group),
+      activity.subject_params["discussion_slug"]
+    )
+  end
+
+  defp event_url(activity) do
+    Routes.page_url(
+      Endpoint,
+      :event,
+      activity.subject_params["event_uuid"]
+    )
+  end
+
+  defp profile(activity), do: Actor.display_name_and_username(activity.author)
+  defp event_title(activity), do: activity.subject_params["event_title"]
+  defp title(activity), do: activity.subject_params["discussion_title"]
+end
diff --git a/lib/service/activity/renderer/renderer.ex b/lib/service/activity/renderer/renderer.ex
index 5a56edb52..475db67b5 100644
--- a/lib/service/activity/renderer/renderer.ex
+++ b/lib/service/activity/renderer/renderer.ex
@@ -5,7 +5,17 @@ defmodule Mobilizon.Service.Activity.Renderer do
 
   alias Mobilizon.Config
   alias Mobilizon.Activities.Activity
-  alias Mobilizon.Service.Activity.Renderer.{Discussion, Event, Group, Member, Post, Resource}
+
+  alias Mobilizon.Service.Activity.Renderer.{
+    Comment,
+    Discussion,
+    Event,
+    Group,
+    Member,
+    Post,
+    Resource
+  }
+
   require Logger
   import Mobilizon.Web.Gettext, only: [dgettext: 3]
 
@@ -41,6 +51,7 @@ defmodule Mobilizon.Service.Activity.Renderer do
       :member -> Member.render(activity, options)
       :post -> Post.render(activity, options)
       :resource -> Resource.render(activity, options)
+      :comment -> Comment.render(activity, options)
       _ -> nil
     end
   end
diff --git a/lib/service/notifier/email.ex b/lib/service/notifier/email.ex
index 2db6aa0b9..f182e02e8 100644
--- a/lib/service/notifier/email.ex
+++ b/lib/service/notifier/email.ex
@@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Notifier.Email do
   alias Mobilizon.Activities.Activity
   alias Mobilizon.{Config, Users}
   alias Mobilizon.Service.Notifier
-  alias Mobilizon.Service.Notifier.Email
+  alias Mobilizon.Service.Notifier.{Email, Filter}
   alias Mobilizon.Users.{NotificationPendingNotificationDelay, Setting, User}
   alias Mobilizon.Web.Email.Activity, as: EmailActivity
   alias Mobilizon.Web.Email.Mailer
@@ -17,6 +17,8 @@ defmodule Mobilizon.Service.Notifier.Email do
     Config.get(__MODULE__, :enabled)
   end
 
+  def send(user, activity, options \\ [])
+
   @impl Notifier
   def send(%User{} = user, %Activity{} = activity, options) do
     Email.send(user, [activity], options)
@@ -25,7 +27,9 @@ defmodule Mobilizon.Service.Notifier.Email do
   @impl Notifier
   def send(%User{email: email, locale: locale} = user, activities, options)
       when is_list(activities) do
-    if can_send?(user) do
+    activities = Enum.filter(activities, &can_send_activity?(&1, user))
+
+    if can_send?(user) && length(activities) > 0 do
       email
       |> EmailActivity.direct_activity(activities, Keyword.put(options, :locale, locale))
       |> Mailer.send_email()
@@ -37,6 +41,34 @@ defmodule Mobilizon.Service.Notifier.Email do
     end
   end
 
+  @spec can_send_activity?(Activity.t(), User.t()) :: boolean()
+  defp can_send_activity?(%Activity{} = activity, %User{} = user) do
+    Filter.can_send_activity?(activity, "email", user, &default_activity_behavior/1)
+  end
+
+  @spec default_activity_behavior(String.t()) :: boolean()
+  defp default_activity_behavior(activity_setting) do
+    case activity_setting do
+      "participation_event_updated" -> true
+      "participation_event_comment" -> true
+      "event_new_pending_participation" -> true
+      "event_new_participation" -> false
+      "event_created" -> false
+      "event_updated" -> false
+      "discussion_updated" -> false
+      "post_published" -> false
+      "post_updated" -> false
+      "resource_updated" -> false
+      "member_request" -> true
+      "member_updated" -> false
+      "user_email_password_updated" -> true
+      "event_comment_mention" -> true
+      "discussion_mention" -> true
+      "event_new_comment" -> true
+      _ -> false
+    end
+  end
+
   @type notification_type ::
           :group_notifications
           | :notification_pending_participation
diff --git a/lib/service/notifier/filter.ex b/lib/service/notifier/filter.ex
new file mode 100644
index 000000000..c763c4d6f
--- /dev/null
+++ b/lib/service/notifier/filter.ex
@@ -0,0 +1,60 @@
+defmodule Mobilizon.Service.Notifier.Filter do
+  alias Mobilizon.Users
+  alias Mobilizon.Activities.Activity
+  alias Mobilizon.Users.{ActivitySetting, User}
+
+  @type method :: String.t()
+
+  @spec can_send_activity?(Activity.t(), method(), User.t(), function()) :: boolean()
+  def can_send_activity?(%Activity{} = activity, method, %User{} = user, get_default) do
+    case map_activity_to_activity_setting(activity) do
+      false -> false
+      key -> user |> Users.activity_setting(key, method) |> enabled?(key, get_default)
+    end
+  end
+
+  @spec enabled?(ActivitySetting.t() | nil, String.t(), function()) :: boolean()
+  defp enabled?(nil, activity_setting, get_default), do: get_default.(activity_setting)
+  defp enabled?(%ActivitySetting{enabled: enabled}, _activity_setting, _get_default), do: enabled
+
+  # Comment mention
+  defp map_activity_to_activity_setting(%Activity{subject: :event_comment_mention}),
+    do: "event_comment_mention"
+
+  # Participation
+  @spec map_activity_to_activity_setting(Activity.t()) :: String.t() | false
+  defp map_activity_to_activity_setting(%Activity{subject: :participation_event_updated}),
+    do: "participation_event_updated"
+
+  defp map_activity_to_activity_setting(%Activity{subject: :participation_event_comment}),
+    do: "participation_event_comment"
+
+  # Organizers
+  defp map_activity_to_activity_setting(%Activity{subject: :event_new_pending_participation}),
+    do: "event_new_pending_participation"
+
+  defp map_activity_to_activity_setting(%Activity{subject: :event_new_participation}),
+    do: "event_new_participation"
+
+  # Event
+  defp map_activity_to_activity_setting(%Activity{subject: :event_created}), do: "event_created"
+  defp map_activity_to_activity_setting(%Activity{type: :event}), do: "event_updated"
+
+  # Post
+  defp map_activity_to_activity_setting(%Activity{subject: :post_created}), do: "post_published"
+  defp map_activity_to_activity_setting(%Activity{type: :post}), do: "post_updated"
+
+  # Discussion
+  defp map_activity_to_activity_setting(%Activity{type: :discussion}), do: "discussion_updated"
+
+  # Resource
+  defp map_activity_to_activity_setting(%Activity{type: :resource}), do: "resource_updated"
+
+  # Member
+  defp map_activity_to_activity_setting(%Activity{subject: :member_request}),
+    do: "member_request"
+
+  defp map_activity_to_activity_setting(%Activity{type: :member}), do: "member"
+
+  defp map_activity_to_activity_setting(_), do: false
+end
diff --git a/lib/service/notifier/push.ex b/lib/service/notifier/push.ex
index 6043e0ebc..e604fa01f 100644
--- a/lib/service/notifier/push.ex
+++ b/lib/service/notifier/push.ex
@@ -6,7 +6,7 @@ defmodule Mobilizon.Service.Notifier.Push do
   alias Mobilizon.{Config, Users}
   alias Mobilizon.Service.Activity.{Renderer, Utils}
   alias Mobilizon.Service.Notifier
-  alias Mobilizon.Service.Notifier.Push
+  alias Mobilizon.Service.Notifier.{Filter, Push}
   alias Mobilizon.Storage.Page
   alias Mobilizon.Users.{PushSubscription, User}
 
@@ -20,11 +20,16 @@ defmodule Mobilizon.Service.Notifier.Push do
   @impl Notifier
   def send(user, activity, options \\ [])
 
-  def send(%User{id: user_id, locale: locale} = _user, %Activity{} = activity, options) do
-    options = Keyword.put_new(options, :locale, locale)
+  def send(%User{id: user_id, locale: locale} = user, %Activity{} = activity, options) do
+    if can_send_activity?(activity, user) do
+      options = Keyword.put_new(options, :locale, locale)
 
-    %Page{elements: subscriptions} = Users.list_user_push_subscriptions(user_id, 1, 100)
-    Enum.map(subscriptions, &send_subscription(activity, convert_subscription(&1), options))
+      %Page{elements: subscriptions} = Users.list_user_push_subscriptions(user_id, 1, 100)
+      Enum.each(subscriptions, &send_subscription(activity, convert_subscription(&1), options))
+      {:ok, :sent}
+    else
+      {:ok, :skipped}
+    end
   end
 
   @impl Notifier
@@ -32,6 +37,34 @@ defmodule Mobilizon.Service.Notifier.Push do
     Enum.map(activities, &Push.send(user, &1, options))
   end
 
+  @spec can_send_activity?(Activity.t(), User.t()) :: boolean()
+  defp can_send_activity?(%Activity{} = activity, %User{} = user) do
+    Filter.can_send_activity?(activity, "push", user, &default_activity_behavior/1)
+  end
+
+  @spec default_activity_behavior(String.t()) :: boolean()
+  defp default_activity_behavior(activity_setting) do
+    case activity_setting do
+      "participation_event_updated" -> true
+      "participation_event_comment" -> true
+      "event_new_pending_participation" -> true
+      "event_new_participation" -> false
+      "event_created" -> false
+      "event_updated" -> false
+      "discussion_updated" -> false
+      "post_published" -> false
+      "post_updated" -> false
+      "resource_updated" -> false
+      "member_request" -> true
+      "member_updated" -> false
+      "user_email_password_updated" -> false
+      "event_comment_mention" -> true
+      "discussion_mention" -> false
+      "event_new_comment" -> false
+      _ -> false
+    end
+  end
+
   defp send_subscription(activity, subscription, options) do
     activity
     |> payload(options)
diff --git a/lib/service/workers/legacy_notifier_builder.ex b/lib/service/workers/legacy_notifier_builder.ex
new file mode 100644
index 000000000..af4305e8f
--- /dev/null
+++ b/lib/service/workers/legacy_notifier_builder.ex
@@ -0,0 +1,71 @@
+defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
+  @moduledoc """
+  Worker to push legacy notifications
+  """
+
+  alias Mobilizon.{Actors, Events, Users}
+  alias Mobilizon.Activities.Activity
+  alias Mobilizon.Service.Notifier
+
+  use Mobilizon.Service.Workers.Helper, queue: "activity"
+
+  @impl Oban.Worker
+  def perform(%Job{args: args}) do
+    with {"legacy_notify", args} <- Map.pop(args, "op") do
+      activity = build_activity(args)
+
+      args
+      |> users_to_notify(args["author_id"])
+      |> Enum.each(&Notifier.notify(&1, activity, single_activity: true))
+    end
+  end
+
+  def build_activity(args) do
+    author = Actors.get_actor(args["author_id"])
+
+    %Activity{
+      type: String.to_existing_atom(args["type"]),
+      subject: String.to_existing_atom(args["subject"]),
+      subject_params: args["subject_params"],
+      inserted_at: DateTime.utc_now(),
+      object_type: String.to_existing_atom(args["object_type"]),
+      object_id: args["object_id"],
+      group: nil,
+      author: author
+    }
+  end
+
+  @spec users_to_notify(map(), integer() | String.t()) :: list(Users.t())
+  defp users_to_notify(
+         %{"subject" => "event_comment_mention", "mentions" => mentionned_actor_ids},
+         author_id
+       ) do
+    users_from_actor_ids(mentionned_actor_ids, author_id)
+  end
+
+  defp users_to_notify(
+         %{
+           "subject" => "participation_event_comment",
+           "subject_params" => subject_params
+         },
+         author_id
+       ) do
+    subject_params
+    |> Map.get("event_id")
+    |> Events.list_actors_participants_for_event()
+    |> Enum.map(& &1.id)
+    |> users_from_actor_ids(author_id)
+  end
+
+  @spec users_from_actor_ids(list(), integer() | String.t()) :: list(Users.t())
+  defp users_from_actor_ids(actor_ids, author_id) do
+    actor_ids
+    |> Enum.filter(&(&1 != author_id))
+    |> Enum.map(&Actors.get_actor/1)
+    |> Enum.filter(& &1)
+    |> Enum.map(& &1.user_id)
+    |> Enum.filter(& &1)
+    |> Enum.uniq()
+    |> Enum.map(&Users.get_user_with_settings!/1)
+  end
+end
diff --git a/lib/web/email/activity.ex b/lib/web/email/activity.ex
index fecdc0d57..d520d75bd 100644
--- a/lib/web/email/activity.ex
+++ b/lib/web/email/activity.ex
@@ -43,8 +43,15 @@ defmodule Mobilizon.Web.Email.Activity do
   @spec chunk_activities(list()) :: map()
   defp chunk_activities(activities) do
     activities
-    |> Enum.reduce(%{}, fn %Activity{group: %Actor{id: group_id}} = activity, acc ->
-      Map.update(acc, group_id, [activity], fn activities -> activities ++ [activity] end)
+    |> Enum.reduce(%{}, fn activity, acc ->
+      case activity do
+        %Activity{group: %Actor{id: group_id}} ->
+          Map.update(acc, group_id, [activity], fn activities -> activities ++ [activity] end)
+
+        # Not a group activity
+        %Activity{} ->
+          Map.update(acc, nil, [activity], fn activities -> activities ++ [activity] end)
+      end
     end)
     |> Enum.map(fn {key, value} ->
       {key, Enum.sort(value, &(&1.inserted_at <= &2.inserted_at))}
@@ -57,20 +64,34 @@ defmodule Mobilizon.Web.Email.Activity do
   # so it will probably not catch much things
   @spec filter_duplicates(list()) :: list()
   defp filter_duplicates(activities) do
-    Enum.uniq_by(activities, fn %Activity{
-                                  author: %Actor{id: author_id},
-                                  group: %Actor{id: group_id},
-                                  type: type,
-                                  subject: subject,
-                                  subject_params: subject_params
-                                } ->
-      %{
-        author_id: author_id,
-        group_id: group_id,
-        type: type,
-        subject: subject,
-        subject_params: subject_params
-      }
+    Enum.uniq_by(activities, fn activity ->
+      case activity do
+        %Activity{
+          author: %Actor{id: author_id},
+          group: %Actor{id: group_id},
+          type: type,
+          subject: subject,
+          subject_params: subject_params
+        } ->
+          %{
+            author_id: author_id,
+            group_id: group_id,
+            type: type,
+            subject: subject,
+            subject_params: subject_params
+          }
+
+        %Activity{
+          type: type,
+          subject: subject,
+          subject_params: subject_params
+        } ->
+          %{
+            type: type,
+            subject: subject,
+            subject_params: subject_params
+          }
+      end
     end)
   end
 end
diff --git a/lib/web/templates/email/activity/_comment_activity_item.html.eex b/lib/web/templates/email/activity/_comment_activity_item.html.eex
index 9b779ea4c..135a194e1 100644
--- a/lib/web/templates/email/activity/_comment_activity_item.html.eex
+++ b/lib/web/templates/email/activity/_comment_activity_item.html.eex
@@ -67,4 +67,35 @@
                     discussion: "<b>#{@activity.subject_params["discussion_title"]}</b>"
                 }
             ) |> raw %>
+
+    <% :event_comment_mention -> %>
+    <%= 
+            dgettext("activity", "%{profile} mentionned you in a comment under event %{event}.", 
+                %{
+                    profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
+                    event: "<a href=\"#{
+                        page_url(
+                            Mobilizon.Web.Endpoint,
+                            :event,
+                            @activity.subject_params["event_uuid"]
+                        ) |> URI.decode()}\">
+                            #{@activity.subject_params["event_title"]}
+                        </a>"
+                }
+            ) |> raw %>
+    <% :participation_event_comment -> %>
+    <%= 
+            dgettext("activity", "%{profile} has posted an announcement under event %{event}.", 
+                %{
+                    profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
+                    event: "<a href=\"#{
+                        page_url(
+                            Mobilizon.Web.Endpoint,
+                            :event,
+                            @activity.subject_params["event_uuid"]
+                        ) |> URI.decode()}\">
+                            #{@activity.subject_params["event_title"]}
+                        </a>"
+                }
+            ) |> raw %>
 <% end %>
\ No newline at end of file
diff --git a/lib/web/templates/email/activity/_comment_activity_item.text.eex b/lib/web/templates/email/activity/_comment_activity_item.text.eex
index 463fbde46..a3fdd1a05 100644
--- a/lib/web/templates/email/activity/_comment_activity_item.text.eex
+++ b/lib/web/templates/email/activity/_comment_activity_item.text.eex
@@ -27,4 +27,17 @@
         profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
         discussion: @activity.subject_params["discussion_title"]
     }
-) %><% end %>
\ No newline at end of file
+) %>
+<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :event_comment_mention -> %><%= dgettext("activity", "%{profile} mentionned you in a comment under %{event}.", 
+    %{
+        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        event: @activity.subject_params["event_title"]
+    }
+) %>
+<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :participation_event_comment -> %><%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.", 
+    %{
+        profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
+        event: @activity.subject_params["event_title"]
+    }
+) %>
+<%= 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/templates/email/email_direct_activity.html.eex b/lib/web/templates/email/email_direct_activity.html.eex
index 109cd37e8..36de71308 100644
--- a/lib/web/templates/email/email_direct_activity.html.eex
+++ b/lib/web/templates/email/email_direct_activity.html.eex
@@ -47,43 +47,45 @@
                 <ul style="margin: 0 auto; padding-left: 15px;">
                   <%= for {_, group_activities} <- @activities do %>
                   <li style="list-style: none;border-bottom: solid 2px #d7d6de;padding: 10px 0;">
-                    <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
-                      <tr>
-                        <td align="left">
-                          <table align="left">
-                            <tr>
-                              <%= if hd(group_activities).group.avatar do %>
-                                <td width="85">
-                                  <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;">
-                                    <img width="80" src="<%= hd(group_activities).group.avatar.url %>" style="width: 80px;max-height: 100px;" style="margin:0; padding:0; border:none; display:block;" border="0" alt="" />
-                                  </a>
-                                </td>
-                              <% end %>
-                              <td width="400">
-                                <table width="" cellpadding="0" cellspacing="0" border="0" style="max-width: 400px;width: 100%;" align="left">
-                                  <tr>
-                                    <td align="left">
-                                      <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 18px;font-weight: bold;line-height: 25px;">
-                                        <%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %>
-                                      </a>
-                                    </td>
-                                  </tr>
-                                  <%= if hd(group_activities).group.name do %>
+                    <%= if hd(group_activities).group do %>
+                      <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+                        <tr>
+                          <td align="left">
+                            <table align="left">
+                              <tr>
+                                <%= if hd(group_activities).group.avatar do %>
+                                  <td width="85">
+                                    <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;">
+                                      <img width="80" src="<%= hd(group_activities).group.avatar.url %>" style="width: 80px;max-height: 100px;" style="margin:0; padding:0; border:none; display:block;" border="0" alt="" />
+                                    </a>
+                                  </td>
+                                <% end %>
+                                <td width="400">
+                                  <table width="" cellpadding="0" cellspacing="0" border="0" style="max-width: 400px;width: 100%;" align="left">
                                     <tr>
                                       <td align="left">
-                                        <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;display: block;color: #7a7a7a;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;">
-                                          @<%= Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group) %>
+                                        <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 18px;font-weight: bold;line-height: 25px;">
+                                          <%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %>
                                         </a>
                                       </td>
                                     </tr>
-                                  <% end %>
-                                </table>
-                              </td>
-                            </tr>
-                          </table>
-                        </td>
-                      </tr>
-                    </table>
+                                    <%= if hd(group_activities).group.name do %>
+                                      <tr>
+                                        <td align="left">
+                                          <a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;display: block;color: #7a7a7a;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;">
+                                            @<%= Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group) %>
+                                          </a>
+                                        </td>
+                                      </tr>
+                                    <% end %>
+                                  </table>
+                                </td>
+                              </tr>
+                            </table>
+                          </td>
+                        </tr>
+                      </table>
+                    <% end %>
                     <ul style="padding-left: 25px;margin-top: 10px;">
                       <%= for activity <- Enum.take(group_activities, 5) do %>
                       <li style="margin-bottom: 7px;">
diff --git a/lib/web/templates/email/email_direct_activity.text.eex b/lib/web/templates/email/email_direct_activity.text.eex
index 4b8a65809..49fb3a607 100644
--- a/lib/web/templates/email/email_direct_activity.text.eex
+++ b/lib/web/templates/email/email_direct_activity.text.eex
@@ -6,7 +6,9 @@
 <%= for {_, group_activities} <- @activities do %>
 
 ==
+<%= if hd(group_activities).group do %>
 <%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %>
+<% end %>
 
 <%= for activity <- Enum.take(group_activities, 5) do %>
 * <%= case activity.type do %><% :discussion -> %><%= render("activity/_discussion_activity_item.text", activity: activity) %><% :event -> %><%= render("activity/_event_activity_item.text", activity: activity) %><% :group -> %><%= render("activity/_group_activity_item.text", activity: activity) %>
diff --git a/priv/repo/migrations/20210505172402_add_group_notification_and_last_notification_date_settings.exs b/priv/repo/migrations/20210505172402_add_group_notification_and_last_notification_date_settings.exs
index bd22dc9e2..f5261b795 100644
--- a/priv/repo/migrations/20210505172402_add_group_notification_and_last_notification_date_settings.exs
+++ b/priv/repo/migrations/20210505172402_add_group_notification_and_last_notification_date_settings.exs
@@ -3,8 +3,8 @@ defmodule Mobilizon.Storage.Repo.Migrations.AddGroupNotificationAndLastNotificat
 
   def change do
     alter table(:user_settings) do
-      add(:group_notifications, :integer, default: 10, nullable: false)
-      add(:last_notification_sent, :utc_datetime, nullable: true)
+      add(:group_notifications, :integer, default: 10, null: false)
+      add(:last_notification_sent, :utc_datetime, null: true)
     end
   end
 end
diff --git a/priv/repo/migrations/20210526203337_add_user_activity_settings.exs b/priv/repo/migrations/20210526203337_add_user_activity_settings.exs
new file mode 100644
index 000000000..2d28de0c2
--- /dev/null
+++ b/priv/repo/migrations/20210526203337_add_user_activity_settings.exs
@@ -0,0 +1,15 @@
+defmodule Mobilizon.Storage.Repo.Migrations.AddUserActivitySettings do
+  use Ecto.Migration
+
+  def change do
+    create table(:user_activity_settings) do
+      add(:key, :string, nulla: false)
+      add(:method, :string, null: false)
+      add(:enabled, :boolean, null: false)
+
+      add(:user_id, references(:users, on_delete: :delete_all), null: false)
+    end
+
+    create(unique_index(:user_activity_settings, [:user_id, :key, :method]))
+  end
+end
diff --git a/priv/repo/migrations/20210601082412_add_is_announcement_to_comments.exs b/priv/repo/migrations/20210601082412_add_is_announcement_to_comments.exs
new file mode 100644
index 000000000..b3e1d312c
--- /dev/null
+++ b/priv/repo/migrations/20210601082412_add_is_announcement_to_comments.exs
@@ -0,0 +1,9 @@
+defmodule Mobilizon.Storage.Repo.Migrations.AddIsAnnouncementToComments do
+  use Ecto.Migration
+
+  def change do
+    alter table(:comments) do
+      add(:is_announcement, :boolean, default: false, null: false)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20210601082613_fix_user_settings_nullable_fields.exs b/priv/repo/migrations/20210601082613_fix_user_settings_nullable_fields.exs
new file mode 100644
index 000000000..46438b83c
--- /dev/null
+++ b/priv/repo/migrations/20210601082613_fix_user_settings_nullable_fields.exs
@@ -0,0 +1,10 @@
+defmodule Mobilizon.Storage.Repo.Migrations.FixUserSettingsNullableFields do
+  use Ecto.Migration
+
+  def change do
+    alter table(:user_settings) do
+      modify(:group_notifications, :integer, default: 10, null: false)
+      modify(:last_notification_sent, :utc_datetime, null: true)
+    end
+  end
+end