aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--docs/API/differences_in_mastoapi_responses.md6
-rw-r--r--lib/pleroma/marker.ex61
-rw-r--r--lib/pleroma/notification.ex62
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/marker_controller.ex8
-rw-r--r--lib/pleroma/web/mastodon_api/views/marker_view.ex5
-rw-r--r--priv/repo/migrations/20191030202008_add_unread_to_marker.exs48
-rw-r--r--test/marker_test.exs35
-rw-r--r--test/notification_test.exs10
-rw-r--r--test/web/mastodon_api/controllers/marker_controller_test.exs10
-rw-r--r--test/web/mastodon_api/views/marker_view_test.exs8
11 files changed, 227 insertions, 27 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d00097748..4a2296cdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,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
- Admin API: Render whole status in grouped reports
</details>
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index 006d17c1b..eea7f0707 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -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
diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
index 7f87c86c3..2d217a0b7 100644
--- a/lib/pleroma/marker.ex
+++ b/lib/pleroma/marker.ex
@@ -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
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 71423ce5e..11adbb77b 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
@@ -36,6 +38,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
+
defp for_user_query_ap_id_opts(user, opts) do
ap_id_relations =
[:block] ++
@@ -185,25 +206,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)
@@ -220,11 +239,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
@@ -277,8 +303,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, []}
@@ -300,8 +329,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/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index ce025624d..6649ffbda 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -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
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..2b3abc682
--- /dev/null
+++ b/priv/repo/migrations/20191030202008_add_unread_to_marker.exs
@@ -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
diff --git a/test/marker_test.exs b/test/marker_test.exs
index 04bd67fe6..7b1d2218a 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)
@@ -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
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 827ac4f06..80a69d4df 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..64bf79bb1 100644
--- a/test/web/mastodon_api/controllers/marker_controller_test.exs
+++ b/test/web/mastodon_api/controllers/marker_controller_test.exs
@@ -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
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