Merge branch 'develop' into issue/1276
authorMaksim Pechnikov <parallel588@gmail.com>
Thu, 5 Dec 2019 09:22:19 +0000 (12:22 +0300)
committerMaksim Pechnikov <parallel588@gmail.com>
Thu, 5 Dec 2019 09:22:19 +0000 (12:22 +0300)
CHANGELOG.md
docs/API/differences_in_mastoapi_responses.md
lib/pleroma/marker.ex
lib/pleroma/notification.ex
lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
lib/pleroma/web/mastodon_api/views/marker_view.ex
priv/repo/migrations/20191030202008_add_unread_to_marker.exs [new file with mode: 0644]
test/marker_test.exs
test/notification_test.exs
test/web/mastodon_api/controllers/marker_controller_test.exs
test/web/mastodon_api/views/marker_view_test.exs

index a06ea211eb9dcf6b401c799c5357d3b8d04a1093..f021fefc3b23ff4969c00090ecbb9dbc3056673d 100644 (file)
@@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: `pleroma.thread_muted` to the Status entity
 - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
 - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
+- Mastodon API: Add `pleroma.unread_count` to the Marker entity
 </details>
 
 ### Added
index 006d17c1bc28abd63ae6331f0cf9e9ab99dbac9a..eea7f07070e15e1a1aa7147d88e09324c6768b73 100644 (file)
@@ -169,3 +169,9 @@ Has theses additionnal parameters (which are the same as in Pleroma-API):
     * `captcha_solution`: optional, contains provider-specific captcha solution,
     * `captcha_token`: optional, contains provider-specific captcha token
     * `token`: invite token required when the registerations aren't public.
+
+## Markers
+
+Has these additional fields under the `pleroma` object:
+
+- `unread_count`: contains number unread notifications
index 7f87c86c36c157b961bc7be8d30153461bb55102..2d217a0b7a7a83e580fe2cecfa837a25fff58fff 100644 (file)
@@ -9,22 +9,36 @@ defmodule Pleroma.Marker do
   import Ecto.Query
 
   alias Ecto.Multi
+  alias Pleroma.Notification
   alias Pleroma.Repo
   alias Pleroma.User
+  alias __MODULE__
 
   @timelines ["notifications"]
+  @type t :: %__MODULE__{}
 
   schema "markers" do
     field(:last_read_id, :string, default: "")
     field(:timeline, :string, default: "")
     field(:lock_version, :integer, default: 0)
+    field(:unread_count, :integer, default: 0)
 
     belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
     timestamps()
   end
 
-  def get_markers(user, timelines \\ []) do
-    Repo.all(get_query(user, timelines))
+  @doc """
+  Gets markers by user and timeline.
+
+  opts:
+  `recount_unread` - run force recount unread notifications for `true` value
+  """
+  @spec get_markers(User.t(), list(String), map()) :: list(t())
+  def get_markers(user, timelines \\ [], opts \\ %{}) do
+    user
+    |> get_query(timelines)
+    |> recount_unread_notifications(opts[:recount_unread])
+    |> Repo.all()
   end
 
   def upsert(%User{} = user, attrs) do
@@ -38,13 +52,38 @@ defmodule Pleroma.Marker do
 
       Multi.insert(multi, timeline, marker,
         returning: true,
-        on_conflict: {:replace, [:last_read_id]},
+        on_conflict: {:replace, [:last_read_id, :unread_count]},
         conflict_target: [:user_id, :timeline]
       )
     end)
     |> Repo.transaction()
   end
 
+  @spec multi_set_unread_count(Multi.t(), User.t(), String.t()) :: Multi.t()
+  def multi_set_unread_count(multi, %User{} = user, "notifications") do
+    multi
+    |> Multi.run(:counters, fn _repo, _changes ->
+      {:ok,
+       %{
+         unread_count: Repo.aggregate(Notification.unread_count_query(user), :count, :id),
+         last_read_id: Repo.one(Notification.last_read_query(user))
+       }}
+    end)
+    |> Multi.insert(
+      :marker,
+      fn %{counters: attrs} ->
+        %Marker{timeline: "notifications", user_id: user.id}
+        |> struct(attrs)
+        |> Ecto.Changeset.change()
+      end,
+      returning: true,
+      on_conflict: {:replace, [:last_read_id, :unread_count]},
+      conflict_target: [:user_id, :timeline]
+    )
+  end
+
+  def multi_set_unread_count(multi, _, _), do: multi
+
   defp get_marker(user, timeline) do
     case Repo.find_resource(get_query(user, timeline)) do
       {:ok, marker} -> %__MODULE__{marker | user: user}
