diff --git a/include/mtx/pushrules.hpp b/include/mtx/pushrules.hpp
index bcc20de8ceb85bee36e6a35c0d724564bfbfa0dd..c891c1e81221ce598ba5b9961124d3c54ceba03b 100644
--- a/include/mtx/pushrules.hpp
+++ b/include/mtx/pushrules.hpp
@@ -14,6 +14,7 @@
 #include <variant>
 #include <vector>
 
+#include "mtx/events/common.hpp"
 #include "mtx/events/power_levels.hpp"
 
 namespace mtx {
@@ -47,6 +48,12 @@ struct PushCondition
     //! defaults to ==.
     std::string is;
 
+    //! The relation type to match on. Only valid for `im.nheko.msc3664.related_event_match`
+    //! conditions.
+    mtx::common::RelationType rel_type = mtx::common::RelationType::Unsupported;
+    //! Wether to match fallback relations or not.
+    bool include_fallback = false;
+
     friend void to_json(nlohmann::json &obj, const PushCondition &condition);
     friend void from_json(const nlohmann::json &obj, PushCondition &condition);
 };
@@ -200,10 +207,14 @@ public:
     //! Evaluate the pushrules for @event .
     ///
     /// You need to have the room_id set for the event.
+    /// `relatedEvents` is a mapping of rel_type to event. Pass all the events that are related to
+    /// by this event here.
     /// \returns the actions to apply.
     [[nodiscard]] std::vector<actions::Action> evaluate(
       const mtx::events::collections::TimelineEvent &event,
-      const RoomContext &ctx) const;
+      const RoomContext &ctx,
+      const std::vector<std::pair<mtx::common::Relation, mtx::events::collections::TimelineEvent>>
+        &relatedEvents) const;
 
 private:
     struct OptimizedRules;
diff --git a/lib/structs/events/common.cpp b/lib/structs/events/common.cpp
index 6983b239e8e9b325f8a38cce37694a1ec33ece34..779972129180617c8e4af0a61345aea7c47c2a34 100644
--- a/lib/structs/events/common.cpp
+++ b/lib/structs/events/common.cpp
@@ -204,7 +204,8 @@ from_json(const json &obj, RelationType &type)
         type = RelationType::Reference;
     else if (obj.get<std::string>() == "m.replace")
         type = RelationType::Replace;
-    else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to")
+    else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to" ||
+             obj.get<std::string>() == "m.in_reply_to")
         type = RelationType::InReplyTo;
     else if (obj.get<std::string>() == "m.thread")
         type = RelationType::Thread;
diff --git a/lib/structs/pushrules.cpp b/lib/structs/pushrules.cpp
index 57d1019bcbcf33c373ed5d78f80102ab7ed3b18f..77e8b4fd0a15cd933919dd3af6975f43a2409d84 100644
--- a/lib/structs/pushrules.cpp
+++ b/lib/structs/pushrules.cpp
@@ -8,6 +8,15 @@
 #include "mtx/events/collections.hpp"
 #include "mtx/log.hpp"
 
