diff --git a/changelogs/84.add b/changelogs/84.add
new file mode 100644
--- /dev/null
+++ b/changelogs/84.add
@@ -0,0 +1 @@
+Display edited version of events
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -191,6 +191,13 @@
         font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
       }
 
+      Label {
+        objectName: 'editedIndicator'
+        visible: !compactMode && event.isEdited
+        text: l10n.get('event-edited-indicator')
+        font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8
+      }
+
       Kazv.EventReadIndicator {
         objectName: 'eventReadIndicator'
         shouldShow: !compactMode && !event.isLocalEcho
diff --git a/src/contents/ui/EventView.qml b/src/contents/ui/EventView.qml
--- a/src/contents/ui/EventView.qml
+++ b/src/contents/ui/EventView.qml
@@ -200,6 +200,10 @@
     if (e.isState) {
       return 'state';
     }
+    // ignore edits of other events
+    if (e.relationType === 'm.replace') {
+      return 'ignore';
+    }
     switch (e.type) {
     case 'm.room.message':
       switch (e.content.msgtype) {
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -155,6 +155,7 @@
 event-cannot-decrypt-text = (加密内容)
 event-read-indicator-more = +{ $rest }
 event-read-indicator-list-title = { $numUsers } 个用户已读
+event-edited-indicator = (编辑过了)
 
 media-file-menu-option-view = 查看
 media-file-menu-option-save-as = 保存为
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -170,6 +170,7 @@
         [1] 1 user
        *[other] { $numUsers } users
     }
+event-edited-indicator = (edited)
 
 media-file-menu-option-view = View
 media-file-menu-option-save-as = Save as
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -29,6 +29,11 @@
     lager::reader<std::optional<Kazv::LocalEchoDesc>> m_localEcho;
     lager::reader<Kazv::Event> m_event;
     std::optional<Kazv::Room> m_room;
+    lager::reader<std::string> m_eventIdStd;
+    lager::reader<std::string> m_senderStd;
+    /// the unedited content of this event
+    lager::reader<QJsonObject> m_originalContent;
+    lager::reader<Kazv::EventList> m_edits;
 
 public:
     explicit MatrixEvent(lager::reader<std::variant<Kazv::Event, Kazv::LocalEchoDesc>> event, std::optional<Kazv::Room> room = std::nullopt, QObject *parent = 0);
@@ -55,6 +60,7 @@
     LAGER_QT_READER(QString, relationType);
     LAGER_QT_READER(QString, relatedEventId);
     LAGER_QT_READER(QString, formattedTime);
+    LAGER_QT_READER(bool, isEdited);
 
     Q_INVOKABLE MatrixEventReaderListModel *readers() const;
 
diff --git a/src/matrix-event.cpp b/src/matrix-event.cpp
--- a/src/matrix-event.cpp
+++ b/src/matrix-event.cpp
@@ -38,11 +38,53 @@
     , m_localEcho(event.map(getLocalEcho))
     , m_event(event.map(getEvent))
     , m_room(room)
-    , LAGER_QT(eventId)(m_event.xform(zug::map([](Event e) { return e.id(); }) | strToQt))
-    , LAGER_QT(sender)(m_event.xform(zug::map([](Event e) { return e.sender(); }) | strToQt))
+    , m_eventIdStd(m_event.map(&Event::id))
+    , m_senderStd(m_event.map(&Event::sender))
+    , m_originalContent(m_event.map([](Event e) { return e.content().get().get<QJsonObject>(); }))
+    , m_edits(m_room.has_value() ? lager::reader<EventList>(lager::with(
+        m_event,
+        m_room->relatedEvents(m_eventIdStd, "m.replace")
+    ).map([](const Event &origEvent, const EventList &edits) -> EventList {
+        // https://spec.matrix.org/v1.10/client-server-api/#event-replacements
+        if (!(
+            // (3) no state key
+            !origEvent.isState()
+            // (4) original event is not an edit
+            && origEvent.relationship().first != "m.replace"
+        )) {
+            return EventList{};
+        }
+        auto origType = origEvent.type();
+        auto origSender = origEvent.sender();
+        return intoImmer(
+            EventList{},
+            zug::filter([&origType, &origSender](const Event &ev) {
+                return
+                    // (0) same room id requirement is implicit.
+                    // (1) same sender
+                    origSender == ev.sender()
+                    // (2) same type
+                    && origType == ev.type()
+                    // (3) no state key
+                    && !ev.isState()
+                    // (4) original event is not an edit is checked above
+                    // (5) replaced event content has `m.new_content`
+                    && ev.content().get().contains("m.new_content");
+            }),
+            edits
+        );
+    })) : lager::make_constant(EventList{}))
+    , LAGER_QT(eventId)(m_eventIdStd.xform(strToQt))
+    , LAGER_QT(sender)(m_senderStd.xform(strToQt))
     , LAGER_QT(type)(m_event.xform(zug::map([](Event e) { return e.type(); }) | strToQt))
     , LAGER_QT(stateKey)(m_event.xform(zug::map([](Event e) { return e.stateKey(); }) | strToQt))
-    , LAGER_QT(content)(m_event.xform(zug::map([](Event e) { return e.content().get().get<QJsonObject>(); })))
+    , LAGER_QT(content)(lager::with(m_originalContent, m_edits).map([](const QJsonObject &origContent, const EventList &edits) {
+        if (edits.empty()) {
+            return origContent;
+        } else {
+            return edits.at(edits.size() - 1).content().get().at("m.new_content").get<QJsonObject>();
+        }
+    }))
     , LAGER_QT(encrypted)(m_event.xform(zug::map([](Event e) { return e.encrypted(); })))
     , LAGER_QT(decrypted)(m_event.map([](Event e) { return e.decrypted(); }))
     , LAGER_QT(isState)(m_event.map([](Event e) { return e.isState(); }))
@@ -91,6 +133,7 @@
                 QLocale::ShortFormat
             );
         }))
