From 6ac5230b2e86fd59426566c756f2a55c9a5d88c8 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 7 Jul 2024 15:54:47 -0400 Subject: [PATCH 1/2] Migration --- .../20240728191353_new_notifications.exs | 109 +++++ priv/repo/structure.sql | 391 ++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 priv/repo/migrations/20240728191353_new_notifications.exs diff --git a/priv/repo/migrations/20240728191353_new_notifications.exs b/priv/repo/migrations/20240728191353_new_notifications.exs new file mode 100644 index 000000000..8ebfd8902 --- /dev/null +++ b/priv/repo/migrations/20240728191353_new_notifications.exs @@ -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 diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 47cf9f042..49678d332 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -198,6 +198,19 @@ CREATE SEQUENCE public.badges_id_seq ALTER SEQUENCE public.badges_id_seq OWNED BY public.badges.id; +-- +-- Name: channel_live_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.channel_live_notifications ( + channel_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: channel_subscriptions; Type: TABLE; Schema: public; Owner: - -- @@ -620,6 +633,20 @@ CREATE SEQUENCE public.fingerprint_bans_id_seq ALTER SEQUENCE public.fingerprint_bans_id_seq OWNED BY public.fingerprint_bans.id; +-- +-- Name: forum_post_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_post_notifications ( + topic_id bigint NOT NULL, + post_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: forum_subscriptions; Type: TABLE; Schema: public; Owner: - -- @@ -630,6 +657,19 @@ CREATE TABLE public.forum_subscriptions ( ); +-- +-- Name: forum_topic_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.forum_topic_notifications ( + topic_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: forums; Type: TABLE; Schema: public; Owner: - -- @@ -709,6 +749,19 @@ CREATE SEQUENCE public.galleries_id_seq ALTER SEQUENCE public.galleries_id_seq OWNED BY public.galleries.id; +-- +-- Name: gallery_image_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.gallery_image_notifications ( + gallery_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: gallery_interactions; Type: TABLE; Schema: public; Owner: - -- @@ -750,6 +803,20 @@ CREATE TABLE public.gallery_subscriptions ( ); +-- +-- Name: image_comment_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.image_comment_notifications ( + image_id bigint NOT NULL, + comment_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: image_faves; Type: TABLE; Schema: public; Owner: - -- @@ -837,6 +904,20 @@ CREATE SEQUENCE public.image_intensities_id_seq ALTER SEQUENCE public.image_intensities_id_seq OWNED BY public.image_intensities.id; +-- +-- Name: image_merge_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.image_merge_notifications ( + target_id bigint NOT NULL, + source_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp(0) without time zone NOT NULL, + updated_at timestamp(0) without time zone NOT NULL, + read boolean DEFAULT false NOT NULL +); + + -- -- Name: image_sources; Type: TABLE; Schema: public; Owner: - -- @@ -2894,6 +2975,160 @@ ALTER TABLE ONLY public.versions ADD CONSTRAINT versions_pkey PRIMARY KEY (id); +-- +-- Name: channel_live_notifications_channel_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX channel_live_notifications_channel_id_index ON public.channel_live_notifications USING btree (channel_id); + + +-- +-- Name: channel_live_notifications_user_id_channel_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX channel_live_notifications_user_id_channel_id_index ON public.channel_live_notifications USING btree (user_id, channel_id); + + +-- +-- Name: channel_live_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX channel_live_notifications_user_id_read_index ON public.channel_live_notifications USING btree (user_id, read); + + +-- +-- Name: channel_live_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX channel_live_notifications_user_id_updated_at_desc_index ON public.channel_live_notifications USING btree (user_id, updated_at DESC); + + +-- +-- Name: forum_post_notifications_post_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_post_notifications_post_id_index ON public.forum_post_notifications USING btree (post_id); + + +-- +-- Name: forum_post_notifications_topic_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_post_notifications_topic_id_index ON public.forum_post_notifications USING btree (topic_id); + + +-- +-- Name: forum_post_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_post_notifications_user_id_read_index ON public.forum_post_notifications USING btree (user_id, read); + + +-- +-- Name: forum_post_notifications_user_id_topic_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX forum_post_notifications_user_id_topic_id_index ON public.forum_post_notifications USING btree (user_id, topic_id); + + +-- +-- Name: forum_post_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_post_notifications_user_id_updated_at_desc_index ON public.forum_post_notifications USING btree (user_id, updated_at DESC); + + +-- +-- Name: forum_topic_notifications_topic_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_topic_notifications_topic_id_index ON public.forum_topic_notifications USING btree (topic_id); + + +-- +-- Name: forum_topic_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_topic_notifications_user_id_read_index ON public.forum_topic_notifications USING btree (user_id, read); + + +-- +-- Name: forum_topic_notifications_user_id_topic_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX forum_topic_notifications_user_id_topic_id_index ON public.forum_topic_notifications USING btree (user_id, topic_id); + + +-- +-- Name: forum_topic_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX forum_topic_notifications_user_id_updated_at_desc_index ON public.forum_topic_notifications USING btree (user_id, updated_at DESC); + + +-- +-- Name: gallery_image_notifications_gallery_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gallery_image_notifications_gallery_id_index ON public.gallery_image_notifications USING btree (gallery_id); + + +-- +-- Name: gallery_image_notifications_user_id_gallery_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX gallery_image_notifications_user_id_gallery_id_index ON public.gallery_image_notifications USING btree (user_id, gallery_id); + + +-- +-- Name: gallery_image_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gallery_image_notifications_user_id_read_index ON public.gallery_image_notifications USING btree (user_id, read); + + +-- +-- Name: gallery_image_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gallery_image_notifications_user_id_updated_at_desc_index ON public.gallery_image_notifications USING btree (user_id, updated_at DESC); + + +-- +-- Name: image_comment_notifications_comment_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_comment_notifications_comment_id_index ON public.image_comment_notifications USING btree (comment_id); + + +-- +-- Name: image_comment_notifications_image_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_comment_notifications_image_id_index ON public.image_comment_notifications USING btree (image_id); + + +-- +-- Name: image_comment_notifications_user_id_image_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX image_comment_notifications_user_id_image_id_index ON public.image_comment_notifications USING btree (user_id, image_id); + + +-- +-- Name: image_comment_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_comment_notifications_user_id_read_index ON public.image_comment_notifications USING btree (user_id, read); + + +-- +-- Name: image_comment_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_comment_notifications_user_id_updated_at_desc_index ON public.image_comment_notifications USING btree (user_id, updated_at DESC); + + -- -- Name: image_intensities_index; Type: INDEX; Schema: public; Owner: - -- @@ -2901,6 +3136,41 @@ ALTER TABLE ONLY public.versions CREATE INDEX image_intensities_index ON public.image_intensities USING btree (nw, ne, sw, se); +-- +-- Name: image_merge_notifications_source_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_merge_notifications_source_id_index ON public.image_merge_notifications USING btree (source_id); + + +-- +-- Name: image_merge_notifications_target_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_merge_notifications_target_id_index ON public.image_merge_notifications USING btree (target_id); + + +-- +-- Name: image_merge_notifications_user_id_read_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_merge_notifications_user_id_read_index ON public.image_merge_notifications USING btree (user_id, read); + + +-- +-- Name: image_merge_notifications_user_id_target_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX image_merge_notifications_user_id_target_id_index ON public.image_merge_notifications USING btree (user_id, target_id); + + +-- +-- Name: image_merge_notifications_user_id_updated_at_desc_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_merge_notifications_user_id_updated_at_desc_index ON public.image_merge_notifications USING btree (user_id, updated_at DESC); + + -- -- Name: image_sources_image_id_source_index; Type: INDEX; Schema: public; Owner: - -- @@ -4175,6 +4445,22 @@ CREATE UNIQUE INDEX user_tokens_context_token_index ON public.user_tokens USING CREATE INDEX user_tokens_user_id_index ON public.user_tokens USING btree (user_id); +-- +-- Name: channel_live_notifications channel_live_notifications_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.channel_live_notifications + ADD CONSTRAINT channel_live_notifications_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE; + + +-- +-- Name: channel_live_notifications channel_live_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.channel_live_notifications + ADD CONSTRAINT channel_live_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: channels fk_rails_021c624081; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4967,6 +5253,110 @@ ALTER TABLE ONLY public.gallery_subscriptions ADD CONSTRAINT fk_rails_fa77f3cebe FOREIGN KEY (gallery_id) REFERENCES public.galleries(id) ON UPDATE CASCADE ON DELETE CASCADE; +-- +-- Name: forum_post_notifications forum_post_notifications_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post_notifications + ADD CONSTRAINT forum_post_notifications_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: forum_post_notifications forum_post_notifications_topic_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post_notifications + ADD CONSTRAINT forum_post_notifications_topic_id_fkey FOREIGN KEY (topic_id) REFERENCES public.topics(id) ON DELETE CASCADE; + + +-- +-- Name: forum_post_notifications forum_post_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_post_notifications + ADD CONSTRAINT forum_post_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: forum_topic_notifications forum_topic_notifications_topic_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_topic_notifications + ADD CONSTRAINT forum_topic_notifications_topic_id_fkey FOREIGN KEY (topic_id) REFERENCES public.topics(id) ON DELETE CASCADE; + + +-- +-- Name: forum_topic_notifications forum_topic_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.forum_topic_notifications + ADD CONSTRAINT forum_topic_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: gallery_image_notifications gallery_image_notifications_gallery_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gallery_image_notifications + ADD CONSTRAINT gallery_image_notifications_gallery_id_fkey FOREIGN KEY (gallery_id) REFERENCES public.galleries(id) ON DELETE CASCADE; + + +-- +-- Name: gallery_image_notifications gallery_image_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gallery_image_notifications + ADD CONSTRAINT gallery_image_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: image_comment_notifications image_comment_notifications_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_comment_notifications + ADD CONSTRAINT image_comment_notifications_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: image_comment_notifications image_comment_notifications_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_comment_notifications + ADD CONSTRAINT image_comment_notifications_image_id_fkey FOREIGN KEY (image_id) REFERENCES public.images(id) ON DELETE CASCADE; + + +-- +-- Name: image_comment_notifications image_comment_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_comment_notifications + ADD CONSTRAINT image_comment_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: image_merge_notifications image_merge_notifications_source_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_merge_notifications + ADD CONSTRAINT image_merge_notifications_source_id_fkey FOREIGN KEY (source_id) REFERENCES public.images(id) ON DELETE CASCADE; + + +-- +-- Name: image_merge_notifications image_merge_notifications_target_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_merge_notifications + ADD CONSTRAINT image_merge_notifications_target_id_fkey FOREIGN KEY (target_id) REFERENCES public.images(id) ON DELETE CASCADE; + + +-- +-- Name: image_merge_notifications image_merge_notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_merge_notifications + ADD CONSTRAINT image_merge_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: image_sources image_sources_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5056,3 +5446,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20211107130226); INSERT INTO public."schema_migrations" (version) VALUES (20211219194836); INSERT INTO public."schema_migrations" (version) VALUES (20220321173359); INSERT INTO public."schema_migrations" (version) VALUES (20240723122759); +INSERT INTO public."schema_migrations" (version) VALUES (20240728191353); From f48a8fc165bce7180eb84d4ac7b9538ecae070f4 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 7 Jul 2024 18:09:20 -0400 Subject: [PATCH 2/2] Frontend changes --- lib/philomena/channels.ex | 16 +- lib/philomena/comments.ex | 17 +- lib/philomena/forums.ex | 1 - lib/philomena/galleries.ex | 34 +- lib/philomena/images.ex | 52 +-- lib/philomena/notifications.ex | 333 +++++++++--------- lib/philomena/notifications/category.ex | 193 ++++++---- .../channel_live_notification.ex | 25 ++ lib/philomena/notifications/creator.ex | 123 +++++++ .../notifications/forum_post_notification.ex | 27 ++ .../notifications/forum_topic_notification.ex | 25 ++ .../gallery_image_notification.ex | 25 ++ .../image_comment_notification.ex | 27 ++ .../notifications/image_merge_notification.ex | 26 ++ lib/philomena/notifications/notification.ex | 26 -- .../notifications/unread_notification.ex | 21 -- lib/philomena/posts.ex | 17 +- lib/philomena/subscriptions.ex | 40 +-- lib/philomena/topics.ex | 43 +-- lib/philomena/users/user.ex | 3 - .../controllers/channel/read_controller.ex | 2 +- .../controllers/channel_controller.ex | 2 +- .../controllers/forum/read_controller.ex | 22 -- .../controllers/gallery/read_controller.ex | 2 +- .../controllers/gallery_controller.ex | 2 +- .../controllers/image/read_controller.ex | 2 +- .../controllers/image_controller.ex | 2 +- .../notification/category_controller.ex | 8 +- .../controllers/notification_controller.ex | 6 +- .../controllers/topic/read_controller.ex | 2 +- .../controllers/topic_controller.ex | 5 +- .../plugs/notification_count_plug.ex | 2 +- lib/philomena_web/router.ex | 2 - .../notification/_channel.html.slime | 10 +- .../notification/_comment.html.slime | 22 ++ .../templates/notification/_forum.html.slime | 25 -- .../notification/_gallery.html.slime | 14 +- .../templates/notification/_image.html.slime | 23 +- .../notification/_notification.html.slime | 7 - .../templates/notification/_post.html.slime | 19 + .../templates/notification/_topic.html.slime | 20 +- .../notification/category/show.html.slime | 7 +- .../templates/notification/index.html.slime | 34 +- .../views/notification/category_view.ex | 3 +- lib/philomena_web/views/notification_view.ex | 20 +- 45 files changed, 784 insertions(+), 553 deletions(-) create mode 100644 lib/philomena/notifications/channel_live_notification.ex create mode 100644 lib/philomena/notifications/creator.ex create mode 100644 lib/philomena/notifications/forum_post_notification.ex create mode 100644 lib/philomena/notifications/forum_topic_notification.ex create mode 100644 lib/philomena/notifications/gallery_image_notification.ex create mode 100644 lib/philomena/notifications/image_comment_notification.ex create mode 100644 lib/philomena/notifications/image_merge_notification.ex delete mode 100644 lib/philomena/notifications/notification.ex delete mode 100644 lib/philomena/notifications/unread_notification.ex delete mode 100644 lib/philomena_web/controllers/forum/read_controller.ex create mode 100644 lib/philomena_web/templates/notification/_comment.html.slime delete mode 100644 lib/philomena_web/templates/notification/_forum.html.slime delete mode 100644 lib/philomena_web/templates/notification/_notification.html.slime create mode 100644 lib/philomena_web/templates/notification/_post.html.slime diff --git a/lib/philomena/channels.ex b/lib/philomena/channels.ex index 0f00cbe95..2ffd4ce46 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 d3a819d98..281de6c0a 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 ba8006bc0..7cec4205c 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 b60b8b728..4c76df354 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 7d7b27ea1..3aefa4c2c 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]) + + {merge_notification_count, nil} = + ImageMergeNotification + |> where(image_id: ^source.id) + |> Repo.update_all(set: [image_id: target.id]) - {:ok, count} + {: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.notify( - nil, - subscriptions, - %{ - actor_id: target.id, - actor_type: "Image", - actor_child_id: nil, - actor_child_type: nil, - action: "merged ##{source_id} into" - } - ) + Notifications.create_image_merge_notification(target, source) + end + + @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 cbff31eea..ea0b60293 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] - end + @doc """ + Returns paginated unread notifications for the user, given the category. - defp union_all_queries([query | rest]) do - Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end) - end + ## Examples - defp load_associations(notifications) do - Polymorphic.load_polymorphic( - notifications, - actor: [actor_id: :actor_type], - actor_child: [actor_child_id: :actor_child_type] - ) + 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 @doc """ - Gets a single notification. - - Raises `Ecto.NoResultsError` if the Notification does not exist. + Creates a channel live notification, returning the number of affected users. ## Examples - iex> get_notification!(123) - %Notification{} - - iex> get_notification!(456) - ** (Ecto.NoResultsError) + iex> create_channel_live_notification(channel) + {:ok, 2} """ - def get_notification!(id), do: Repo.get!(Notification, id) + def create_channel_live_notification(channel) do + Creator.create_single(ChannelSubscription, ChannelLiveNotification, :channel_id, channel) + end @doc """ - Creates a notification. + Creates a forum post notification, returning the number of affected users. ## Examples - iex> create_notification(%{field: value}) - {:ok, %Notification{}} - - iex> create_notification(%{field: bad_value}) - {:error, %Ecto.Changeset{}} + iex> create_forum_post_notification(topic, post) + {:ok, 2} """ - def create_notification(attrs \\ %{}) do - %Notification{} - |> Notification.changeset(attrs) - |> Repo.insert() + def create_forum_post_notification(topic, post) do + Creator.create_double( + TopicSubscription, + ForumPostNotification, + :topic_id, + topic, + :post_id, + post + ) end @doc """ - Updates a notification. + Creates a forum topic 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_forum_topic_notification(topic) + {:ok, 2} """ - def update_notification(%Notification{} = notification, attrs) do - notification - |> Notification.changeset(attrs) - |> Repo.insert_or_update() + def create_forum_topic_notification(topic) do + Creator.create_single(ForumSubscription, ForumTopicNotification, :topic_id, topic) end @doc """ - Deletes a Notification. + Creates a gallery image notification, returning the number of affected users. ## Examples - iex> delete_notification(notification) - {:ok, %Notification{}} - - iex> delete_notification(notification) - {:error, %Ecto.Changeset{}} + iex> create_gallery_image_notification(gallery) + {:ok, 2} """ - def delete_notification(%Notification{} = notification) do - Repo.delete(notification) + def create_gallery_image_notification(gallery) do + Creator.create_single(GallerySubscription, GalleryImageNotification, :gallery_id, gallery) end @doc """ - Returns an `%Ecto.Changeset{}` for tracking notification changes. + Creates an image comment notification, returning the number of affected users. ## Examples - iex> change_notification(notification) - %Ecto.Changeset{source: %Notification{}} + iex> create_image_comment_notification(image, comment) + {:ok, 2} """ - def change_notification(%Notification{} = notification) do - Notification.changeset(notification, %{}) + def create_image_comment_notification(image, comment) do + Creator.create_double( + ImageSubscription, + ImageCommentNotification, + :image_id, + image, + :comment_id, + comment + ) end - def count_unread_notifications(user) do - UnreadNotification - |> where(user_id: ^user.id) - |> Repo.aggregate(:count, :notification_id) + @doc """ + Creates an image merge notification, returning the number of affected users. + + ## Examples + + iex> create_image_merge_notification(target, source) + {:ok, 2} + + """ + 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> clear_forum_topic_notification(topic, user) + {:ok, 2} + + """ + def clear_forum_topic_notification(topic, user) do + ForumTopicNotification + |> where(topic_id: ^topic.id) + |> Creator.clear(user) + end + + @doc """ + Removes the gallery image notification for a given gallery and user, returning + the number of affected notifications. - iex> delete_unread_notification(unread_notification) - {:error, %Ecto.Changeset{}} + ## Examples + + iex> clear_gallery_image_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_gallery_image_notification(gallery, user) do + GalleryImageNotification + |> where(gallery_id: ^gallery.id) + |> Creator.clear(user) end @doc """ - Returns an `%Ecto.Changeset{}` for tracking unread_notification changes. + Removes the image comment notification for a given image 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_image_comment_notification(image, user) do + ImageCommentNotification + |> where(image_id: ^image.id) + |> Creator.clear(user) end - def notify(_actor_child, [], _params), do: nil - - 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)) - - _ -> - subscriptions - end - - Repo.transaction(fn -> - notification = - Notification - |> Repo.get_by(actor_id: params.actor_id, actor_type: params.actor_type) - - {:ok, notification} = - (notification || %Notification{}) - |> update_notification(params) - - # Insert the notification to any watchers who do not have it - unreads = - subscriptions - |> Enum.map(&%{user_id: &1.user_id, notification_id: notification.id}) - - UnreadNotification - |> Repo.insert_all(unreads, on_conflict: :nothing) - end) + @doc """ + Removes the image merge notification for a given image and user, returning + the number of affected notifications. + + ## Examples + + 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 775b888dd..249dd838a 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 - - {"Gallery", _} -> - :gallery_image - - {"Image", "Comment"} -> - :image_comment - - {"Image", _} -> - :image_merge - - {"Topic", "Post"} -> - if n.action == "posted a new reply in" do - :forum_post - else - :forum_topic - end - end + 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 + + defp union_all_queries([query | rest]) do + Enum.reduce(rest, query, fn q, acc -> union_all(acc, ^q) end) + end + + @doc """ + Gather up and return the top N notifications for the user, for each category of + unread notification currently existing. + + ## Examples + + iex> unread_notifications_for_user(user, page_size: 10) + %{ + channel_live: [], + forum_post: [%ForumPostNotification{...}, ...], + forum_topic: [%ForumTopicNotification{...}, ...], + gallery_image: [], + image_comment: [%ImageCommentNotification{...}, ...], + image_merge: [] + } + + """ + def unread_notifications_for_user(user, pagination) do + Map.new(categories(), fn category -> + results = + category + |> query_for_category_and_user(user) + |> order_by(desc: :updated_at) + |> Repo.paginate(pagination) + + {category, results} + end) + end + + @doc """ + Returns paginated unread notifications for the user, given the category. + + ## Examples + + iex> unread_notifications_for_user_and_category(user, :image_comment) + [%ImageCommentNotification{...}] + + """ + def unread_notifications_for_user_and_category(user, category, pagination) do + category + |> query_for_category_and_user(user) + |> order_by(desc: :updated_at) + |> Repo.paginate(pagination) end @doc """ - Returns an `m:Ecto.Query` that finds notifications for the given type. + Determine the category of a notification. + + ## Examples + + iex> notification_category(%ImageCommentNotification{}) + :image_comment + """ - def query_for_type(type) do - base = from(n in Notification) - - case type do - :channel_live -> - where(base, [n], n.actor_type == "Channel") - - :gallery_image -> - where(base, [n], n.actor_type == "Gallery") - - :image_comment -> - where(base, [n], n.actor_type == "Image" and n.actor_child_type == "Comment") - - :image_merge -> - where(base, [n], n.actor_type == "Image" and is_nil(n.actor_child_type)) - - :forum_topic -> - where( - base, - [n], - n.actor_type == "Topic" and n.actor_child_type == "Post" and - n.action != "posted a new reply in" - ) - - :forum_post -> - where( - base, - [n], - n.actor_type == "Topic" and n.actor_child_type == "Post" and - n.action == "posted a new reply in" - ) + 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 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_category_and_user(category, user) do + query = + case category do + :channel_live -> + from(n in ChannelLiveNotification, preload: :channel) + + :gallery_image -> + from(n in GalleryImageNotification, preload: [gallery: :creator]) + + :image_comment -> + from(n in ImageCommentNotification, + preload: [image: [:sources, tags: :aliases], comment: :user] + ) + + :image_merge -> + from(n in ImageMergeNotification, + preload: [:source, target: [:sources, tags: :aliases]] + ) + + :forum_topic -> + from(n in ForumTopicNotification, preload: [topic: [:forum, :user]]) + + :forum_post -> + from(n in ForumPostNotification, preload: [topic: :forum, post: :user]) + 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 000000000..b60fb8e6e --- /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 000000000..5a04b7240 --- /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 000000000..0d2ad20a1 --- /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 000000000..862f42ae1 --- /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 000000000..1d00d7c95 --- /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 000000000..08a2ddff3 --- /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 000000000..5546707ed --- /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 72951bbeb..000000000 --- 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 1d1111416..000000000 --- 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 723ea6b6c..2de6cfbbb 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 8c0d53f0f..8e67183f4 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 1a96f7571..3ff4aba57 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 c005d92e1..6b300a7c1 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 415c6b577..91787262d 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 6d88d257e..a548dda93 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 cca7ee695..000000000 --- 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 eee4e3d02..ffe1eb551 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 64a020e08..0f1f5a71e 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 965b7fdc2..c1715a667 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 9cb0914ae..990fcfd3b 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 76142581f..c050c4e9a 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 a21f345f7..158fc5f35 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 1c5c45b43..0ac80560c 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 e88670a0e..f68fbcda9 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 d8afbef9a..8f4f79134 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 be9f48e0b..2ed82dc19 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 1fc57157c..0c4c5b739 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 000000000..4e9efeb6f --- /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 f7edb1985..000000000 --- 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 09e3eccc7..0192b4495 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 89814c399..d0007f08d 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 dfc34b18e..000000000 --- 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 000000000..bac4acb9f --- /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 5ecefcfd8..cf2bd5df9 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 59f2f9d56..a8a39ab55 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 fa9574425..ab6b4a283 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 148d94f56..8c6717a60 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 dcaf81dd9..5d30e4d93 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"