+namespace {
+struct RelatedEvents
+{
+    std::vector<std::unordered_map<std::string, std::string>>
+      fallbacks;                                                      //!< fallback related events
+    std::vector<std::unordered_map<std::string, std::string>> events; //!< related events
+};
+}
+
 namespace mtx {
 namespace pushrules {
 
@@ -21,15 +30,19 @@ to_json(nlohmann::json &obj, const PushCondition &condition)
         obj["pattern"] = condition.pattern;
     if (!condition.is.empty())
         obj["is"] = condition.is;
+    if (condition.rel_type != mtx::common::RelationType::Unsupported)
+        obj["rel_type"] = condition.rel_type;
 }
 
 void
 from_json(const nlohmann::json &obj, PushCondition &condition)
 {
-    condition.kind    = obj["kind"].get<std::string>();
-    condition.key     = obj.value("key", "");
-    condition.pattern = obj.value("pattern", "");
-    condition.is      = obj.value("is", "");
+    condition.kind             = obj["kind"].get<std::string>();
+    condition.key              = obj.value("key", "");
+    condition.pattern          = obj.value("pattern", "");
+    condition.is               = obj.value("is", "");
+    condition.rel_type         = obj.value("rel_type", mtx::common::RelationType::Unsupported);
+    condition.include_fallback = obj.value("include_fallback", false);
 }
 
 namespace actions {
@@ -180,11 +193,40 @@ struct PushRuleEvaluator::OptimizedRules
         //! a pattern condition to match
         struct PatternCondition
         {
-            std::unique_ptr<re2::RE2> pattern; //< the pattern
-            std::string field;                 //< the field to match with pattern
+            std::unique_ptr<re2::RE2> pattern; //!< the pattern
+            std::string field;                 //!< the field to match with pattern
+
+            bool matches(const std::unordered_map<std::string, std::string> &ev) const
+            {
+                if (auto it = ev.find(field); it != ev.end()) {
+                    if (pattern) {
+                        if (field == "content.body") {
+                            if (!re2::RE2::PartialMatch(it->second, *pattern))
+                                return false;
+                        } else {
+                            if (!re2::RE2::FullMatch(it->second, *pattern))
+                                return false;
+                        }
+                    }
+                } else {
+                    return false;
+                }
+
+                return true;
+            }
         };
         // TODO(Nico): Sort by field for faster matching?
-        std::vector<PatternCondition> patterns; //< conditions that match on a field
+        std::vector<PatternCondition> patterns; //!< conditions that match on a field
+
+        //! a pattern condition to match on a related event
+        struct RelatedEventCondition
+        {
+            PatternCondition ev_match;
+            mtx::common::RelationType rel_type = mtx::common::RelationType::Unsupported;
+            bool include_fallbacks             = false;
+        };
+        std::vector<RelatedEventCondition>
+          related_event_patterns; //!< conditions that match on fields of the related event.
 
         //! a member count condition
         struct MemberCountCondition
@@ -212,8 +254,10 @@ struct PushRuleEvaluator::OptimizedRules
 
         std::vector<actions::Action> actions; //< the actions to apply on match
 
-        [[nodiscard]] bool matches(const std::unordered_map<std::string, std::string> &ev,
-                                   const PushRuleEvaluator::RoomContext &ctx) const
+        [[nodiscard]] bool matches(
+          const std::unordered_map<std::string, std::string> &ev,
+          const PushRuleEvaluator::RoomContext &ctx,
+          const std::map<mtx::common::RelationType, RelatedEvents> &relatedEventsFlat) const
         {
             for (const auto &cond : membercounts) {
                 if (![&cond, &ctx] {
@@ -249,19 +293,34 @@ struct PushRuleEvaluator::OptimizedRules
             }
 
             for (const auto &cond : patterns) {
-                if (auto it = ev.find(cond.field); it != ev.end()) {
-                    if (cond.pattern) {
-                        if (cond.field == "content.body") {
-                            if (!re2::RE2::PartialMatch(it->second, *cond.pattern))
-                                return false;
-                        } else {
-                            if (!re2::RE2::FullMatch(it->second, *cond.pattern))
-                                return false;
+                if (!cond.matches(ev))
+                    return false;
+            }
+
+            for (const auto &cond : related_event_patterns) {
+                bool matched = false;
+                for (const auto &[rel_type, rel_ev] : relatedEventsFlat) {
+                    if (cond.rel_type == rel_type) {
+                        for (const auto &e : rel_ev.events) {
+                            if (cond.ev_match.field.empty() || !cond.ev_match.pattern ||
+                                cond.ev_match.matches(e)) {
+                                matched = true;
+                                break;
+                            }
+                        }
+                        if (cond.include_fallbacks) {
+                            for (const auto &e : rel_ev.fallbacks) {
+                                if (cond.ev_match.field.empty() || !cond.ev_match.pattern ||
+                                    cond.ev_match.matches(e)) {
+                                    matched = true;
+                                    break;
+                                }
+                            }
                         }
                     }
-                } else {
-                    return false;
                 }
+                if (!matched)
+                    return false;
             }
 
             if (check_displayname) {
@@ -325,6 +384,23 @@ PushRuleEvaluator::PushRuleEvaluator(const Ruleset &rules_)
                 c.pattern = construct_re_from_pattern(cond.pattern, cond.key);
                 if (c.pattern)
                     rule.patterns.push_back(std::move(c));
+            } else if (cond.kind == "im.nheko.msc3664.related_event_match") {
+                OptimizedRules::OptimizedRule::RelatedEventCondition c;
+
+                if (cond.rel_type != mtx::common::RelationType::Unsupported) {
+                    c.rel_type          = cond.rel_type;
+                    c.include_fallbacks = cond.include_fallback;
+
+                    if (!cond.key.empty() && !cond.pattern.empty()) {
+                        c.ev_match.field   = cond.key;
+                        c.ev_match.pattern = construct_re_from_pattern(cond.pattern, cond.key);
+                    }
+                    rule.related_event_patterns.push_back(std::move(c));
+                } else {
+                    mtx::utils::log::log()->info(
+                      "Skipping rel_event_match rule with unknown rel_type.");
+                    return false;
+                }
             } else if (cond.kind == "contains_display_name") {
                 rule.check_displayname = true;
             } else if (cond.kind == "room_member_count") {
@@ -479,19 +555,33 @@ flatten_event(const nlohmann::json &j)
 }
 
 std::vector<actions::Action>
-PushRuleEvaluator::evaluate(const mtx::events::collections::TimelineEvent &event,
-                            const RoomContext &ctx) const
+PushRuleEvaluator::evaluate(
+  const mtx::events::collections::TimelineEvent &event,
+  const RoomContext &ctx,
+  const std::vector<std::pair<mtx::common::Relation, mtx::events::collections::TimelineEvent>>
+    &relatedEvents) const
 {
     auto event_json = nlohmann::json(event);
     auto flat_event = flatten_event(event_json);
 
+    std::map<mtx::common::RelationType, RelatedEvents> relatedEventsFlat;
+    for (const auto &[rel, ev] : relatedEvents) {
+        if (rel.rel_type != mtx::common::RelationType::Unsupported) {
+            if (rel.is_fallback)
+                relatedEventsFlat[rel.rel_type].fallbacks.push_back(
+                  flatten_event(nlohmann::json(ev)));
+            else
+                relatedEventsFlat[rel.rel_type].events.push_back(flatten_event(nlohmann::json(ev)));
+        }
+    }
+
     for (const auto &rule : rules->override_) {
-        if (rule.matches(flat_event, ctx))
+        if (rule.matches(flat_event, ctx, relatedEventsFlat))
             return rule.actions;
     }
 
     for (const auto &rule : rules->content) {
-        if (rule.matches(flat_event, ctx))
+        if (rule.matches(flat_event, ctx, relatedEventsFlat))
             return rule.actions;
     }
 
@@ -508,7 +598,7 @@ PushRuleEvaluator::evaluate(const mtx::events::collections::TimelineEvent &event
     }
 
     for (const auto &rule : rules->underride) {
-        if (rule.matches(flat_event, ctx))
+        if (rule.matches(flat_event, ctx, relatedEventsFlat))
             return rule.actions;
     }
     return {};
diff --git a/tests/pushrules.cpp b/tests/pushrules.cpp
index a4f1265e26a27b6908d6075487947ffbdaa33bd7..07b340abc7f50f7383c87f2054adbeeff6b3564d 100644
--- a/tests/pushrules.cpp
+++ b/tests/pushrules.cpp
@@ -436,37 +436,37 @@ TEST(Pushrules, EventMatches)
     auto testEval = [actions = event_match_rule.actions,
                      &textEv](const mtx::pushrules::PushRuleEvaluator &evaluator) {
         mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
-        EXPECT_EQ(evaluator.evaluate({textEv}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions);
 
         auto textEvEnd         = textEv;
         textEvEnd.content.body = "abc honk";
-        EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx, {}), actions);
         auto textEvStart         = textEv;
         textEvStart.content.body = "honk abc";
-        EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx, {}), actions);
         auto textEvNL         = textEv;
         textEvNL.content.body = "abc\nhonk\nabc";
-        EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx, {}), actions);
         auto textEvFull         = textEv;
         textEvFull.content.body = "honk";
-        EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx, {}), actions);
         auto textEvCase         = textEv;
         textEvCase.content.body = "HoNk";
-        EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx, {}), actions);
         auto textEvNo         = textEv;
         textEvNo.content.body = "HoN";
-        EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx).empty());
+        EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx, {}).empty());
         auto textEvNo2         = textEv;
         textEvNo2.content.body = "honkb";
