Merge pull request #328 from philomena-dev/notifications-v3

New notifications tables
This commit is contained in:
liamwhite 2024-07-29 08:33:07 -04:00 committed by GitHub
commit fe59b046f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1273 additions and 542 deletions

View file

@ -8,10 +8,10 @@ defmodule Philomena.Channels do
alias Philomena.Channels.AutomaticUpdater
alias Philomena.Channels.Channel
alias Philomena.Notifications
alias Philomena.Tags
use Philomena.Subscriptions,
actor_types: ~w(Channel LivestreamChannel),
id_name: :channel_id
@doc """
@ -139,4 +139,18 @@ defmodule Philomena.Channels do
def change_channel(%Channel{} = channel) do
Channel.changeset(channel, %{})
end
@doc """
Removes all channel notifications for a given channel and user.
## Examples
iex> clear_channel_notification(channel, user)
:ok
"""
def clear_channel_notification(%Channel{} = channel, user) do
Notifications.clear_channel_live_notification(channel, user)
:ok
end
end

View file

@ -79,22 +79,7 @@ defmodule Philomena.Comments do
|> Repo.preload(:image)
|> Map.fetch!(:image)
subscriptions =
image
|> Repo.preload(:subscriptions)
|> Map.fetch!(:subscriptions)
Notifications.notify(
comment,
subscriptions,
%{
actor_id: image.id,
actor_type: "Image",
actor_child_id: comment.id,
actor_child_type: "Comment",
action: "commented on"
}
)
Notifications.create_image_comment_notification(image, comment)
end
@doc """

View file

@ -9,7 +9,6 @@ defmodule Philomena.Forums do
alias Philomena.Forums.Forum
use Philomena.Subscriptions,
actor_types: ~w(Forum),
id_name: :forum_id
@doc """

View file

@ -19,7 +19,6 @@ defmodule Philomena.Galleries do
alias Philomena.Images
use Philomena.Subscriptions,
actor_types: ~w(Gallery),
id_name: :gallery_id
@doc """
@ -269,25 +268,10 @@ defmodule Philomena.Galleries do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Galleries", [gallery.id, image.id]])
end
def perform_notify([gallery_id, image_id]) do
def perform_notify([gallery_id, _image_id]) do
gallery = get_gallery!(gallery_id)
subscriptions =
gallery
|> Repo.preload(:subscriptions)
|> Map.fetch!(:subscriptions)
Notifications.notify(
gallery,
subscriptions,
%{
actor_id: gallery.id,
actor_type: "Gallery",
actor_child_id: image_id,
actor_child_type: "Image",
action: "added images to"
}
)
Notifications.create_gallery_image_notification(gallery)
end
def reorder_gallery(gallery, image_ids) do
@ -360,4 +344,18 @@ defmodule Philomena.Galleries do
defp position_order(%{order_position_asc: true}), do: [asc: :position]
defp position_order(_gallery), do: [desc: :position]
@doc """
Removes all gallery notifications for a given gallery and user.
## Examples
iex> clear_gallery_notification(gallery, user)
:ok
"""
def clear_gallery_notification(%Gallery{} = gallery, user) do
Notifications.clear_gallery_image_notification(gallery, user)
:ok
end
end

View file

