Merge branch 'pin-chats' into 'develop'

Chats: pin/unpin chats

See merge request pleroma/pleroma!3637
This commit is contained in:
nicole mikołajczyk 2025-11-29 18:45:42 +01:00
commit ca03d94f52
12 changed files with 191 additions and 17 deletions

View file

@ -0,0 +1 @@
Allow to pin/unpip chats

View file

@ -66,9 +66,9 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 2, "unread": 2,
"last_message" : {...}, // The last message in that chat "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
``` ```
@ -93,8 +93,8 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 0, "unread": 0,
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
``` ```
@ -111,7 +111,7 @@ The modified chat message
### Getting a list of Chats ### Getting a list of Chats
`GET /api/v1/pleroma/chats` `GET /api/v2/pleroma/chats`
This will return a list of chats that you have been involved in, sorted by their This will return a list of chats that you have been involved in, sorted by their
last update (so new chats will be at the top). last update (so new chats will be at the top).
@ -119,6 +119,7 @@ last update (so new chats will be at the top).
Parameters: Parameters:
- with_muted: Include chats from muted users (boolean). - with_muted: Include chats from muted users (boolean).
- pinned: Include only pinned chats (boolean).
Returned data: Returned data:
@ -130,16 +131,16 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 2, "unread": 2,
"last_message" : {...}, // The last message in that chat "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
] ]
``` ```
The recipient of messages that are sent to this chat is given by their AP ID. The recipient of messages that are sent to this chat is given by their AP ID.
No pagination is implemented for now. The usual pagination options are implemented.
### Getting the messages for a Chat ### Getting the messages for a Chat
@ -226,6 +227,32 @@ Deleting a chat message for given Chat id works like this:
Returned data is the deleted message. Returned data is the deleted message.
### Pinning a chat
Pinning a chat works like this:
`POST /api/v1/pleroma/chats/:id/pin`
Returned data:
```json
{
"account": {
"id": "someflakeid",
"username": "somenick",
...
},
"id": "1",
"unread": 0,
"updated_at": "2020-04-21T15:11:46.000Z",
"pinned": true,
}
```
To unpin a pinned chat, use:
`POST /api/v1/pleroma/chats/:id/unpin`
### Notifications ### Notifications
There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`: There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:

View file

@ -25,6 +25,8 @@ defmodule Pleroma.Chat do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:recipient, :string) field(:recipient, :string)
field(:pinned, :boolean)
timestamps() timestamps()
end end
@ -94,4 +96,16 @@ defmodule Pleroma.Chat do
order_by: [desc: c.updated_at] order_by: [desc: c.updated_at]
) )
end end
def pin(%__MODULE__{} = chat) do
chat
|> cast(%{pinned: true}, [:pinned])
|> Repo.update()
end
def unpin(%__MODULE__{} = chat) do
chat
|> cast(%{pinned: false}, [:pinned])
|> Repo.update()
end
end end

View file

@ -142,7 +142,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query, :query,
BooleanLike.schema(), BooleanLike.schema(),
"Include chats from muted users" "Include chats from muted users"
) ),
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
], ],
responses: %{ responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response()) 200 => Operation.response("The chats of the user", "application/json", chats_response())
@ -166,7 +167,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query, :query,
BooleanLike.schema(), BooleanLike.schema(),
"Include chats from muted users" "Include chats from muted users"
) ),
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
| pagination_params() | pagination_params()
], ],
responses: %{ responses: %{
@ -257,6 +259,44 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
} }
end end
def pin_operation do
%Operation{
tags: ["Chats"],
summary: "Pin a chat",
operationId: "ChatController.pin",
parameters: [
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
],
responses: %{
200 => Operation.response("The existing chat", "application/json", Chat)
},
security: [
%{
"oAuth" => ["write:chats"]
}
]
}
end
def unpin_operation do
%Operation{
tags: ["Chats"],
summary: "Unpin a chat",
operationId: "ChatController.unpin",
parameters: [
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
],
responses: %{
200 => Operation.response("The existing chat", "application/json", Chat)
},
security: [
%{
"oAuth" => ["write:chats"]
}
]
}
end
def chats_response do def chats_response do
%Schema{ %Schema{
title: "ChatsResponse", title: "ChatsResponse",

View file

@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
account: %Schema{type: :object}, account: %Schema{type: :object},
unread: %Schema{type: :integer}, unread: %Schema{type: :integer},
last_message: ChatMessage, last_message: ChatMessage,
updated_at: %Schema{type: :string, format: :"date-time"} updated_at: %Schema{type: :string, format: :"date-time"},
pinned: %Schema{type: :boolean}
}, },
example: %{ example: %{
"account" => %{ "account" => %{
@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"id" => "1", "id" => "1",
"unread" => 2, "unread" => 2,
"last_message" => ChatMessage.schema().example, "last_message" => ChatMessage.schema().example,
"updated_at" => "2020-04-21T15:06:45.000Z" "updated_at" => "2020-04-21T15:06:45.000Z",
"pinned" => false
} }
}) })
end end

View file

@ -146,6 +146,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma_emoji_reactions", "pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions", "pleroma_custom_emoji_reactions",
"pleroma_chat_messages", "pleroma_chat_messages",
"pleroma:pin_chats",
if Config.get([:instance, :show_reactions]) do if Config.get([:instance, :show_reactions]) do
"exposable_reactions" "exposable_reactions"
end, end,

View file

@ -29,7 +29,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
:create, :create,
:mark_as_read, :mark_as_read,
:mark_message_as_read, :mark_message_as_read,
:delete_message :delete_message,
:pin,
:unpin
] ]
) )
@ -199,8 +201,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
user_id user_id
|> Chat.for_user_query() |> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users) |> where([c], c.recipient not in ^exclude_users)
|> restrict_pinned(params)
end end
defp restrict_pinned(query, %{pinned: pinned}) when is_boolean(pinned) do
query
|> where([c], c.pinned == ^pinned)
end
defp restrict_pinned(query, _), do: query
def create(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do def create(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id), with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
@ -214,6 +224,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end end
end end
def pin(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{:ok, chat} <- Chat.pin(chat) do
render(conn, "show.json", chat: chat)
end
end
def unpin(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{:ok, chat} <- Chat.unpin(chat) do
render(conn, "show.json", chat: chat)
end
end
defp idempotency_key(conn) do defp idempotency_key(conn) do
case get_req_header(conn, "idempotency-key") do case get_req_header(conn, "idempotency-key") do
[key] -> key [key] -> key

View file

@ -24,7 +24,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
last_message: last_message:
last_message && last_message &&
MessageReferenceView.render("show.json", chat_message_reference: last_message), MessageReferenceView.render("show.json", chat_message_reference: last_message),
updated_at: Utils.to_masto_date(chat.updated_at) updated_at: Utils.to_masto_date(chat.updated_at),
pinned: chat.pinned
} }
end end

View file

@ -581,6 +581,8 @@ defmodule Pleroma.Web.Router do
delete("/chats/:id/messages/:message_id", ChatController, :delete_message) delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read) post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read) post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
post("/chats/:id/pin", ChatController, :pin)
post("/chats/:id/unpin", ChatController, :unpin)
get("/conversations/:id/statuses", ConversationController, :statuses) get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show) get("/conversations/:id", ConversationController, :show)

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddPinnedToChats do
use Ecto.Migration
def change do
alter table(:chats) do
add(:pinned, :boolean, default: false, null: false)
end
create(index(:chats, [:pinned]))
end
end

View file

@ -337,6 +337,41 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
end end
end end
describe "POST /api/v1/pleroma/chats/:id/pin" do
setup do: oauth_access(["write:chats"])
test "it pins a chat", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
result =
conn
|> post("/api/v1/pleroma/chats/#{chat.id}/pin")
|> json_response_and_validate_schema(200)
assert %{"pinned" => true} = result
end
end
describe "POST /api/v1/pleroma/chats/:id/unpin" do
setup do: oauth_access(["write:chats"])
test "it unpins a chat", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
{:ok, chat} = Chat.pin(chat)
result =
conn
|> post("/api/v1/pleroma/chats/#{chat.id}/unpin")
|> json_response_and_validate_schema(200)
assert %{"pinned" => false} = result
end
end
for tested_endpoint <- ["/api/v1/pleroma/chats", "/api/v2/pleroma/chats"] do for tested_endpoint <- ["/api/v1/pleroma/chats", "/api/v2/pleroma/chats"] do
describe "GET #{tested_endpoint}" do describe "GET #{tested_endpoint}" do
setup do: oauth_access(["read:chats"]) setup do: oauth_access(["read:chats"])
@ -407,6 +442,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
assert length(result) == 1 assert length(result) == 1
end end
test "it only returns pinned chats", %{conn: conn, user: user} do
recipient1 = insert(:user)
recipient2 = insert(:user)
{:ok, %{id: id} = chat} = Chat.get_or_create(user.id, recipient1.ap_id)
{:ok, _} = Chat.get_or_create(user.id, recipient2.ap_id)
Chat.pin(chat)
[%{"id" => ^id, "pinned" => true}] =
conn
|> get("#{unquote(tested_endpoint)}?pinned=true")
|> json_response_and_validate_schema(200)
end
if tested_endpoint == "/api/v1/pleroma/chats" do if tested_endpoint == "/api/v1/pleroma/chats" do
test "it returns all chats", %{conn: conn, user: user} do test "it returns all chats", %{conn: conn, user: user} do
Enum.each(1..30, fn _ -> Enum.each(1..30, fn _ ->

View file

@ -30,7 +30,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
AccountView.render("show.json", user: recipient, skip_visibility_check: true), AccountView.render("show.json", user: recipient, skip_visibility_check: true),
unread: 0, unread: 0,
last_message: nil, last_message: nil,
updated_at: Utils.to_masto_date(chat.updated_at) updated_at: Utils.to_masto_date(chat.updated_at),
pinned: false
} }
{:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello")