diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index fc191addf..173ca688d 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -787,4 +787,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     y = activity.data["to"] ++ (activity.data["cc"] || [])
     visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
   end
+
+  # guard
+  def entire_thread_visible_for_user?(nil, user), do: false
+
+  # child
+  def entire_thread_visible_for_user?(
+        %Activity{data: %{"object" => %{"inReplyTo" => _parent_id}}} = tail,
+        user
+      ) do
+    parent = Activity.get_in_reply_to_activity(tail)
+    visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
+  end
+
+  # root
+  def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
+
+  # filter out broken threads
+  def contain_broken_threads(%Activity{} = activity, %User{} = user) do
+    entire_thread_visible_for_user?(activity, user)
+  end
+
+  # do post-processing on a specific activity
+  def contain_activity(%Activity{} = activity, %User{} = user) do
+    contain_broken_threads(activity, user)
+  end
+
+  # do post-processing on a timeline
+  def contain_timeline(timeline, user) do
+    timeline
+    |> Enum.filter(fn activity ->
+      contain_activity(activity, user)
+    end)
+  end
 end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 751698ca8..bc7558cb8 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -223,6 +223,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
     activities =
       ActivityPub.fetch_activities([user.ap_id | user.following], params)
+      |> ActivityPub.contain_timeline(user)
       |> Enum.reverse()
 
     conn
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index c6637e38d..4fc32b50c 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -79,7 +79,9 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
       |> Map.put("blocking_user", user)
       |> Map.put("user", user)
 
-    activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
+    activities =
+      ActivityPub.fetch_activities([user.ap_id | user.following], params)
+      |> ActivityPub.contain_timeline(user)
 
     conn
     |> render(ActivityView, "index.json", %{activities: activities, for: user})
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 1cf7d6bbc..35c381ac3 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -476,6 +476,54 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
+  describe "timeline post-processing" do
+    test "it filters broken threads" do
+      user1 = insert(:user)
+      user2 = insert(:user)
+      user3 = insert(:user)
+
+      {:ok, user1} = User.follow(user1, user3)
+      assert User.following?(user1, user3)
+
+      {:ok, user2} = User.follow(user2, user3)
+      assert User.following?(user2, user3)
+
+      {:ok, user3} = User.follow(user3, user2)
+      assert User.following?(user3, user2)
+
+      {:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
+
+      {:ok, private_activity_1} =
+        CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
+
+      {:ok, private_activity_2} =
+        CommonAPI.post(user2, %{
+          "status" => "hi 3",
+          "visibility" => "private",
+          "in_reply_to_status_id" => private_activity_1.id
+        })
+
+      {:ok, private_activity_3} =
+        CommonAPI.post(user3, %{
+          "status" => "hi 4",
+          "visibility" => "private",
+          "in_reply_to_status_id" => private_activity_2.id
+        })
+
+      assert user1.following == [user3.ap_id <> "/followers", user1.ap_id]
+
+      activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
+
+      assert [public_activity, private_activity_1, private_activity_3] == activities
+      assert length(activities) == 3
+
+      activities = ActivityPub.contain_timeline(activities, user1)
+
+      assert [public_activity, private_activity_1] == activities
+      assert length(activities) == 2
+    end
+  end
+
   test "it can fetch plume articles" do
     {:ok, object} =
       ActivityPub.fetch_object_from_id(