@ -22,7 +22,8 @@ defmodule Philomena.Images do
alias Philomena.IndexWorker
alias Philomena.ImageFeatures.ImageFeature
alias Philomena.SourceChanges.SourceChange
alias Philomena.Notifications.Notification
alias Philomena.Notifications.ImageCommentNotification
alias Philomena.Notifications.ImageMergeNotification
alias Philomena.NotificationWorker
alias Philomena.TagChanges.Limits
alias Philomena.TagChanges.TagChange
@ -38,7 +39,6 @@ defmodule Philomena.Images do
alias Philomena.Users.User
use Philomena.Subscriptions,
actor_types: ~w(Image),
id_name: :image_id
@doc """
@ -905,12 +905,17 @@ defmodule Philomena.Images do
Repo.insert_all(Subscription, subscriptions, on_conflict: :nothing)
{count, nil} =
Notification
|> where(actor_type: "Image", actor_id: ^source.id)
|> Repo.delete_all()
{comment_notification_count, nil} =
ImageCommentNotification
|> where(image_id: ^source.id)
|> Repo.update_all(set: [image_id: target.id])
{:ok, count}
{merge_notification_count, nil} =
ImageMergeNotification
|> where(image_id: ^source.id)
|> Repo.update_all(set: [image_id: target.id])
{:ok, {comment_notification_count, merge_notification_count}}
end
def migrate_sources(source, target) do
@ -930,23 +935,24 @@ defmodule Philomena.Images do
end
def perform_notify([source_id, target_id]) do
source = get_image!(source_id)
target = get_image!(target_id)
subscriptions =
target
|> Repo.preload(:subscriptions)
|> Map.fetch!(:subscriptions)
Notifications.create_image_merge_notification(target, source)
end
Notifications.notify(
nil,
subscriptions,
%{
actor_id: target.id,
actor_type: "Image",
actor_child_id: nil,
actor_child_type: nil,
action: "merged ##{source_id} into"
}
)
@doc """
Removes all image notifications for a given image and user.
## Examples
iex> clear_image_notification(image, user)
:ok
"""
def clear_image_notification(%Image{} = image, user) do
Notifications.clear_image_comment_notification(image, user)
Notifications.clear_image_merge_notification(image, user)
:ok
end
end

View file

@ -4,277 +4,262 @@ defmodule Philomena.Notifications do
"""
import Ecto.Query, warn: false
alias Philomena.Repo
alias Philomena.Channels.Subscription, as: ChannelSubscription
alias Philomena.Forums.Subscription, as: ForumSubscription
alias Philomena.Galleries.Subscription, as: GallerySubscription
alias Philomena.Images.Subscription, as: ImageSubscription
alias Philomena.Topics.Subscription, as: TopicSubscription
alias Philomena.Notifications.ChannelLiveNotification
alias Philomena.Notifications.ForumPostNotification
alias Philomena.Notifications.ForumTopicNotification
alias Philomena.Notifications.GalleryImageNotification
alias Philomena.Notifications.ImageCommentNotification
alias Philomena.Notifications.ImageMergeNotification
alias Philomena.Notifications.Category
alias Philomena.Notifications.Notification
alias Philomena.Notifications.UnreadNotification
alias Philomena.Polymorphic
alias Philomena.Notifications.Creator
@doc """
Returns the list of unread notifications of the given type.
The set of valid types is `t:Philomena.Notifications.Category.t/0`.
Return the count of all currently unread notifications for the user in all categories.
## Examples
iex> unread_notifications_for_user_and_type(user, :image_comment, ...)
[%Notification{}, ...]
iex> total_unread_notification_count(user)
15
"""
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))
def total_unread_notification_count(user) do
Category.total_unread_notification_count(user)
end
@doc """
Gather up and return the top N notifications for the user, for each type of
Gather up and return the top N notifications for the user, for each category of
unread notification currently existing.
## Examples
iex> unread_notifications_for_user(user)
[
forum_topic: [%Notification{...}, ...],
forum_post: [%Notification{...}, ...],
image_comment: [%Notification{...}, ...]
]
iex> unread_notifications_for_user(user, page_size: 10)
%{
channel_live: [],
forum_post: [%ForumPostNotification{...}, ...],
forum_topic: [%ForumTopicNotification{...}, ...],
gallery_image: [],
image_comment: [%ImageCommentNotification{...}, ...],
image_merge: []
}
"""
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)
def unread_notifications_for_user(user, pagination) do
Category.unread_notifications_for_user(user, pagination)
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]
@doc """
Returns paginated unread notifications for the user, given the category.
## Examples
iex> unread_notifications_for_user_and_category(user, :image_comment)
[%ImageCommentNotification{...}]
"""
def unread_notifications_for_user_and_category(user, category, pagination) do
Category.unread_notifications_for_user_and_category(user, category, pagination)
end
defp union_all_queries([query | rest]) do
Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end)
@doc """
Creates a channel live notification, returning the number of affected users.
## Examples
iex> create_channel_live_notification(channel)
{:ok, 2}
"""
def create_channel_live_notification(channel) do
Creator.create_single(ChannelSubscription, ChannelLiveNotification, :channel_id, channel)
end
defp load_associations(notifications) do
Polymorphic.load_polymorphic(
notifications,
actor: [actor_id: :actor_type],
actor_child: [actor_child_id: :actor_child_type]
@doc """
Creates a forum post notification, returning the number of affected users.
## Examples
iex> create_forum_post_notification(topic, post)
{:ok, 2}
"""
def create_forum_post_notification(topic, post) do
Creator.create_double(
TopicSubscription,
ForumPostNotification,
:topic_id,
topic,
:post_id,
post
)
end
@doc """
Gets a single notification.
Raises `Ecto.NoResultsError` if the Notification does not exist.
Creates a forum topic notification, returning the number of affected users.
## Examples
iex> get_notification!(123)
%Notification{}
iex> get_notification!(456)
** (Ecto.NoResultsError)
iex> create_forum_topic_notification(topic)
{:ok, 2}
"""
def get_notification!(id), do: Repo.get!(Notification, id)
@doc """
Creates a notification.
## Examples
iex> create_notification(%{field: value})
{:ok, %Notification{}}
iex> create_notification(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_notification(attrs \\ %{}) do
%Notification{}
|> Notification.changeset(attrs)
|> Repo.insert()
def create_forum_topic_notification(topic) do
Creator.create_single(ForumSubscription, ForumTopicNotification, :topic_id, topic)
end
@doc """
Updates a notification.
Creates a gallery image notification, returning the number of affected users.
## Examples
iex> update_notification(notification, %{field: new_value})
{:ok, %Notification{}}
iex> update_notification(notification, %{field: bad_value})
{:error, %Ecto.Changeset{}}
iex> create_gallery_image_notification(gallery)
{:ok, 2}
"""
def update_notification(%Notification{} = notification, attrs) do
notification
|> Notification.changeset(attrs)
|> Repo.insert_or_update()
def create_gallery_image_notification(gallery) do
Creator.create_single(GallerySubscription, GalleryImageNotification, :gallery_id, gallery)
end
@doc """
Deletes a Notification.
Creates an image comment notification, returning the number of affected users.
## Examples
iex> delete_notification(notification)
{:ok, %Notification{}}
iex> delete_notification(notification)
{:error, %Ecto.Changeset{}}
iex> create_image_comment_notification(image, comment)
{:ok, 2}
"""
def delete_notification(%Notification{} = notification) do
Repo.delete(notification)
def create_image_comment_notification(image, comment) do
Creator.create_double(
ImageSubscription,
ImageCommentNotification,
:image_id,
image,
:comment_id,
comment
)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking notification changes.
Creates an image merge notification, returning the number of affected users.
## Examples
iex> change_notification(notification)
%Ecto.Changeset{source: %Notification{}}
iex> create_image_merge_notification(target, source)
{:ok, 2}
"""
def change_notification(%Notification{} = notification) do
Notification.changeset(notification, %{})
end
def count_unread_notifications(user) do
UnreadNotification
|> where(user_id: ^user.id)
|> Repo.aggregate(:count, :notification_id)
def create_image_merge_notification(target, source) do
Creator.create_double(
ImageSubscription,
ImageMergeNotification,
:target_id,
target,
:source_id,
source
)
end
@doc """
Creates a unread_notification.
Removes the channel live notification for a given channel and user, returning
the number of affected users.
## Examples
iex> create_unread_notification(%{field: value})
{:ok, %UnreadNotification{}}
iex> create_unread_notification(%{field: bad_value})
{:error, %Ecto.Changeset{}}
iex> clear_channel_live_notification(channel, user)
{:ok, 2}
"""
def create_unread_notification(attrs \\ %{}) do
%UnreadNotification{}
|> UnreadNotification.changeset(attrs)
|> Repo.insert()
def clear_channel_live_notification(channel, user) do
ChannelLiveNotification
|> where(channel_id: ^channel.id)
|> Creator.clear(user)
end
@doc """
Updates a unread_notification.
Removes the forum post notification for a given topic and user, returning
the number of affected notifications.
## Examples
iex> update_unread_notification(unread_notification, %{field: new_value})
{:ok, %UnreadNotification{}}
iex> update_unread_notification(unread_notification, %{field: bad_value})
{:error, %Ecto.Changeset{}}
iex> clear_forum_post_notification(topic, user)
{:ok, 2}
"""
def update_unread_notification(%UnreadNotification{} = unread_notification, attrs) do
unread_notification
|> UnreadNotification.changeset(attrs)
|> Repo.update()
def clear_forum_post_notification(topic, user) do
ForumPostNotification
|> where(topic_id: ^topic.id)
|> Creator.clear(user)
end
@doc """
Deletes a UnreadNotification.
Removes the forum topic notification for a given topic and user, returning
the number of affected notifications.
## Examples
iex> delete_unread_notification(unread_notification)
{:ok, %UnreadNotification{}}
iex> delete_unread_notification(unread_notification)
{:error, %Ecto.Changeset{}}
iex> clear_forum_topic_notification(topic, user)
{:ok, 2}
"""
def delete_unread_notification(actor_type, actor_id, user) do
notification =
Notification
|> where(actor_type: ^actor_type, actor_id: ^actor_id)
|> Repo.one()
if notification do
UnreadNotification
|> where(notification_id: ^notification.id, user_id: ^user.id)
|> Repo.delete_all()
end
def clear_forum_topic_notification(topic, user) do
ForumTopicNotification
|> where(topic_id: ^topic.id)
|> Creator.clear(user)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking unread_notification changes.
Removes the gallery image notification for a given gallery and user, returning
the number of affected notifications.
## Examples
iex> change_unread_notification(unread_notification)
%Ecto.Changeset{source: %UnreadNotification{}}
iex> clear_gallery_image_notification(topic, user)
{:ok, 2}
"""
def change_unread_notification(%UnreadNotification{} = unread_notification) do
UnreadNotification.changeset(unread_notification, %{})
def clear_gallery_image_notification(gallery, user) do
GalleryImageNotification
|> where(gallery_id: ^gallery.id)
|> Creator.clear(user)
end
def notify(_actor_child, [], _params), do: nil
@doc """
Removes the image comment notification for a given image and user, returning
the number of affected notifications.
def notify(actor_child, subscriptions, params) do
# Don't push to the user that created the notification
subscriptions =
case actor_child do
%{user_id: id} ->
subscriptions
|> Enum.reject(&(&1.user_id == id))
## Examples
_ ->
subscriptions
iex> clear_gallery_image_notification(topic, user)
{:ok, 2}
"""
def clear_image_comment_notification(image, user) do
ImageCommentNotification
|> where(image_id: ^image.id)
|> Creator.clear(user)
end
Repo.transaction(fn ->
notification =
Notification
|> Repo.get_by(actor_id: params.actor_id, actor_type: params.actor_type)
@doc """
Removes the image merge notification for a given image and user, returning
the number of affected notifications.
{:ok, notification} =
(notification || %Notification{})
|> update_notification(params)
## Examples
# Insert the notification to any watchers who do not have it
unreads =
subscriptions
|> Enum.map(&%{user_id: &1.user_id, notification_id: notification.id})
iex> clear_image_merge_notification(topic, user)
{:ok, 2}
UnreadNotification
|> Repo.insert_all(unreads, on_conflict: :nothing)
end)
"""
def clear_image_merge_notification(image, user) do
ImageMergeNotification
|> where(target_id: ^image.id)
|> Creator.clear(user)
end
end

View file

@ -1,10 +1,17 @@
defmodule Philomena.Notifications.Category do
@moduledoc """
Notification category determination.
Notification category querying.
"""
import Ecto.Query, warn: false
alias Philomena.Notifications.Notification
alias Philomena.Repo
alias Philomena.Notifications.ChannelLiveNotification
alias Philomena.Notifications.ForumPostNotification
alias Philomena.Notifications.ForumTopicNotification
alias Philomena.Notifications.GalleryImageNotification
alias Philomena.Notifications.ImageCommentNotification
alias Philomena.Notifications.ImageMergeNotification
@type t ::
:channel_live
@ -15,79 +22,145 @@ defmodule Philomena.Notifications.Category do
| :image_merge
@doc """
Return a list of all supported types.
Return a list of all supported categories.
"""
def types do
def categories do
[
:channel_live,
:forum_post,
:forum_topic,
:gallery_image,
:image_comment,
:image_merge,
:forum_post
:image_merge
]
end
@doc """
Determine the type of a `m:Philomena.Notifications.Notification`.
Return the count of all currently unread notifications for the user in all categories.
## Examples
iex> total_unread_notification_count(user)
15
"""
def notification_type(n) do
case {n.actor_type, n.actor_child_type} do
{"Channel", _} ->
:channel_live
def total_unread_notification_count(user) do
categories()
|> Enum.map(fn category ->
category
|> query_for_category_and_user(user)
|> exclude(:preload)
|> select([_], %{one: 1})
end)
|> union_all_queries()
|> Repo.aggregate(:count)
end
{"Gallery", _} ->
:gallery_image
defp union_all_queries([query | rest]) do
Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end)
end
{"Image", "Comment"} ->
@doc """
Gather up and return the top N notifications for the user, for each category of
unread notification currently existing.
## Examples
iex> unread_notifications_for_user(user, page_size: 10)
%{
channel_live: [],
forum_post: [%ForumPostNotification{...}, ...],
forum_topic: [%ForumTopicNotification{...}, ...],
gallery_image: [],
image_comment: [%ImageCommentNotification{...}, ...],
image_merge: []
}
"""
def unread_notifications_for_user(user, pagination) do
Map.new(categories(), fn category ->
results =
category
|> query_for_category_and_user(user)
|> order_by(desc: :updated_at)
|> Repo.paginate(pagination)
{category, results}
end)
end
@doc """
Returns paginated unread notifications for the user, given the category.
## Examples
iex> unread_notifications_for_user_and_category(user, :image_comment)
[%ImageCommentNotification{...}]
"""
def unread_notifications_for_user_and_category(user, category, pagination) do
category
|> query_for_category_and_user(user)
|> order_by(desc: :updated_at)
|> Repo.paginate(pagination)
end
@doc """
Determine the category of a notification.
## Examples
iex> notification_category(%ImageCommentNotification{})
:image_comment
{"Image", _} ->
:image_merge
{"Topic", "Post"} ->
if n.action == "posted a new reply in" do
:forum_post
else
:forum_topic
end
"""
def notification_category(n) do
case n.__struct__ do
ChannelLiveNotification -> :channel_live
GalleryImageNotification -> :gallery_image
ImageCommentNotification -> :image_comment
ImageMergeNotification -> :image_merge
ForumPostNotification -> :forum_post
ForumTopicNotification -> :forum_topic
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)
Returns an `m:Ecto.Query` that finds unread notifications for the given category,
for the given user, with preloads applied.
case type do
## Examples
iex> query_for_category_and_user(:channel_live, user)
#Ecto.Query<from c0 in ChannelLiveNotification, where: c0.user_id == ^1, preload: [:channel]>
"""
def query_for_category_and_user(category, user) do
query =
case category do
:channel_live ->
where(base, [n], n.actor_type == "Channel")
from(n in ChannelLiveNotification, preload: :channel)
:gallery_image ->
where(base, [n], n.actor_type == "Gallery")
from(n in GalleryImageNotification, preload: [gallery: :creator])
:image_comment ->
where(base, [n], n.actor_type == "Image" and n.actor_child_type == "Comment")
from(n in ImageCommentNotification,
preload: [image: [:sources, tags: :aliases], comment: :user]
)
:image_merge ->
where(base, [n], n.actor_type == "Image" and is_nil(n.actor_child_type))
from(n in ImageMergeNotification,
preload: [:source, target: [:sources, tags: :aliases]]
)
:forum_topic ->
where(
base,
[n],
n.actor_type == "Topic" and n.actor_child_type == "Post" and
n.action != "posted a new reply in"
)
from(n in ForumTopicNotification, preload: [topic: [:forum, :user]])
:forum_post ->
where(
base,
[n],
n.actor_type == "Topic" and n.actor_child_type == "Post" and
n.action == "posted a new reply in"
)
from(n in ForumPostNotification, preload: [topic: :forum, post: :user])
end
where(query, user_id: ^user.id)
end
end

