Page MenuHomePhorge

D325.1783882742.diff
No OneTemporary

Size
32 KB
Referenced Files
None
Subscribers
None

D325.1783882742.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
@@ -159,8 +159,7 @@
Loader {
id: inReplyToLoader
clip: true
- source: Qt.resolvedUrl('EventViewCompact.qml')
- active: bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode
+ source: (bubbleRootLayout.replyToOrAnnotatedEventId && !compactMode) ? Qt.resolvedUrl('EventViewCompact.qml') : undefined
asynchronous: true
Layout.fillWidth: true
// BlockQuote is usable in Qt 6.9 and later
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
@@ -268,13 +268,20 @@
}
onIsGappedChanged: maybePaginateBack()
- onEventChanged: maybePaginateBack()
- Component.onCompleted: maybePaginateBack()
+ onEventChanged: {
+ maybePaginateBack();
+ ensureEvent();
+ }
+ Component.onCompleted: {
+ maybePaginateBack();
+ ensureEvent();
+ }
Connections {
target: eventView.event
function onEventIdChanged() {
eventView.maybePaginateBack();
+ ensureEvent();
}
}
@@ -285,4 +292,10 @@
backfill();
}
}
+
+ function ensureEvent() {
+ if (!event.isAvailable) {
+ loadEventRequested(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
@@ -183,6 +183,7 @@
RoomTimelineView {
id: roomTimelineView
objectName: 'roomTimelineView'
+ room: roomPage.room
timeline: roomPage.roomTimeline
}
diff --git a/src/contents/ui/RoomPinnedEventsPage.qml b/src/contents/ui/RoomPinnedEventsPage.qml
--- a/src/contents/ui/RoomPinnedEventsPage.qml
+++ b/src/contents/ui/RoomPinnedEventsPage.qml
@@ -22,6 +22,7 @@
title: l10n.get('room-pinned-events-page-title', { room: roomNameProvider.name })
RoomTimelineView {
+ room: roomPinnedEventsPage.room
timeline: roomPinnedEventsPage.pinnedEvents
}
}
diff --git a/src/contents/ui/RoomTimelineView.qml b/src/contents/ui/RoomTimelineView.qml
--- a/src/contents/ui/RoomTimelineView.qml
+++ b/src/contents/ui/RoomTimelineView.qml
@@ -16,7 +16,9 @@
objectName: 'roomTimelineView'
Accessible.role: Accessible.List
- property var timeline
+ required property var room
+ required property var timeline
+ property var loadEventRequests: ({})
property string selectedEventId
@@ -24,6 +26,7 @@
signal unpinEventRequested(string eventId)
signal deleteEventRequested(eventIdOrTxnId: string, isLocalEcho: bool)
signal deleteEventRequestedNoConfirm(eventIdOrTxnId: string, isLocalEcho: bool)
+ signal loadEventRequested(string eventId)
property var pinEvent: Kazv.AsyncHandler {
property var eventIds
@@ -86,6 +89,19 @@
doDeleteEvent(eventIdOrTxnId, isLocalEcho);
}
+ onLoadEventRequested: (eventId) => {
+ if (!(eventId in loadEventRequests)) {
+ loadEventRequests[eventId] = matrixSession.loadEvent(room.roomId, eventId);
+ loadEventRequests[eventId].resolved.connect((isSuccess) => {
+ if (isSuccess) {
+ delete loadEventRequests[eventId];
+ } else {
+ loadEventRequests[eventId] = undefined;
+ }
+ });
+ }
+ }
+
function doDeleteEvent(eventIdOrTxnId, isLocalEcho) {
if (isLocalEcho) {
deleteLocalEcho.txnId = eventIdOrTxnId;
@@ -171,6 +187,7 @@
delegate: EventViewWrapper {
property var eventListView: ListView.view
+ objectName: `event-${event.eventId || event.txnId}`
prevEvent: index < timeline.count - 1 ? timeline.at(index + 1) : undefined
event: timeline.at(index)
width: ListView.view.width
diff --git a/src/contents/ui/SelfDestroyableOverlaySheet.qml b/src/contents/ui/SelfDestroyableOverlaySheet.qml
--- a/src/contents/ui/SelfDestroyableOverlaySheet.qml
+++ b/src/contents/ui/SelfDestroyableOverlaySheet.qml
@@ -17,15 +17,6 @@
property var selfDestroyConn: Connections {
target: selfDestroyableOverlaySheet
- // for KF5
- function onSheetOpenChanged() {
- if (!selfDestroyableOverlaySheet.sheetOpen
- && selfDestroyableOverlaySheet.shouldSelfDestroy) {
- selfDestroyableOverlaySheet.destroy();
- }
- }
-
- // for KF6
function onClosed() {
if (selfDestroyableOverlaySheet.shouldSelfDestroy) {
selfDestroyableOverlaySheet.destroy();
diff --git a/src/matrix-event.hpp b/src/matrix-event.hpp
--- a/src/matrix-event.hpp
+++ b/src/matrix-event.hpp
@@ -39,7 +39,7 @@
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);
+ explicit MatrixEvent(lager::reader<std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string>> event, std::optional<Kazv::Room> room = std::nullopt, QObject *parent = 0);
explicit MatrixEvent(lager::reader<Kazv::Event> event, std::optional<Kazv::Room> room = std::nullopt, QObject *parent = 0);
~MatrixEvent() override;
@@ -65,6 +65,7 @@
LAGER_QT_READER(QString, formattedTime);
LAGER_QT_READER(QString, formattedDateTime);
LAGER_QT_READER(bool, isEdited);
+ LAGER_QT_READER(bool, isAvailable);
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
@@ -15,25 +15,35 @@
using namespace Kazv;
-static std::optional<LocalEchoDesc> getLocalEcho(std::variant<Event, LocalEchoDesc> event)
+static std::optional<LocalEchoDesc> getLocalEcho(std::variant<Event, LocalEchoDesc, std::string> event)
{
- if (std::holds_alternative<LocalEchoDesc>(event)) {
- return std::get<LocalEchoDesc>(event);
- } else {
- return std::nullopt;
- }
+ using R = std::optional<LocalEchoDesc>;
+ return lager::match(std::move(event))(
+ [](const LocalEchoDesc &l) -> R { return std::optional(l); },
+ [](auto) -> R { return std::nullopt; }
+ );
}
-static Event getEvent(std::variant<Event, LocalEchoDesc> event)
+static Event getEvent(std::variant<Event, LocalEchoDesc, std::string> event)
{
- if (std::holds_alternative<LocalEchoDesc>(event)) {
- return std::get<LocalEchoDesc>(event).event;
- } else {
- return std::get<Event>(event);
- }
+ return lager::match(std::move(event))(
+ [](const LocalEchoDesc &l) -> Event { return l.event; },
+ [](const Event &e) -> Event { return e; },
+ [](const std::string &id) -> Event {
+ return Event(json{{"event_id", id}});
+ }
+ );
+}
+
+static bool getAvailable(std::variant<Event, LocalEchoDesc, std::string> event)
+{
+ return lager::match(std::move(event))(
+ [](const std::string &) { return false; },
+ [](auto) { return true; }
+ );
}
-MatrixEvent::MatrixEvent(lager::reader<std::variant<Event, LocalEchoDesc>> event, std::optional<Room> room, QObject *parent)
+MatrixEvent::MatrixEvent(lager::reader<std::variant<Event, LocalEchoDesc, std::string>> event, std::optional<Room> room, QObject *parent)
: QObject(parent)
, m_localEcho(event.map(getLocalEcho))
, m_event(event.map(getEvent))
@@ -143,11 +153,12 @@
);
}))
, LAGER_QT(isEdited)(m_edits.map([](const auto &edits) { return !edits.empty(); }))
+ , LAGER_QT(isAvailable)(event.map(getAvailable))
{
}
MatrixEvent::MatrixEvent(lager::reader<Event> event, std::optional<Kazv::Room> room, QObject *parent)
- : MatrixEvent(event.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }), room, parent)
+ : MatrixEvent(event.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> { return e; }), room, parent)
{
}
diff --git a/src/matrix-room-timeline.cpp b/src/matrix-room-timeline.cpp
--- a/src/matrix-room-timeline.cpp
+++ b/src/matrix-room-timeline.cpp
@@ -61,10 +61,16 @@
MatrixEvent *MatrixRoomTimeline::at(int index) const
{
return new MatrixEvent(lager::with(m_timelineEventIds, m_messagesMap, m_localEchoes, LAGER_QT(count))
- .map([index](const auto &tlEventIds, const auto &messagesMap, const auto &localEchoes, const auto &count) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> {
+ .map([index](const auto &tlEventIds, const auto &messagesMap, const auto &localEchoes, const auto &count) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> {
auto rIndex = count - index - 1;
if (static_cast<unsigned int>(rIndex) < tlEventIds.size()) {
- return messagesMap[tlEventIds[rIndex]];
+ auto eventId = tlEventIds[rIndex];
+ // For some timelines like pinned events, the message may not be available
+ if (messagesMap.count(eventId)) {
+ return messagesMap[eventId];
+ } else {
+ return eventId;
+ }
} else {
auto remainingIndex = rIndex - tlEventIds.size();
if (remainingIndex < localEchoes.size()) {
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -38,6 +38,7 @@
QML_UNCREATABLE("")
Kazv::Room m_room;
+ lager::reader<immer::map<std::string, Kazv::Event>> m_messages;
lager::reader<Kazv::Event> m_userGivenNicknameEvent;
lager::reader<std::string> m_selfUserId;
lager::reader<immer::flex_vector<std::string>> m_memberNames;
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -42,6 +42,7 @@
MatrixRoom::MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, lager::reader<Kazv::Event> userGivenNicknameEvent, QObject *parent)
: QObject(parent)
, m_room(room)
+ , m_messages(room.messagesMap())
, m_userGivenNicknameEvent(userGivenNicknameEvent)
, m_selfUserId(selfUserId)
, m_memberNames(m_room.members())
@@ -455,9 +456,16 @@
MatrixEvent *MatrixRoom::messageById(QString eventId) const
{
+ auto eventIdStd = eventId.toStdString();
return new MatrixEvent(
- m_room.message(lager::make_constant(eventId.toStdString()))
- .map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
+ m_messages[eventIdStd]
+ .map([eventIdStd](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> {
+ if (e.has_value()) {
+ return e.value();
+ } else {
+ return eventIdStd;
+ }
+ }),
m_room
);
}
@@ -466,7 +474,7 @@
{
return new MatrixEvent(
m_room.localEcho(lager::make_constant(txnId.toStdString()))
- .map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
+ .map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc, std::string> { return e; }),
m_room
);
}
diff --git a/src/matrix-session.hpp b/src/matrix-session.hpp
--- a/src/matrix-session.hpp
+++ b/src/matrix-session.hpp
@@ -276,6 +276,16 @@
*/
MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
+ /**
+ * Load one event from the store.
+ *
+ * @param roomId The id of the room to operate on.
+ * @param eventId The id of the event to load.
+ * @return A MatrixPromise that resolves when loading is finished.
+ * The Promise is considered successful if and only if the event is loaded.
+ */
+ MatrixPromise *loadEvent(const QString &roomId, const QString &eventId);
+
/**
* Import from key backup file.
*
diff --git a/src/matrix-session.cpp b/src/matrix-session.cpp
--- a/src/matrix-session.cpp
+++ b/src/matrix-session.cpp
@@ -566,6 +566,38 @@
);
}
+MatrixPromise *MatrixSession::loadEvent(const QString &roomId, const QString &eventId)
+{
+ return new MatrixPromise(
+ m_context.createPromise([
+ dbStoreGetter=m_dbStoreGetter,
+ client=m_clientOnSecondaryRoot.toEventLoop(),
+ roomId,
+ eventId
+ ](auto resolve) {
+ dbStoreGetter().getEventById(roomId, eventId).then([client, resolve, roomId](auto &&res) {
+ if (!res.has_value()) {
+ resolve(EffectStatus(/* succ = */ false));
+ return;
+ }
+ auto [event, _isInTimeline] = std::move(res).value();
+ // Disregard isInTimeline because when we load one single event,
+ // we do not want to add it to the timeline. If we add it,
+ // we lose info about the which events are before/after it,
+ // causing the remaining timeline to be gapped without a way
+ // to refill it.
+ immer::map<std::string, Kazv::EventList> relatedEvents = {
+ {roomId.toStdString(), {event}},
+ };
+ client.loadEventsFromStorage({}, relatedEvents)
+ .then([resolve](auto &&) {
+ resolve(EffectStatus(/* succ = */ true));
+ });
+ });
+ })
+ );
+}
+
MatrixPromise *MatrixSession::importFromKeyBackupFile(QUrl fileUrl, QString password)
{
if (!fileUrl.isLocalFile()) {
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -23,6 +23,7 @@
matrix-sdk-sessions-test.cpp
matrix-promise-test.cpp
matrix-room-timeline-test.cpp
+ matrix-room-pinned-events-timeline-test.cpp
matrix-room-member-list-model-test.cpp
matrix-room-list-test.cpp
matrix-room-state-test.cpp
@@ -135,6 +136,8 @@
set(appium_tests_SOURCES
TestStickerPicker.py
+ TestEventViewUnavailable.py
+ TestRoomTimelineView.py
)
foreach(testfilename ${appium_tests_SOURCES})
message(STATUS "Adding appium test ${testfilename}")
diff --git a/src/tests/appium-tests/TestEventViewUnavailable.py b/src/tests/appium-tests/TestEventViewUnavailable.py
new file mode 100755
--- /dev/null
+++ b/src/tests/appium-tests/TestEventViewUnavailable.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+# This file is part of kazv.
+# SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+from helpers import utils
+from appium.webdriver.common.appiumby import AppiumBy
+
+class EventViewUnavailableTest(utils.ComponentTest):
+ def test_load(self):
+ create_button = self.driver.find_element(by=AppiumBy.NAME, value='create-event-view')
+ create_button.click()
+ self.wait_for(lambda: self.get_json_for('spy-count') >= 1)
+ self.driver.find_element(by=AppiumBy.NAME, value='spy-last-arg=$abcdef')
+
+if __name__ == '__main__':
+ utils.main(__file__)
diff --git a/src/tests/appium-tests/TestEventViewUnavailable.qml b/src/tests/appium-tests/TestEventViewUnavailable.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/appium-tests/TestEventViewUnavailable.qml
@@ -0,0 +1,87 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtTest
+
+import '../../contents/ui' as Kazv
+import 'qml-helpers' as AppiumQmlHelpers
+import '../quick-tests/test-helpers' as QmlHelpers
+
+QmlHelpers.TestItem {
+ id: upper
+ property var unavailableEvent: QtObject {
+ property string eventId: '$abcdef'
+ property string sender: ''
+ property string type: ''
+ property string stateKey: ''
+ property var content: {}
+ property bool isAvailable: false
+ }
+ property var sender: ({
+ membership: 'join',
+ userId: '@foo:tusooa.xyz',
+ name: 'foo',
+ avatarMxcUri: '',
+ })
+
+ signal loadEventRequested(string eventId)
+
+ SignalSpy {
+ id: loadEventRequestedSpy
+ signalName: 'loadEventRequested'
+ }
+
+ function reset() {
+ loadEventRequestedSpy.clear();
+ loadEventRequestedSpy.target = upper;
+ spyLastArgLabel.value = spyLastArgLabel.getValue('');
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ Button {
+ text: 'create-event-view'
+ onClicked: {
+ const ev = upper.eventViewComp.createObject(innerLayout);
+ }
+ }
+
+ AppiumQmlHelpers.InfoLabel {
+ tag: 'spy-count'
+ value: loadEventRequestedSpy.count
+ }
+
+ AppiumQmlHelpers.InfoLabel {
+ id: spyLastArgLabel
+ Connections {
+ target: upper
+ function onLoadEventRequested(json) {
+ spyLastArgLabel.value = spyLastArgLabel.getValue(json);
+ }
+ }
+ tag: 'spy-last-arg'
+ value: getValue('')
+
+ function getValue(v) {
+ return v;
+ }
+ }
+ ColumnLayout {
+ id: innerLayout
+ }
+ }
+
+ property var eventViewComp: Component {
+ Kazv.EventView {
+ Layout.fillWidth: true
+ event: upper.unavailableEvent
+ sender: upper.sender
+ }
+ }
+}
diff --git a/src/tests/appium-tests/TestRoomTimelineView.py b/src/tests/appium-tests/TestRoomTimelineView.py
new file mode 100755
--- /dev/null
+++ b/src/tests/appium-tests/TestRoomTimelineView.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# This file is part of kazv.
+# SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+from helpers import utils
+from appium.webdriver.common.appiumby import AppiumBy
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support.relative_locator import locate_with
+import json
+
+class RoomTimelineViewTest(utils.ComponentTest):
+ def test_delete_event(self):
+ event = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='event-$1')
+ event.find_element(AppiumBy.NAME, utils.l10n('event-popup-action')).click()
+ self.driver.find_element(AppiumBy.NAME, utils.l10n('event-delete')).click()
+ self.driver.find_element(AppiumBy.NAME, utils.l10n('confirm-deletion-popup-confirm-action')).click()
+ self.driver.find_element(AppiumBy.NAME, 'redact-last-arg=$1')
+ self.driver.find_element(AppiumBy.NAME, 'make-redact-fail').click()
+ expected_notif = utils.l10n('event-delete-failed', { 'errorMsg': 'You are not allowed to do so', 'errorCode': 'M_FORBIDDEN' })
+ self.driver.find_element(AppiumBy.NAME, 'passive-notification=' + expected_notif)
+
+ def test_delete_local_echo(self):
+ event = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'event-some-txn-id0')
+ event.find_element(AppiumBy.NAME, utils.l10n('event-popup-action')).click()
+ self.driver.find_element(AppiumBy.NAME, utils.l10n('event-delete')).click()
+ self.driver.find_element(AppiumBy.NAME, utils.l10n('confirm-deletion-popup-confirm-action')).click()
+ self.driver.find_element(AppiumBy.NAME, 'remove-local-echo-last-arg=some-txn-id0')
+
+if __name__ == '__main__':
+ utils.main(__file__)
diff --git a/src/tests/appium-tests/TestRoomTimelineView.qml b/src/tests/appium-tests/TestRoomTimelineView.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/appium-tests/TestRoomTimelineView.qml
@@ -0,0 +1,117 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtTest
+
+import '../../contents/ui' as Kazv
+import 'qml-helpers' as AppiumQmlHelpers
+
+AppiumQmlHelpers.TestItem {
+ id: upper
+
+ signal loadEventRequested(string eventId)
+ property var makeLocalEcho: (i) => ({
+ eventId: '',
+ sender: '',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body' + i,
+ },
+ encrypted: false,
+ isState: false,
+ unsignedData: {},
+ isLocalEcho: true,
+ isSending: true,
+ isFailed: false,
+ isAvailable: true,
+ txnId: 'some-txn-id' + i,
+ })
+
+ property var makeTextEvent: (i) => ({
+ eventId: '$' + i,
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ isAvailable: true,
+ formattedTime: '4:06 P.M.',
+ })
+
+ property var room: ({
+ pinEvents: mockHelper.promise(),
+ unpinEvents: mockHelper.promise(),
+ redactEvent: mockHelper.funcThenPromise((eventId) => {
+ console.log('redactEvent:', eventId);
+ redactLastArgLabel.value = eventId;
+ }),
+ removeLocalEcho: mockHelper.funcThenPromise((eventId) => {
+ console.log('rm local echo:', eventId);
+ removeLocalEchoLastArgLabel.value = eventId;
+ }),
+ messageById(id) {
+ return { eventId: id };
+ },
+ localEchoById(id) {
+ return { txnId: id };
+ }
+ })
+
+ property var timeline: ListModel {
+ ListElement {}
+ ListElement {}
+
+ function at(index) {
+ if (index < 1) {
+ return makeLocalEcho(index);
+ } else {
+ return makeTextEvent(index);
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ RowLayout {
+ AppiumQmlHelpers.InfoLabel {
+ id: redactLastArgLabel
+ tag: 'redact-last-arg'
+ value: ''
+ }
+ AppiumQmlHelpers.InfoLabel {
+ id: removeLocalEchoLastArgLabel
+ tag: 'remove-local-echo-last-arg'
+ value: ''
+ }
+ Button {
+ text: 'make-redact-fail'
+ onClicked: () => {
+ room.redactEvent.lastRetVal().resolve(false, { errorCode: 'M_FORBIDDEN', error: 'You are not allowed to do so' });
+ }
+ }
+ }
+
+ Kazv.RoomTimelineView {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ id: roomTimelineView
+ room: upper.room
+ timeline: upper.timeline
+ }
+ }
+
+ function reset() {
+ redactLastArgLabel.value = '';
+ removeLocalEchoLastArgLabel.value = '';
+ }
+}
diff --git a/src/tests/appium-tests/helpers/utils.py b/src/tests/appium-tests/helpers/utils.py
--- a/src/tests/appium-tests/helpers/utils.py
+++ b/src/tests/appium-tests/helpers/utils.py
@@ -51,6 +51,13 @@
def tearDownClass(cls):
cls.driver.quit()
+def l10n(key, args=None):
+ if args is None:
+ stringifiedArgs = 'undefined'
+ else:
+ stringifiedArgs = json.dumps(args, separators=(',', ':'))
+ return f'{key}, {stringifiedArgs}'
+
def main(filename):
global tests_dir, program_name, module_name
tests_dir = os.path.dirname(filename)
diff --git a/src/tests/appium-tests/qml-helpers/TestItem.qml b/src/tests/appium-tests/qml-helpers/TestItem.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/appium-tests/qml-helpers/TestItem.qml
@@ -0,0 +1,30 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import '../../quick-tests/test-helpers' as QmlHelpers
+
+QmlHelpers.TestItem {
+ id: item
+ showPassiveNotification: mockHelper.func((text) => {
+ notificationLabel.value = text;
+ })
+ default property alias content_: innerLayout.data
+
+ ColumnLayout {
+ anchors.fill: parent
+ InfoLabel {
+ id: notificationLabel
+ tag: 'passive-notification'
+ value: ''
+ }
+ ColumnLayout {
+ id: innerLayout
+ }
+ }
+}
diff --git a/src/tests/matrix-event-test.cpp b/src/tests/matrix-event-test.cpp
--- a/src/tests/matrix-event-test.cpp
+++ b/src/tests/matrix-event-test.cpp
@@ -32,6 +32,7 @@
void testEdits();
void testEncryptedEdits();
void testReactions();
+ void testAvailable();
};
void MatrixEventTest::testEdits()
@@ -164,6 +165,25 @@
QVERIFY(r0->key() == u"meww"_s || r1->key() == u"meww"_s);
}
+void MatrixEventTest::testAvailable()
+{
+ auto r = makeRoom();
+ auto events = EventList{
+ makeEvent(withEventId("$0") | withEventKV("/room_id"_json_pointer, r.roomId)),
+ makeEvent(withEventId("$1") | withEventKV("/room_id"_json_pointer, r.roomId)),
+ };
+ withRoomTimeline(events)(r);
+
+ auto model = SdkModel{makeClient(withRoom(r))};
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
+ auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+ auto e0 = toUniquePtr(room->messageById(u"$0"_s));
+ QVERIFY(e0->isAvailable());
+ auto e2 = toUniquePtr(room->messageById(u"$2"_s));
+ QVERIFY(!e2->isAvailable());
+}
+
QTEST_MAIN(MatrixEventTest)
#include "matrix-event-test.moc"
diff --git a/src/tests/matrix-room-pinned-events-timeline-test.cpp b/src/tests/matrix-room-pinned-events-timeline-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/matrix-room-pinned-events-timeline-test.cpp
@@ -0,0 +1,60 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+#include "test-model.hpp"
+#include "test-utils.hpp"
+#include <matrix-event.hpp>
+#include <matrix-room.hpp>
+#include <matrix-room-pinned-events-timeline.hpp>
+#include <matrix-room-list.hpp>
+#include <matrix-session.hpp>
+#include <factory.hpp>
+#include <QtTest>
+
+using namespace Qt::Literals::StringLiterals;
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+class MatrixRoomPinnedEventsTimelineTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testUnavailable();
+};
+
+void MatrixRoomPinnedEventsTimelineTest::testUnavailable()
+{
+ auto r = makeRoom(withRoomTimeline({
+ makeEvent(withEventId("$1")),
+ makeEvent(withEventId("$2")),
+ }) | withRoomState({
+ makeEvent(withEventType("m.room.pinned_events")
+ | withEventContent(json::object({
+ {"pinned", json::array({"$1", "$3"})},
+ }))
+ | withStateKey(""))
+ }));
+ auto model = SdkModel{makeClient(withRoom(r))};
+ SessionSetup s(model);
+ auto roomList = toUniquePtr(s.session.roomList());
+ auto room = toUniquePtr(roomList->room(QString::fromStdString(r.roomId)));
+ auto timeline = toUniquePtr(room->pinnedEventsTimeline());
+ QCOMPARE(timeline->count(), 2);
+
+ auto e1 = toUniquePtr(timeline->at(1));
+ QCOMPARE(e1->eventId(), u"$1"_s);
+ QVERIFY(e1->isAvailable());
+
+ auto e0 = toUniquePtr(timeline->at(0));
+ QCOMPARE(e0->eventId(), u"$3"_s);
+ QVERIFY(!e0->isAvailable());
+}
+
+QTEST_MAIN(MatrixRoomPinnedEventsTimelineTest)
+
+#include "matrix-room-pinned-events-timeline-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/MockHelper.qml b/src/tests/quick-tests/test-helpers/MockHelper.qml
--- a/src/tests/quick-tests/test-helpers/MockHelper.qml
+++ b/src/tests/quick-tests/test-helpers/MockHelper.qml
@@ -74,6 +74,13 @@
return func(createPromise, t);
}
+ function funcThenPromise(f, t) {
+ return func(function () {
+ f.apply(this, arguments);
+ return createPromise();
+ }, t);
+ }
+
function noop(t) {
return func(() => {}, t);
}
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
@@ -57,8 +57,9 @@
isState: false,
unsignedData: {},
isLocalEcho: true,
- isSending: true,
+ isSending: true,
isFailed: false,
+ isAvailable: true,
txnId: 'some-txn-id',
})
@@ -73,6 +74,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -87,6 +89,7 @@
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -102,6 +105,7 @@
format: 'org.matrix.custom.html',
formatted_body: '<mx-reply><blockquote>some quote</blockquote></mx-reply><strong>some body</strong>',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -117,6 +121,7 @@
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -127,6 +132,7 @@
stateKey: '',
content: {},
redacted: true,
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -143,6 +149,7 @@
w: Kirigami.Units.gridUnit * 10,
}
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -154,6 +161,7 @@
content: {
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -166,6 +174,7 @@
msgtype: 'm.image',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -178,6 +187,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -191,6 +201,7 @@
msgtype: 'm.image',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -203,6 +214,7 @@
msgtype: 'm.image',
body: 'some body',
},
+ isAvailable: true,
})
property var unknownMsgtypeEvent: ({
@@ -214,6 +226,7 @@
msgtype: 'moe.kazv.mxc.unknown.msgtype',
body: 'some body',
},
+ isAvailable: true,
})
property var cannotDecryptEvent: ({
@@ -225,6 +238,7 @@
msgtype: 'moe.kazv.mxc.cannot.decrypt',
body: 'some body',
},
+ isAvailable: true,
})
property var editedEvent: ({
@@ -236,6 +250,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
isEdited: true,
})
@@ -248,6 +263,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -259,6 +275,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
diff --git a/src/tests/quick-tests/tst_RoomTimelineView.qml b/src/tests/quick-tests/tst_RoomTimelineView.qml
--- a/src/tests/quick-tests/tst_RoomTimelineView.qml
+++ b/src/tests/quick-tests/tst_RoomTimelineView.qml
@@ -19,6 +19,7 @@
QmlHelpers.TestItem {
id: item
+ signal loadEventRequested(string eventId)
property var makeLocalEcho: (i) => ({
eventId: '',
sender: '',
@@ -34,6 +35,7 @@
isLocalEcho: true,
isSending: true,
isFailed: false,
+ isAvailable: true,
txnId: 'some-txn-id' + i,
})
@@ -46,6 +48,7 @@
msgtype: 'm.text',
body: 'some body',
},
+ isAvailable: true,
formattedTime: '4:06 P.M.',
})
@@ -55,16 +58,17 @@
redactEvent: mockHelper.promise(),
removeLocalEcho: mockHelper.promise(),
messageById(id) {
- return { eventId: id };
+ return { eventId: id, isAvailable: true };
},
localEchoById(id) {
- return { txnId: id };
+ return { txnId: id, isAvailable: true };
}
})
Kazv.RoomTimelineView {
anchors.fill: parent
id: roomTimelineView
+ room: item.room
timeline: item.timeline
}
@@ -165,32 +169,5 @@
compare(showPassiveNotification.calledTimes(), 1);
compare(showPassiveNotification.lastArgs()[0], l10n.get('event-unpin-failed-prompt', { errorCode: 'M_FORBIDDEN', errorMsg: 'You are not allowed to do so' }));
}
-
- function test_deleteEvent() {
- roomTimelineView.deleteEventRequested("some-event-id", false);
- const popup = findChild(Overlay.overlay, 'confirmDeletionPopup');
- verify(popup);
- tryVerify(() => popup.opened);
- mouseClick(findChild(popup, 'confirmButton'));
- verify(room.redactEvent.calledTimes() === 1);
- verify(room.redactEvent.lastArgs()[0], ['some-eventId']);
- tryVerify(() => !popup.opened);
- tryVerify(() => !findChild(Overlay.overlay, 'confirmDeletionPopup'));
- room.redactEvent.lastRetVal().resolve(false, { errorCode: 'M_FORBIDDEN', error: 'You are not allowed to do so' });
- compare(showPassiveNotification.calledTimes(), 1);
- compare(showPassiveNotification.lastArgs()[0], l10n.get('event-delete-failed', { errorMsg: 'You are not allowed to do so', errorCode: 'M_FORBIDDEN' }));
- }
-
- function test_deleteLocalEcho() {
- roomTimelineView.deleteEventRequested("some-txnId", true);
- const popup = findChild(Overlay.overlay, 'confirmDeletionPopup');
- verify(popup);
- tryVerify(() => popup.opened);
- mouseClick(findChild(popup, 'confirmButton'));
- verify(room.removeLocalEcho.calledTimes() === 1);
- verify(room.removeLocalEcho.lastArgs()[0], ['some-txnId']);
- tryVerify(() => !popup.opened);
- tryVerify(() => !findChild(Overlay.overlay, 'confirmDeletionPopup'));
- }
}
}

File Metadata

Mime Type
text/plain
Expires
Sun, Jul 12, 11:59 AM (23 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1683380
Default Alt Text
D325.1783882742.diff (32 KB)

Event Timeline