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"