diff --git a/lib/admin/accounts.ex b/lib/admin/accounts.ex
index 65ea33359..07c7e918f 100644
--- a/lib/admin/accounts.ex
+++ b/lib/admin/accounts.ex
@@ -386,13 +386,25 @@ defmodule Admin.Accounts do
end
end
- @type audience :: %{name: String.t(), email: String.t(), lang: String.t()}
+ @type audience :: %{
+ id: Ecto.UUID.t(),
+ name: String.t(),
+ email: String.t(),
+ lang: String.t(),
+ marketing_emails_subscribed_at: DateTime.t()
+ }
@spec get_active_members() :: [audience]
def get_active_members do
Repo.all(
from(m in Account,
- select: %{name: m.name, email: m.email, lang: fragment("?->>?", m.extra, "lang")},
+ select: %{
+ id: m.id,
+ name: m.name,
+ email: m.email,
+ lang: fragment("?->>?", m.extra, "lang"),
+ marketing_emails_subscribed_at: m.marketing_emails_subscribed_at
+ },
where:
not is_nil(m.last_authenticated_at) and m.last_authenticated_at > ago(90, "day") and
m.type == "individual"
@@ -404,7 +416,13 @@ defmodule Admin.Accounts do
def get_members_by_language(language) do
Repo.all(
from(m in Account,
- select: %{name: m.name, email: m.email, lang: fragment("?->>?", m.extra, "lang")},
+ select: %{
+ id: m.id,
+ name: m.name,
+ email: m.email,
+ lang: fragment("?->>?", m.extra, "lang"),
+ marketing_emails_subscribed_at: m.marketing_emails_subscribed_at
+ },
where: fragment("?->>? = ?", m.extra, "lang", ^language) and m.type == "individual"
)
)
@@ -412,7 +430,11 @@ defmodule Admin.Accounts do
def create_member(attrs \\ %{}) do
%Account{}
- |> Account.changeset(attrs)
+ |> Account.create_changeset(attrs)
|> Repo.insert()
end
+
+ def member_marketing_emails(%Account{} = account, enable_emails) do
+ account |> Account.marketing_emails_changeset(enable_emails) |> Repo.update()
+ end
end
diff --git a/lib/admin/accounts/account.ex b/lib/admin/accounts/account.ex
index c46a7d72f..4aad7cac5 100644
--- a/lib/admin/accounts/account.ex
+++ b/lib/admin/accounts/account.ex
@@ -11,6 +11,7 @@ defmodule Admin.Accounts.Account do
field :type, :string
field :extra, :map
field :last_authenticated_at, :utc_datetime
+ field :marketing_emails_subscribed_at, :utc_datetime
timestamps(type: :utc_datetime)
end
@@ -24,6 +25,28 @@ defmodule Admin.Accounts.Account do
|> validate_change(:extra, fn _, value -> validate_lang(value) end)
end
+ @doc false
+ def create_changeset(account, attrs) do
+ account
+ |> changeset(attrs)
+ |> put_change(:marketing_emails_subscribed_at, DateTime.utc_now(:second))
+ end
+
+ def marketing_emails_changeset(account, true) do
+ account
+ |> change(%{
+ marketing_emails_subscribed_at: DateTime.utc_now(:second)
+ })
+ |> validate_required([:marketing_emails_subscribed_at])
+ end
+
+ def marketing_emails_changeset(account, false) do
+ account
+ |> change(%{
+ marketing_emails_subscribed_at: nil
+ })
+ end
+
defp validate_email(changeset) do
changeset
|> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/,
diff --git a/lib/admin/accounts/user_notifier.ex b/lib/admin/accounts/user_notifier.ex
index 1dbec2268..13fa0e803 100644
--- a/lib/admin/accounts/user_notifier.ex
+++ b/lib/admin/accounts/user_notifier.ex
@@ -78,7 +78,8 @@ defmodule Admin.Accounts.UserNotifier do
message: message_text,
button_text: button_text,
button_url: button_url,
- pixel: pixel
+ pixel: pixel,
+ account: user
})
deliver(
diff --git a/lib/admin/mailing_worker.ex b/lib/admin/mailing_worker.ex
index c04493ffb..cd55bb88e 100644
--- a/lib/admin/mailing_worker.ex
+++ b/lib/admin/mailing_worker.ex
@@ -24,7 +24,7 @@ defmodule Admin.MailingWorker do
with {:ok, notification} <- Notifications.get_notification(scope, notification_id),
included_langs = notification.localized_emails |> Enum.map(& &1.language),
- {:ok, audience} <-
+ {:ok, audience, _meta} <-
Notifications.get_target_audience(
scope,
notification.audience,
diff --git a/lib/admin/notifications.ex b/lib/admin/notifications.ex
index 9a09213b7..ddd727dc2 100644
--- a/lib/admin/notifications.ex
+++ b/lib/admin/notifications.ex
@@ -308,7 +308,7 @@ defmodule Admin.Notifications do
@type audience :: %{name: String.t(), email: String.t(), lang: String.t()}
@spec get_target_audience(Scope.t(), String.t(), Keyword.t()) ::
- {:ok, [audience]} | {:error, String.t()}
+ {:ok, [audience], %{total: integer, excluded: integer}} | {:error, String.t()}
@doc """
Get the target audience for a notification.
@@ -331,34 +331,51 @@ defmodule Admin.Notifications do
def get_target_audience(scope, target_audience, opts \\ [])
def get_target_audience(%Scope{} = _scope, "active", opts) do
- audience =
+ {audience, meta} =
Accounts.get_active_members()
- |> Enum.map(&%{name: &1.name, email: &1.email, lang: &1.lang})
+ |> Enum.map(
+ &%{
+ id: &1.id,
+ name: &1.name,
+ email: &1.email,
+ lang: &1.lang,
+ marketing_emails_subscribed_at: &1.marketing_emails_subscribed_at
+ }
+ )
|> filter_audience_with_options(opts)
- {:ok, audience}
+ {:ok, audience, meta}
end
def get_target_audience(%Scope{} = _scope, "french", opts) do
- audience =
+ {audience, meta} =
Accounts.get_members_by_language("fr")
- |> Enum.map(&%{name: &1.name, email: &1.email, lang: &1.lang})
+ |> Enum.map(
+ &%{
+ id: &1.id,
+ name: &1.name,
+ email: &1.email,
+ lang: &1.lang,
+ marketing_emails_subscribed_at: &1.marketing_emails_subscribed_at
+ }
+ )
|> filter_audience_with_options(opts)
- {:ok, audience}
+ {:ok, audience, meta}
end
def get_target_audience(%Scope{} = _scope, "graasp_team", opts) do
- audience =
+ {audience, meta} =
Accounts.list_users()
- |> Enum.map(&%{name: &1.name, email: &1.email, lang: &1.language})
+ |> Enum.map(&%{id: &1.id, name: &1.name, email: &1.email, lang: &1.language})
|> filter_audience_with_options(opts)
- {:ok, audience}
+ {:ok, audience, meta}
end
# support legacy audience, this is what the pervious audience is converted to.
- def get_target_audience(%Scope{} = _scope, "custom", _opts), do: {:ok, []}
+ def get_target_audience(%Scope{} = _scope, "custom", _opts),
+ do: {:ok, [], %{total: 0, excluded: 0}}
def get_target_audience(%Scope{} = _scope, target_audience, _opts) do
Logger.error("Invalid target audience: #{target_audience}")
@@ -367,7 +384,15 @@ defmodule Admin.Notifications do
defp filter_audience_with_options(audience, opts) do
only_langs = Keyword.get(opts, :only_langs, Admin.Languages.all_values()) |> MapSet.new()
- audience |> Enum.filter(fn user -> MapSet.member?(only_langs, user.lang) end)
+
+ filtered_audience =
+ audience
+ |> Enum.filter(fn user ->
+ MapSet.member?(only_langs, user.lang) and user.marketing_emails_subscribed_at != nil
+ end)
+
+ {filtered_audience,
+ %{total: length(audience), excluded: length(audience) - length(filtered_audience)}}
end
def create_pixel(%Scope{} = scope, %Admin.Notifications.Notification{} = notification) do
diff --git a/lib/admin_web/components/layouts.ex b/lib/admin_web/components/layouts.ex
index 614a0d99b..3ab660d6a 100644
--- a/lib/admin_web/components/layouts.ex
+++ b/lib/admin_web/components/layouts.ex
@@ -75,6 +75,34 @@ defmodule AdminWeb.Layouts do
"""
end
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+
+ attr :current_scope, :map,
+ default: nil,
+ doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
+
+ slot :inner_block, required: true, doc: "the inner block of the layout"
+
+ def simple(assigns) do
+ ~H"""
+
A tracking pixel is a small image that is embedded in an email to track user interactions.
The interactions are recorded in the Umami analytics platform.
@@ -136,7 +145,7 @@ defmodule AdminWeb.NotificationLive.Show do
<.button navigate={~p"/admin/notifications/#{@notification}/messages/new"}>
Add a localized message
- <%= if length(@recipients) > 0 do %>
+ <%= if @recipients.included > 0 do %>
<.button variant="primary" phx-click="confirm_send_notification">
Send Notification
@@ -158,7 +167,7 @@ defmodule AdminWeb.NotificationLive.Show do
Confirm Send Notification
Are you sure you want to send this notification?
- We will send an email to {length(@recipients)} users
+ We will send an email to {@recipients.included} users
matching the audience criteria.
@@ -184,7 +193,7 @@ defmodule AdminWeb.NotificationLive.Show do
notification = Notifications.get_notification!(socket.assigns.current_scope, id)
included_langs = notification.localized_emails |> Enum.map(& &1.language)
- {:ok, recipients} =
+ {:ok, _recipients, recipients_meta} =
Notifications.get_target_audience(
socket.assigns.current_scope,
notification.audience,
@@ -198,7 +207,10 @@ defmodule AdminWeb.NotificationLive.Show do
:notification,
notification
)
- |> assign(:recipients, recipients)
+ |> assign(:recipients, %{
+ included: recipients_meta.total - recipients_meta.excluded,
+ excluded: recipients_meta.excluded
+ })
|> assign(:show_modal, false)}
end
@@ -212,14 +224,19 @@ defmodule AdminWeb.NotificationLive.Show do
included_langs = notification.localized_emails |> Enum.map(& &1.language)
- {:ok, recipients} =
+ {:ok, _recipients, recipients_meta} =
Notifications.get_target_audience(
socket.assigns.current_scope,
notification.audience,
if(notification.use_strict_languages, do: [only_langs: included_langs], else: [])
)
- {:noreply, socket |> assign(:recipients, recipients)}
+ {:noreply,
+ socket
+ |> assign(:recipients, %{
+ included: recipients_meta.total - recipients_meta.excluded,
+ excluded: recipients_meta.excluded
+ })}
end
def handle_event("delete", %{"id" => id} = _params, socket) do
diff --git a/lib/admin_web/router.ex b/lib/admin_web/router.ex
index 729dc6e80..4c69b0d13 100644
--- a/lib/admin_web/router.ex
+++ b/lib/admin_web/router.ex
@@ -60,6 +60,14 @@ defmodule AdminWeb.Router do
# redirections for now
get "/library", RedirectionController, :library
get "/auth/login", RedirectionController, :login
+
+ get "/accounts/:account_id/marketing/unsubscribe",
+ AccountController,
+ :marketing_emails_unsubscribe
+
+ get "/accounts/:account_id/marketing/subscribe",
+ AccountController,
+ :marketing_emails_subscribe
end
scope "/admin", AdminWeb do
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index da5c02db6..9913ac7bc 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -12,23 +12,23 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Aktionen"
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Verbindungsversuch"
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "Etwas ist schiefgelaufen!"
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "Wir können das Internet nicht finden."
@@ -59,44 +59,44 @@ msgstr ""
msgid "Recent Posts"
msgstr ""
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr ""
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr ""
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr "Jetzt starten"
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr ""
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr "Einloggen"
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr "Über uns"
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr "Kontakt"
@@ -143,17 +143,17 @@ msgstr ""
msgid "Home"
msgstr ""
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr ""
@@ -167,3 +167,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr ""
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index cb5aa7eb8..68014c343 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -12,23 +12,23 @@ msgid ""
msgstr ""
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
@@ -59,44 +59,44 @@ msgstr ""
msgid "Recent Posts"
msgstr ""
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr ""
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr ""
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr ""
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr ""
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr ""
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr ""
@@ -143,17 +143,17 @@ msgstr ""
msgid "Home"
msgstr ""
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr ""
@@ -167,3 +167,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr ""
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 93ab53541..b44a068ec 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -12,23 +12,23 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Actions"
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Attempting to reconnect"
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "Something went wrong!"
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "We can't find the internet"
@@ -59,44 +59,44 @@ msgstr "Read more"
msgid "Recent Posts"
msgstr "Recent Posts"
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr "Admin"
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr "Blog"
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr "Get started"
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr "Library"
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr "Log in"
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr "About"
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr "Contact"
@@ -143,17 +143,17 @@ msgstr "Graasp is a platform for collaborative learning and teaching."
msgid "Home"
msgstr "Home"
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr "Use dark theme"
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr "Use light theme"
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr "Use system theme"
@@ -167,3 +167,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr ""
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext/es/LC_MESSAGES/default.po b/priv/gettext/es/LC_MESSAGES/default.po
index 8c6be08ee..2fa59925e 100644
--- a/priv/gettext/es/LC_MESSAGES/default.po
+++ b/priv/gettext/es/LC_MESSAGES/default.po
@@ -12,23 +12,23 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Comportamiento"
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Intentando reconectarse"
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "¡Algo salió mal!"
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "No podemos encontrar internet"
@@ -59,44 +59,44 @@ msgstr ""
msgid "Recent Posts"
msgstr ""
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr ""
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr ""
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr ""
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr ""
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr ""
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr ""
@@ -143,17 +143,17 @@ msgstr ""
msgid "Home"
msgstr ""
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr ""
@@ -167,3 +167,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr ""
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext/fr/LC_MESSAGES/default.po b/priv/gettext/fr/LC_MESSAGES/default.po
index 95a20967d..0f8520d17 100644
--- a/priv/gettext/fr/LC_MESSAGES/default.po
+++ b/priv/gettext/fr/LC_MESSAGES/default.po
@@ -12,23 +12,23 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n>1);\n"
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Actions"
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Tentative de reconnexion"
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "Quelque chose a mal fonctionné !"
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "Nous ne trouvons pas l'internet"
@@ -59,44 +59,44 @@ msgstr "Lire plus"
msgid "Recent Posts"
msgstr "Articles récents"
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr "Admin"
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr "Blog"
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr "Commencer"
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr "Library"
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr "Se connecter"
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr "À propos"
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr "Contact"
@@ -143,17 +143,17 @@ msgstr "Graasp est une plateforme d'enseignement et d'apprentissages collaborati
msgid "Home"
msgstr "Accueil"
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr "Utiliser le thème sombre"
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr "Utiliser le thème clair"
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr "Utiliser le thème système"
@@ -167,3 +167,8 @@ msgstr "Cette page est disponible dans votre langue préférée :"
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr "Vous regardez la version %{locale} de cette page"
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext/it/LC_MESSAGES/default.po b/priv/gettext/it/LC_MESSAGES/default.po
index 7e9d9e0f1..9db703866 100644
--- a/priv/gettext/it/LC_MESSAGES/default.po
+++ b/priv/gettext/it/LC_MESSAGES/default.po
@@ -12,23 +12,23 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/admin_web/components/core_components.ex:353
-#: lib/admin_web/live/notification_live/show.ex:61
+#: lib/admin_web/live/notification_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Azioni"
-#: lib/admin_web/components/layouts.ex:103
-#: lib/admin_web/components/layouts.ex:115
+#: lib/admin_web/components/layouts.ex:131
+#: lib/admin_web/components/layouts.ex:143
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Tentativo di riconnessione"
-#: lib/admin_web/components/layouts.ex:110
+#: lib/admin_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "Qualcosa è andato storto!"
-#: lib/admin_web/components/layouts.ex:98
+#: lib/admin_web/components/layouts.ex:126
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "Non riusciamo a trovare Internet"
@@ -59,44 +59,44 @@ msgstr ""
msgid "Recent Posts"
msgstr ""
-#: lib/admin_web/components/layouts.ex:319
-#: lib/admin_web/components/layouts.ex:349
+#: lib/admin_web/components/layouts.ex:347
+#: lib/admin_web/components/layouts.ex:374
#, elixir-autogen, elixir-format
msgid "Admin"
msgstr ""
-#: lib/admin_web/components/layouts.ex:310
-#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:338
+#: lib/admin_web/components/layouts.ex:369
#, elixir-autogen, elixir-format
msgid "Blog"
msgstr ""
-#: lib/admin_web/components/layouts.ex:326
-#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:354
+#: lib/admin_web/components/layouts.ex:383
#, elixir-autogen, elixir-format
msgid "Get started"
msgstr ""
-#: lib/admin_web/components/layouts.ex:307
-#: lib/admin_web/components/layouts.ex:343
+#: lib/admin_web/components/layouts.ex:335
+#: lib/admin_web/components/layouts.ex:368
#, elixir-autogen, elixir-format
msgid "Library"
msgstr ""
-#: lib/admin_web/components/layouts.ex:330
-#: lib/admin_web/components/layouts.ex:362
+#: lib/admin_web/components/layouts.ex:358
+#: lib/admin_web/components/layouts.ex:387
#, elixir-autogen, elixir-format
msgid "Log in"
msgstr ""
-#: lib/admin_web/components/layouts.ex:313
-#: lib/admin_web/components/layouts.ex:345
+#: lib/admin_web/components/layouts.ex:341
+#: lib/admin_web/components/layouts.ex:370
#, elixir-autogen, elixir-format
msgid "About"
msgstr ""
-#: lib/admin_web/components/layouts.ex:316
-#: lib/admin_web/components/layouts.ex:346
+#: lib/admin_web/components/layouts.ex:344
+#: lib/admin_web/components/layouts.ex:371
#, elixir-autogen, elixir-format
msgid "Contact"
msgstr ""
@@ -143,17 +143,17 @@ msgstr ""
msgid "Home"
msgstr ""
-#: lib/admin_web/components/layouts.ex:154
+#: lib/admin_web/components/layouts.ex:182
#, elixir-autogen, elixir-format
msgid "Use dark theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:145
+#: lib/admin_web/components/layouts.ex:173
#, elixir-autogen, elixir-format
msgid "Use light theme"
msgstr ""
-#: lib/admin_web/components/layouts.ex:136
+#: lib/admin_web/components/layouts.ex:164
#, elixir-autogen, elixir-format
msgid "Use system theme"
msgstr ""
@@ -167,3 +167,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "You are viewing this page in: %{locale}"
msgstr ""
+
+#: lib/admin_web/live/notification_live/show.ex:28
+#, elixir-autogen, elixir-format
+msgid "Excluded: %{count} (incompatible language, or not subscribed)"
+msgstr ""
diff --git a/priv/gettext_email_templates/de/LC_MESSAGES/default.po b/priv/gettext_email_templates/de/LC_MESSAGES/default.po
index df15acc96..9bb0fe982 100644
--- a/priv/gettext_email_templates/de/LC_MESSAGES/default.po
+++ b/priv/gettext_email_templates/de/LC_MESSAGES/default.po
@@ -11,12 +11,12 @@ msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr "Graasp.org ist eine Lernplattform."
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr "Sie erhalten diese E-Mail, weil Sie ein Konto bei Graasp haben:"
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr "Ihr Graasp-Team"
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/gettext_email_templates/default.pot b/priv/gettext_email_templates/default.pot
index 072fa0ece..1dd3c59fa 100644
--- a/priv/gettext_email_templates/default.pot
+++ b/priv/gettext_email_templates/default.pot
@@ -11,12 +11,12 @@
msgid ""
msgstr ""
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr ""
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr ""
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/gettext_email_templates/en/LC_MESSAGES/default.po b/priv/gettext_email_templates/en/LC_MESSAGES/default.po
index 812349579..eea9c1bb3 100644
--- a/priv/gettext_email_templates/en/LC_MESSAGES/default.po
+++ b/priv/gettext_email_templates/en/LC_MESSAGES/default.po
@@ -11,12 +11,12 @@ msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr "Graasp.org is a learning experience platform."
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr "You are receiving this email because you have an account on Graasp: "
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr "Your Graasp Team"
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/gettext_email_templates/es/LC_MESSAGES/default.po b/priv/gettext_email_templates/es/LC_MESSAGES/default.po
index dd6723c9c..785860b2e 100644
--- a/priv/gettext_email_templates/es/LC_MESSAGES/default.po
+++ b/priv/gettext_email_templates/es/LC_MESSAGES/default.po
@@ -11,12 +11,12 @@ msgstr ""
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr "Graasp.org es una plataforma de experiencia de aprendizaje."
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr "Estás recibiendo este correo electrónico porque tienes una cuenta en G
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr "Tu equipo Graasp"
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/gettext_email_templates/fr/LC_MESSAGES/default.po b/priv/gettext_email_templates/fr/LC_MESSAGES/default.po
index 525f30c01..ece7cd80e 100644
--- a/priv/gettext_email_templates/fr/LC_MESSAGES/default.po
+++ b/priv/gettext_email_templates/fr/LC_MESSAGES/default.po
@@ -11,12 +11,12 @@ msgstr ""
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n>1);\n"
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr "Graasp.org est une plateforme éducative."
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr "Vous recevez ce courriel car vous possédez un compte sur Graasp:"
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr "Votre équipe Graasp"
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/gettext_email_templates/it/LC_MESSAGES/default.po b/priv/gettext_email_templates/it/LC_MESSAGES/default.po
index d5c731523..5e2b071d7 100644
--- a/priv/gettext_email_templates/it/LC_MESSAGES/default.po
+++ b/priv/gettext_email_templates/it/LC_MESSAGES/default.po
@@ -11,12 +11,12 @@ msgstr ""
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/admin/accounts/user_notifier.ex:99
+#: lib/admin/accounts/user_notifier.ex:100
#, elixir-autogen, elixir-format
msgid "Graasp.org is a learning experience platform."
msgstr "Graasp.org è una piattaforma di esperienze di apprendimento."
-#: lib/admin/accounts/user_notifier.ex:92
+#: lib/admin/accounts/user_notifier.ex:93
#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:5
#, elixir-autogen, elixir-format
msgid "Hi %{name},"
@@ -36,3 +36,8 @@ msgstr "Stai ricevendo questa email perché hai un account su Graasp:"
#, elixir-autogen, elixir-format
msgid "Your Graasp Team"
msgstr "Il tuo team Graasp"
+
+#: lib/admin_web/email_templates/templates_html/call_to_action.html.heex:43
+#, elixir-autogen, elixir-format
+msgid "unsubscribe"
+msgstr ""
diff --git a/priv/repo/migrations/20250806110912_create_users_auth_tables.exs b/priv/repo/migrations/20250806110912_create_users_auth_tables.exs
index b48540ae7..20c11689e 100644
--- a/priv/repo/migrations/20250806110912_create_users_auth_tables.exs
+++ b/priv/repo/migrations/20250806110912_create_users_auth_tables.exs
@@ -36,6 +36,7 @@ defmodule Admin.Repo.Migrations.CreateUsersAuthTables do
add :type, :string
add :extra, :map
add :last_authenticated_at, :utc_datetime
+ add :marketing_emails_subscribed_at, :utc_datetime, default: fragment("NOW()")
timestamps(type: :utc_datetime)
end