-        EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx).empty());
+        EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx, {}).empty());
         auto textEvWordBoundaries         = textEv;
         textEvWordBoundaries.content.body = "@honk:";
-        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx, {}), actions);
 
         // It is what the spec says ¯\_(ツ)_/¯
         auto textEvWordBoundaries2         = textEv;
         textEvWordBoundaries2.content.body = "ähonkü";
-        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx, {}), actions);
     };
 
     mtx::pushrules::Ruleset override_ruleset;
@@ -484,14 +484,14 @@ TEST(Pushrules, EventMatches)
     room_rule.rule_id = "!abc:def.ghi";
     room_ruleset.room.push_back(room_rule);
     mtx::pushrules::PushRuleEvaluator room_evaluator{room_ruleset};
-    EXPECT_EQ(room_evaluator.evaluate({textEv}, {}), room_rule.actions);
+    EXPECT_EQ(room_evaluator.evaluate({textEv}, {}, {}), room_rule.actions);
 
     mtx::pushrules::Ruleset sender_ruleset;
     auto sender_rule    = event_match_rule;
     sender_rule.rule_id = "@me:def.ghi";
     sender_ruleset.sender.push_back(sender_rule);
     mtx::pushrules::PushRuleEvaluator sender_evaluator{sender_ruleset};
