From 767a2ba9499c711670e9517f737157dd23a76c66 Mon Sep 17 00:00:00 2001 From: VcSaJen Date: Wed, 10 Feb 2021 07:21:30 +0900 Subject: [PATCH] User settings (#96) Resolves derpibooru/philomena#121 Resolves derpibooru/philomena#122 --- lib/philomena/comments.ex | 16 +++- lib/philomena/images.ex | 16 +++- lib/philomena/posts.ex | 16 +++- lib/philomena/topics.ex | 16 +++- lib/philomena/users.ex | 6 ++ lib/philomena/users/user.ex | 23 +++++- lib/philomena_web/comment_loader.ex | 12 +++ .../controllers/activity_controller.ex | 11 ++- .../controllers/conversation_controller.ex | 6 +- .../controllers/filter/clear_recent.ex | 15 ++++ .../controllers/image_controller.ex | 10 +++ lib/philomena_web/router.ex | 3 +- .../templates/activity/index.html.slime | 77 ++++++++++--------- .../templates/filter/index.html.slime | 12 ++- .../templates/setting/edit.html.slime | 54 ++++++++++--- 15 files changed, 226 insertions(+), 67 deletions(-) create mode 100644 lib/philomena_web/controllers/filter/clear_recent.ex diff --git a/lib/philomena/comments.ex b/lib/philomena/comments.ex index 8fb0a411..34527361 100644 --- a/lib/philomena/comments.ex +++ b/lib/philomena/comments.ex @@ -18,6 +18,7 @@ defmodule Philomena.Comments do alias Philomena.NotificationWorker alias Philomena.Versions alias Philomena.Reports + alias Philomena.Users.User @doc """ Gets a single comment. @@ -59,12 +60,21 @@ defmodule Philomena.Comments do Multi.new() |> Multi.insert(:comment, comment) |> Multi.update_all(:image, image_query, inc: [comments_count: 1]) - |> Multi.run(:subscribe, fn _repo, _changes -> - Images.create_subscription(image, attribution[:user]) - end) + |> maybe_create_subscription_on_reply(image, attribution[:user]) |> Repo.transaction() end + defp maybe_create_subscription_on_reply(multi, image, %User{watch_on_reply: true} = user) do + multi + |> Multi.run(:subscribe, fn _repo, _changes -> + Images.create_subscription(image, user) + end) + end + + defp maybe_create_subscription_on_reply(multi, _image, _user) do + multi + end + def notify_comment(comment) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Comments", comment.id]) end diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 074e9862..12bdf399 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -34,6 +34,7 @@ defmodule Philomena.Images do alias Philomena.Comments alias Philomena.Galleries.Gallery alias Philomena.Galleries.Interaction + alias Philomena.Users.User @doc """ Gets a single image. @@ -95,9 +96,7 @@ defmodule Philomena.Images do {:ok, count} end) - |> Multi.run(:subscribe, fn _repo, %{image: image} -> - create_subscription(image, attribution[:user]) - end) + |> maybe_create_subscription_on_upload(attribution[:user]) |> Repo.transaction() |> case do {:ok, %{image: image}} = result -> @@ -116,6 +115,17 @@ defmodule Philomena.Images do end end + defp maybe_create_subscription_on_upload(multi, %User{watch_on_upload: true} = user) do + multi + |> Multi.run(:subscribe, fn _repo, %{image: image} -> + create_subscription(image, user) + end) + end + + defp maybe_create_subscription_on_upload(multi, _user) do + multi + end + def feature_image(featurer, %Image{} = image) do %ImageFeature{user_id: featurer.id, image_id: image.id} |> ImageFeature.changeset(%{}) diff --git a/lib/philomena/posts.ex b/lib/philomena/posts.ex index c8484de9..01b32452 100644 --- a/lib/philomena/posts.ex +++ b/lib/philomena/posts.ex @@ -19,6 +19,7 @@ defmodule Philomena.Posts do alias Philomena.Versions alias Philomena.Reports alias Philomena.Reports.Report + alias Philomena.Users.User @doc """ Gets a single post. @@ -88,9 +89,7 @@ defmodule Philomena.Posts do {:ok, count} end) - |> Multi.run(:subscribe, fn _repo, _changes -> - Topics.create_subscription(topic, attributes[:user]) - end) + |> maybe_create_subscription_on_reply(topic, attributes[:user]) |> Repo.transaction() |> case do {:ok, %{post: post}} = result -> @@ -103,6 +102,17 @@ defmodule Philomena.Posts do end end + defp maybe_create_subscription_on_reply(multi, topic, %User{watch_on_reply: true} = user) do + multi + |> Multi.run(:subscribe, fn _repo, _changes -> + Topics.create_subscription(topic, user) + end) + end + + defp maybe_create_subscription_on_reply(multi, _topic, _user) do + multi + end + def notify_post(post) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) end diff --git a/lib/philomena/topics.ex b/lib/philomena/topics.ex index fae15f4a..c4a02bdc 100644 --- a/lib/philomena/topics.ex +++ b/lib/philomena/topics.ex @@ -12,6 +12,7 @@ defmodule Philomena.Topics do alias Philomena.Posts alias Philomena.Notifications alias Philomena.NotificationWorker + alias Philomena.Users.User @doc """ Gets a single topic. @@ -69,9 +70,7 @@ defmodule Philomena.Topics do {:ok, count} end) - |> Multi.run(:subscribe, fn _repo, %{topic: topic} -> - create_subscription(topic, attribution[:user]) - end) + |> maybe_create_subscription_on_new_topic(attribution[:user]) |> Repo.transaction() |> case do {:ok, %{topic: topic}} = result -> @@ -84,6 +83,17 @@ defmodule Philomena.Topics do end end + defp maybe_create_subscription_on_new_topic(multi, %User{watch_on_new_topic: true} = user) do + multi + |> Multi.run(:subscribe, fn _repo, %{topic: topic} -> + create_subscription(topic, user) + end) + end + + defp maybe_create_subscription_on_new_topic(multi, _user) do + multi + end + def notify_topic(topic, post) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]]) end diff --git a/lib/philomena/users.ex b/lib/philomena/users.ex index 6efe6657..a940ce70 100644 --- a/lib/philomena/users.ex +++ b/lib/philomena/users.ex @@ -654,6 +654,12 @@ defmodule Philomena.Users do |> Repo.update() end + def clear_recent_filters(%User{} = user) do + user + |> User.clear_recent_filters_changeset() + |> Repo.update() + end + defp load_with_roles(query) do query |> Repo.one() diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index d1ac7834..dc41404d 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -314,7 +314,13 @@ defmodule Philomena.Users.User do :watched_images_exclude_str, :use_centered_layout, :hide_vote_counts, - :comments_newest_first + :comments_newest_first, + :watch_on_reply, + :watch_on_upload, + :watch_on_new_topic, + :comments_always_jump_to_last, + :messages_newest_first, + :show_sidebar_and_watched_images ]) |> validate_required([ :images_per_page, @@ -326,7 +332,13 @@ defmodule Philomena.Users.User do :theme, :no_spoilered_in_watched, :use_centered_layout, - :hide_vote_counts + :hide_vote_counts, + :watch_on_reply, + :watch_on_upload, + :watch_on_new_topic, + :comments_always_jump_to_last, + :messages_newest_first, + :show_sidebar_and_watched_images ]) |> TagList.propagate_tag_list(:watched_tag_list, :watched_tag_ids) |> validate_inclusion(:theme, ~W(default dark red)) @@ -511,6 +523,13 @@ defmodule Philomena.Users.User do ) end + def clear_recent_filters_changeset(user) do + user + |> change(%{ + recent_filter_ids: [user.current_filter_id] + }) + end + defp enable_totp_changeset(user, backup_codes) do hashed_codes = Enum.map(backup_codes, &Password.hash_pwd_salt/1) diff --git a/lib/philomena_web/comment_loader.ex b/lib/philomena_web/comment_loader.ex index 542d9461..cd6cab34 100644 --- a/lib/philomena_web/comment_loader.ex +++ b/lib/philomena_web/comment_loader.ex @@ -34,6 +34,18 @@ defmodule PhilomenaWeb.CommentLoader do div(offset, page_size) + 1 end + def last_page(conn, image) do + offset = + Comment + |> where(image_id: ^image.id) + |> Repo.aggregate(:count, :id) + + page_size = conn.assigns.comment_scrivener[:page_size] + + # Pagination starts at page 1 + div(offset, page_size) + 1 + end + defp load_direction(%{comments_newest_first: false}), do: :asc defp load_direction(_user), do: :desc diff --git a/lib/philomena_web/controllers/activity_controller.ex b/lib/philomena_web/controllers/activity_controller.ex index 2439500f..59a65760 100644 --- a/lib/philomena_web/controllers/activity_controller.ex +++ b/lib/philomena_web/controllers/activity_controller.ex @@ -116,7 +116,8 @@ defmodule PhilomenaWeb.ActivityController do streams: streams, topics: topics, interactions: interactions, - layout_class: "layout--wide" + layout_class: "layout--wide", + show_sidebar: show_sidebar?(user) || !index_page?(conn) ) end @@ -165,4 +166,12 @@ defmodule PhilomenaWeb.ActivityController do ] ) end + + defp index_page?(%{assigns: %{index: true}}), do: true + + defp index_page?(_conn), do: false + + defp show_sidebar?(%{show_sidebar_and_watched_images: false}), do: false + + defp show_sidebar?(_user), do: true end diff --git a/lib/philomena_web/controllers/conversation_controller.ex b/lib/philomena_web/controllers/conversation_controller.ex index bfb9cf7d..1292c441 100644 --- a/lib/philomena_web/controllers/conversation_controller.ex +++ b/lib/philomena_web/controllers/conversation_controller.ex @@ -61,11 +61,12 @@ defmodule PhilomenaWeb.ConversationController do def show(conn, _params) do conversation = conn.assigns.conversation user = conn.assigns.current_user + pref = load_direction(user) messages = Message |> where(conversation_id: ^conversation.id) - |> order_by(asc: :created_at) + |> order_by([{^pref, :created_at}]) |> preload([:from]) |> Repo.paginate(conn.assigns.scrivener) @@ -115,4 +116,7 @@ defmodule PhilomenaWeb.ConversationController do |> render("new.html", changeset: changeset) end end + + defp load_direction(%{messages_newest_first: false}), do: :asc + defp load_direction(_user), do: :desc end diff --git a/lib/philomena_web/controllers/filter/clear_recent.ex b/lib/philomena_web/controllers/filter/clear_recent.ex new file mode 100644 index 00000000..12453fcf --- /dev/null +++ b/lib/philomena_web/controllers/filter/clear_recent.ex @@ -0,0 +1,15 @@ +defmodule PhilomenaWeb.Filter.ClearRecentController do + use PhilomenaWeb, :controller + + alias Philomena.Users + + plug PhilomenaWeb.RequireUserPlug + + def delete(conn, _params) do + {:ok, user} = Users.clear_recent_filters(conn.assigns.current_user) + + conn + |> put_flash(:info, "Cleared recent filters") + |> redirect(external: conn.assigns.referrer) + end +end diff --git a/lib/philomena_web/controllers/image_controller.ex b/lib/philomena_web/controllers/image_controller.ex index de7710eb..bd88d4ec 100644 --- a/lib/philomena_web/controllers/image_controller.ex +++ b/lib/philomena_web/controllers/image_controller.ex @@ -59,6 +59,8 @@ defmodule PhilomenaWeb.ImageController do # Update the notification ticker in the header conn = NotificationCountPlug.call(conn) + conn = maybe_skip_to_last_comment_page(conn, image, user) + comments = CommentLoader.load_comments(conn, image) rendered = TextileRenderer.render_collection(comments.entries, conn) @@ -132,6 +134,14 @@ defmodule PhilomenaWeb.ImageController do end end + defp maybe_skip_to_last_comment_page(conn, image, %{comments_newest_first: false, comments_always_jump_to_last: true}) do + page = CommentLoader.last_page(conn, image) + conn + |> assign(:comment_scrivener, Keyword.merge(conn.assigns.comment_scrivener, [page: page])) + end + + defp maybe_skip_to_last_comment_page(conn, _image, _user), do: conn + defp user_galleries(_image, nil), do: [] defp user_galleries(image, user) do diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 4b32c494..0c68f519 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -418,7 +418,7 @@ defmodule PhilomenaWeb.Router do scope "/", PhilomenaWeb do pipe_through [:browser, :ensure_totp, :ensure_tor_authorized] - get "/", ActivityController, :index + get "/", ActivityController, :index, [assigns: %{index: true}] resources "/activity", ActivityController, only: [:index] @@ -474,6 +474,7 @@ defmodule PhilomenaWeb.Router do scope "/filters", Filter, as: :filter do resources "/current", CurrentController, only: [:update], singleton: true + resources "/clear_recent", ClearRecentController, only: [:delete], singleton: true end resources "/filters", FilterController do diff --git a/lib/philomena_web/templates/activity/index.html.slime b/lib/philomena_web/templates/activity/index.html.slime index 28a8ddba..167296f9 100644 --- a/lib/philomena_web/templates/activity/index.html.slime +++ b/lib/philomena_web/templates/activity/index.html.slime @@ -1,45 +1,46 @@ .column-layout - aside.column-layout__left#activity-side - = if not is_nil(@featured_image) and not PhilomenaWeb.ImageView.filter_or_spoiler_hits?(@conn, @featured_image) do - .center - h4.remove-top-margin Featured Image - = render PhilomenaWeb.ImageView, "_image_box.html", image: @featured_image, size: :medium, conn: @conn - .block.block--fixed.block--fixed--sub.block--success.center.hide-mobile - ' Enjoy the site? - a href="/pages/donations" - ' Become a patron or donate! - .block.block--fixed.block--fixed--sub.center.hide-mobile - ' Issues? Want to chat? - a href="/pages/contact" Contact us! - .block.hide-mobile - a.block__header--single-item.center href="/search?q=first_seen_at.gt:3 days ago&sf=wilson_score&sd=desc" - ' Trending Images - .block__content.flex.flex--centered.flex--wrap.image-flex-grid - = for image <- @top_scoring do - = render PhilomenaWeb.ImageView, "_image_box.html", image: image, size: :thumb_small, conn: @conn - a.block__header--single-item.center href="/search?q=*&sf=score&sd=desc" - ' All Time Top Scoring - .block.hide-mobile - a.block__header--single-item.center href="/channels" - ' Streams - = for channel <- @streams do - = render PhilomenaWeb.ActivityView, "_channel_strip.html", channel: channel, conn: @conn - .block.hide-mobile - a.block__header--single-item.center href="/forums" - ' Forum Activity - = for topic <- @topics do - = render PhilomenaWeb.ActivityView, "_topic_strip.html", topic: topic, conn: @conn - .block.hide-mobile - a.block__header--single-item.center href="/comments" - ' Recent Comments - = for comment <- @comments do - = render PhilomenaWeb.ActivityView, "_comment_strip.html", comment: comment, conn: @conn - a.block__header--single-item.center href="/search?q=first_seen_at.gt:3 days ago&sf=comment_count&sd=desc" - ' Most Commented-on Images + = if @show_sidebar do + aside.column-layout__left#activity-side + = if not is_nil(@featured_image) and not PhilomenaWeb.ImageView.filter_or_spoiler_hits?(@conn, @featured_image) do + .center + h4.remove-top-margin Featured Image + = render PhilomenaWeb.ImageView, "_image_box.html", image: @featured_image, size: :medium, conn: @conn + .block.block--fixed.block--fixed--sub.block--success.center.hide-mobile + ' Enjoy the site? + a href="/pages/donations" + ' Become a patron or donate! + .block.block--fixed.block--fixed--sub.center.hide-mobile + ' Issues? Want to chat? + a href="/pages/contact" Contact us! + .block.hide-mobile + a.block__header--single-item.center href="/search?q=first_seen_at.gt:3 days ago&sf=wilson_score&sd=desc" + ' Trending Images + .block__content.flex.flex--centered.flex--wrap.image-flex-grid + = for image <- @top_scoring do + = render PhilomenaWeb.ImageView, "_image_box.html", image: image, size: :thumb_small, conn: @conn + a.block__header--single-item.center href="/search?q=*&sf=score&sd=desc" + ' All Time Top Scoring + .block.hide-mobile + a.block__header--single-item.center href="/channels" + ' Streams + = for channel <- @streams do + = render PhilomenaWeb.ActivityView, "_channel_strip.html", channel: channel, conn: @conn + .block.hide-mobile + a.block__header--single-item.center href="/forums" + ' Forum Activity + = for topic <- @topics do + = render PhilomenaWeb.ActivityView, "_topic_strip.html", topic: topic, conn: @conn + .block.hide-mobile + a.block__header--single-item.center href="/comments" + ' Recent Comments + = for comment <- @comments do + = render PhilomenaWeb.ActivityView, "_comment_strip.html", comment: comment, conn: @conn + a.block__header--single-item.center href="/search?q=first_seen_at.gt:3 days ago&sf=comment_count&sd=desc" + ' Most Commented-on Images .column-layout__main = render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, size: :thumb - = if not is_nil(@watched) and Enum.any?(@watched) do + = if @show_sidebar and not is_nil(@watched) and Enum.any?(@watched) do .block .block__header span.block__header__title diff --git a/lib/philomena_web/templates/filter/index.html.slime b/lib/philomena_web/templates/filter/index.html.slime index aad09625..1598658f 100644 --- a/lib/philomena_web/templates/filter/index.html.slime +++ b/lib/philomena_web/templates/filter/index.html.slime @@ -39,6 +39,12 @@ = for filter <- @system_filters do = render PhilomenaWeb.FilterView, "_filter.html", conn: @conn, filter: filter + = if @current_user do + h2 Recent Filters + p + ' Clicking this button will clear the recent filters list in the header dropdown. + = button_to "Clear recent filter list", Routes.filter_clear_recent_path(@conn, :delete), method: "delete", class: "button" + h2 Search Filters p ' Some users maintain custom filters which are publicly shared; you can search these filters with the box below. @@ -50,7 +56,7 @@ .fieldlabel ' For more information, see the a href="/pages/search_syntax" search syntax documentation - ' . Search results are sorted by creation date. + ' . Search results are sorted alphabetically. = if @conn.params["fq"] do h2 Search Results @@ -107,7 +113,7 @@ table.table td Full Text td Matches the description of this filter. td - code = link "description:show's rating", to: Routes.filter_path(@conn, :index, fq: "description:the show's rating") + code = link "description:the show's rating", to: Routes.filter_path(@conn, :index, fq: "description:the show's rating") tr td code created_at @@ -171,4 +177,4 @@ table.table code = link "user_id:307505", to: Routes.filter_path(@conn, :index, fq: "user_id:307505") = if @conn.params["fq"] do - p = link("Back to filters", to: Routes.filter_path(@conn, :index)) \ No newline at end of file + p = link("Back to filters", to: Routes.filter_path(@conn, :index)) diff --git a/lib/philomena_web/templates/setting/edit.html.slime b/lib/philomena_web/templates/setting/edit.html.slime index 002c9bf3..59843d3a 100644 --- a/lib/philomena_web/templates/setting/edit.html.slime +++ b/lib/philomena_web/templates/setting/edit.html.slime @@ -9,6 +9,8 @@ h1 Content Settings = if @conn.assigns.current_user do = link "Watch List", to: "#", class: "selected", data: [click_tab: "watched"] = link "Display", to: "#", data: [click_tab: "display"] + = link "Comments", to: "#", data: [click_tab: "comments"] + = link "Notifications", to: "#", data: [click_tab: "notifications"] = link "Metadata", to: "#", data: [click_tab: "metadata"] = link "Local", to: "#", data: [click_tab: "local"] - else @@ -55,14 +57,14 @@ h1 Content Settings => label f, :use_centered_layout => checkbox f, :use_centered_layout, class: "checkbox" .fieldlabel: i Align content to the center of the page - try this option out if you browse the site on a tablet or a fairly wide screen. + .field + => label f, :show_sidebar_and_watched_images + => checkbox f, :show_sidebar_and_watched_images, class: "checkbox" + .fieldlabel: i Show the sidebar and new watched images on the homepage (the default) or hide it. .field => label f, :hide_vote_counts => checkbox f, :hide_vote_counts, class: "checkbox" .fieldlabel: i Hide upvote and downvote counts on images, showing only the overall score - .field - => label f, :comments_newest_first, "Newest comments first" - => checkbox f, :comments_newest_first - .fieldlabel: i Display the newest comments at the top of the page. .field => label f, :images_per_page => number_input f, :images_per_page, min: 1, max: 50, step: 1, class: "input" @@ -71,11 +73,6 @@ h1 Content Settings i ' This is the number of images per page that are displayed on image listings and searches, up to a maximum of 50. ' For 1080p monitors, try 24. - .field - => label f, :comments_per_page - => number_input f, :comments_per_page, min: 1, max: 100, step: 1, class: "input" - = error_tag f, :comments_per_page - .fieldlabel: i This is the number of comments per page that are displayed on image pages. .field => label f, :scale_large_images => checkbox f, :scale_large_images, class: "checkbox" @@ -86,6 +83,45 @@ h1 Content Settings = error_tag f, :theme .fieldlabel: i Preview themes by selecting one from the dropdown. Saving sets the currently selected theme. + .block__tab.hidden.flex.flex--maybe-wrap data-tab="comments" + div + .field + => label f, :comments_newest_first, "Newest comments first" + => checkbox f, :comments_newest_first + .fieldlabel: i Display the newest comments at the top of the page. + .field + => label f, :comments_always_jump_to_last, "Show latest comment page" + => checkbox f, :comments_always_jump_to_last + .fieldlabel + i + ' This setting takes effect when the previous is disabled. Always jump to the latest page (enabled) or show the first page if the oldest comments are shown at the top of the page. + br + ' Posting will always direct you to the latest page so that you can see your comment in context. + .field + => label f, :comments_per_page + => number_input f, :comments_per_page, min: 1, max: 100, step: 1, class: "input" + = error_tag f, :comments_per_page + .fieldlabel: i This is the number of comments per page that are displayed on image pages. + .field + => label f, :messages_newest_first, "Newest messages first" + => checkbox f, :messages_newest_first + .fieldlabel: i Show the newest messages first (enabled) or show the oldest messages at the top of a conversation. Enabling this makes it feel more like a top-posted email quote chain. + + .block__tab.hidden.flex.flex--maybe-wrap data-tab="notifications" + div + .field + => label f, :watch_on_reply, "Subscribe on Reply" + => checkbox f, :watch_on_reply, class: "checkbox" + .fieldlabel: i Subscribe on Reply means you'll be subscribed things (images or topics) automatically as soon as you post a comment or reply, keeping you in the conversation. + .field + => label f, :watch_on_upload, "Subscribe on Upload" + => checkbox f, :watch_on_upload, class: "checkbox" + .fieldlabel: i Subscribe on Upload means you'll be subscribed to images automatically as soon as you upload, to help you keep track of comments. + .field + => label f, :watch_on_new_topic, "Subscribe on New Threads" + => checkbox f, :watch_on_new_topic, class: "checkbox" + .fieldlabel: i Subscribe on New Threads means you'll be subscribed to threads automatically as soon as you post, to help you keep track of replies. + .block__tab.hidden.flex.flex--maybe-wrap data-tab="metadata" div .field