diff --git a/lib/philomena/channels.ex b/lib/philomena/channels.ex index 0f00cbe9..2ffd4ce4 100644 --- a/lib/philomena/channels.ex +++ b/lib/philomena/channels.ex @@ -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 diff --git a/lib/philomena/comments.ex b/lib/philomena/comments.ex index d3a819d9..281de6c0 100644 --- a/lib/philomena/comments.ex +++ b/lib/philomena/comments.ex @@ -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 """ diff --git a/lib/philomena/forums.ex b/lib/philomena/forums.ex index ba8006bc..7cec4205 100644 --- a/lib/philomena/forums.ex +++ b/lib/philomena/forums.ex @@ -9,7 +9,6 @@ defmodule Philomena.Forums do alias Philomena.Forums.Forum use Philomena.Subscriptions, - actor_types: ~w(Forum), id_name: :forum_id @doc """ diff --git a/lib/philomena/galleries.ex b/lib/philomena/galleries.ex index b60b8b72..4c76df35 100644 --- a/lib/philomena/galleries.ex +++ b/lib/philomena/galleries.ex @@ -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 diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 7d7b27ea..3aefa4c2 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -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 diff --git a/lib/philomena/notifications.ex b/lib/philomena/notifications.ex index cbff31ee..ea0b6029 100644 --- a/lib/philomena/notifications.ex +++ b/lib/philomena/notifications.ex @@ -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 - end + iex> clear_gallery_image_notification(topic, user) + {:ok, 2} - Repo.transaction(fn -> - notification = - Notification - |> Repo.get_by(actor_id: params.actor_id, actor_type: params.actor_type) + """ + def clear_image_comment_notification(image, user) do + ImageCommentNotification + |> where(image_id: ^image.id) + |> Creator.clear(user) + end - {:ok, notification} = - (notification || %Notification{}) - |> update_notification(params) + @doc """ + Removes the image merge notification for a given image and user, returning + the number of affected notifications. - # Insert the notification to any watchers who do not have it - unreads = - subscriptions - |> Enum.map(&%{user_id: &1.user_id, notification_id: notification.id}) + ## Examples - UnreadNotification - |> Repo.insert_all(unreads, on_conflict: :nothing) - end) + iex> clear_image_merge_notification(topic, user) + {:ok, 2} + + """ + def clear_image_merge_notification(image, user) do + ImageMergeNotification + |> where(target_id: ^image.id) + |> Creator.clear(user) end end diff --git a/lib/philomena/notifications/category.ex b/lib/philomena/notifications/category.ex index 775b888d..249dd838 100644 --- a/lib/philomena/notifications/category.ex +++ b/lib/philomena/notifications/category.ex @@ -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"} -> - :image_comment + @doc """ + Gather up and return the top N notifications for the user, for each category of + unread notification currently existing. - {"Image", _} -> - :image_merge + ## Examples - {"Topic", "Post"} -> - if n.action == "posted a new reply in" do - :forum_post - else - :forum_topic - end + 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 + + """ + 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. + 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 + """ - def query_for_type(type) do - base = from(n in Notification) + def query_for_category_and_user(category, user) do + query = + case category do + :channel_live -> + from(n in ChannelLiveNotification, preload: :channel) - case type do - :channel_live -> - where(base, [n], n.actor_type == "Channel") + :gallery_image -> + from(n in GalleryImageNotification, preload: [gallery: :creator]) - :gallery_image -> - where(base, [n], n.actor_type == "Gallery") + :image_comment -> + from(n in ImageCommentNotification, + preload: [image: [:sources, tags: :aliases], comment: :user] + ) - :image_comment -> - where(base, [n], n.actor_type == "Image" and n.actor_child_type == "Comment") + :image_merge -> + from(n in ImageMergeNotification, + preload: [:source, target: [:sources, tags: :aliases]] + ) - :image_merge -> - where(base, [n], n.actor_type == "Image" and is_nil(n.actor_child_type)) + :forum_topic -> + from(n in ForumTopicNotification, preload: [topic: [:forum, :user]]) - :forum_topic -> - where( - base, - [n], - n.actor_type == "Topic" and n.actor_child_type == "Post" and - n.action != "posted a new reply in" - ) + :forum_post -> + from(n in ForumPostNotification, preload: [topic: :forum, post: :user]) + end - :forum_post -> - where( - base, - [n], - n.actor_type == "Topic" and n.actor_child_type == "Post" and - n.action == "posted a new reply in" - ) - end + where(query, user_id: ^user.id) end end diff --git a/lib/philomena/notifications/channel_live_notification.ex b/lib/philomena/notifications/channel_live_notification.ex new file mode 100644 index 00000000..b60fb8e6 --- /dev/null +++ b/lib/philomena/notifications/channel_live_notification.ex @@ -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 diff --git a/lib/philomena/notifications/creator.ex b/lib/philomena/notifications/creator.ex new file mode 100644 index 00000000..5a04b724 --- /dev/null +++ b/lib/philomena/notifications/creator.ex @@ -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 diff --git a/lib/philomena/notifications/forum_post_notification.ex b/lib/philomena/notifications/forum_post_notification.ex new file mode 100644 index 00000000..0d2ad20a --- /dev/null +++ b/lib/philomena/notifications/forum_post_notification.ex @@ -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 diff --git a/lib/philomena/notifications/forum_topic_notification.ex b/lib/philomena/notifications/forum_topic_notification.ex new file mode 100644 index 00000000..862f42ae --- /dev/null +++ b/lib/philomena/notifications/forum_topic_notification.ex @@ -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 diff --git a/lib/philomena/notifications/gallery_image_notification.ex b/lib/philomena/notifications/gallery_image_notification.ex new file mode 100644 index 00000000..1d00d7c9 --- /dev/null +++ b/lib/philomena/notifications/gallery_image_notification.ex @@ -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 diff --git a/lib/philomena/notifications/image_comment_notification.ex b/lib/philomena/notifications/image_comment_notification.ex new file mode 100644 index 00000000..08a2ddff --- /dev/null +++ b/lib/philomena/notifications/image_comment_notification.ex @@ -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 diff --git a/lib/philomena/notifications/image_merge_notification.ex b/lib/philomena/notifications/image_merge_notification.ex new file mode 100644 index 00000000..5546707e --- /dev/null +++ b/lib/philomena/notifications/image_merge_notification.ex @@ -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 diff --git a/lib/philomena/notifications/notification.ex b/lib/philomena/notifications/notification.ex deleted file mode 100644 index 72951bbe..00000000 --- a/lib/philomena/notifications/notification.ex +++ /dev/null @@ -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 diff --git a/lib/philomena/notifications/unread_notification.ex b/lib/philomena/notifications/unread_notification.ex deleted file mode 100644 index 1d111141..00000000 --- a/lib/philomena/notifications/unread_notification.ex +++ /dev/null @@ -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 diff --git a/lib/philomena/posts.ex b/lib/philomena/posts.ex index 723ea6b6..2de6cfbb 100644 --- a/lib/philomena/posts.ex +++ b/lib/philomena/posts.ex @@ -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 """ diff --git a/lib/philomena/subscriptions.ex b/lib/philomena/subscriptions.ex index 8c0d53f0..8e67183f 100644 --- a/lib/philomena/subscriptions.ex +++ b/lib/philomena/subscriptions.ex @@ -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 diff --git a/lib/philomena/topics.ex b/lib/philomena/topics.ex index 1a96f757..3ff4aba5 100644 --- a/lib/philomena/topics.ex +++ b/lib/philomena/topics.ex @@ -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 diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index c005d92e..6b300a7c 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -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 diff --git a/lib/philomena_web/controllers/channel/read_controller.ex b/lib/philomena_web/controllers/channel/read_controller.ex index 415c6b57..91787262 100644 --- a/lib/philomena_web/controllers/channel/read_controller.ex +++ b/lib/philomena_web/controllers/channel/read_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/channel_controller.ex b/lib/philomena_web/controllers/channel_controller.ex index 6d88d257..a548dda9 100644 --- a/lib/philomena_web/controllers/channel_controller.ex +++ b/lib/philomena_web/controllers/channel_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/forum/read_controller.ex b/lib/philomena_web/controllers/forum/read_controller.ex deleted file mode 100644 index cca7ee69..00000000 --- a/lib/philomena_web/controllers/forum/read_controller.ex +++ /dev/null @@ -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 diff --git a/lib/philomena_web/controllers/gallery/read_controller.ex b/lib/philomena_web/controllers/gallery/read_controller.ex index eee4e3d0..ffe1eb55 100644 --- a/lib/philomena_web/controllers/gallery/read_controller.ex +++ b/lib/philomena_web/controllers/gallery/read_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/gallery_controller.ex b/lib/philomena_web/controllers/gallery_controller.ex index 64a020e0..0f1f5a71 100644 --- a/lib/philomena_web/controllers/gallery_controller.ex +++ b/lib/philomena_web/controllers/gallery_controller.ex @@ -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([]) diff --git a/lib/philomena_web/controllers/image/read_controller.ex b/lib/philomena_web/controllers/image/read_controller.ex index 965b7fdc..c1715a66 100644 --- a/lib/philomena_web/controllers/image/read_controller.ex +++ b/lib/philomena_web/controllers/image/read_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/image_controller.ex b/lib/philomena_web/controllers/image_controller.ex index 9cb0914a..990fcfd3 100644 --- a/lib/philomena_web/controllers/image_controller.ex +++ b/lib/philomena_web/controllers/image_controller.ex @@ -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) diff --git a/lib/philomena_web/controllers/notification/category_controller.ex b/lib/philomena_web/controllers/notification/category_controller.ex index 76142581..c050c4e9 100644 --- a/lib/philomena_web/controllers/notification/category_controller.ex +++ b/lib/philomena_web/controllers/notification/category_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/notification_controller.ex b/lib/philomena_web/controllers/notification_controller.ex index a21f345f..158fc5f3 100644 --- a/lib/philomena_web/controllers/notification_controller.ex +++ b/lib/philomena_web/controllers/notification_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/topic/read_controller.ex b/lib/philomena_web/controllers/topic/read_controller.ex index 1c5c45b4..0ac80560 100644 --- a/lib/philomena_web/controllers/topic/read_controller.ex +++ b/lib/philomena_web/controllers/topic/read_controller.ex @@ -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 diff --git a/lib/philomena_web/controllers/topic_controller.ex b/lib/philomena_web/controllers/topic_controller.ex index e88670a0..f68fbcda 100644 --- a/lib/philomena_web/controllers/topic_controller.ex +++ b/lib/philomena_web/controllers/topic_controller.ex @@ -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) diff --git a/lib/philomena_web/plugs/notification_count_plug.ex b/lib/philomena_web/plugs/notification_count_plug.ex index d8afbef9..8f4f7913 100644 --- a/lib/philomena_web/plugs/notification_count_plug.ex +++ b/lib/philomena_web/plugs/notification_count_plug.ex @@ -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 diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index be9f48e0..2ed82dc1 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -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 diff --git a/lib/philomena_web/templates/notification/_channel.html.slime b/lib/philomena_web/templates/notification/_channel.html.slime index 1fc57157..0c4c5b73 100644 --- a/lib/philomena_web/templates/notification/_channel.html.slime +++ b/lib/philomena_web/templates/notification/_channel.html.slime @@ -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}" - i.fa.fa-bell-slash \ No newline at end of file + a.button title="Unsubscribe" href=~p"/channels/#{@notification.channel}/subscription" data-method="delete" data-remote="true" + i.fa.fa-bell-slash diff --git a/lib/philomena_web/templates/notification/_comment.html.slime b/lib/philomena_web/templates/notification/_comment.html.slime new file mode 100644 index 00000000..4e9efeb6 --- /dev/null +++ b/lib/philomena_web/templates/notification/_comment.html.slime @@ -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 diff --git a/lib/philomena_web/templates/notification/_forum.html.slime b/lib/philomena_web/templates/notification/_forum.html.slime deleted file mode 100644 index f7edb198..00000000 --- a/lib/philomena_web/templates/notification/_forum.html.slime +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lib/philomena_web/templates/notification/_gallery.html.slime b/lib/philomena_web/templates/notification/_gallery.html.slime index 09e3eccc..0192b449 100644 --- a/lib/philomena_web/templates/notification/_gallery.html.slime +++ b/lib/philomena_web/templates/notification/_gallery.html.slime @@ -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}" - i.fa.fa-bell-slash \ No newline at end of file + a.button title="Unsubscribe" href=~p"/galleries/#{gallery}/subscription" data-method="delete" data-remote="true" + i.fa.fa-bell-slash diff --git a/lib/philomena_web/templates/notification/_image.html.slime b/lib/philomena_web/templates/notification/_image.html.slime index 89814c39..d0007f08 100644 --- a/lib/philomena_web/templates/notification/_image.html.slime +++ b/lib/philomena_web/templates/notification/_image.html.slime @@ -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 + ' Someone + | 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}" - i.fa.fa-bell-slash \ No newline at end of file + a.button title="Unsubscribe" href=~p"/images/#{target}/subscription" data-method="delete" data-remote="true" + i.fa.fa-bell-slash diff --git a/lib/philomena_web/templates/notification/_notification.html.slime b/lib/philomena_web/templates/notification/_notification.html.slime deleted file mode 100644 index dfc34b18..00000000 --- a/lib/philomena_web/templates/notification/_notification.html.slime +++ /dev/null @@ -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 diff --git a/lib/philomena_web/templates/notification/_post.html.slime b/lib/philomena_web/templates/notification/_post.html.slime new file mode 100644 index 00000000..bac4acb9 --- /dev/null +++ b/lib/philomena_web/templates/notification/_post.html.slime @@ -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 diff --git a/lib/philomena_web/templates/notification/_topic.html.slime b/lib/philomena_web/templates/notification/_topic.html.slime index 5ecefcfd..cf2bd5df 100644 --- a/lib/philomena_web/templates/notification/_topic.html.slime +++ b/lib/philomena_web/templates/notification/_topic.html.slime @@ -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}" - i.fa.fa-bell-slash \ No newline at end of file + a.button title="Unsubscribe" href=~p"/forums/#{forum}/subscription" data-method="delete" data-remote="true" + i.fa.fa-bell-slash diff --git a/lib/philomena_web/templates/notification/category/show.html.slime b/lib/philomena_web/templates/notification/category/show.html.slime index 59f2f9d5..a8a39ab5 100644 --- a/lib/philomena_web/templates/notification/category/show.html.slime +++ b/lib/philomena_web/templates/notification/category/show.html.slime @@ -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 diff --git a/lib/philomena_web/templates/notification/index.html.slime b/lib/philomena_web/templates/notification/index.html.slime index fa957442..ab6b4a28 100644 --- a/lib/philomena_web/templates/notification/index.html.slime +++ b/lib/philomena_web/templates/notification/index.html.slime @@ -1,22 +1,22 @@ h1 Notification Area .walloftext - = cond do - - Enum.any?(@notifications) -> - = for {type, notifications} <- @notifications do - .block.notification-type-block - .block__header - span.block__header__title = name_of_type(type) + = for {category, notifications} <- @notifications, Enum.any?(notifications) do + .block.notification-type-block + .block__header + span.block__header__title = name_of_category(category) - div - = for notification <- notifications do - = render PhilomenaWeb.NotificationView, "_notification.html", notification: notification, conn: @conn + div + = for notification <- notifications do + .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 + .block__header.block__header--light + 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. - ' You'll get notifications here for any new posts or comments. + 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. + ' You'll get notifications here for any new posts or comments. diff --git a/lib/philomena_web/views/notification/category_view.ex b/lib/philomena_web/views/notification/category_view.ex index 148d94f5..8c6717a6 100644 --- a/lib/philomena_web/views/notification/category_view.ex +++ b/lib/philomena_web/views/notification/category_view.ex @@ -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 diff --git a/lib/philomena_web/views/notification_view.ex b/lib/philomena_web/views/notification_view.ex index dcaf81dd..5d30e4d9 100644 --- a/lib/philomena_web/views/notification_view.ex +++ b/lib/philomena_web/views/notification_view.ex @@ -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"