View file

@ -0,0 +1,25 @@
defmodule Philomena.Notifications.ChannelLiveNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Channels.Channel
@primary_key false
schema "channel_live_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :channel, Channel, primary_key: true
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(channel_live_notification, attrs) do
channel_live_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -0,0 +1,123 @@
defmodule Philomena.Notifications.Creator do
@moduledoc """
Internal notifications creation logic.
Supports two formats for notification creation:
- Key-only (`create_single/4`): The object's id is the only other component inserted.
- Non-key (`create_double/6`): The object's id plus another object's id are inserted.
See the respective documentation for each function for more details.
"""
import Ecto.Query, warn: false
alias Philomena.Repo
@doc """
Propagate notifications for a notification table type containing a single reference column.
The single reference column (`name`, `object`) is also part of the unique key for the table,
and is used to select which object to act on.
Returns `{:ok, count}`, where `count` is the number of affected rows.
## Example
iex> create_single(GallerySubscription, GalleryImageNotification, :gallery_id, gallery)
{:ok, 2}
"""
def create_single(subscription, notification, name, object) do
subscription
|> create_notification_query(name, object)
|> create_notification(notification, name)
end
@doc """
Propagate notifications for a notification table type containing two reference columns.
The first reference column (`name1`, `object1`) is also part of the unique key for the table,
and is used to select which object to act on.
Returns `{:ok, count}`, where `count` is the number of affected rows.
## Example
iex> create_double(
...> ImageSubscription,
...> ImageCommentNotification,
...> :image_id,
...> image,
...> :comment_id,
...> comment
...> )
{:ok, 2}
"""
def create_double(subscription, notification, name1, object1, name2, object2) do
subscription
|> create_notification_query(name1, object1, name2, object2)
|> create_notification(notification, name1)
end
@doc """
Clear all unread notifications using the given query.
Returns `{:ok, count}`, where `count` is the number of affected rows.
"""
def clear(query, user) do
if user do
{count, nil} =
query
|> where(user_id: ^user.id)
|> Repo.delete_all()
{:ok, count}
else
{:ok, 0}
end
end
# TODO: the following cannot be accomplished with a single query expression
# due to this Ecto bug: https://github.com/elixir-ecto/ecto/issues/4430
defp create_notification_query(subscription, name, object) do
now = DateTime.utc_now(:second)
from s in subscription,
where: field(s, ^name) == ^object.id,
select: %{
^name => type(^object.id, :integer),
user_id: s.user_id,
created_at: ^now,
updated_at: ^now,
read: false
}
end
defp create_notification_query(subscription, name1, object1, name2, object2) do
now = DateTime.utc_now(:second)
from s in subscription,
where: field(s, ^name1) == ^object1.id,
select: %{
^name1 => type(^object1.id, :integer),
^name2 => type(^object2.id, :integer),
user_id: s.user_id,
created_at: ^now,
updated_at: ^now,
read: false
}
end
defp create_notification(query, notification, name) do
{count, nil} =
Repo.insert_all(
notification,
query,
on_conflict: {:replace_all_except, [:created_at]},
conflict_target: [name, :user_id]
)
{:ok, count}
end
end

