Presently, we support sending notifications via email. We might not always have a user's email address, or they might not want to receive emails. We'll add an in-app notification system that will allow you to see notifications without requiring us to have your email address. Let's get started.
Project
We'll start by writing a test:
defmodule FirestormWeb.ForumsTest do
# ...
describe "notifications" do
setup [:create_user, :create_category, :create_thread]
test "getting a user's notifications when there aren't any", %{user: user} do
assert [] = Forums.notifications_for(user)
end
end
# ...
end
It will fail because there's no such function - let's write a function that makes this test pass:
defmodule FirestormWeb.Forums do
# ...
def notifications_for(%User{} = user) do
[]
end
end
Next, we know we want to store these in the database. Let's create a schema:
defmodule FirestormWeb.Forums.Notification do
@moduledoc """
Schema for notifications.
"""
use Ecto.Schema
alias FirestormWeb.Forums.{User, Thread, View}
schema "forums_notifications" do
field :body, :string
belongs_to :user, User
timestamps()
end
end
And write the migration:
mix ecto.gen.migration add_notifications
defmodule FirestormWeb.Repo.Migrations.AddNotifications do
use Ecto.Migration
def change do
create table(:forums_notifications) do
add :body, :text
add :user_id, references(:forums_users)
timestamps()
end
end
end
Let's write a test to prove that we can generate notifications and fetch them:
defmodule FirestormWeb.ForumsTest do
# ...
describe "notifications" do
setup [:create_user, :create_category, :create_thread]
test "getting a user's notifications when there aren't any", %{user: user} do
assert [] = Forums.notifications_for(user)
end
test "getting a user's notifications", %{user: user} do
body = "You are looking dapper today!"
{:ok, _} = Forums.notify(user, body)
[first_notification | _] = Forums.notifications_for(user)
assert body == first_notification.body
end
end
# ...
end
We need to implement
notify
and tweak
notifications_for
:
defmodule FirestormWeb.Forums do
# ...
def notifications_for(%User{} = user) do
Notification
|> where([n], n.user_id == ^user.id)
|> Repo.all()
end
defp notification_changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:user_id, :body])
|> validate_required([:user_id, :body])
end
@doc """
Send a notification to a user:
iex> %User{} |> notify("Nice shoes")
{:ok, %Notification{}}
"""
def notify(%User{} = user, body) do
%Notification{}
|> notification_changeset(%{body: body, user_id: user.id})
|> Repo.insert()
end
# ...
end
Now that we can create notifications, we'll modify the notifications service to create a notification at the same time that it sends out emails:
vim lib/firestorm_web/notifications.ex
defmodule FirestormWeb.Notifications do
# ...
alias FirestormWeb.Forums
# ...
def handle_cast({:post_created, %Post{} = post}, :nostate) do
# 1) Find all users that are watching this thread
with post when not is_nil(post) <- Repo.preload(post, [thread: [:watchers]]),
thread when not is_nil(thread) <- post.thread,
users <- thread.watchers |> Enum.uniq do
# 2) Send each of them an email about it and notify them via the
# internal notifications system.
for user <- users do
# Send internal notification
{:ok, _} = Forums.notify(user, "There was a new post in a thread you are watching.")
# Send email
user
|> Emails.thread_new_post_notification(post.thread, post)
|> Mailer.deliver_now()
end
else
_ ->
:ok
end
{:noreply, :nostate}
end
Next, we want to show you your notifications. I'm not going to build out the ability to actually view the notifications, but we'll show a link in the drawer. To do that, since the drawer is in the layout, we need to always fetch and assign your notifications. We'll write a plug for this:
defmodule FirestormWeb.Web.Plugs.Notifications do
@moduledoc """
A `Plug` to assign `:notifications` based on the current user
"""
import Plug.Conn
import FirestormWeb.Web.Session, only: [current_user: 1]
alias FirestormWeb.Forums
def init(options), do: options
def call(conn, _opts) do
case current_user(conn) do
nil ->
conn
|> assign(:notifications, [])
user ->
conn
|> assign(:notifications, Forums.notifications_for(user))
end
end
end
Then we'll wire it up in the browser pipeline:
defmodule FirestormWeb.Web.Router do
# ...
pipeline :browser do
# ...
plug FirestormWeb.Web.Plugs.Notifications
end
# ...
end
From here, all we need to do is modify the layout to pass this assign to the drawer, and show them in the drawer:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body class="layout-app <%= page_class(@conn) %>">
<!-- ... -->
<div class="pure-container" data-effect="pure-effect-slide">
<!-- ... -->
<div class="pure-drawer" data-position="right">
<%= render "_drawer.html", conn: @conn, notifications: @notifications %>
</div>
<!-- ... -->
<div class="pure-pusher-container">
<!-- ... -->
<div class="pure-pusher">
<!-- ... -->
<div class="layout-content">
<!-- ... -->
<div class="layout-drawer-tablet">
<!-- ... -->
<%= render "_drawer.html", conn: @conn, notifications: @notifications %>
</div>
<!-- ... -->
</div>
<!-- ... -->
</div>
<!-- ... -->
</div>
<!-- ... -->
</div>
</body>
</html>
<div class="navigation-drawer">
<!-- ... -->
<%= if current_user(@conn) do %>
<ul>
<!-- ... -->
<%= render "_navigation_item.html", conn: @conn, text: "[#{Enum.count(@notifications)}] Notifications", url: "#" %>
</ul>
<% end %>
<!-- ... -->
</div>
Now, if we reply to a post, we'll see our notifications count increase in the drawer.
Summary
Today, we started to add in-app notifications. There's a lot left to build, but the 'hard' part is done - though it honestly wasn't hard, of course. We'll make this better over time, but now we have the kernel we can expand on. I hope you enjoyed it. See you soon!
Comments for In-App Notifications