Merge branch 'develop' into issue/1276
authorMaksim Pechnikov <parallel588@gmail.com>
Wed, 30 Oct 2019 20:07:18 +0000 (23:07 +0300)
committerMaksim Pechnikov <parallel588@gmail.com>
Wed, 30 Oct 2019 20:07:18 +0000 (23:07 +0300)
lib/pleroma/marker.ex
lib/pleroma/notification.ex
lib/pleroma/web/mastodon_api/views/marker_view.ex
priv/repo/migrations/20191021113356_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 7f87c86c36c157b961bc7be8d30153461bb55102..a7ea542dd009b8be54452dc0e379c6691dbc82e3 100644 (file)
@@ -11,6 +11,7 @@ defmodule Pleroma.Marker do
   alias Ecto.Multi
   alias Pleroma.Repo
   alias Pleroma.User
+  alias __MODULE__
 
   @timelines ["notifications"]
 
@@ -18,6 +19,7 @@ defmodule Pleroma.Marker 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()
@@ -38,13 +40,34 @@ 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, Repo.one(Pleroma.Notification.notifications_info_query(user))}
+    end)
+    |> Multi.insert(
+      :marker,
+      fn %{counters: attrs} ->
+        Marker
+        |> 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 +78,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
index b7ecf51e4681fc9a25b8d1cd9d596c928f930bba..373f9b06a85b7f2771be63ab45e217d61f4313f9 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,20 @@ defmodule Pleroma.Notification do
     |> cast(attrs, [:seen])
   end
 
+  @spec notifications_info_query(User.t()) :: Ecto.Queryable.t()
+  def notifications_info_query(user) do
+    from(q in Pleroma.Notification,
+      where: q.user_id == ^user.id,
+      select: %{
+        timeline: "notifications",
+        user_id: type(^user.id, :string),
+        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)
+      }
+    )
+  end
+
   def for_user_query(user, opts \\ []) do
     Notification
     |> where(user_id: ^user.id)
@@ -151,25 +167,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 +200,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 +264,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, []}
@@ -253,8 +277,11 @@ defmodule Pleroma.Notification do
 
   def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
       when type in ["Like", "Announce", "Follow"] 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}
   end
 
@@ -263,8 +290,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 38fbeed5f2b6c62ddf51ca5c02dc2028fc9e9745..1501c2a30fb07b16408b675214878125523f91c7 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
       Map.put_new(acc, m.timeline, %{
         last_read_id: m.last_read_id,
         version: m.lock_version,
+        unread_count: m.unread_count,
         updated_at: NaiveDateTime.to_iso8601(m.updated_at)
       })
     end)
diff --git a/priv/repo/migrations/20191021113356_add_unread_to_marker.exs b/priv/repo/migrations/20191021113356_add_unread_to_marker.exs
new file mode 100644 (file)
index 0000000..c15e2ff
--- /dev/null
@@ -0,0 +1,46 @@
+defmodule Pleroma.Repo.Migrations.AddUnreadToMarker do
+  use Ecto.Migration
+  import Ecto.Query
+  alias Pleroma.Repo
+  alias Pleroma.Notification
+
+  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
+    from(q in Notification,
+      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.each(fn attrs ->
+      Pleroma.Marker
+      |> struct(attrs)
+      |> Ecto.Changeset.change()
+      |> Pleroma.Repo.insert(
+        returning: true,
+        on_conflict: {:replace, [:last_read_id, :unread_count]},
+        conflict_target: [:user_id, :timeline]
+      )
+    end)
+  end
+end
index 04bd67fe672028f3d91898e397bd17277a2a716a..5d03db48ef1c7b42ec83ee08d8f2df37419012e5 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)
index f8d42922322353a2d509dc8fc2c4c33313db186a..d358e433fabb788ea6f862a17168782b2e14a8da 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..5e7b4001f257e8009f16156f4e1af8aef1fdc11f 100644 (file)
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
       {:ok, %{"notifications" => marker}} =
         Pleroma.Marker.upsert(
           user,
-          %{"notifications" => %{"last_read_id" => "69420"}}
+          %{"notifications" => %{"last_read_id" => "69420", "unread_count" => 7}}
         )
 
       response =
@@ -28,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
       assert response == %{
                "notifications" => %{
                  "last_read_id" => "69420",
+                 "unread_count" => 7,
                  "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
                  "version" => 0
                }
@@ -70,7 +71,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
                "notifications" => %{
                  "last_read_id" => "69420",
                  "updated_at" => _,
-                 "version" => 0
+                 "version" => 0,
+                 "unread_count" => 0
                }
              } = response
     end
@@ -98,6 +100,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
       assert response == %{
                "notifications" => %{
                  "last_read_id" => "69888",
+                 "unread_count" => 0,
                  "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
                  "version" => 0
                }
index 8a5c89d565a0640eb672d7a31d0d3baef02b726c..3ce794617f275f23c49925a5f6ef18421c053b69 100644 (file)
@@ -8,17 +8,19 @@ 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",
+               unread_count: 0,
                updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
                version: 0
              },
              "notifications" => %{
                last_read_id: "17",
+               unread_count: 5,
                updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
                version: 0
              }