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.AutomaticUpdater
alias Philomena.Channels.Channel alias Philomena.Channels.Channel
alias Philomena.Notifications
alias Philomena.Tags alias Philomena.Tags
use Philomena.Subscriptions, use Philomena.Subscriptions,
actor_types: ~w(Channel LivestreamChannel),
id_name: :channel_id id_name: :channel_id
@doc """ @doc """
@ -139,4 +139,18 @@ defmodule Philomena.Channels do
def change_channel(%Channel{} = channel) do def change_channel(%Channel{} = channel) do
Channel.changeset(channel, %{}) Channel.changeset(channel, %{})
end 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 end

View file

@ -79,22 +79,7 @@ defmodule Philomena.Comments do
|> Repo.preload(:image) |> Repo.preload(:image)
|> Map.fetch!(:image) |> Map.fetch!(:image)
subscriptions = Notifications.create_image_comment_notification(image, comment)
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"
}
)
end end
@doc """ @doc """

View file

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

View file

@ -19,7 +19,6 @@ defmodule Philomena.Galleries do
alias Philomena.Images alias Philomena.Images
use Philomena.Subscriptions, use Philomena.Subscriptions,
actor_types: ~w(Gallery),
id_name: :gallery_id id_name: :gallery_id
@doc """ @doc """
@ -269,25 +268,10 @@ defmodule Philomena.Galleries do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Galleries", [gallery.id, image.id]]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Galleries", [gallery.id, image.id]])
end end
def perform_notify([gallery_id, image_id]) do def perform_notify([gallery_id, _image_id]) do
gallery = get_gallery!(gallery_id) gallery = get_gallery!(gallery_id)
subscriptions = Notifications.create_gallery_image_notification(gallery)
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"
}
)
end end
def reorder_gallery(gallery, image_ids) do 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(%{order_position_asc: true}), do: [asc: :position]
defp position_order(_gallery), do: [desc: :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 end

View file

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

View file

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

View file

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

View file

@ -2,35 +2,26 @@ defmodule Philomena.Subscriptions do
@moduledoc """ @moduledoc """
Common subscription logic. Common subscription logic.
`use Philomena.Subscriptions` requires the following properties: `use Philomena.Subscriptions` requires the following option:
- `:actor_types`
This is the "actor_type" in the notifications table.
For `Philomena.Images`, this would be `["Image"]`.
- `:id_name` - `:id_name`
This is the name of the object field in the subscription table. 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: The following functions and documentation are produced in the calling module:
- `subscribed?/2` - `subscribed?/2`
- `subscriptions/2` - `subscriptions/2`
- `create_subscription/2` - `create_subscription/2`
- `delete_subscription/2` - `delete_subscription/2`
- `clear_notification/2`
- `maybe_subscribe_on/4` - `maybe_subscribe_on/4`
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Ecto.Multi alias Ecto.Multi
alias Philomena.Notifications
alias Philomena.Repo alias Philomena.Repo
defmacro __using__(opts) do defmacro __using__(opts) do
# For Philomena.Images, this yields ["Image"]
actor_types = Keyword.fetch!(opts, :actor_types)
# For Philomena.Images, this yields :image_id # For Philomena.Images, this yields :image_id
field_name = Keyword.fetch!(opts, :id_name) field_name = Keyword.fetch!(opts, :id_name)
@ -109,8 +100,6 @@ defmodule Philomena.Subscriptions do
""" """
def delete_subscription(object, user) do def delete_subscription(object, user) do
clear_notification(object, user)
Philomena.Subscriptions.delete_subscription( Philomena.Subscriptions.delete_subscription(
unquote(subscription_module), unquote(subscription_module),
unquote(field_name), unquote(field_name),
@ -119,23 +108,6 @@ defmodule Philomena.Subscriptions do
) )
end 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 """ @doc """
Creates a subscription inside the `m:Ecto.Multi` flow if `user` is not nil Creates a subscription inside the `m:Ecto.Multi` flow if `user` is not nil
and `field` in `user` is `true`. and `field` in `user` is `true`.
@ -199,14 +171,6 @@ defmodule Philomena.Subscriptions do
|> Repo.delete() |> Repo.delete()
end 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 @doc false
def maybe_subscribe_on(multi, module, change_name, user, field) def maybe_subscribe_on(multi, module, change_name, user, field)
when field in [:watch_on_reply, :watch_on_upload, :watch_on_new_topic] do 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 alias Philomena.NotificationWorker
use Philomena.Subscriptions, use Philomena.Subscriptions,
actor_types: ~w(Topic),
id_name: :topic_id id_name: :topic_id
@doc """ @doc """
@ -91,31 +90,10 @@ defmodule Philomena.Topics do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]])
end end
def perform_notify([topic_id, post_id]) do def perform_notify([topic_id, _post_id]) do
topic = get_topic!(topic_id) topic = get_topic!(topic_id)
post = Posts.get_post!(post_id)
forum = Notifications.create_forum_topic_notification(topic)
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}"
}
)
end end
@doc """ @doc """
@ -242,4 +220,19 @@ defmodule Philomena.Topics do
|> Topic.title_changeset(attrs) |> Topic.title_changeset(attrs)
|> Repo.update() |> Repo.update()
end 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 end