@@ -55,7 +94,7 @@ defmodule Pleroma.Marker do
   @doc false
   defp changeset(marker, attrs) do
     marker
-    |> cast(attrs, [:last_read_id])
+    |> cast(attrs, [:last_read_id, :unread_count])
     |> validate_required([:user_id, :timeline, :last_read_id])
     |> validate_inclusion(:timeline, @timelines)
   end
@@ -71,4 +110,18 @@ defmodule Pleroma.Marker do
     |> by_user_id(user.id)
     |> by_timeline(timelines)
   end
+
+  defp recount_unread_notifications(query, true) do
+    from(
+      q in query,
+      left_join: n in "notifications",
+      on: n.user_id == q.user_id and n.seen == false,
+      group_by: [:id],
+      select_merge: %{
+        unread_count: fragment("count(?)", n.id)
+      }
+    )
+  end
+
+  defp recount_unread_notifications(query, _), do: query
 end
index f37e7ec671ee88c39848cc92a7f715621766999e..d6149cd0da2bf8500bab549e7b3fd0efc063fa6e 100644 (file)
@@ -5,7 +5,9 @@
 defmodule Pleroma.Notification do
   use Ecto.Schema
 
+  alias Ecto.Multi
   alias Pleroma.Activity
+  alias Pleroma.Marker
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Pagination
@@ -34,6 +36,25 @@ defmodule Pleroma.Notification do
     |> cast(attrs, [:seen])
   end
 
+  @spec unread_count_query(User.t()) :: Ecto.Queryable.t()
+  def unread_count_query(user) do
+    from(q in Pleroma.Notification,
+      where: q.user_id == ^user.id,
+      where: q.seen == false
+    )
+  end
+
+  @spec last_read_query(User.t()) :: Ecto.Queryable.t()
+  def last_read_query(user) do
+    from(q in Pleroma.Notification,
+      where: q.user_id == ^user.id,
+      where: q.seen == true,
+      select: type(q.id, :string),
+      limit: 1,
+      order_by: [desc: :id]
+    )
+  end
+
   def for_user_query(user, opts \\ []) do
     Notification
     |> where(user_id: ^user.id)
@@ -151,25 +172,23 @@ defmodule Pleroma.Notification do
     |> Repo.all()
   end
 
-  def set_read_up_to(%{id: user_id} = _user, id) do
+  def set_read_up_to(%{id: user_id} = user, id) do
     query =
       from(
         n in Notification,
         where: n.user_id == ^user_id,
         where: n.id <= ^id,
         where: n.seen == false,
-        update: [
-          set: [
-            seen: true,
-            updated_at: ^NaiveDateTime.utc_now()
-          ]
-        ],
         # Ideally we would preload object and activities here
         # but Ecto does not support preloads in update_all
         select: n.id
       )
 
-    {_, notification_ids} = Repo.update_all(query, [])
+    {:ok, %{ids: {_, notification_ids}}} =
+      Multi.new()
+      |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+      |> Marker.multi_set_unread_count(user, "notifications")
+      |> Repo.transaction()
 
     Notification
     |> where([n], n.id in ^notification_ids)
@@ -186,11 +205,18 @@ defmodule Pleroma.Notification do
     |> Repo.all()
   end
 
