Mastodon API: Add pleroma.unread_conversation_count
to the Account entity
This commit is contained in:
parent
4fe2af3b2d
commit
06d9df79c5
11 changed files with 211 additions and 14 deletions
|
@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
|
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
|
||||||
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
|
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
|
||||||
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
|
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
|
||||||
|
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||||
|
|
|
@ -56,6 +56,7 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
|
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
|
||||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
|
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
|
||||||
- `deactivated`: boolean, true when the user is deactivated
|
- `deactivated`: boolean, true when the user is deactivated
|
||||||
|
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,8 @@ def create_or_bump_for(activity, opts \\ []) do
|
||||||
|
|
||||||
participations =
|
participations =
|
||||||
Enum.map(users, fn user ->
|
Enum.map(users, fn user ->
|
||||||
|
User.increment_unread_conversation_count(conversation, user)
|
||||||
|
|
||||||
{:ok, participation} =
|
{:ok, participation} =
|
||||||
Participation.create_for_user_and_conversation(user, conversation, opts)
|
Participation.create_for_user_and_conversation(user, conversation, opts)
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,15 @@ def mark_as_read(participation) do
|
||||||
participation
|
participation
|
||||||
|> read_cng(%{read: true})
|
|> read_cng(%{read: true})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|> case do
|
||||||
|
{:ok, participation} ->
|
||||||
|
participation = Repo.preload(participation, :user)
|
||||||
|
User.set_unread_conversation_count(participation.user)
|
||||||
|
{:ok, participation}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_as_unread(participation) do
|
def mark_as_unread(participation) do
|
||||||
|
@ -135,4 +144,12 @@ def set_recipients(participation, user_ids) do
|
||||||
|
|
||||||
{:ok, Repo.preload(participation, :recipients, force: true)}
|
{:ok, Repo.preload(participation, :recipients, force: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_conversation_count_for_user(user) do
|
||||||
|
from(p in __MODULE__,
|
||||||
|
where: p.user_id == ^user.id,
|
||||||
|
where: not p.read,
|
||||||
|
select: %{count: count(p.id)}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.User do
|
||||||
alias Comeonin.Pbkdf2
|
alias Comeonin.Pbkdf2
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Conversation.Participation
|
||||||
alias Pleroma.Delivery
|
alias Pleroma.Delivery
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
|
@ -842,6 +843,61 @@ def maybe_update_following_count(%User{local: false} = user) do
|
||||||
|
|
||||||
def maybe_update_following_count(user), do: user
|
def maybe_update_following_count(user), do: user
|
||||||
|
|
||||||
|
def set_unread_conversation_count(%User{local: true} = user) do
|
||||||
|
unread_query = Participation.unread_conversation_count_for_user(user)
|
||||||
|
|
||||||
|
User
|
||||||
|
|> where([u], u.id == ^user.id)
|
||||||
|
|> join(:inner, [u], p in subquery(unread_query))
|
||||||
|
|> update([u, p],
|
||||||
|
set: [
|
||||||
|
info:
|
||||||
|
fragment(
|
||||||
|
"jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
|
||||||
|
u.info,
|
||||||
|
p.count
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|> select([u], u)
|
||||||
|
|> Repo.update_all([])
|
||||||
|
|> case do
|
||||||
|
{1, [%{info: %User.Info{}} = user]} -> set_cache(user)
|
||||||
|
_ -> {:error, user}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_unread_conversation_count(_), do: :noop
|
||||||
|
|
||||||
|
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
|
||||||
|
unread_query =
|
||||||
|
Participation.unread_conversation_count_for_user(user)
|
||||||
|
|> where([p], p.conversation_id == ^conversation.id)
|
||||||
|
|
||||||
|
User
|
||||||
|
|> join(:inner, [u], p in subquery(unread_query))
|
||||||
|
|> update([u, p],
|
||||||
|
set: [
|
||||||
|
info:
|
||||||
|
fragment(
|
||||||
|
"jsonb_set(?, '{unread_conversation_count}', ((?->>'unread_conversation_count')::int + 1)::varchar::jsonb, true)",
|
||||||
|
u.info,
|
||||||
|
u.info
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|> where([u], u.id == ^user.id)
|
||||||
|
|> where([u, p], p.count == 0)
|
||||||
|
|> select([u], u)
|
||||||
|
|> Repo.update_all([])
|
||||||
|
|> case do
|
||||||
|
{1, [%{info: %User.Info{}} = user]} -> set_cache(user)
|
||||||
|
_ -> {:error, user}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def increment_unread_conversation_count(_, _), do: :noop
|
||||||
|
|
||||||
def remove_duplicated_following(%User{following: following} = user) do
|
def remove_duplicated_following(%User{following: following} = user) do
|
||||||
uniq_following = Enum.uniq(following)
|
uniq_following = Enum.uniq(following)
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ defmodule Pleroma.User.Info do
|
||||||
field(:hide_followers, :boolean, default: false)
|
field(:hide_followers, :boolean, default: false)
|
||||||
field(:hide_follows, :boolean, default: false)
|
field(:hide_follows, :boolean, default: false)
|
||||||
field(:hide_favorites, :boolean, default: true)
|
field(:hide_favorites, :boolean, default: true)
|
||||||
|
field(:unread_conversation_count, :integer, default: 0)
|
||||||
field(:pinned_activities, {:array, :string}, default: [])
|
field(:pinned_activities, {:array, :string}, default: [])
|
||||||
field(:email_notifications, :map, default: %{"digest" => false})
|
field(:email_notifications, :map, default: %{"digest" => false})
|
||||||
field(:mascot, :map, default: nil)
|
field(:mascot, :map, default: nil)
|
||||||
|
|
|
@ -167,6 +167,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
|> maybe_put_chat_token(user, opts[:for], opts)
|
|> maybe_put_chat_token(user, opts[:for], opts)
|
||||||
|> maybe_put_activation_status(user, opts[:for])
|
|> maybe_put_activation_status(user, opts[:for])
|
||||||
|> maybe_put_follow_requests_count(user, opts[:for])
|
|> maybe_put_follow_requests_count(user, opts[:for])
|
||||||
|
|> maybe_put_unread_conversation_count(user, opts[:for])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp username_from_nickname(string) when is_binary(string) do
|
defp username_from_nickname(string) when is_binary(string) do
|
||||||
|
@ -248,6 +249,16 @@ defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
|
||||||
|
|
||||||
defp maybe_put_activation_status(data, _, _), do: data
|
defp maybe_put_activation_status(data, _, _), do: data
|
||||||
|
|
||||||
|
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||||
|
data
|
||||||
|
|> Kernel.put_in(
|
||||||
|
[:pleroma, :unread_conversation_count],
|
||||||
|
user.info.unread_conversation_count
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_unread_conversation_count(data, _, _), do: data
|
||||||
|
|
||||||
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
|
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
|
||||||
defp image_url(_), do: nil
|
defp image_url(_), do: nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
|
||||||
use Pleroma.DataCase
|
use Pleroma.DataCase
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
alias Pleroma.Conversation.Participation
|
alias Pleroma.Conversation.Participation
|
||||||
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
test "getting a participation will also preload things" do
|
test "getting a participation will also preload things" do
|
||||||
|
@ -30,6 +31,8 @@ test "for a new conversation, it sets the recipents of the participation" do
|
||||||
{:ok, activity} =
|
{:ok, activity} =
|
||||||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
other_user = User.get_cached_by_id(user.id)
|
||||||
[participation] = Participation.for_user(user)
|
[participation] = Participation.for_user(user)
|
||||||
participation = Pleroma.Repo.preload(participation, :recipients)
|
participation = Pleroma.Repo.preload(participation, :recipients)
|
||||||
|
|
||||||
|
@ -155,6 +158,7 @@ test "it sets recipients, always keeping the owner of the participation even whe
|
||||||
[participation] = Participation.for_user_with_last_activity_id(user)
|
[participation] = Participation.for_user_with_last_activity_id(user)
|
||||||
|
|
||||||
participation = Repo.preload(participation, :recipients)
|
participation = Repo.preload(participation, :recipients)
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
|
|
||||||
assert participation.recipients |> length() == 1
|
assert participation.recipients |> length() == 1
|
||||||
assert user in participation.recipients
|
assert user in participation.recipients
|
||||||
|
|
|
@ -10,19 +10,23 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
|
||||||
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
test "Conversations", %{conn: conn} do
|
test "returns a list of conversations", %{conn: conn} do
|
||||||
user_one = insert(:user)
|
user_one = insert(:user)
|
||||||
user_two = insert(:user)
|
user_two = insert(:user)
|
||||||
user_three = insert(:user)
|
user_three = insert(:user)
|
||||||
|
|
||||||
{:ok, user_two} = User.follow(user_two, user_one)
|
{:ok, user_two} = User.follow(user_two, user_one)
|
||||||
|
|
||||||
|
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
|
||||||
|
|
||||||
{:ok, direct} =
|
{:ok, direct} =
|
||||||
CommonAPI.post(user_one, %{
|
CommonAPI.post(user_one, %{
|
||||||
"status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
|
"status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
|
||||||
"visibility" => "direct"
|
"visibility" => "direct"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1
|
||||||
|
|
||||||
{:ok, _follower_only} =
|
{:ok, _follower_only} =
|
||||||
CommonAPI.post(user_one, %{
|
CommonAPI.post(user_one, %{
|
||||||
"status" => "Hi @#{user_two.nickname}!",
|
"status" => "Hi @#{user_two.nickname}!",
|
||||||
|
@ -52,23 +56,100 @@ test "Conversations", %{conn: conn} do
|
||||||
assert is_binary(res_id)
|
assert is_binary(res_id)
|
||||||
assert unread == true
|
assert unread == true
|
||||||
assert res_last_status["id"] == direct.id
|
assert res_last_status["id"] == direct.id
|
||||||
|
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates the last_status on reply", %{conn: conn} do
|
||||||
|
user_one = insert(:user)
|
||||||
|
user_two = insert(:user)
|
||||||
|
|
||||||
|
{:ok, direct} =
|
||||||
|
CommonAPI.post(user_one, %{
|
||||||
|
"status" => "Hi @#{user_two.nickname}",
|
||||||
|
"visibility" => "direct"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, direct_reply} =
|
||||||
|
CommonAPI.post(user_two, %{
|
||||||
|
"status" => "reply",
|
||||||
|
"visibility" => "direct",
|
||||||
|
"in_reply_to_status_id" => direct.id
|
||||||
|
})
|
||||||
|
|
||||||
|
[%{"last_status" => res_last_status}] =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_one)
|
||||||
|
|> get("/api/v1/conversations")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert res_last_status["id"] == direct_reply.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "the user marks a conversation as read", %{conn: conn} do
|
||||||
|
user_one = insert(:user)
|
||||||
|
user_two = insert(:user)
|
||||||
|
|
||||||
|
{:ok, direct} =
|
||||||
|
CommonAPI.post(user_one, %{
|
||||||
|
"status" => "Hi @#{user_two.nickname}",
|
||||||
|
"visibility" => "direct"
|
||||||
|
})
|
||||||
|
|
||||||
|
[%{"id" => direct_conversation_id, "unread" => true}] =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_one)
|
||||||
|
|> get("/api/v1/conversations")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
%{"unread" => false} =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_one)
|
||||||
|
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
|
||||||
|
|
||||||
|
# The conversation is marked as unread on reply
|
||||||
|
{:ok, _} =
|
||||||
|
CommonAPI.post(user_two, %{
|
||||||
|
"status" => "reply",
|
||||||
|
"visibility" => "direct",
|
||||||
|
"in_reply_to_status_id" => direct.id
|
||||||
|
})
|
||||||
|
|
||||||
|
[%{"unread" => true}] =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user_one)
|
||||||
|
|> get("/api/v1/conversations")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||||
|
|
||||||
|
# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
|
||||||
|
{:ok, _} =
|
||||||
|
CommonAPI.post(user_two, %{
|
||||||
|
"status" => "reply",
|
||||||
|
"visibility" => "direct",
|
||||||
|
"in_reply_to_status_id" => direct.id
|
||||||
|
})
|
||||||
|
|
||||||
|
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
|
||||||
|
user_one = insert(:user)
|
||||||
|
user_two = insert(:user)
|
||||||
|
|
||||||
|
{:ok, direct} =
|
||||||
|
CommonAPI.post(user_one, %{
|
||||||
|
"status" => "Hi @#{user_two.nickname}!",
|
||||||
|
"visibility" => "direct"
|
||||||
|
})
|
||||||
|
|
||||||
# Apparently undocumented API endpoint
|
|
||||||
res_conn =
|
res_conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user_one)
|
|> assign(:user, user_one)
|
||||||
|> post("/api/v1/conversations/#{res_id}/read")
|
|> get("/api/v1/statuses/#{direct.id}/context")
|
||||||
|
|
||||||
assert response = json_response(res_conn, 200)
|
|
||||||
assert length(response["accounts"]) == 2
|
|
||||||
assert response["last_status"]["id"] == direct.id
|
|
||||||
assert response["unread"] == false
|
|
||||||
|
|
||||||
# (vanilla) Mastodon frontend behaviour
|
|
||||||
res_conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, user_one)
|
|
||||||
|> get("/api/v1/statuses/#{res_last_status["id"]}/context")
|
|
||||||
|
|
||||||
assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
|
assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
|
||||||
end
|
end
|
||||||
|
|
|
@ -418,6 +418,27 @@ test "shows actual follower/following count to the account owner" do
|
||||||
following_count: 1
|
following_count: 1
|
||||||
} = AccountView.render("show.json", %{user: user, for: user})
|
} = AccountView.render("show.json", %{user: user, for: user})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows unread_conversation_count only to the account owner" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Hey @#{other_user.nickname}.",
|
||||||
|
"visibility" => "direct"
|
||||||
|
})
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
|
||||||
|
:unread_conversation_count
|
||||||
|
] == nil
|
||||||
|
|
||||||
|
assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
|
||||||
|
:unread_conversation_count
|
||||||
|
] == 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "follow requests counter" do
|
describe "follow requests counter" do
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
|
||||||
alias Pleroma.Conversation.Participation
|
alias Pleroma.Conversation.Participation
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
@ -73,6 +74,7 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
|
||||||
|
|
||||||
participation = Repo.preload(participation, :recipients)
|
participation = Repo.preload(participation, :recipients)
|
||||||
|
|
||||||
|
user = User.get_cached_by_id(user.id)
|
||||||
assert [user] == participation.recipients
|
assert [user] == participation.recipients
|
||||||
assert other_user not in participation.recipients
|
assert other_user not in participation.recipients
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue