Add grouped notifications API #7910

Merged
lambadalambda merged 5 commits from issue-7905-grouped-notifications into develop 2026-06-05 09:23:26 +00:00

Summary

  • Add Mastodon-compatible grouped notifications endpoints under /api/v2/notifications.
  • Group favourite, reblog, and follow notifications with stable group keys.
  • Add grouped notification OpenAPI schemas, rendering, pagination, unread counts, dismiss, account-list endpoints, tests, and changelog entry.

Testing

  • mix format --check-formatted on docker.io/elixir:1.15-alpine passed.
  • git diff --check passed.
  • Full test suite on CI-like non-root/chowned copy with docker.io/elixir:1.18-otp-27-alpine: 4812 tests, 0 failures, 2 excluded, 4 skipped.
  • Full test suite on CI-like non-root/chowned copy with docker.io/elixir:1.15-alpine: 4812 tests, 0 failures, 2 excluded, 4 skipped.

Closes #7905.

## Summary - Add Mastodon-compatible grouped notifications endpoints under `/api/v2/notifications`. - Group favourite, reblog, and follow notifications with stable group keys. - Add grouped notification OpenAPI schemas, rendering, pagination, unread counts, dismiss, account-list endpoints, tests, and changelog entry. ## Testing - `mix format --check-formatted` on `docker.io/elixir:1.15-alpine` passed. - `git diff --check` passed. - Full test suite on CI-like non-root/chowned copy with `docker.io/elixir:1.18-otp-27-alpine`: `4812 tests, 0 failures, 2 excluded, 4 skipped`. - Full test suite on CI-like non-root/chowned copy with `docker.io/elixir:1.15-alpine`: `4812 tests, 0 failures, 2 excluded, 4 skipped`. Closes #7905.
Add grouped notifications API
All checks were successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.15 Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.18 Pipeline was successful
1e65c7a69e
Persist notification group keys
Some checks failed
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.15 Pipeline failed
ci/woodpecker/pr/unit-testing-elixir-1.18 Pipeline failed
add3844c6d
Run group key migration before notification backfill
Some checks failed
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.15 Pipeline failed
ci/woodpecker/pr/unit-testing-elixir-1.18 Pipeline failed
1db2b99e48
Keep cursor cleanup scoped to grouped notifications
All checks were successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.15 Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.18 Pipeline was successful
a2a23b327b
lambadalambda changed title from WIP: Add grouped notifications API to Add grouped notifications API 2026-05-23 10:55:56 +00:00
lambadalambda changed title from Add grouped notifications API to WIP: Add grouped notifications API 2026-05-25 08:06:34 +00:00
phnt requested changes 2026-05-27 17:40:18 +00:00
Dismissed
@ -78,0 +73,4 @@
def get_grouped_notification_page(user, params \\ %{}) do
grouped_types =
params
|> Map.get("grouped_types", Map.get(params, :grouped_types))
Owner

I guess this is here for uncasted and atom casted grouped_types which doesn't seem to be used. There doesn't seem to be any test that would use the atom representation. Ideally there should be only one representation of the same thing.

The non-atom representation is triggered with this test:

test "round-trips ungrouped group keys when grouped_types excludes a type" do

I guess this is here for uncasted and atom casted `grouped_types` which doesn't seem to be used. There doesn't seem to be any test that would use the atom representation. Ideally there should be only one representation of the same thing. The non-atom representation is triggered with this test: https://git.pleroma.social/pleroma/pleroma/src/commit/a2a23b327b9c32420b5a6aed4198073b4f363864/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs#L635
phnt marked this conversation as resolved
@ -78,0 +98,4 @@
}
end
def get_grouped_notification_groups(user, params \\ %{}) do
Owner

unused?

unused?
phnt marked this conversation as resolved
@ -78,0 +177,4 @@
since_id = Map.get(params, "since_id", Map.get(params, :since_id)) ->
{:desc, [{:gt, since_id}]}
max_id = Map.get(params, "max_id", Map.get(params, :max_id)) ->
Owner

Same as grouped_limit

Same as `grouped_limit`
phnt marked this conversation as resolved
@ -78,0 +187,4 @@
defp grouped_limit(params) do
params
|> Map.get("limit", Map.get(params, :limit, 40))
Owner

Same as get_grouped_notification_page, although this is a pagination cursor, so I guess this is relevant:

# Some OpenApiSpex routes cast cursor params to atoms. Keep the historical default for

Same as get_grouped_notification_page, although this is a pagination cursor, so I guess this is relevant: https://git.pleroma.social/pleroma/pleroma/src/commit/a2a23b327b9c32420b5a6aed4198073b4f363864/lib/pleroma/web/controller_helper.ex#L77
phnt marked this conversation as resolved
@ -78,0 +195,4 @@
def unread_notification_group_count(user, params \\ %{}) do
grouped_types =
params
|> Map.get("grouped_types", Map.get(params, :grouped_types))
Owner