+  @spec read_one(User.t(), String.t()) ::
+          {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
   def read_one(%User{} = user, notification_id) do
     with {:ok, %Notification{} = notification} <- get(user, notification_id) do
-      notification
-      |> changeset(%{seen: true})
-      |> Repo.update()
+      Multi.new()
+      |> Multi.update(:update, changeset(notification, %{seen: true}))
+      |> Marker.multi_set_unread_count(user, "notifications")
+      |> Repo.transaction()
+      |> case do
+        {:ok, %{update: notification}} -> {:ok, notification}
+        {:error, :update, changeset, _} -> {:error, changeset}
+      end
     end
   end
 
@@ -243,8 +269,11 @@ defmodule Pleroma.Notification do
     object = Object.normalize(activity)
 
     unless object && object.data["type"] == "Answer" do
-      users = get_notified_from_activity(activity)
-      notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
+      notifications =
+        activity
+        |> get_notified_from_activity()
+        |> Enum.map(&create_notification(activity, &1))
+
       {:ok, notifications}
     else
       {:ok, []}
@@ -266,8 +295,11 @@ defmodule Pleroma.Notification do
   # TODO move to sql, too.
   def create_notification(%Activity{} = activity, %User{} = user) do
     unless skip?(activity, user) do
-      notification = %Notification{user_id: user.id, activity: activity}
-      {:ok, notification} = Repo.insert(notification)
+      {:ok, %{notification: notification}} =
+        Multi.new()
+        |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
+        |> Marker.multi_set_unread_count(user, "notifications")
+        |> Repo.transaction()
 
       ["user", "user:notification"]
       |> Streamer.stream(notification)
index ce025624d3f0d8ca1997d8d0a72ff6149e146757..6649ffbda106eb5b1f0401f115f957075d9657c6 100644 (file)
@@ -18,7 +18,13 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
 
   # GET /api/v1/markers
   def index(%{assigns: %{user: user}} = conn, params) do
-    markers = Pleroma.Marker.get_markers(user, params["timeline"])
+    markers =
+      Pleroma.Marker.get_markers(
+        user,
+        params["timeline"],
+        %{recount_unread: true}
+      )
+
     render(conn, "markers.json", %{markers: markers})
   end
 
index 38fbeed5f2b6c62ddf51ca5c02dc2028fc9e9745..81545cff08e2b82166ce87b44c150829c0d09026 100644 (file)
@@ -10,7 +10,10 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
       Map.put_new(acc, m.timeline, %{
         last_read_id: m.last_read_id,
         version: m.lock_version,
-        updated_at: NaiveDateTime.to_iso8601(m.updated_at)
+        updated_at: NaiveDateTime.to_iso8601(m.updated_at),
+        pleroma: %{
+          unread_count: m.unread_count
+        }
       })
     end)
   end
diff --git a/priv/repo/migrations/20191030202008_add_unread_to_marker.exs b/priv/repo/migrations/20191030202008_add_unread_to_marker.exs
new file mode 100644 (file)
index 0000000..2b3abc6
--- /dev/null
@@ -0,0 +1,48 @@
+defmodule Pleroma.Repo.Migrations.AddUnreadToMarker do
+  use Ecto.Migration
+  import Ecto.Query
+  alias Pleroma.Repo
+
+  def up do
+    alter table(:markers) do
+      add_if_not_exists(:unread_count, :integer, default: 0)
+    end
+
+    flush()
+
+    update_markers()
+  end
+
+  def down do
+    alter table(:markers) do
+      remove_if_exists(:unread_count, :integer)
+    end
+  end
+
+  def update_markers do
+    now = NaiveDateTime.utc_now()
+
+    markers_attrs =
+      from(q in "notifications",
+        select: %{
+          timeline: "notifications",
+          user_id: q.user_id,
+          unread_count: fragment("SUM( CASE WHEN seen = false THEN 1 ELSE 0 END )"),
+          last_read_id:
+            type(fragment("MAX( CASE WHEN seen = true THEN id ELSE null END )"), :string)
+        },
+        group_by: [q.user_id]
+      )
+      |> Repo.all()
+      |> Enum.map(fn attrs ->
+        attrs
+        |> Map.put_new(:inserted_at, now)
+        |> Map.put_new(:updated_at, now)
+      end)
+
+    Repo.insert_all("markers", markers_attrs,
+      on_conflict: {:replace, [:last_read_id, :unread_count]},
+      conflict_target: [:user_id, :timeline]
+    )
+  end
+end
index 04bd67fe672028f3d91898e397bd17277a2a716a..7b1d2218ab0f57c566a70e61472450d1d41f5069 100644 (file)
@@ -8,6 +8,27 @@ defmodule Pleroma.MarkerTest do
 
   import Pleroma.Factory
 