+    , LAGER_QT(isEdited)(m_edits.map([](const auto &edits) { return !edits.empty(); }))
 {
 }
 
diff --git a/src/tests/matrix-room-timeline-test.cpp b/src/tests/matrix-room-timeline-test.cpp
--- a/src/tests/matrix-room-timeline-test.cpp
+++ b/src/tests/matrix-room-timeline-test.cpp
@@ -34,6 +34,7 @@
 private Q_SLOTS:
     void testLocalEcho();
     void testReadReceipts();
+    void testEdits();
 };
 
 void MatrixRoomTimelineTest::testLocalEcho()
@@ -91,6 +92,42 @@
     }
 }
 
+void MatrixRoomTimelineTest::testEdits()
+{
+    auto r = makeRoom(withRoomTimeline({
+        makeEvent(withEventId("$0")),
+        makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0")),
+        makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1")),
+        // valid
+        makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1")),
+        // also valid
+        makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1")),
+        // invalid, changed event type
+        makeEvent(withEventId("$5") | withEventContent(json{{"m.new_content", {{"body", "fourth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
+        // invalid, changed event sender
+        makeEvent(withEventId("$6") | withEventContent(json{{"m.new_content", {{"body", "fifth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withEventSenderId("@othersender:example.com")),
+        // invalid, has state key
+        makeEvent(withEventId("$7") | withEventContent(json{{"m.new_content", {{"body", "sixth"}}}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type") | withStateKey("")),
+        // invalid, no m.new_content
+        makeEvent(withEventId("$8") | withEventContent(json{{"body", "seventh"}}) | withEventRelationship("m.replace", "$1") | withEventType("moe.kazv.mxc.some-other-type")),
+    }));
+    auto model = SdkModel{makeClient(withRoom(r))};
+    std::unique_ptr<MatrixSdk> sdk{makeTestSdk(model)};
+    auto roomList = toUniquePtr(sdk->roomList());
+    auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+    auto timeline = toUniquePtr(room->timeline());
+
+    auto event = toUniquePtr(timeline->at(7));
+    QCOMPARE(event->eventId(), "$1");
+    // the content is taken from the newest valid edit
+    qDebug() << event->content();
+    QCOMPARE(event->content(), (QJsonObject{{"body", "third"}}));
+    // the relation is taken from the original content
+    QCOMPARE(event->relationType(), "moe.kazv.mxc.some-rel");
+    QCOMPARE(event->relatedEventId(), "$0");
+    QVERIFY(event->isEdited());
+}
+
 QTEST_MAIN(MatrixRoomTimelineTest)
 
 #include "matrix-room-timeline-test.moc"