View file

@ -0,0 +1,27 @@
defmodule Philomena.Notifications.ForumPostNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Topics.Topic
alias Philomena.Posts.Post
@primary_key false
schema "forum_post_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :topic, Topic, primary_key: true
belongs_to :post, Post
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(forum_post_notification, attrs) do
forum_post_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -0,0 +1,25 @@
defmodule Philomena.Notifications.ForumTopicNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Topics.Topic
@primary_key false
schema "forum_topic_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :topic, Topic, primary_key: true
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(forum_topic_notification, attrs) do
forum_topic_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -0,0 +1,25 @@
defmodule Philomena.Notifications.GalleryImageNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Galleries.Gallery
@primary_key false
schema "gallery_image_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :gallery, Gallery, primary_key: true
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(gallery_image_notification, attrs) do
gallery_image_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -0,0 +1,27 @@
defmodule Philomena.Notifications.ImageCommentNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Images.Image
alias Philomena.Comments.Comment
@primary_key false
schema "image_comment_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :image, Image, primary_key: true
belongs_to :comment, Comment
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(image_comment_notification, attrs) do
image_comment_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -0,0 +1,26 @@
defmodule Philomena.Notifications.ImageMergeNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Images.Image
@primary_key false
schema "image_merge_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :target, Image, primary_key: true
belongs_to :source, Image
field :read, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(image_merge_notification, attrs) do
image_merge_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -1,26 +0,0 @@
defmodule Philomena.Notifications.Notification do
use Ecto.Schema
import Ecto.Changeset
schema "notifications" do
field :action, :string
# fixme: rails polymorphic relation
field :actor_id, :integer
field :actor_type, :string
field :actor_child_id, :integer
field :actor_child_type, :string
field :actor, :any, virtual: true
field :actor_child, :any, virtual: true
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
@doc false
def changeset(notification, attrs) do
notification
|> cast(attrs, [:actor_id, :actor_type, :actor_child_id, :actor_child_type, :action])
|> validate_required([:actor_id, :actor_type, :action])
end
end

View file

@ -1,21 +0,0 @@
defmodule Philomena.Notifications.UnreadNotification do
use Ecto.Schema
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Notifications.Notification
@primary_key false
schema "unread_notifications" do
belongs_to :user, User, primary_key: true
belongs_to :notification, Notification, primary_key: true
end
@doc false
def changeset(unread_notification, attrs) do
unread_notification
|> cast(attrs, [])
|> validate_required([])
end
end

View file