-    EXPECT_EQ(sender_evaluator.evaluate({textEv}, {}), sender_rule.actions);
+    EXPECT_EQ(sender_evaluator.evaluate({textEv}, {}, {}), sender_rule.actions);
 
     mtx::pushrules::Ruleset content_ruleset;
     mtx::pushrules::PushRule content_match_rule;
@@ -527,37 +527,37 @@ TEST(Pushrules, DisplaynameMatches)
         mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
         ctx.user_display_name = "honk";
 
-        EXPECT_EQ(evaluator.evaluate({textEv}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions);
 
         auto textEvEnd         = textEv;
         textEvEnd.content.body = "abc honk";
-        EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx, {}), actions);
         auto textEvStart         = textEv;
         textEvStart.content.body = "honk abc";
-        EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx, {}), actions);
         auto textEvNL         = textEv;
         textEvNL.content.body = "abc\nhonk\nabc";
-        EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx, {}), actions);
         auto textEvFull         = textEv;
         textEvFull.content.body = "honk";
-        EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx, {}), actions);
         auto textEvCase         = textEv;
         textEvCase.content.body = "HoNk";
-        EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx, {}), actions);
         auto textEvNo         = textEv;
         textEvNo.content.body = "HoN";
-        EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx).empty());
+        EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx, {}).empty());
         auto textEvNo2         = textEv;
         textEvNo2.content.body = "honkb";
-        EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx).empty());
+        EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx, {}).empty());
         auto textEvWordBoundaries         = textEv;
         textEvWordBoundaries.content.body = "@honk:";
-        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx, {}), actions);
 
         // It is what the spec says ¯\_(ツ)_/¯
         auto textEvWordBoundaries2         = textEv;
         textEvWordBoundaries2.content.body = "ähonkü";
-        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx), actions);
+        EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx, {}), actions);
     };
 
     mtx::pushrules::Ruleset override_ruleset;
@@ -602,10 +602,10 @@ TEST(Pushrules, PowerLevelMatches)
             .power_levels      = pls,
           };
 
-          EXPECT_EQ(evaluator.evaluate({textEv}, ctx), actions);
+          EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions);
 
           ctx.power_levels.users["@me:def.ghi"] = 0;
-          EXPECT_TRUE(evaluator.evaluate({textEv}, ctx).empty());
+          EXPECT_TRUE(evaluator.evaluate({textEv}, ctx, {}).empty());
       };
 
     mtx::pushrules::Ruleset override_ruleset;
@@ -648,11 +648,11 @@ TEST(Pushrules, MemberCountMatches)
         mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
 
         ctx.member_count = 99;
-        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), lt);
+        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), lt);
         ctx.member_count = 100;
-        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), eq);
+        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), eq);
         ctx.member_count = 101;
-        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), gt);
+        EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), gt);
     };
 
     testEval("100", false, true, false);
@@ -1021,7 +1021,7 @@ TEST(Pushrules, ContentOverRoomRulesMatches)
     mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global};
     mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
 
