Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115721
D117.1732758135.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D117.1732758135.diff
View Options
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
@@ -20,7 +20,7 @@
default property var children
property var currentEvent: event
property var menuContent: []
-
+ property var shouldDisplayTime: displayTime || (!compactMode && !event.isLocalEcho)
topPadding: 0
bottomPadding: 0
readonly property var bubbleSpacing: leftPadding + rightPadding
@@ -93,6 +93,12 @@
Kirigami.Action {
text: l10n.get('event-view-source')
onTriggered: eventSourcePopupComp.createObject(applicationWindow().overlay).open()
+ },
+ Kirigami.Action {
+ objectName: 'viewHistoryMenuItem'
+ text: l10n.get('event-view-history')
+ enabled: event && !!event.isEdited
+ onTriggered: viewEventHistoryRequested(event.eventId)
}
]
@@ -213,7 +219,7 @@
Label {
objectName: 'timeIndicator'
- visible: !compactMode && !event.isLocalEcho
+ visible: shouldDisplayTime
text: event.formattedTime
ToolTip.text: event.formattedDateTime
ToolTip.delay: Kirigami.Units.toolTipDelay
diff --git a/src/contents/ui/EventHistoryView.qml b/src/contents/ui/EventHistoryView.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/EventHistoryView.qml
@@ -0,0 +1,23 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import org.kde.kirigami as Kirigami
+import '.' as Kazv
+
+ListView {
+ id: eventHistoryView
+ property var history
+ model: history
+ delegate: Kazv.EventViewWrapper {
+ event: history.at(index)
+ width: ListView.view.width
+ compactMode: true
+ displayTime: true
+ }
+}
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
@@ -22,6 +22,7 @@
property var sender
property var stateKeyUser
property var isSelected: false
+ property var displayTime
property var messageType: getMessageType(event)
diff --git a/src/contents/ui/EventViewWrapper.qml b/src/contents/ui/EventViewWrapper.qml
--- a/src/contents/ui/EventViewWrapper.qml
+++ b/src/contents/ui/EventViewWrapper.qml
@@ -15,5 +15,5 @@
Kazv.EventView {
sender: room.member(event.sender || matrixSdk.userId)
stateKeyUser: event.stateKey ? room.member(event.stateKey) : {}
- isGapped: timeline.gaps.includes(event.eventId)
+ isGapped: timeline?.gaps?.includes(event.eventId)
}
diff --git a/src/contents/ui/RoomPage.qml b/src/contents/ui/RoomPage.qml
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -30,12 +30,28 @@
signal mentionUserRequested(string userId)
signal replaceDraftRequested(string newDraft)
signal paginateBackRequested(string eventId)
+ signal viewEventHistoryRequested(string eventId)
title: roomNameProvider.name
property var isInvite: room.membership === MK.MatrixRoom.Invite
property var isJoin: room.membership === MK.MatrixRoom.Join
+ property var eventHistoryPopupComp: Component {
+ Kazv.SelfDestroyableOverlaySheet {
+ id: historyPopup
+ property var eventId
+ property var event: room.messageById(eventId)
+ shouldSelfDestroy: true
+ title: l10n.get('event-history-popup-title')
+
+ Kazv.EventHistoryView {
+ Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
+ history: historyPopup.event.history()
+ }
+ }
+ }
+
contextualActions: [
Kirigami.Action {
id: acceptInviteAction
@@ -222,4 +238,8 @@
console.debug('pagination request of', eventId, 'is already under way');
}
}
+
+ onViewEventHistoryRequested: (eventId) => {
+ eventHistoryPopupComp.createObject(applicationWindow().overlay, { eventId }).open();
+ }
}
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
@@ -173,9 +173,11 @@
event-delete = 删除
event-delete-failed = 删除事件出错。错误码:{ $errorCode }。错误讯息:{ $errorMsg }。
event-view-source = 查看源码...
+event-view-history = 查看编辑历史...
event-source-popup-title = 事件源码
event-source-decrypted = 解密的事件源码
event-source-original = 原始的事件源码
+event-history-popup-title = 事件编辑历史
event-reply-action = 回复
event-popup-action = 更多关于这个事件...
event-reacted-with = 回应了「{ $key }」
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
@@ -189,9 +189,11 @@
event-delete = Delete
event-delete-failed = Error deleting event. Error code: { $errorCode }. Error message: { $errorMsg }.
event-view-source = View source...
+event-view-history = View edit history...
event-source-popup-title = Event source
event-source-decrypted = Decrypted event source
event-source-original = Original event source
+event-history-popup-title = Event edit history
event-reply-action = Reply
event-popup-action = More about this event...
event-reacted-with = Reacted with "{ $key }"
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -15,9 +15,11 @@
#include <client/room/room.hpp>
#include "qt-json.hpp"
+Q_MOC_INCLUDE("matrix-event-list.hpp")
Q_MOC_INCLUDE("matrix-event-reader-list-model.hpp")
class MatrixEventReaderListModel;
+class MatrixEventList;
class MatrixEvent : public QObject
{
@@ -65,4 +67,6 @@
Q_INVOKABLE MatrixEventReaderListModel *readers() const;
Kazv::Event underlyingEvent() const;
+
+ Q_INVOKABLE MatrixEventList *history() const;
};
diff --git a/src/matrix-event.cpp b/src/matrix-event.cpp
--- a/src/matrix-event.cpp
+++ b/src/matrix-event.cpp
@@ -8,7 +8,7 @@
#include "matrix-event.hpp"
#include "matrix-event-reader-list-model.hpp"
-
+#include "matrix-event-list.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
@@ -188,3 +188,26 @@
{
return m_event.get();
}
+
+MatrixEventList *MatrixEvent::history() const
+{
+ return new MatrixEventList(lager::with(m_event, m_edits)
+ .map([](const auto &event, const auto &edits) {
+ return intoImmer(
+ EventList{event},
+ zug::map([event](const auto &e) {
+ auto j = event.originalJson().get();
+ auto ret = j;
+ ret["type"] = e.type();
+ ret["content"] = e.content().get().at("m.new_content");
+ ret["event_id"] = e.id();
+ ret["origin_server_ts"] = e.originServerTs();
+ if (j.contains("/content/m.relates_to"_json_pointer)) {
+ ret["content"]["m.relates_to"] = j["content"]["m.relates_to"];
+ }
+ return Event(ret);
+ }),
+ edits
+ );
+ }));
+}
diff --git a/src/resources.qrc b/src/resources.qrc
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -17,6 +17,7 @@
<file alias="EventViewWrapper.qml">contents/ui/EventViewWrapper.qml</file>
<file alias="EventViewCompact.qml">contents/ui/EventViewCompact.qml</file>
<file alias="EventSourceView.qml">contents/ui/EventSourceView.qml</file>
+ <file alias="EventHistoryView.qml">contents/ui/EventHistoryView.qml</file>
<file alias="Bubble.qml">contents/ui/Bubble.qml</file>
<file alias="MediaFileMenu.qml">contents/ui/MediaFileMenu.qml</file>
<file alias="KazvIOMenu.qml">contents/ui/KazvIOMenu.qml</file>
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -30,6 +30,7 @@
matrix-user-given-attrs-map-test.cpp
kazv-abstract-list-model-test.cpp
matrix-event-list-test.cpp
+ matrix-event-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
diff --git a/src/tests/matrix-event-test.cpp b/src/tests/matrix-event-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/matrix-event-test.cpp
@@ -0,0 +1,111 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include <QtTest>
+#include <QSignalSpy>
+#include <lager/state.hpp>
+#include <testfixtures/factory.hpp>
+#include <matrix-room-timeline.hpp>
+#include <matrix-sdk.hpp>
+#include <matrix-room-list.hpp>
+#include <matrix-room.hpp>
+#include <matrix-event-list.hpp>
+#include <matrix-event.hpp>
+#include "test-model.hpp"
+#include "test-utils.hpp"
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+class MatrixEventTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testEdits();
+ void testEncryptedEdits();
+};
+
+void MatrixEventTest::testEdits()
+{
+ auto r = makeRoom(withRoomTimeline({
+ makeEvent(withEventId("$0")),
+ makeEvent(withEventId("$1") | withEventRelationship("moe.kazv.mxc.some-rel", "$0") | withEventKV("/origin_server_ts"_json_pointer, 1720751684500)),
+ // this one is invalid because there is no m.new_content
+ makeEvent(withEventId("$2") | withEventContent(json{{"body", "first"}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684700)),
+ makeEvent(withEventId("$3") | withEventContent(json{{"m.new_content", {{"body", "second"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684800)),
+ makeEvent(withEventId("$4") | withEventContent(json{{"m.new_content", {{"body", "third"}}}}) | withEventRelationship("m.replace", "$1") | withEventKV("/origin_server_ts"_json_pointer, 1720751684900)),
+ }));
+
+ 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(3));
+ QCOMPARE(event->eventId(), "$1");
+ auto history = toUniquePtr(event->history());
+ // three versions including the original one
+ QCOMPARE(history->count(), 3);
+
+ QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
+
+ auto v1 = toUniquePtr(history->at(1));
+ QCOMPARE(v1->content()["body"], QStringLiteral("second"));
+ QCOMPARE(v1->eventId(), QStringLiteral("$3"));
+ QCOMPARE(v1->underlyingEvent().originServerTs(), 1720751684800);
+ QCOMPARE(v1->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
+
+ auto v2 = toUniquePtr(history->at(2));
+ QCOMPARE(v2->content()["body"], QStringLiteral("third"));
+ QCOMPARE(v2->eventId(), QStringLiteral("$4"));
+ QCOMPARE(v2->underlyingEvent().originServerTs(), 1720751684900);
+ QCOMPARE(v2->relationType(), QStringLiteral("moe.kazv.mxc.some-rel"));
+}
+
+void MatrixEventTest::testEncryptedEdits()
+{
+ auto r = makeRoom();
+ auto events = EventList{
+ makeEvent(withEventId("$0") | withEventType("m.room.encrypted")).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.room.message"},
+ {"content", {{"body", "first"}}},
+ }, Event::Decrypted),
+ makeEvent(withEventId("$1") | withEventType("m.room.encrypted") | withEventRelationship("m.replace", "$0")).setDecryptedJson(json{
+ {"room_id", r.roomId},
+ {"type", "m.room.message"},
+ {"content", {{"m.new_content", {{"body", "second"}}}}},
+ }, Event::Decrypted),
+ };
+ withRoomTimeline(events)(r);
+
+ 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(1));
+ QCOMPARE(event->eventId(), "$0");
+ auto history = toUniquePtr(event->history());
+ // three versions including the original one
+ QCOMPARE(history->count(), 2);
+
+ QCOMPARE(toUniquePtr(history->at(0))->underlyingEvent(), event->underlyingEvent());
+
+ auto v1 = toUniquePtr(history->at(1));
+ QCOMPARE(v1->sender(), QString::fromStdString(events[1].sender()));
+ QCOMPARE(v1->content()["body"], QStringLiteral("second"));
+ QCOMPARE(v1->eventId(), QStringLiteral("$1"));
+ QCOMPARE(v1->relationType(), QStringLiteral());
+}
+
+QTEST_MAIN(MatrixEventTest)
+
+#include "matrix-event-test.moc"
diff --git a/src/tests/quick-tests/tst_EventHistoryView.qml b/src/tests/quick-tests/tst_EventHistoryView.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/tst_EventHistoryView.qml
@@ -0,0 +1,61 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtTest
+
+import '../../contents/ui' as Kazv
+import 'test-helpers.js' as Helpers
+import 'test-helpers' as TestHelpers
+
+Item {
+ id: item
+ width: 800
+ height: 600
+
+ property var mockHelper: TestHelpers.MockHelper {}
+
+ property var room: ({
+ messageById: (_id) => item.textEvent,
+ member: (_id) => ({}),
+ })
+ property var history: ListModel {
+ ListElement {}
+ ListElement {}
+
+ function at(index) {
+ return {
+ type: 'm.room.message',
+ content: {
+ msgtype: 'm.text',
+ body: `version ${index}`,
+ },
+ sender: '@foo:example.com',
+ };
+ }
+ }
+
+ Kazv.EventHistoryView {
+ id: eventHistoryView
+ anchors.fill: parent
+ history: item.history
+ }
+
+ TestCase {
+ id: eventHistoryViewTest
+ name: 'EventHistoryViewTest'
+
+ function test_history() {
+ tryVerify(() => eventHistoryView.itemAtIndex(0));
+ const v0 = eventHistoryView.itemAtIndex(0);
+ verify(findChild(v0, 'timeIndicator').visible);
+ compare(findChild(v0, 'textEventContent').text, 'version 0');
+ tryVerify(() => eventHistoryView.itemAtIndex(1));
+ compare(findChild(eventHistoryView.itemAtIndex(1), 'textEventContent').text, 'version 1');
+ }
+ }
+}
diff --git a/src/tests/quick-tests/tst_EventView.qml b/src/tests/quick-tests/tst_EventView.qml
--- a/src/tests/quick-tests/tst_EventView.qml
+++ b/src/tests/quick-tests/tst_EventView.qml
@@ -23,6 +23,7 @@
property var activateUserPage: mockHelper.noop()
property var paginateBackRequested: mockHelper.noop()
+ property var viewEventHistoryRequested: mockHelper.noop()
property var timeline: ({
gaps: [],
@@ -229,6 +230,19 @@
},
})
+ property var editedEvent: ({
+ eventId: '!some-event-id',
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ formattedTime: '4:06 P.M.',
+ isEdited: true,
+ })
+
property var sender: ({
membership: 'join',
userId: '@foo:tusooa.xyz',
@@ -397,6 +411,13 @@
event: item.stickerEvent
sender: item.sender
}
+
+ Kazv.EventView {
+ Layout.fillWidth: true
+ id: eventViewEdited
+ event: item.editedEvent
+ sender: item.sender
+ }
}
TestCase {
@@ -597,5 +618,24 @@
const image = findChild(eventViewImageCompact, 'mainImage');
verify(image.height <= Kirigami.Units.gridUnit * 5);
}
+
+ function test_history() {
+ {
+ const menuComp = findChild(eventViewText, 'bubble').menuComp;
+ const menu = menuComp.createObject(menuComp.parent);
+ const action = findChild(menu, 'viewHistoryMenuItem');
+ verify(!action.enabled);
+ }
+
+ {
+ const menuComp = findChild(eventViewEdited, 'bubble').menuComp;
+ const menu = menuComp.createObject(menuComp.parent);
+ const action = findChild(menu, 'viewHistoryMenuItem');
+ verify(action.enabled);
+ action.trigger();
+ compare(viewEventHistoryRequested.calledTimes(), 1);
+ compare(viewEventHistoryRequested.lastArgs()[0], '!some-event-id');
+ }
+ }
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Nov 27, 5:42 PM (17 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41078
Default Alt Text
D117.1732758135.diff (16 KB)
Attached To
Mode
D117: Support viewing event history
Attached
Detach File
Event Timeline
Log In to Comment