@ -128,22 +128,7 @@ defmodule Philomena.Posts do
|> Repo.preload(:topic)
|> Map.fetch!(:topic)
subscriptions =
topic
|> Repo.preload(:subscriptions)
|> Map.fetch!(:subscriptions)
Notifications.notify(
post,
subscriptions,
%{
actor_id: topic.id,
actor_type: "Topic",
actor_child_id: post.id,
actor_child_type: "Post",
action: "posted a new reply in"
}
)
Notifications.create_forum_post_notification(topic, post)
end
@doc """

View file

@ -2,35 +2,26 @@ defmodule Philomena.Subscriptions do
@moduledoc """
Common subscription logic.
`use Philomena.Subscriptions` requires the following properties:
- `:actor_types`
This is the "actor_type" in the notifications table.
For `Philomena.Images`, this would be `["Image"]`.
`use Philomena.Subscriptions` requires the following option:
- `:id_name`
This is the name of the object field in the subscription table.
For `Philomena.Images`, this would be `:image_id`.
For `m:Philomena.Images`, this would be `:image_id`.
The following functions and documentation are produced in the calling module:
- `subscribed?/2`
- `subscriptions/2`
- `create_subscription/2`
- `delete_subscription/2`
- `clear_notification/2`
- `maybe_subscribe_on/4`
"""
import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Notifications
alias Philomena.Repo
defmacro __using__(opts) do
# For Philomena.Images, this yields ["Image"]
actor_types = Keyword.fetch!(opts, :actor_types)
# For Philomena.Images, this yields :image_id
field_name = Keyword.fetch!(opts, :id_name)
@ -109,8 +100,6 @@ defmodule Philomena.Subscriptions do
"""
def delete_subscription(object, user) do
clear_notification(object, user)
Philomena.Subscriptions.delete_subscription(
unquote(subscription_module),
unquote(field_name),
@ -119,23 +108,6 @@ defmodule Philomena.Subscriptions do
)
end
@doc """
Deletes any active notifications for a subscription.
## Examples
iex> clear_notification(object, user)
:ok
"""
def clear_notification(object, user) do
for type <- unquote(actor_types) do
Philomena.Subscriptions.clear_notification(type, object, user)
end
:ok
end
@doc """
Creates a subscription inside the `m:Ecto.Multi` flow if `user` is not nil
and `field` in `user` is `true`.
@ -199,14 +171,6 @@ defmodule Philomena.Subscriptions do
|> Repo.delete()
end
@doc false
def clear_notification(type, object, user) do
case user do
nil -> nil
_ -> Notifications.delete_unread_notification(type, object.id, user)
end
end
@doc false
def maybe_subscribe_on(multi, module, change_name, user, field)
when field in [:watch_on_reply, :watch_on_upload, :watch_on_new_topic] do

View file

@ -14,7 +14,6 @@ defmodule Philomena.Topics do
alias Philomena.NotificationWorker
use Philomena.Subscriptions,
actor_types: ~w(Topic),
id_name: :topic_id
@doc """
@ -91,31 +90,10 @@ defmodule Philomena.Topics do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]])
end
def perform_notify([topic_id, post_id]) do
def perform_notify([topic_id, _post_id]) do
topic = get_topic!(topic_id)
post = Posts.get_post!(post_id)
forum =
topic
|> Repo.preload(:forum)
|> Map.fetch!(:forum)
subscriptions =
forum
|> Repo.preload(:subscriptions)
|> Map.fetch!(:subscriptions)
Notifications.notify(
post,
subscriptions,
%{
actor_id: topic.id,
actor_type: "Topic",
actor_child_id: post.id,
actor_child_type: "Post",
action: "posted a new topic in #{forum.name}"
}
)
Notifications.create_forum_topic_notification(topic)
end
@doc """
@ -242,4 +220,19 @@ defmodule Philomena.Topics do
|> Topic.title_changeset(attrs)
|> Repo.update()
end
@doc """
Removes all topic notifications for a given topic and user.
## Examples
iex> clear_topic_notification(topic, user)
:ok
"""
def clear_topic_notification(%Topic{} = topic, user) do
Notifications.clear_forum_post_notification(topic, user)
Notifications.clear_forum_topic_notification(topic, user)
:ok
end
end

View file

@ -12,7 +12,6 @@ defmodule Philomena.Users.User do
alias Philomena.Filters.Filter
alias Philomena.ArtistLinks.ArtistLink
alias Philomena.Badges
alias Philomena.Notifications.UnreadNotification
alias Philomena.Galleries.Gallery
alias Philomena.Users.User
alias Philomena.Commissions.Commission
@ -30,8 +29,6 @@ defmodule Philomena.Users.User do
has_many :public_links, ArtistLink, where: [public: true, aasm_state: "verified"]
has_many :galleries, Gallery, foreign_key: :creator_id
has_many :awards, Badges.Award
has_many :unread_notifications, UnreadNotification
has_many :notifications, through: [:unread_notifications, :notification]
has_many :linked_tags, through: [:verified_links, :tag]
has_many :user_ips, UserIp
has_many :user_fingerprints, UserFingerprint

View file

@ -11,7 +11,7 @@ defmodule PhilomenaWeb.Channel.ReadController do
channel = conn.assigns.channel
user = conn.assigns.current_user
Channels.clear_notification(channel, user)
Channels.clear_channel_notification(channel, user)
send_resp(conn, :ok, "")
end

View file

@ -37,7 +37,7 @@ defmodule PhilomenaWeb.ChannelController do
channel = conn.assigns.channel
user = conn.assigns.current_user
if user, do: Channels.clear_notification(channel, user)
Channels.clear_channel_notification(channel, user)
redirect(conn, external: channel_url(channel))
end

View file

@ -1,22 +0,0 @@
defmodule PhilomenaWeb.Forum.ReadController do
import Plug.Conn
use PhilomenaWeb, :controller
alias Philomena.Forums.Forum
alias Philomena.Forums
plug :load_resource,
model: Forum,
id_name: "forum_id",
id_field: "short_name",
persisted: true
def create(conn, _params) do
forum = conn.assigns.forum
user = conn.assigns.current_user
Forums.clear_notification(forum, user)
send_resp(conn, :ok, "")
end
end

View file

@ -11,7 +11,7 @@ defmodule PhilomenaWeb.Gallery.ReadController do
gallery = conn.assigns.gallery
user = conn.assigns.current_user
Galleries.clear_notification(gallery, user)
Galleries.clear_gallery_notification(gallery, user)
send_resp(conn, :ok, "")
end

View file

@ -80,7 +80,7 @@ defmodule PhilomenaWeb.GalleryController do
gallery_json = Jason.encode!(Enum.map(gallery_images, &elem(&1, 0).id))
Galleries.clear_notification(gallery, user)
Galleries.clear_gallery_notification(gallery, user)
conn
|> NotificationCountPlug.call([])

View file

@ -11,7 +11,7 @@ defmodule PhilomenaWeb.Image.ReadController do
image = conn.assigns.image
user = conn.assigns.current_user
Images.clear_notification(image, user)
Images.clear_image_notification(image, user)
send_resp(conn, :ok, "")
end

View file

@ -56,7 +56,7 @@ defmodule PhilomenaWeb.ImageController do
image = conn.assigns.image
user = conn.assigns.current_user
Images.clear_notification(image, user)
Images.clear_image_notification(image, user)
# Update the notification ticker in the header
conn = NotificationCountPlug.call(conn)

View file

@ -4,19 +4,19 @@ defmodule PhilomenaWeb.Notification.CategoryController do
alias Philomena.Notifications
def show(conn, params) do
type = category(params)
category_param = category(params)
notifications =
Notifications.unread_notifications_for_user_and_type(
Notifications.unread_notifications_for_user_and_category(
conn.assigns.current_user,
type,
category_param,
conn.assigns.scrivener
)
render(conn, "show.html",
title: "Notification Area",
notifications: notifications,
type: type
category: category_param
)
end

View file

@ -4,7 +4,11 @@ defmodule PhilomenaWeb.NotificationController do
alias Philomena.Notifications
def index(conn, _params) do
notifications = Notifications.unread_notifications_for_user(conn.assigns.current_user, 15)
notifications =
Notifications.unread_notifications_for_user(
conn.assigns.current_user,
page_size: 10
)
render(conn, "index.html", title: "Notification Area", notifications: notifications)
end

View file

@ -16,7 +16,7 @@ defmodule PhilomenaWeb.Topic.ReadController do
def create(conn, _params) do
user = conn.assigns.current_user
Topics.clear_notification(conn.assigns.topic, user)
Topics.clear_topic_notification(conn.assigns.topic, user)
send_resp(conn, :ok, "")
end

View file

@ -3,7 +3,7 @@ defmodule PhilomenaWeb.TopicController do
alias PhilomenaWeb.NotificationCountPlug
alias Philomena.{Forums.Forum, Topics.Topic, Posts.Post, Polls.Poll, PollOptions.PollOption}
alias Philomena.{Forums, Topics, Polls, Posts}
alias Philomena.{Topics, Polls, Posts}
alias Philomena.PollVotes
alias PhilomenaWeb.MarkdownRenderer
alias Philomena.Repo
@ -34,8 +34,7 @@ defmodule PhilomenaWeb.TopicController do
user = conn.assigns.current_user
Topics.clear_notification(topic, user)
Forums.clear_notification(forum, user)
Topics.clear_topic_notification(topic, user)
# Update the notification ticker in the header
conn = NotificationCountPlug.call(conn)

View file

@ -32,7 +32,7 @@ defmodule PhilomenaWeb.NotificationCountPlug do
defp maybe_assign_notifications(conn, nil), do: conn
defp maybe_assign_notifications(conn, user) do
notifications = Notifications.count_unread_notifications(user)
notifications = Notifications.total_unread_notification_count(user)
Conn.assign(conn, :notification_count, notifications)
end

View file

@ -263,8 +263,6 @@ defmodule PhilomenaWeb.Router do
resources "/subscription", Forum.SubscriptionController,
only: [:create, :delete],
singleton: true
resources "/read", Forum.ReadController, only: [:create], singleton: true
end
resources "/profiles", ProfileController, only: [] do

View file

@ -1,14 +1,14 @@
.flex.flex--centered.flex__grow
div
strong>
= link @notification.actor.title, to: ~p"/channels/#{@notification.actor}"
=<> @notification.action
= link @notification.channel.title, to: ~p"/channels/#{@notification.channel}"
' went live
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/channels/#{@notification.actor}/read" data-method="post" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button.button--separate-right title="Delete" href=~p"/channels/#{@notification.channel}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/channels/#{@notification.actor}/subscription" data-method="delete" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button title="Unsubscribe" href=~p"/channels/#{@notification.channel}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -0,0 +1,22 @@
- comment = @notification.comment
- image = @notification.image
.flex.flex--centered.flex__fixed.thumb-tiny-container.spacing-right
= render PhilomenaWeb.ImageView, "_image_container.html", image: image, size: :thumb_tiny, conn: @conn
.flex.flex--centered.flex__grow
div
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: comment, conn: @conn
' commented on
strong>
= link "##{image.id}", to: ~p"/images/#{image}" <> "#comments"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/images/#{image}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/images/#{image}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -1,25 +0,0 @@
- forum = @notification.actor
- topic = @notification.actor_child
.flex.flex--centered.flex__grow
div
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: topic, conn: @conn
=> @notification.action
' titled
strong>
= link topic.title, to: ~p"/forums/#{forum}/topics/#{topic}"
' in
=> link forum.name, to: ~p"/forums/#{forum}"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/forums/#{forum}/read" data-method="post" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/forums/#{forum}/subscription" data-method="delete" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
i.fa.fa-bell-slash

View file

@ -1,16 +1,18 @@
- gallery = @notification.gallery
.flex.flex--centered.flex__grow
div
=> render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: @notification.actor.creator}, conn: @conn
=> @notification.action
=> render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: gallery.creator}, conn: @conn
' added images to
strong>
= link @notification.actor.title, to: ~p"/galleries/#{@notification.actor}"
= link gallery.title, to: ~p"/galleries/#{gallery}"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/galleries/#{@notification.actor}/read" data-method="post" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button.button--separate-right title="Delete" href=~p"/galleries/#{gallery}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/galleries/#{@notification.actor}/subscription" data-method="delete" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button title="Unsubscribe" href=~p"/galleries/#{gallery}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -1,19 +1,24 @@
- target = @notification.target
- source = @notification.source
.flex.flex--centered.flex__fixed.thumb-tiny-container.spacing-right
= render PhilomenaWeb.ImageView, "_image_container.html", image: target, size: :thumb_tiny, conn: @conn
.flex.flex--centered.flex__grow
div
= if @notification.actor_child do
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @notification.actor_child, conn: @conn
- else
' Someone
=> @notification.action
| merged #
= source.id
' into
strong>
= link "##{@notification.actor_id}", to: ~p"/images/#{@notification.actor}" <> "#comments"
= link "##{target.id}", to: ~p"/images/#{target}" <> "#comments"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/images/#{@notification.actor}/read" data-method="post" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button.button--separate-right title="Delete" href=~p"/images/#{target}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/images/#{@notification.actor}/subscription" data-method="delete" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button title="Unsubscribe" href=~p"/images/#{target}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -1,7 +0,0 @@
= if @notification.actor do
.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
=> render PhilomenaWeb.NotificationView, notification_template_path(@notification.actor_type), notification: @notification, conn: @conn

View file

@ -0,0 +1,19 @@
- topic = @notification.topic
- post = @notification.post
.flex.flex--centered.flex__grow
div
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: post, conn: @conn
' posted a new reply in
strong>
= link topic.title, to: ~p"/forums/#{topic.forum}/topics/#{topic}?#{[post_id: post.id]}" <> "#post_#{post.id}"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/forums/#{topic.forum}/topics/#{topic}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/forums/#{topic.forum}/topics/#{topic}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -1,19 +1,23 @@
- topic = @notification.actor
- post = @notification.actor_child
- topic = @notification.topic
- forum = topic.forum
.flex.flex--centered.flex__grow
div
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: post, conn: @conn
=> @notification.action
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: topic, conn: @conn
' posted a new topic titled
strong>
= link topic.title, to: ~p"/forums/#{topic.forum}/topics/#{topic}?#{[post_id: post.id]}" <> "#post_#{post.id}"
= link topic.title, to: ~p"/forums/#{forum}/topics/#{topic}"
' in
=> link forum.name, to: ~p"/forums/#{forum}"
=> pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap
a.button.button--separate-right title="Delete" href=~p"/forums/#{topic.forum}/topics/#{topic}/read" data-method="post" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button.button--separate-right title="Delete" href=~p"/forums/#{forum}/topics/#{topic}/read" data-method="post" data-remote="true"
i.fa.fa-trash
a.button title="Unsubscribe" href=~p"/forums/#{topic.forum}/topics/#{topic}/subscription" data-method="delete" data-remote="true" data-fetchcomplete-hide="#notification-#{@notification.id}"
a.button title="Unsubscribe" href=~p"/forums/#{forum}/subscription" data-method="delete" data-remote="true"
i.fa.fa-bell-slash

View file

@ -2,18 +2,19 @@ h1 Notification Area
.walloftext
= cond do
- Enum.any?(@notifications) ->
- route = fn p -> ~p"/notifications/categories/#{@type}?#{p}" end
- route = fn p -> ~p"/notifications/categories/#{@category}?#{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)
span.block__header__title = name_of_category(@category)
.block__header.block__header__sub
= pagination
div
= for notification <- @notifications do
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
.block.block--fixed.flex.notification
= render PhilomenaWeb.NotificationView, notification_template_path(@category), notification: notification, conn: @conn
.block__header.block__header--light
= pagination

View file

@ -1,21 +1,21 @@
h1 Notification Area
.walloftext
= cond do
- Enum.any?(@notifications) ->
= for {type, notifications} <- @notifications do
= for {category, notifications} <- @notifications, Enum.any?(notifications) do
.block.notification-type-block
.block__header
span.block__header__title = name_of_type(type)
span.block__header__title = name_of_category(category)
div
= for notification <- notifications do
= render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn
.block.block--fixed.flex.notification
= render PhilomenaWeb.NotificationView, notification_template_path(category), notification: notification, conn: @conn
.block__header.block__header--light
a href=~p"/notifications/categories/#{type}"
| View category
a href=~p"/notifications/categories/#{category}"
| View category (
= notifications.total_entries
| )
- true ->
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.

View file

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

View file

@ -2,20 +2,20 @@ defmodule PhilomenaWeb.NotificationView do
use PhilomenaWeb, :view
@template_paths %{
"Channel" => "_channel.html",
"Forum" => "_forum.html",
"Gallery" => "_gallery.html",
"Image" => "_image.html",
"LivestreamChannel" => "_channel.html",
"Topic" => "_topic.html"
"channel_live" => "_channel.html",
"forum_post" => "_post.html",
"forum_topic" => "_topic.html",
"gallery_image" => "_gallery.html",
"image_comment" => "_comment.html",
"image_merge" => "_image.html"
}
def notification_template_path(actor_type) do
@template_paths[actor_type]
def notification_template_path(category) do
@template_paths[to_string(category)]
end
def name_of_type(notification_type) do
case notification_type do
def name_of_category(category) do
case category do
:channel_live ->
"Live channels"

View file

@ -0,0 +1,109 @@
defmodule Philomena.Repo.Migrations.NewNotifications do
use Ecto.Migration
@categories [
channel_live: [channels: :channel_id],
forum_post: [topics: :topic_id, posts: :post_id],
forum_topic: [topics: :topic_id],
gallery_image: [galleries: :gallery_id],
image_comment: [images: :image_id, comments: :comment_id],
image_merge: [images: :target_id, images: :source_id]
]
def up do
for {category, refs} <- @categories do
create table("#{category}_notifications", primary_key: false) do
for {target_table_name, reference_name} <- refs do
add reference_name, references(target_table_name, on_delete: :delete_all), null: false
end
add :user_id, references(:users, on_delete: :delete_all), null: false
timestamps(inserted_at: :created_at, type: :utc_datetime)
add :read, :boolean, default: false, null: false
end
{_primary_table_name, primary_ref_name} = hd(refs)
create index("#{category}_notifications", [:user_id, primary_ref_name], unique: true)
create index("#{category}_notifications", [:user_id, "updated_at desc"])
create index("#{category}_notifications", [:user_id, :read])
for {_target_table_name, reference_name} <- refs do
create index("#{category}_notifications", [reference_name])
end
end
insert_statements =
"""
insert into channel_live_notifications (channel_id, user_id, created_at, updated_at)
select n.actor_id, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Channel'
and exists(select 1 from channels c where c.id = n.actor_id)
and exists(select 1 from users u where u.id = un.user_id);
insert into forum_post_notifications (topic_id, post_id, user_id, created_at, updated_at)
select n.actor_id, n.actor_child_id, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Topic'
and n.actor_child_type = 'Post'
and n.action = 'posted a new reply in'
and exists(select 1 from topics t where t.id = n.actor_id)
and exists(select 1 from posts p where p.id = n.actor_child_id)
and exists(select 1 from users u where u.id = un.user_id);
insert into forum_topic_notifications (topic_id, user_id, created_at, updated_at)
select n.actor_id, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Topic'
and n.actor_child_type = 'Post'
and n.action <> 'posted a new reply in'
and exists(select 1 from topics t where t.id = n.actor_id)
and exists(select 1 from users u where u.id = un.user_id);
insert into gallery_image_notifications (gallery_id, user_id, created_at, updated_at)
select n.actor_id, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Gallery'
and exists(select 1 from galleries g where g.id = n.actor_id)
and exists(select 1 from users u where u.id = un.user_id);
insert into image_comment_notifications (image_id, comment_id, user_id, created_at, updated_at)
select n.actor_id, n.actor_child_id, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Image'
and n.actor_child_type = 'Comment'
and exists(select 1 from images i where i.id = n.actor_id)
and exists(select 1 from comments c where c.id = n.actor_child_id)
and exists(select 1 from users u where u.id = un.user_id);
insert into image_merge_notifications (target_id, source_id, user_id, created_at, updated_at)
select n.actor_id, regexp_replace(n.action, '[a-z#]+', '', 'g')::bigint, un.user_id, n.created_at, n.updated_at
from unread_notifications un
join notifications n on un.notification_id = n.id
where n.actor_type = 'Image'
and n.actor_child_type is null
and exists(select 1 from images i where i.id = n.actor_id)
and exists(select 1 from images i where i.id = regexp_replace(n.action, '[a-z#]+', '', 'g')::integer)
and exists(select 1 from users u where u.id = un.user_id);
"""
# These statements should not be run by the migration in production.
# Run them manually in psql instead.
if System.get_env("MIX_ENV") != "prod" do
for stmt <- String.split(insert_statements, "\n\n") do
execute(stmt)
end
end
end
def down do
for {category, _refs} <- @categories do
drop table("#{category}_notifications")
end
end
end

View file

@ -198,6 +198,19 @@ CREATE SEQUENCE public.badges_id_seq
ALTER SEQUENCE public.badges_id_seq OWNED BY public.badges.id;
--
-- Name: channel_live_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.channel_live_notifications (
channel_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: channel_subscriptions; Type: TABLE; Schema: public; Owner: -
--
@ -620,6 +633,20 @@ CREATE SEQUENCE public.fingerprint_bans_id_seq
ALTER SEQUENCE public.fingerprint_bans_id_seq OWNED BY public.fingerprint_bans.id;
--
-- Name: forum_post_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.forum_post_notifications (
topic_id bigint NOT NULL,
post_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: forum_subscriptions; Type: TABLE; Schema: public; Owner: -
--
@ -630,6 +657,19 @@ CREATE TABLE public.forum_subscriptions (
);
--
-- Name: forum_topic_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.forum_topic_notifications (
topic_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: forums; Type: TABLE; Schema: public; Owner: -
--
@ -709,6 +749,19 @@ CREATE SEQUENCE public.galleries_id_seq
ALTER SEQUENCE public.galleries_id_seq OWNED BY public.galleries.id;
--
-- Name: gallery_image_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.gallery_image_notifications (
gallery_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: gallery_interactions; Type: TABLE; Schema: public; Owner: -
--
@ -750,6 +803,20 @@ CREATE TABLE public.gallery_subscriptions (
);
--
-- Name: image_comment_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.image_comment_notifications (
image_id bigint NOT NULL,
comment_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: image_faves; Type: TABLE; Schema: public; Owner: -
--
@ -837,6 +904,20 @@ CREATE SEQUENCE public.image_intensities_id_seq
ALTER SEQUENCE public.image_intensities_id_seq OWNED BY public.image_intensities.id;
--
-- Name: image_merge_notifications; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.image_merge_notifications (
target_id bigint NOT NULL,
source_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp(0) without time zone NOT NULL,
updated_at timestamp(0) without time zone NOT NULL,
read boolean DEFAULT false NOT NULL
);
--
-- Name: image_sources; Type: TABLE; Schema: public; Owner: -
--
@ -2894,6 +2975,160 @@ ALTER TABLE ONLY public.versions
ADD CONSTRAINT versions_pkey PRIMARY KEY (id);
--
-- Name: channel_live_notifications_channel_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX channel_live_notifications_channel_id_index ON public.channel_live_notifications USING btree (channel_id);
--
-- Name: channel_live_notifications_user_id_channel_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX channel_live_notifications_user_id_channel_id_index ON public.channel_live_notifications USING btree (user_id, channel_id);
--
-- Name: channel_live_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX channel_live_notifications_user_id_read_index ON public.channel_live_notifications USING btree (user_id, read);
--
-- Name: channel_live_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX channel_live_notifications_user_id_updated_at_desc_index ON public.channel_live_notifications USING btree (user_id, updated_at DESC);
--
-- Name: forum_post_notifications_post_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_post_notifications_post_id_index ON public.forum_post_notifications USING btree (post_id);
--
-- Name: forum_post_notifications_topic_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_post_notifications_topic_id_index ON public.forum_post_notifications USING btree (topic_id);
--
-- Name: forum_post_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_post_notifications_user_id_read_index ON public.forum_post_notifications USING btree (user_id, read);
--
-- Name: forum_post_notifications_user_id_topic_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX forum_post_notifications_user_id_topic_id_index ON public.forum_post_notifications USING btree (user_id, topic_id);
--
-- Name: forum_post_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_post_notifications_user_id_updated_at_desc_index ON public.forum_post_notifications USING btree (user_id, updated_at DESC);
--
-- Name: forum_topic_notifications_topic_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_topic_notifications_topic_id_index ON public.forum_topic_notifications USING btree (topic_id);
--
-- Name: forum_topic_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_topic_notifications_user_id_read_index ON public.forum_topic_notifications USING btree (user_id, read);
--
-- Name: forum_topic_notifications_user_id_topic_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX forum_topic_notifications_user_id_topic_id_index ON public.forum_topic_notifications USING btree (user_id, topic_id);
--
-- Name: forum_topic_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX forum_topic_notifications_user_id_updated_at_desc_index ON public.forum_topic_notifications USING btree (user_id, updated_at DESC);
--
-- Name: gallery_image_notifications_gallery_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX gallery_image_notifications_gallery_id_index ON public.gallery_image_notifications USING btree (gallery_id);
--
-- Name: gallery_image_notifications_user_id_gallery_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX gallery_image_notifications_user_id_gallery_id_index ON public.gallery_image_notifications USING btree (user_id, gallery_id);
--
-- Name: gallery_image_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX gallery_image_notifications_user_id_read_index ON public.gallery_image_notifications USING btree (user_id, read);
--
-- Name: gallery_image_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX gallery_image_notifications_user_id_updated_at_desc_index ON public.gallery_image_notifications USING btree (user_id, updated_at DESC);
--
-- Name: image_comment_notifications_comment_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_comment_notifications_comment_id_index ON public.image_comment_notifications USING btree (comment_id);
--
-- Name: image_comment_notifications_image_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_comment_notifications_image_id_index ON public.image_comment_notifications USING btree (image_id);
--
-- Name: image_comment_notifications_user_id_image_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX image_comment_notifications_user_id_image_id_index ON public.image_comment_notifications USING btree (user_id, image_id);
--
-- Name: image_comment_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_comment_notifications_user_id_read_index ON public.image_comment_notifications USING btree (user_id, read);
--
-- Name: image_comment_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_comment_notifications_user_id_updated_at_desc_index ON public.image_comment_notifications USING btree (user_id, updated_at DESC);
--
-- Name: image_intensities_index; Type: INDEX; Schema: public; Owner: -
--
@ -2901,6 +3136,41 @@ ALTER TABLE ONLY public.versions
CREATE INDEX image_intensities_index ON public.image_intensities USING btree (nw, ne, sw, se);
--
-- Name: image_merge_notifications_source_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_merge_notifications_source_id_index ON public.image_merge_notifications USING btree (source_id);
--
-- Name: image_merge_notifications_target_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_merge_notifications_target_id_index ON public.image_merge_notifications USING btree (target_id);
--
-- Name: image_merge_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_merge_notifications_user_id_read_index ON public.image_merge_notifications USING btree (user_id, read);
--
-- Name: image_merge_notifications_user_id_target_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX image_merge_notifications_user_id_target_id_index ON public.image_merge_notifications USING btree (user_id, target_id);
--
-- Name: image_merge_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX image_merge_notifications_user_id_updated_at_desc_index ON public.image_merge_notifications USING btree (user_id, updated_at DESC);
--
-- Name: image_sources_image_id_source_index; Type: INDEX; Schema: public; Owner: -
--
@ -4175,6 +4445,22 @@ CREATE UNIQUE INDEX user_tokens_context_token_index ON public.user_tokens USING
CREATE INDEX user_tokens_user_id_index ON public.user_tokens USING btree (user_id);
--
-- Name: channel_live_notifications channel_live_notifications_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.channel_live_notifications
ADD CONSTRAINT channel_live_notifications_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;
--
-- Name: channel_live_notifications channel_live_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.channel_live_notifications
ADD CONSTRAINT channel_live_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: channels fk_rails_021c624081; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -4967,6 +5253,110 @@ ALTER TABLE ONLY public.gallery_subscriptions
ADD CONSTRAINT fk_rails_fa77f3cebe FOREIGN KEY (gallery_id) REFERENCES public.galleries(id) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- Name: forum_post_notifications forum_post_notifications_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.forum_post_notifications
ADD CONSTRAINT forum_post_notifications_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;
--
-- Name: forum_post_notifications forum_post_notifications_topic_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.forum_post_notifications
ADD CONSTRAINT forum_post_notifications_topic_id_fkey FOREIGN KEY (topic_id) REFERENCES public.topics(id) ON DELETE CASCADE;
--
-- Name: forum_post_notifications forum_post_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.forum_post_notifications
ADD CONSTRAINT forum_post_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: forum_topic_notifications forum_topic_notifications_topic_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.forum_topic_notifications
ADD CONSTRAINT forum_topic_notifications_topic_id_fkey FOREIGN KEY (topic_id) REFERENCES public.topics(id) ON DELETE CASCADE;
--
-- Name: forum_topic_notifications forum_topic_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.forum_topic_notifications
ADD CONSTRAINT forum_topic_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: gallery_image_notifications gallery_image_notifications_gallery_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.gallery_image_notifications
ADD CONSTRAINT gallery_image_notifications_gallery_id_fkey FOREIGN KEY (gallery_id) REFERENCES public.galleries(id) ON DELETE CASCADE;
--
-- Name: gallery_image_notifications gallery_image_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.gallery_image_notifications
ADD CONSTRAINT gallery_image_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: image_comment_notifications image_comment_notifications_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_comment_notifications
ADD CONSTRAINT image_comment_notifications_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE;
--
-- Name: image_comment_notifications image_comment_notifications_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_comment_notifications
ADD CONSTRAINT image_comment_notifications_image_id_fkey FOREIGN KEY (image_id) REFERENCES public.images(id) ON DELETE CASCADE;
--
-- Name: image_comment_notifications image_comment_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_comment_notifications
ADD CONSTRAINT image_comment_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: image_merge_notifications image_merge_notifications_source_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_merge_notifications
ADD CONSTRAINT image_merge_notifications_source_id_fkey FOREIGN KEY (source_id) REFERENCES public.images(id) ON DELETE CASCADE;
--
-- Name: image_merge_notifications image_merge_notifications_target_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_merge_notifications
ADD CONSTRAINT image_merge_notifications_target_id_fkey FOREIGN KEY (target_id) REFERENCES public.images(id) ON DELETE CASCADE;
--
-- Name: image_merge_notifications image_merge_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.image_merge_notifications
ADD CONSTRAINT image_merge_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- Name: image_sources image_sources_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -5056,3 +5446,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20211107130226);
INSERT INTO public."schema_migrations" (version) VALUES (20211219194836);
INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);
INSERT INTO public."schema_migrations" (version) VALUES (20240723122759);
INSERT INTO public."schema_migrations" (version) VALUES (20240728191353);