-    auto actions = evaluator.evaluate({text}, ctx);
+    auto actions = evaluator.evaluate({text}, ctx, {});
 
     auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) {
         for (const auto &action : a) {
@@ -1181,7 +1181,8 @@ TEST(Pushrules, ReactionDoesNotMatch)
                 {
                     "key": "sender",
                     "kind": "im.nheko.msc3664.related_event_match",
-                    "pattern": "@deepbluev7:neko.dev"
+                    "pattern": "@deepbluev7:neko.dev",
+                    "rel_type": "m.in_reply_to"
                 }
             ],
             "default": true,
@@ -1471,7 +1472,7 @@ TEST(Pushrules, ReactionDoesNotMatch)
     mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global};
     mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
 
-    auto actions = evaluator.evaluate({text}, ctx);
+    auto actions = evaluator.evaluate({text}, ctx, {});
 
     auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) {
         for (const auto &action : a) {
@@ -1871,7 +1872,7 @@ TEST(Pushrules, NormalMessageDoesNotHighlight)
     mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global};
     mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
 
-    auto actions = evaluator.evaluate({text}, ctx);
+    auto actions = evaluator.evaluate({text}, ctx, {});
 
     auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) {
         for (const auto &action : a) {
@@ -1888,3 +1889,451 @@ TEST(Pushrules, NormalMessageDoesNotHighlight)
                   mtx::pushrules::actions::Action{mtx::pushrules::actions::set_tweak_highlight{}});
     }
 }