View file

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

View file

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

View file

@ -37,7 +37,7 @@ defmodule PhilomenaWeb.ChannelController do
channel = conn.assigns.channel channel = conn.assigns.channel
user = conn.assigns.current_user 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)) redirect(conn, external: channel_url(channel))
end 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 gallery = conn.assigns.gallery
user = conn.assigns.current_user user = conn.assigns.current_user
Galleries.clear_notification(gallery, user) Galleries.clear_gallery_notification(gallery, user)
send_resp(conn, :ok, "") send_resp(conn, :ok, "")
end end

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,11 @@ defmodule PhilomenaWeb.NotificationController do
alias Philomena.Notifications alias Philomena.Notifications
def index(conn, _params) do 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) render(conn, "index.html", title: "Notification Area", notifications: notifications)
end end

View file

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

View file

@ -3,7 +3,7 @@ defmodule PhilomenaWeb.TopicController do
alias PhilomenaWeb.NotificationCountPlug alias PhilomenaWeb.NotificationCountPlug
alias Philomena.{Forums.Forum, Topics.Topic, Posts.Post, Polls.Poll, PollOptions.PollOption} 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 Philomena.PollVotes
alias PhilomenaWeb.MarkdownRenderer alias PhilomenaWeb.MarkdownRenderer
alias Philomena.Repo alias Philomena.Repo
@ -34,8 +34,7 @@ defmodule PhilomenaWeb.TopicController do
user = conn.assigns.current_user user = conn.assigns.current_user
Topics.clear_notification(topic, user) Topics.clear_topic_notification(topic, user)
Forums.clear_notification(forum, user)
# Update the notification ticker in the header # Update the notification ticker in the header
conn = NotificationCountPlug.call(conn) 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, nil), do: conn
defp maybe_assign_notifications(conn, user) do 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) Conn.assign(conn, :notification_count, notifications)
end end

View file

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

View file

@ -1,14 +1,14 @@
.flex.flex--centered.flex__grow .flex.flex--centered.flex__grow
div div
strong> strong>
= link @notification.actor.title, to: ~p"/channels/#{@notification.actor}" = link @notification.channel.title, to: ~p"/channels/#{@notification.channel}"
=<> @notification.action ' went live
=> pretty_time @notification.updated_at => pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap .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 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 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 .flex.flex--centered.flex__grow
div div
=> render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: @notification.actor.creator}, conn: @conn => render PhilomenaWeb.UserAttributionView, "_user.html", object: %{user: gallery.creator}, conn: @conn
=> @notification.action ' added images to
strong> strong>
= link @notification.actor.title, to: ~p"/galleries/#{@notification.actor}" = link gallery.title, to: ~p"/galleries/#{gallery}"
=> pretty_time @notification.updated_at => pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap .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 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 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 .flex.flex--centered.flex__grow
div div
= if @notification.actor_child do ' Someone
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @notification.actor_child, conn: @conn | merged #
- else = source.id
' Someone ' into
=> @notification.action
strong> strong>
= link "##{@notification.actor_id}", to: ~p"/images/#{@notification.actor}" <> "#comments" = link "##{target.id}", to: ~p"/images/#{target}" <> "#comments"
=> pretty_time @notification.updated_at => pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap .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 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 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 - topic = @notification.topic
- post = @notification.actor_child - forum = topic.forum
.flex.flex--centered.flex__grow .flex.flex--centered.flex__grow
div div
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: post, conn: @conn => render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: topic, conn: @conn
=> @notification.action ' posted a new topic titled
strong> 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 => pretty_time @notification.updated_at
.flex.flex--centered.flex--no-wrap .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 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 i.fa.fa-bell-slash

