logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/
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:

Achangelog.d/pin-chats.fix1+
Mdocs/development/API/chats.md47+++++++++++++++++++++++++++++++++++++----------
Mlib/pleroma/chat.ex14++++++++++++++
Mlib/pleroma/web/api_spec/operations/chat_operation.ex44++++++++++++++++++++++++++++++++++++++++++--
Mlib/pleroma/web/api_spec/schemas/chat.ex6++++--
Mlib/pleroma/web/mastodon_api/views/instance_view.ex1+
Mlib/pleroma/web/pleroma_api/controllers/chat_controller.ex26+++++++++++++++++++++++++-
Mlib/pleroma/web/pleroma_api/views/chat_view.ex3++-
Mlib/pleroma/web/router.ex2++
Apriv/repo/migrations/20220222203933_add_pinned_to_chats.exs11+++++++++++
Mtest/pleroma/web/pleroma_api/controllers/chat_controller_test.exs50++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/pleroma/web/pleroma_api/views/chat_view_test.exs3++-
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")