+  describe "multi_set_unread_count/3" do
+    test "returns multi" do
+      user = insert(:user)
+
+      assert %Ecto.Multi{
+               operations: [marker: {:run, _}, counters: {:run, _}]
+             } =
+               Marker.multi_set_unread_count(
+                 Ecto.Multi.new(),
+                 user,
+                 "notifications"
+               )
+    end
+
+    test "return empty multi" do
+      user = insert(:user)
+      multi = Ecto.Multi.new()
+      assert Marker.multi_set_unread_count(multi, user, "home") == multi
+    end
+  end
+
   describe "get_markers/2" do
     test "returns user markers" do
       user = insert(:user)
@@ -15,6 +36,20 @@ defmodule Pleroma.MarkerTest do
       insert(:marker, timeline: "home", user: user)
       assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
     end
+
+    test "returns user markers with recount unread notifications" do
+      user = insert(:user)
+      marker = insert(:marker, user: user)
+      insert(:notification, user: user)
+      insert(:notification, user: user)
+      insert(:marker, timeline: "home", user: user)
+
+      assert Marker.get_markers(
+               user,
+               ["notifications"],
+               %{recount_unread: true}
+             ) == [%Marker{refresh_record(marker) | unread_count: 2}]
+    end
   end
 
   describe "upsert/2" do
index dcbffeafe39b16c1afdf7505fc4744cf2119ce25..8db3495262a0e44e0c3cf2deef6f462d8130d650 100644 (file)
@@ -31,6 +31,9 @@ defmodule Pleroma.NotificationTest do
       assert notified_ids == [other_user.id, third_user.id]
       assert notification.activity_id == activity.id
       assert other_notification.activity_id == activity.id
+
+      assert [%Pleroma.Marker{unread_count: 2}] =
+               Pleroma.Marker.get_markers(other_user, ["notifications"])
     end
 
     test "it creates a notification for subscribed users" do
@@ -310,6 +313,13 @@ defmodule Pleroma.NotificationTest do
       assert n1.seen == true
       assert n2.seen == true
       assert n3.seen == false
+
+      assert %Pleroma.Marker{unread_count: 1} =
+               Pleroma.Repo.get_by(
+                 Pleroma.Marker,
+                 user_id: other_user.id,
+                 timeline: "notifications"
+               )
     end
   end
 
index 1fcad873dc68eae5326c94f13524e635a2071bb2..64bf79bb1e482508e97cf374b7bcfaac8316da68 100644 (file)
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
     test "gets markers with correct scopes", %{conn: conn} do
       user = insert(:user)
       token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
+      insert_list(7, :notification, user: user)
 
       {:ok, %{"notifications" => marker}} =
         Pleroma.Marker.upsert(
@@ -29,7 +30,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
                "notifications" => %{
                  "last_read_id" => "69420",
                  "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
-                 "version" => 0
+                 "version" => 0,
+                 "pleroma" => %{"unread_count" => 7}
                }
              }
     end
@@ -70,7 +72,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
                "notifications" => %{
                  "last_read_id" => "69420",
                  "updated_at" => _,
-                 "version" => 0
+                 "version" => 0,
+                 "pleroma" => %{"unread_count" => 0}
                }
              } = response
     end
@@ -99,7 +102,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
                "notifications" => %{
                  "last_read_id" => "69888",
                  "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
-                 "version" => 0
+                 "version" => 0,
+                 "pleroma" => %{"unread_count" => 0}
                }
              }
     end
index 8a5c89d565a0640eb672d7a31d0d3baef02b726c..f172e50235307abdec3c46a4363767489e91bd92 100644 (file)
@@ -8,19 +8,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
   import Pleroma.Factory
 
   test "returns markers" do
-    marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
+    marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5)
     marker2 = insert(:marker, timeline: "home", last_read_id: "42")
 
     assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
              "home" => %{
                last_read_id: "42",
                updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
-               version: 0
+               version: 0,
+               pleroma: %{unread_count: 0}
              },
              "notifications" => %{
                last_read_id: "17",
                updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
-               version: 0
+               version: 0,
+               pleroma: %{unread_count: 5}
              }
            }
   end