Page MenuHomePhorge

D117.1732758135.diff
No OneTemporary

Size
16 KB
Referenced Files
None
Subscribers
None

D117.1732758135.diff

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

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)

Event Timeline