+
+TEST(Pushrules, RelatedEventMatch)
+{
+    json raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "key": "sender",
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "pattern": "@deepbluev7:neko.dev",
+                    "rel_type": "m.in_reply_to"
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    mtx::pushrules::GlobalRuleset ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+
+    json raw_event         = R"(
+{
+    "content": {
+        "body": "You can hide them in the room settings",
+        "msgtype": "m.text",
+        "m.relates_to": {
+            "m.in_reply_to": {
+              "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de"
+            },
+            "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de",
+            "rel_type": "m.thread"
+        }
+    },
+    "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg",
+    "origin_server_ts": 1666006933326,
+    "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im",
+    "sender": "@nico:neko.dev",
+    "type": "m.room.message"
+})"_json;
+    json related_raw_event = R"(
+{
+    "content": {
+        "body": "You can hide them in the room settings",
+        "msgtype": "m.text"
+    },
+    "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de",
+    "origin_server_ts": 1666006933326,
+    "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im",
+    "sender": "@deepbluev7:neko.dev",
+    "type": "m.room.message"
+})"_json;
+
+    auto text         = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>();
+    auto related_text = related_raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>();
+    mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global};
+    mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
+
+    auto actions = evaluator.evaluate({text},
+                                      ctx,
+                                      {
+                                        {text.content.relations.relations.at(0), {related_text}},
+                                        {text.content.relations.relations.at(1), {related_text}},
+                                      });
+
+    auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) {
+        for (const auto &action : a) {
+            if (action == mtx::pushrules::actions::Action{mtx::pushrules::actions::notify{}}) {
+                return true;
+            }
+        }
+        return false;
+    };
+
+    EXPECT_TRUE(
+      notifies(evaluator.evaluate({text},
+                                  ctx,
+                                  {
+                                    {text.content.relations.relations.at(0), {related_text}},
+                                    {text.content.relations.relations.at(1), {related_text}},
+                                  })));
+    EXPECT_FALSE(notifies(evaluator.evaluate({text}, ctx, {})));
+
+    raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "key": "sender",
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "pattern": "@deepbluev7:neko.dev",
+                    "rel_type": "m.thread"
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+    mtx::pushrules::PushRuleEvaluator evaluator1{ruleset.global};
+    EXPECT_TRUE(
+      notifies(evaluator1.evaluate({text},
+                                   ctx,
+                                   {
+                                     {text.content.relations.relations.at(0), {related_text}},
+                                     {text.content.relations.relations.at(1), {related_text}},
+                                   })));
+    EXPECT_FALSE(notifies(evaluator1.evaluate({text}, ctx, {})));
+
+    raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "rel_type": "m.thread"
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+    mtx::pushrules::PushRuleEvaluator evaluator2{ruleset.global};
+    EXPECT_TRUE(
+      notifies(evaluator2.evaluate({text},
+                                   ctx,
+                                   {
+                                     {text.content.relations.relations.at(0), {related_text}},
+                                     {text.content.relations.relations.at(1), {related_text}},
+                                   })));
+    EXPECT_FALSE(notifies(evaluator2.evaluate({text}, ctx, {})));
+
+    raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "key": "sender",
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "pattern": "@deepbluev8:neko.dev",
+                    "rel_type": "m.in_reply_to"
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+    mtx::pushrules::PushRuleEvaluator evaluator3{ruleset.global};
+    EXPECT_FALSE(
+      notifies(evaluator3.evaluate({text},
+                                   ctx,
+                                   {
+                                     {text.content.relations.relations.at(0), {related_text}},
+                                     {text.content.relations.relations.at(1), {related_text}},
+                                   })));
+    EXPECT_FALSE(notifies(evaluator3.evaluate({text}, ctx, {})));
+
+    raw_event = R"(
+{
+    "content": {
+        "body": "You can hide them in the room settings",
+        "msgtype": "m.text",
+        "m.relates_to": {
+            "m.in_reply_to": {
+              "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de"
+            },
+            "is_falling_back": true,
+            "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de",
+            "rel_type": "m.thread"
+        }
+    },
+    "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg",
+    "origin_server_ts": 1666006933326,
+    "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im",
+    "sender": "@nico:neko.dev",
+    "type": "m.room.message"
+})"_json;
+    text      = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>();
+    EXPECT_FALSE(
+      notifies(evaluator.evaluate({text},
+                                  ctx,
+                                  {
+                                    {text.content.relations.relations.at(0), {related_text}},
+                                    {text.content.relations.relations.at(1), {related_text}},
+                                  })));
+    EXPECT_FALSE(notifies(evaluator.evaluate({text}, ctx, {})));
+
+    raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "key": "sender",
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "pattern": "@deepbluev7:neko.dev",
+                    "rel_type": "m.in_reply_to",
+                    "include_fallback": true
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+    mtx::pushrules::PushRuleEvaluator evaluator4{ruleset.global};
+    EXPECT_TRUE(
+      notifies(evaluator4.evaluate({text},
+                                   ctx,
+                                   {
+                                     {text.content.relations.relations.at(0), {related_text}},
+                                     {text.content.relations.relations.at(1), {related_text}},
+                                   })));
+    EXPECT_FALSE(notifies(evaluator4.evaluate({text}, ctx, {})));
+
+    raw_rule = R"(
+{
+  "global": {
+    "content": [
+    ],
+    "override": [
+        {
+            "actions": [
+                "dont_notify"
+            ],
+            "default": true,
+            "enabled": false,
+            "rule_id": ".m.rule.master"
+        },
+        {
+            "actions": [
+                "notify",
+                {
+                    "set_tweak": "highlight"
+                },
+                {
+                    "set_tweak": "sound",
+                    "value": "default"
+                }
+            ],
+            "conditions": [
+                {
+                    "key": "sender",
+                    "kind": "im.nheko.msc3664.related_event_match",
+                    "pattern": "@deepbluev7:neko.dev",
+                    "rel_type": "m.in_reply_to"
+                }
+            ],
+            "default": true,
+            "rule_id": ".im.nheko.msc3664.reply"
+        }
+    ],
+    "room": [
+    ],
+    "sender": [
+    ],
+    "underride": [
+    ]
+}
+})"_json;
+
+    raw_event  = R"(
+{
+    "content": {
+        "body": "You can hide them in the room settings",
+        "msgtype": "m.emote",
+        "m.relates_to": {
+            "m.in_reply_to": {
+              "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de"
+            },
+            "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de",
+            "rel_type": "m.thread"
+        }
+    },
+    "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg",
+    "origin_server_ts": 1666006933326,
+    "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im",
+    "sender": "@nico:neko.dev",
+    "type": "m.room.message"
+})"_json;
+    auto emote = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Emote>>();
+
+    ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>();
+    mtx::pushrules::PushRuleEvaluator evaluator5{ruleset.global};
+    EXPECT_TRUE(
+      notifies(evaluator5.evaluate({emote},
+                                   ctx,
+                                   {
+                                     {emote.content.relations.relations.at(0), {related_text}},
+                                     {emote.content.relations.relations.at(1), {related_text}},
+                                   })));
+    EXPECT_FALSE(notifies(evaluator5.evaluate({emote}, ctx, {})));
+}