Merge pull request #321 from philomena-dev/notifications-v2

New notifications UI: separated by category
This commit is contained in:
liamwhite 2024-07-08 08:30:08 -04:00 committed by GitHub
commit 41219bb217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 276 additions and 39 deletions

View file

@ -480,6 +480,7 @@ span.stat {
@import "views/filters";
@import "views/galleries";
@import "views/images";
@import "views/notifications";
@import "views/pages";
@import "views/polls";
@import "views/posts";

View file

@ -0,0 +1,11 @@
.notification-type-block:not(:last-child) {
margin-bottom: 20px;
}
.notification {
margin-bottom: 0;
}
.notification:not(:last-child) {
border-bottom: 0;
}

View file

@ -6,19 +6,82 @@ defmodule Philomena.Notifications do
import Ecto.Query, warn: false
alias Philomena.Repo
alias Philomena.Notifications.Category
alias Philomena.Notifications.Notification
alias Philomena.Notifications.UnreadNotification
alias Philomena.Polymorphic
@doc """
Returns the list of notifications.
Returns the list of unread notifications of the given type.
The set of valid types is `t:Philomena.Notifications.Category.t/0`.
## Examples
iex> list_notifications()
iex> unread_notifications_for_user_and_type(user, :image_comment, ...)
[%Notification{}, ...]
"""
def list_notifications do
Repo.all(Notification)
def unread_notifications_for_user_and_type(user, type, pagination) do
notifications =
user
|> unread_query_for_type(type)
|> Repo.paginate(pagination)
put_in(notifications.entries, load_associations(notifications.entries))
end
@doc """
Gather up and return the top N notifications for the user, for each type of
unread notification currently existing.
## Examples
iex> unread_notifications_for_user(user)
[
forum_topic: [%Notification{...}, ...],
forum_post: [%Notification{...}, ...],
image_comment: [%Notification{...}, ...]
]
"""
def unread_notifications_for_user(user, n) do
Category.types()
|> Enum.map(fn type ->
q =
user
|> unread_query_for_type(type)
|> limit(^n)
# Use a subquery to ensure the order by is applied to the
# subquery results only, and not the main query results
from(n in subquery(q))
end)
|> union_all_queries()
|> Repo.all()
|> load_associations()
|> Enum.group_by(&Category.notification_type/1)
|> Enum.sort_by(fn {k, _v} -> k end)
end
defp unread_query_for_type(user, type) do
from n in Category.query_for_type(type),
join: un in UnreadNotification,
on: un.notification_id == n.id,
where: un.user_id == ^user.id,
order_by: [desc: :updated_at]
end
defp union_all_queries([query | rest]) do
Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end)
end
defp load_associations(notifications) do
Polymorphic.load_polymorphic(
notifications,
actor: [actor_id: :actor_type],
actor_child: [actor_child_id: :actor_child_type]
)
end
@doc """
@ -102,8 +165,6 @@ defmodule Philomena.Notifications do
Notification.changeset(notification, %{})
end
alias Philomena.Notifications.UnreadNotification
def count_unread_notifications(user) do
UnreadNotification
|> where(user_id: ^user.id)

View file

@ -0,0 +1,93 @@
defmodule Philomena.Notifications.Category do
@moduledoc """
Notification category determination.
"""
import Ecto.Query, warn: false
alias Philomena.Notifications.Notification
@type t ::
:channel_live
| :forum_post
| :forum_topic
| :gallery_image
| :image_comment
| :image_merge
@doc """
Return a list of all supported types.
"""
def types do
[
:channel_live,
:forum_topic,
:gallery_image,
:image_comment,
:image_merge,
:forum_post
]
end
@doc """
Determine the type of a `m:Philomena.Notifications.Notification`.
"""
def notification_type(n) do
case {n.actor_type, n.actor_child_type} do
{"Channel", _} ->
:channel_live
{"Gallery", _} ->
:gallery_image
{"Image", "Comment"} ->
:image_comment
{"Image", _} ->
:image_merge
{"Topic", "Post"} ->
if n.action == "posted a new reply in" do
:forum_post
else
:forum_topic
end
end
end
@doc """
Returns an `m:Ecto.Query` that finds notifications for the given type.
"""
def query_for_type(type) do
base = from(n in Notification)
case type do
:channel_live ->
where(base, [n], n.actor_type == "Channel")
:gallery_image ->
where(base, [n], n.actor_type == "Gallery")
:image_comment ->
where(base, [n], n.actor_type == "Image" and n.actor_child_type == "Comment")
:image_merge ->
where(base, [n], n.actor_type == "Image" and is_nil(n.actor_child_type))
:forum_topic ->
where(
base,
[n],
n.actor_type == "Topic" and n.actor_child_type == "Post" and
n.action != "posted a new reply in"
)
:forum_post ->
where(
base,
[n],
n.actor_type == "Topic" and n.actor_child_type == "Post" and
n.action == "posted a new reply in"
)
end
end
end

View file

@ -0,0 +1,33 @@
defmodule PhilomenaWeb.Notification.CategoryController do
use PhilomenaWeb, :controller
alias Philomena.Notifications
def show(conn, params) do
type = category(params)
notifications =
Notifications.unread_notifications_for_user_and_type(
conn.assigns.current_user,
type,
conn.assigns.scrivener
)
render(conn, "show.html",
title: "Notification Area",
notifications: notifications,
type: type
)
end
defp category(params) do
case params["id"] do
"channel_live" -> :channel_live
"gallery_image" -> :gallery_image
"image_comment" -> :image_comment
"image_merge" -> :image_merge
"forum_topic" -> :forum_topic
_ -> :forum_post
end
end
end

View file

@ -1,33 +1,10 @@
defmodule PhilomenaWeb.NotificationController do
use PhilomenaWeb, :controller
alias Philomena.Notifications.{UnreadNotification, Notification}
alias Philomena.Polymorphic
alias Philomena.Repo
import Ecto.Query
alias Philomena.Notifications
def index(conn, _params) do
user = conn.assigns.current_user
notifications =
from n in Notification,
join: un in UnreadNotification,
on: un.notification_id == n.id,
where: un.user_id == ^user.id
notifications =
notifications
|> order_by(desc: :updated_at)
|> Repo.paginate(conn.assigns.scrivener)
entries =
notifications.entries
|> Polymorphic.load_polymorphic(
actor: [actor_id: :actor_type],
actor_child: [actor_child_id: :actor_child_type]
)
notifications = %{notifications | entries: entries}
notifications = Notifications.unread_notifications_for_user(conn.assigns.current_user, 15)
render(conn, "index.html", title: "Notification Area", notifications: notifications)
end

View file

@ -173,6 +173,7 @@ defmodule PhilomenaWeb.Router do
scope "/notifications", Notification, as: :notification do
resources "/unread", UnreadController, only: [:index]
resources "/categories", CategoryController, only: [:show]
end
resources "/notifications", NotificationController, only: [:index, :delete]

View file

@ -1,5 +1,5 @@
= if @notification.actor do
.block.block--fixed.flex id="notification-#{@notification.id}"
.block.block--fixed.flex.notification id="notification-#{@notification.id}"
= if @notification.actor_type == "Image" and @notification.actor do
.flex.flex--centered.flex__fixed.thumb-tiny-container.spacing-right
= render PhilomenaWeb.ImageView, "_image_container.html", image: @notification.actor, size: :thumb_tiny, conn: @conn

View file

@ -0,0 +1,28 @@
h1 Notification Area
.walloftext
= cond do
- Enum.any?(@notifications) ->
- route = fn p -> ~p"/notifications/categories/#{@type}?#{p}" end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @notifications, route: route, conn: @conn
.block.notification-type-block
.block__header
span.block__header__title = name_of_type(@type)
.block__header.block__header__sub
= pagination
div
= for notification <- @notifications do
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
.block__header.block__header--light
= pagination
- true ->
p You currently have no notifications of this category.
p
' To get notifications on new comments and forum posts, click the
' 'Subscribe' button in the bar at the top of an image or forum topic.
a.button href=~p"/notifications"
' View all notifications

View file

@ -1,14 +1,19 @@
- route = fn p -> ~p"/notifications?#{p}" end
h1 Notification Area
.walloftext
.block__header
= render PhilomenaWeb.PaginationView, "_pagination.html", page: @notifications, route: route, conn: @conn
= cond do
- Enum.any?(@notifications) ->
= for notification <- @notifications do
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
= for {type, notifications} <- @notifications do
.block.notification-type-block
.block__header
span.block__header__title = name_of_type(type)
div
= for notification <- notifications do
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
.block__header.block__header--light
a href=~p"/notifications/categories/#{type}"
| View category
- true ->
p

View file

@ -0,0 +1,5 @@
defmodule PhilomenaWeb.Notification.CategoryView do
use PhilomenaWeb, :view
defdelegate name_of_type(type), to: PhilomenaWeb.NotificationView
end

View file

@ -13,4 +13,26 @@ defmodule PhilomenaWeb.NotificationView do
def notification_template_path(actor_type) do
@template_paths[actor_type]
end
def name_of_type(notification_type) do
case notification_type do
:channel_live ->
"Live channels"
:forum_post ->
"New replies in topics"
:forum_topic ->
"New topics"
:gallery_image ->
"Updated galleries"
:image_comment ->
"New replies on images"
:image_merge ->
"Image merges"
end
end
end