diff options
| -rw-r--r-- | lib/pleroma/marker.ex | 27 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 64 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/marker_view.ex | 1 | ||||
| -rw-r--r-- | priv/repo/migrations/20191021113356_add_unread_to_marker.exs | 46 | ||||
| -rw-r--r-- | test/marker_test.exs | 21 | ||||
| -rw-r--r-- | test/notification_test.exs | 10 | ||||
| -rw-r--r-- | test/web/mastodon_api/controllers/marker_controller_test.exs | 7 | ||||
| -rw-r--r-- | test/web/mastodon_api/views/marker_view_test.exs | 4 | 
8 files changed, 158 insertions, 22 deletions
| diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 7f87c86c3..a7ea542dd 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -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 diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..373f9b06a 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -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) diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex index 38fbeed5f..1501c2a30 100644 --- a/lib/pleroma/web/mastodon_api/views/marker_view.ex +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -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 index 000000000..c15e2ff13 --- /dev/null +++ b/priv/repo/migrations/20191021113356_add_unread_to_marker.exs @@ -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 diff --git a/test/marker_test.exs b/test/marker_test.exs index 04bd67fe6..5d03db48e 100644 --- a/test/marker_test.exs +++ b/test/marker_test.exs @@ -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) diff --git a/test/notification_test.exs b/test/notification_test.exs index f8d429223..d358e433f 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -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 diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs index 1fcad873d..5e7b4001f 100644 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -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                 } diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs index 8a5c89d56..3ce794617 100644 --- a/test/web/mastodon_api/views/marker_view_test.exs +++ b/test/web/mastodon_api/views/marker_view_test.exs @@ -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               } | 
