diff --git a/changelog.d/hashtag-timeline.fix b/changelog.d/hashtag-timeline.fix new file mode 100644 index 000000000..c5814d9c9 --- /dev/null +++ b/changelog.d/hashtag-timeline.fix @@ -0,0 +1 @@ +Fix hashtag timeline query for additional tag handling diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1e8bfe3dd..4efd18d39 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -893,14 +893,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> Repo.all() # Note: NO extra ordering should be done on "activities.id desc nulls last" for optimal plan - from( - [_activity, object] in query, - join: hto in "hashtags_objects", - on: hto.object_id == object.id, - where: hto.hashtag_id in ^hashtag_ids, - distinct: [desc: object.id], - order_by: [desc: object.id] - ) + query = + from( + [_activity, object] in query, + join: hto in "hashtags_objects", + on: hto.object_id == object.id, + where: hto.hashtag_id in ^hashtag_ids + ) + + if is_nil(query.distinct) do + from([_activity, object] in query, + distinct: [desc: object.id], + order_by: [desc: object.id] + ) + else + query + end end defp restrict_hashtag_any(query, %{tag: tag}) when is_binary(tag) do diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index b685f4ff8..bed897e23 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -907,21 +907,53 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"}) {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"}) - any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") + for hashtag_timeline_mode <- [:enabled, :disabled] do + clear_config([:features, :improved_hashtag_timeline], hashtag_timeline_mode) - [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok) + any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") - assert to_string(activity_test.id) == status_test["id"] - assert to_string(activity_test1.id) == status_test1["id"] - assert to_string(activity_none.id) == status_none["id"] + [status_none, status_test1, status_test] = + json_response_and_validate_schema(any_test, :ok) - restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") + assert to_string(activity_test.id) == status_test["id"] + assert to_string(activity_test1.id) == status_test1["id"] + assert to_string(activity_none.id) == status_none["id"] - assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) + restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") - all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") + assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) - assert [status_none] == json_response_and_validate_schema(all_test, :ok) + all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") + + assert [status_none] == json_response_and_validate_schema(all_test, :ok) + end + end + + test "multi-hashtag timeline pagination returns unique statuses", %{conn: conn} do + user = insert(:user) + + {:ok, activity_one} = CommonAPI.post(user, %{status: "#test #test1"}) + {:ok, activity_two} = CommonAPI.post(user, %{status: "#test #test1"}) + {:ok, activity_three} = CommonAPI.post(user, %{status: "#test #test1"}) + + for hashtag_timeline_mode <- [:enabled, :disabled] do + clear_config([:features, :improved_hashtag_timeline], hashtag_timeline_mode) + + first_page = + conn + |> get("/api/v1/timelines/tag/test?any[]=test1&limit=2") + |> json_response_and_validate_schema(:ok) + + assert Enum.map(first_page, & &1["id"]) == + Enum.map([activity_three, activity_two], &to_string(&1.id)) + + second_page = + conn + |> get("/api/v1/timelines/tag/test?any[]=test1&limit=2&max_id=#{activity_two.id}") + |> json_response_and_validate_schema(:ok) + + assert Enum.map(second_page, & &1["id"]) == [to_string(activity_one.id)] + end end test "muted emotions", %{conn: conn} do