View file

@ -2,18 +2,19 @@ h1 Notification Area
.walloftext .walloftext
= cond do = cond do
- Enum.any?(@notifications) -> - 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 - pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @notifications, route: route, conn: @conn
.block.notification-type-block .block.notification-type-block
.block__header .block__header
span.block__header__title = name_of_type(@type) span.block__header__title = name_of_category(@category)
.block__header.block__header__sub .block__header.block__header__sub
= pagination = pagination
div div
= for notification <- @notifications do = 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 .block__header.block__header--light
= pagination = pagination

View file

@ -1,22 +1,22 @@
h1 Notification Area h1 Notification Area
.walloftext .walloftext
= cond do = for {category, notifications} <- @notifications, Enum.any?(notifications) do
- Enum.any?(@notifications) -> .block.notification-type-block
= for {type, notifications} <- @notifications do .block__header
.block.notification-type-block span.block__header__title = name_of_category(category)
.block__header
span.block__header__title = name_of_type(type)
div div
= for notification <- notifications do = 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 .block__header.block__header--light
a href=~p"/notifications/categories/#{type}" a href=~p"/notifications/categories/#{category}"
| View category | View category (
= notifications.total_entries
| )
- true -> p
p ' To get notifications on new comments and forum posts, click the
' 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.
' 'Subscribe' button in the bar at the top of an image or forum topic. ' You'll get notifications here for any new posts or comments.
' You'll get notifications here for any new posts or comments.

View file

@ -1,5 +1,6 @@
defmodule PhilomenaWeb.Notification.CategoryView do defmodule PhilomenaWeb.Notification.CategoryView do
use PhilomenaWeb, :view 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 end

View file

@ -2,20 +2,20 @@ defmodule PhilomenaWeb.NotificationView do
use PhilomenaWeb, :view use PhilomenaWeb, :view
@template_paths %{ @template_paths %{
"Channel" => "_channel.html", "channel_live" => "_channel.html",
"Forum" => "_forum.html", "forum_post" => "_post.html",
"Gallery" => "_gallery.html", "forum_topic" => "_topic.html",
"Image" => "_image.html", "gallery_image" => "_gallery.html",
"LivestreamChannel" => "_channel.html", "image_comment" => "_comment.html",
"Topic" => "_topic.html" "image_merge" => "_image.html"
} }
def notification_template_path(actor_type) do def notification_template_path(category) do
@template_paths[actor_type] @template_paths[to_string(category)]
end end
def name_of_type(notification_type) do def name_of_category(category) do
case notification_type do case category do
:channel_live -> :channel_live ->
"Live channels" "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; 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: - -- 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; 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: - -- 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: - -- 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; 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: - -- 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: - -- 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; 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: - -- Name: image_sources; Type: TABLE; Schema: public; Owner: -
-- --
@ -2894,6 +2975,160 @@ ALTER TABLE ONLY public.versions
ADD CONSTRAINT versions_pkey PRIMARY KEY (id); 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: - -- 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); 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: - -- 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); 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: - -- 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; 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: - -- 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 (20211219194836);
INSERT INTO public."schema_migrations" (version) VALUES (20220321173359); INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);
INSERT INTO public."schema_migrations" (version) VALUES (20240723122759); INSERT INTO public."schema_migrations" (version) VALUES (20240723122759);
INSERT INTO public."schema_migrations" (version) VALUES (20240728191353);