Same as get_grouped_notification_page

Same as get_grouped_notification_page
phnt marked this conversation as resolved
@ -87,0 +406,4 @@
defp unread_count_limit(params) do
params
|> Map.get("limit", Map.get(params, :limit, 100))
Owner

Same as grouped_limit.

Same as `grouped_limit`.
phnt marked this conversation as resolved
@ -75,0 +95,4 @@
statuses =
notification_groups
|> Enum.map(&List.first/1)
Owner

If I'm reading this right, this would cause only the first post in each group to be rendered, which I'm not sure if that is correct. The Mastodon docs aren't explicit about that, but the given example implies that this would be intended.

If I'm reading this right, this would cause only the first post in each group to be rendered, which I'm not sure if that is correct. The Mastodon docs aren't explicit about that, but the given example implies that this would be intended.
@ -580,0 +837,4 @@
refute link_header =~ "since_id="
end
test "lists accounts from and dismisses a notification group" do
Owner

Nitpick: split into two tests

Nitpick: split into two tests
phnt marked this conversation as resolved
Address grouped notification review cleanup
All checks were successful
ci/woodpecker/pr/changelog Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.15 Pipeline was successful
ci/woodpecker/pr/unit-testing-elixir-1.18 Pipeline was successful
fc9e6793ce
Author
Owner

Addressed review cleanup in fc9e6793c:\n\n- normalized grouped notification service params to the string-keyed form produced by NotificationController\n- removed the unused get_grouped_notification_groups/2 helper\n- split the group accounts and dismiss coverage into separate tests\n\nI left grouped_index status rendering as one status per notification group. The grouped response exposes one status_id per group, and rendering all sampled notifications' statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data.\n\nVerification:\n- focused notification controller/view tests passed locally: 49 tests, 0 failures\n- mix format --check-formatted passed\n- git diff --check passed\n- CI pipeline 366 passed

Addressed review cleanup in fc9e6793c:\n\n- normalized grouped notification service params to the string-keyed form produced by NotificationController\n- removed the unused get_grouped_notification_groups/2 helper\n- split the group accounts and dismiss coverage into separate tests\n\nI left grouped_index status rendering as one status per notification group. The grouped response exposes one status_id per group, and rendering all sampled notifications' statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data.\n\nVerification:\n- focused notification controller/view tests passed locally: 49 tests, 0 failures\n- mix format --check-formatted passed\n- git diff --check passed\n- CI pipeline 366 passed
Author
Owner

Formatted version of my previous comment:

Addressed review cleanup in fc9e6793c:

  • normalized grouped notification service params to the string-keyed form produced by NotificationController
  • removed the unused get_grouped_notification_groups/2 helper
  • split the group accounts and dismiss coverage into separate tests

I left grouped_index status rendering as one status per notification group. The grouped response exposes one status_id per group, and rendering all sampled notification statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data.

Verification:

  • focused notification controller/view tests passed locally: 49 tests, 0 failures
  • mix format --check-formatted passed
  • git diff --check passed
  • CI pipeline 366 passed
Formatted version of my previous comment: Addressed review cleanup in fc9e6793c: - normalized grouped notification service params to the string-keyed form produced by NotificationController - removed the unused get_grouped_notification_groups/2 helper - split the group accounts and dismiss coverage into separate tests I left grouped_index status rendering as one status per notification group. The grouped response exposes one status_id per group, and rendering all sampled notification statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data. Verification: - focused notification controller/view tests passed locally: 49 tests, 0 failures - mix format --check-formatted passed - git diff --check passed - CI pipeline 366 passed
Owner

@lambadalambda wrote in #7910 (comment):

The grouped response exposes one status_id per group, and rendering all sampled notification statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data.

In the way it currently works and the way how Mastodon currently implemented it, that is the case. If we ever in the future will want to group replies/mentions to a post for example, the current way would no longer work as it would only render one reply instead of all of them.

So good enough for now.


Anyways, I've played with it locally and everything seems to be working as expected besides the unimplemented supported_types and expand_accounts query params. I don't think we need those and they can be added later.

@lambadalambda wrote in https://git.pleroma.social/pleroma/pleroma/pulls/7910#issuecomment-115933: > The grouped response exposes one status_id per group, and rendering all sampled notification statuses would duplicate the same status for grouped favourites/reblogs rather than adding useful response data. In the way it currently works and the way how Mastodon currently implemented it, that is the case. If we ever in the future will want to group replies/mentions to a post for example, the current way would no longer work as it would only render one reply instead of all of them. So good enough for now. --- Anyways, I've played with it locally and everything seems to be working as expected besides the unimplemented `supported_types` and `expand_accounts` query params. I don't think we need those and they can be added later.
phnt approved these changes 2026-06-03 21:14:01 +00:00
lambadalambda changed title from WIP: Add grouped notifications API to Add grouped notifications API 2026-06-05 09:23:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pleroma/pleroma!7910
No description provided.