diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | docs/API/differences_in_mastoapi_responses.md | 6 | ||||
-rw-r--r-- | lib/mix/tasks/pleroma/marker.ex | 36 | ||||
-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 | 5 | ||||
-rw-r--r-- | priv/repo/migrations/20191030202008_add_unread_to_marker.exs | 18 | ||||
-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 | 11 | ||||
-rw-r--r-- | test/web/mastodon_api/views/marker_view_test.exs | 8 |
11 files changed, 180 insertions, 27 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e5424c6..c14c8c0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) - 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: Add `pleroma.unread_count` to the Marker entity </details> ### Added diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index aca0f5e0e..d18b976b6 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -155,3 +155,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 diff --git a/lib/mix/tasks/pleroma/marker.ex b/lib/mix/tasks/pleroma/marker.ex new file mode 100644 index 000000000..bebef0d6a --- /dev/null +++ b/lib/mix/tasks/pleroma/marker.ex @@ -0,0 +1,36 @@ +defmodule Mix.Tasks.Pleroma.Marker do + use Mix.Task + import Mix.Pleroma + import Ecto.Query + + alias Pleroma.Notification + alias Pleroma.Repo + + def run(["update_markers"]) do + start_pleroma() + + 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) + + shell_info("Done") + end +end 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..81545cff0 100644 --- a/lib/pleroma/web/mastodon_api/views/marker_view.ex +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -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 index 000000000..f81339c9f --- /dev/null +++ b/priv/repo/migrations/20191030202008_add_unread_to_marker.exs @@ -0,0 +1,18 @@ +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 + end + + def down do + alter table(:markers) do + remove_if_exists(:unread_count, :integer) + 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..8bcfcb7e1 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 = @@ -29,7 +29,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 +71,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 +101,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 diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs index 8a5c89d56..f172e5023 100644 --- a/test/web/mastodon_api/views/marker_view_test.exs +++ b/test/web/mastodon_api/views/marker_view_test.exs @@ -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 |