commit: ca03d94f52f3494c767267a31840224b23c7b1b4
parent 6b8bc3bb4c3605f9e9fa5cbf4ee8bb7aefe1a402
Author: nicole mikołajczyk <me@mkljczk.pl>
Date: Sat, 29 Nov 2025 18:45:42 +0100
Merge branch 'pin-chats' into 'develop'
Chats: pin/unpin chats
See merge request pleroma/pleroma!3637
Diffstat:
12 files changed, 191 insertions(+), 17 deletions(-)
diff --git a/changelog.d/pin-chats.fix b/changelog.d/pin-chats.fix
@@ -0,0 +1 @@
+Allow to pin/unpip chats
diff --git a/docs/development/API/chats.md b/docs/development/API/chats.md
@@ -66,9 +66,9 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 2,
- "last_message" : {...}, // The last message in that chat
+ "id": "1",
+ "unread": 2,
+ "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
@@ -93,8 +93,8 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 0,
+ "id": "1",
+ "unread": 0,
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
@@ -111,7 +111,7 @@ The modified chat message
### 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
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:
- with_muted: Include chats from muted users (boolean).
+- pinned: Include only pinned chats (boolean).
Returned data:
@@ -130,16 +131,16 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 2,
- "last_message" : {...}, // The last message in that chat
+ "id": "1",
+ "unread": 2,
+ "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
]
```
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
@@ -226,6 +227,32 @@ Deleting a chat message for given Chat id works like this:
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
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`:
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
@@ -25,6 +25,8 @@ defmodule Pleroma.Chat do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:recipient, :string)
+ field(:pinned, :boolean)
+
timestamps()
end
@@ -94,4 +96,16 @@ defmodule Pleroma.Chat do
order_by: [desc: c.updated_at]
)
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
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -142,7 +142,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query,
BooleanLike.schema(),
"Include chats from muted users"
- )
+ ),
+ Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
@@ -166,7 +167,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query,
BooleanLike.schema(),
"Include chats from muted users"
- )
+ ),
+ Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
| pagination_params()
],
responses: %{
@@ -257,6 +259,44 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
}
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
%Schema{
title: "ChatsResponse",
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
account: %Schema{type: :object},
unread: %Schema{type: :integer},
last_message: ChatMessage,
- updated_at: %Schema{type: :string, format: :"date-time"}
+ updated_at: %Schema{type: :string, format: :"date-time"},
+ pinned: %Schema{type: :boolean}
},
example: %{
"account" => %{
@@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"id" => "1",
"unread" => 2,
"last_message" => ChatMessage.schema().example,
- "updated_at" => "2020-04-21T15:06:45.000Z"
+ "updated_at" => "2020-04-21T15:06:45.000Z",
+ "pinned" => false
}
})
end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -146,6 +146,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages",
+ "pleroma:pin_chats",
if Config.get([:instance, :show_reactions]) do
"exposable_reactions"
end,
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -29,7 +29,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
:create,
:mark_as_read,
:mark_message_as_read,
- :delete_message
+ :delete_message,
+ :pin,
+ :unpin
]
)
@@ -199,8 +201,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
user_id
|> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users)
+ |> restrict_pinned(params)
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
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
@@ -214,6 +224,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
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
case get_req_header(conn, "idempotency-key") do
[key] -> key
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -24,7 +24,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
last_message:
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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
@@ -581,6 +581,8 @@ defmodule Pleroma.Web.Router do
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_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", ConversationController, :show)
diff --git a/priv/repo/migrations/20220222203933_add_pinned_to_chats.exs b/priv/repo/migrations/20220222203933_add_pinned_to_chats.exs
@@ -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
diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs
@@ -337,6 +337,41 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
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
describe "GET #{tested_endpoint}" do
setup do: oauth_access(["read:chats"])
@@ -407,6 +442,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
assert length(result) == 1
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
test "it returns all chats", %{conn: conn, user: user} do
Enum.each(1..30, fn _ ->
diff --git a/test/pleroma/web/pleroma_api/views/chat_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_view_test.exs
@@ -30,7 +30,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
AccountView.render("show.json", user: recipient, skip_visibility_check: true),
unread: 0,
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")