mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
Merge pull request #321 from philomena-dev/notifications-v2
New notifications UI: separated by category
This commit is contained in:
commit
41219bb217
12 changed files with 276 additions and 39 deletions
|
@ -480,6 +480,7 @@ span.stat {
|
||||||
@import "views/filters";
|
@import "views/filters";
|
||||||
@import "views/galleries";
|
@import "views/galleries";
|
||||||
@import "views/images";
|
@import "views/images";
|
||||||
|
@import "views/notifications";
|
||||||
@import "views/pages";
|
@import "views/pages";
|
||||||
@import "views/polls";
|
@import "views/polls";
|
||||||
@import "views/posts";
|
@import "views/posts";
|
||||||
|
|
11
assets/css/views/_notifications.scss
Normal file
11
assets/css/views/_notifications.scss
Normal 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;
|
||||||
|
}
|
|
@ -6,19 +6,82 @@ defmodule Philomena.Notifications do
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
|
||||||
|
alias Philomena.Notifications.Category
|
||||||
alias Philomena.Notifications.Notification
|
alias Philomena.Notifications.Notification
|
||||||
|
alias Philomena.Notifications.UnreadNotification
|
||||||
|
alias Philomena.Polymorphic
|
||||||
|
|
||||||
@doc """
|
@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
|
## Examples
|
||||||
|
|
||||||
iex> list_notifications()
|
iex> unread_notifications_for_user_and_type(user, :image_comment, ...)
|
||||||
[%Notification{}, ...]
|
[%Notification{}, ...]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def list_notifications do
|
def unread_notifications_for_user_and_type(user, type, pagination) do
|
||||||
Repo.all(Notification)
|
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
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -102,8 +165,6 @@ defmodule Philomena.Notifications do
|
||||||
Notification.changeset(notification, %{})
|
Notification.changeset(notification, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
alias Philomena.Notifications.UnreadNotification
|
|
||||||
|
|
||||||
def count_unread_notifications(user) do
|
def count_unread_notifications(user) do
|
||||||
UnreadNotification
|
UnreadNotification
|
||||||
|> where(user_id: ^user.id)
|
|> where(user_id: ^user.id)
|
||||||
|
|
93
lib/philomena/notifications/category.ex
Normal file
93
lib/philomena/notifications/category.ex
Normal 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
|
|
@ -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
|
|
@ -1,33 +1,10 @@
|
||||||
defmodule PhilomenaWeb.NotificationController do
|
defmodule PhilomenaWeb.NotificationController do
|
||||||
use PhilomenaWeb, :controller
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
alias Philomena.Notifications.{UnreadNotification, Notification}
|
alias Philomena.Notifications
|
||||||
alias Philomena.Polymorphic
|
|
||||||
alias Philomena.Repo
|
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
user = conn.assigns.current_user
|
notifications = Notifications.unread_notifications_for_user(conn.assigns.current_user, 15)
|
||||||
|
|
||||||
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}
|
|
||||||
|
|
||||||
render(conn, "index.html", title: "Notification Area", notifications: notifications)
|
render(conn, "index.html", title: "Notification Area", notifications: notifications)
|
||||||
end
|
end
|
||||||
|
|
|
@ -173,6 +173,7 @@ defmodule PhilomenaWeb.Router do
|
||||||
|
|
||||||
scope "/notifications", Notification, as: :notification do
|
scope "/notifications", Notification, as: :notification do
|
||||||
resources "/unread", UnreadController, only: [:index]
|
resources "/unread", UnreadController, only: [:index]
|
||||||
|
resources "/categories", CategoryController, only: [:show]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources "/notifications", NotificationController, only: [:index, :delete]
|
resources "/notifications", NotificationController, only: [:index, :delete]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
= if @notification.actor do
|
= 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
|
= if @notification.actor_type == "Image" and @notification.actor do
|
||||||
.flex.flex--centered.flex__fixed.thumb-tiny-container.spacing-right
|
.flex.flex--centered.flex__fixed.thumb-tiny-container.spacing-right
|
||||||
= render PhilomenaWeb.ImageView, "_image_container.html", image: @notification.actor, size: :thumb_tiny, conn: @conn
|
= render PhilomenaWeb.ImageView, "_image_container.html", image: @notification.actor, size: :thumb_tiny, conn: @conn
|
||||||
|
|
|
@ -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
|
|
@ -1,14 +1,19 @@
|
||||||
- route = fn p -> ~p"/notifications?#{p}" end
|
|
||||||
|
|
||||||
h1 Notification Area
|
h1 Notification Area
|
||||||
.walloftext
|
.walloftext
|
||||||
.block__header
|
|
||||||
= render PhilomenaWeb.PaginationView, "_pagination.html", page: @notifications, route: route, conn: @conn
|
|
||||||
|
|
||||||
= cond do
|
= cond do
|
||||||
- Enum.any?(@notifications) ->
|
- Enum.any?(@notifications) ->
|
||||||
= for notification <- @notifications do
|
= for {type, notifications} <- @notifications do
|
||||||
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
|
.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 ->
|
- true ->
|
||||||
p
|
p
|
||||||
|
|
5
lib/philomena_web/views/notification/category_view.ex
Normal file
5
lib/philomena_web/views/notification/category_view.ex
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule PhilomenaWeb.Notification.CategoryView do
|
||||||
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
|
defdelegate name_of_type(type), to: PhilomenaWeb.NotificationView
|
||||||
|
end
|
|
@ -13,4 +13,26 @@ defmodule PhilomenaWeb.NotificationView do
|
||||||
def notification_template_path(actor_type) do
|
def notification_template_path(actor_type) do
|
||||||
@template_paths[actor_type]
|
@template_paths[actor_type]
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue