Page MenuHomePhorge

D131.1732546459.diff
No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None

D131.1732546459.diff

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -16,6 +16,7 @@
matrix-room.cpp
matrix-room-list.cpp
matrix-room-timeline.cpp
+ matrix-room-pinned-events-timeline.cpp
matrix-room-member.cpp
matrix-room-member-list-model.cpp
matrix-event-reader.cpp
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
@@ -21,6 +21,10 @@
property var currentEvent: event
property var menuContent: []
property var shouldDisplayTime: displayTime || (!compactMode && !event.isLocalEcho)
+ property var eventPinned: event.eventId && pinnedEvents.eventIds.includes(event.eventId)
+
+ onEventPinnedChanged: console.log(event.eventId, eventPinned ? 'is pinned' : 'is not pinned', pinnedEvents, pinnedEvents.count, pinnedEvents.eventIds.includes(event.eventId))
+
topPadding: 0
bottomPadding: 0
readonly property var bubbleSpacing: leftPadding + rightPadding
@@ -89,6 +93,12 @@
onTriggered: reactionPopupComp.createObject(applicationWindow().overlay).open()
enabled: event && !event.redacted
},
+ Kirigami.Action {
+ objectName: 'pinUnpinMenuItem'
+ enabled: !event.isLocalEcho
+ text: eventPinned ? l10n.get('event-unpin-action') : l10n.get('event-pin-action')
+ onTriggered: eventPinned ? unpinEventRequested(event.eventId) : pinEventRequested(event.eventId)
+ },
MenuSeparator {},
Kirigami.Action {
text: l10n.get('event-view-source')
diff --git a/src/contents/ui/ConfirmationOverlay.qml b/src/contents/ui/ConfirmationOverlay.qml
--- a/src/contents/ui/ConfirmationOverlay.qml
+++ b/src/contents/ui/ConfirmationOverlay.qml
@@ -9,24 +9,34 @@
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
+import '.' as Kazv
-Kirigami.OverlaySheet {
+Kazv.SelfDestroyableOverlaySheet {
id: confirmationOverlay
property var message
property var confirmActionText
property var cancelActionText
+ default property alias contentData: itemsLayout.data
+
signal accepted()
ColumnLayout {
Label {
+ Layout.fillWidth: true
text: message
+ wrapMode: Text.Wrap
+ }
+
+ ColumnLayout {
+ id: itemsLayout
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Button {
+ objectName: 'confirmButton'
text: confirmActionText
onClicked: {
accepted();
@@ -35,6 +45,7 @@
}
Button {
+ objectName: 'cancelButton'
text: cancelActionText
onClicked: confirmationOverlay.close();
}
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
@@ -17,11 +17,11 @@
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomPinnedEventsPage.room
}
- property var pinnedTimeline: room.pinnedEventsTimeline()
+ property var pinnedEvents: room.pinnedEventsTimeline()
title: l10n.get('room-pinned-events-page-title', { room: roomNameProvider.name })
RoomTimelineView {
- timeline: roomPinnedEventsPage.pinnedTimeline
+ 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
@@ -9,6 +9,7 @@
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
+import '.' as Kazv
ListView {
id: roomTimelineView
@@ -19,6 +20,85 @@
property string selectedEventId
+ signal pinEventRequested(string eventId)
+ signal unpinEventRequested(string eventId)
+
+ property var pinEvent: Kazv.AsyncHandler {
+ property var eventIds
+ trigger: () => room.pinEvents(eventIds)
+ onResolved: (success, data) => {
+ if (success) {
+ showPassiveNotification(l10n.get('event-pin-success-prompt'));
+ } else {
+ showPassiveNotification(l10n.get('event-pin-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
+ }
+ }
+ }
+
+ property var unpinEvent: Kazv.AsyncHandler {
+ property var eventIds
+ trigger: () => room.unpinEvents(eventIds)
+ onResolved: (success, data) => {
+ if (success) {
+ showPassiveNotification(l10n.get('event-unpin-success-prompt'));
+ } else {
+ showPassiveNotification(l10n.get('event-unpin-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
+ }
+ }
+ }
+
+ onPinEventRequested: (eventId) => {
+ pinEventPopupComp.createObject(Overlay.overlay, { eventId }).open()
+ }
+
+ onUnpinEventRequested: (eventId) => {
+ unpinEventPopupComp.createObject(Overlay.overlay, { eventId }).open()
+ }
+
+ property var pinEventPopupComp: Component {
+ Kazv.ConfirmationOverlay {
+ objectName: 'pinEventPopup'
+ property var eventId
+ shouldSelfDestroy: true
+ title: l10n.get('event-pin-confirmation-title')
+ message: l10n.get('event-pin-confirmation')
+ confirmActionText: l10n.get('event-pin-confirm-action')
+ cancelActionText: l10n.get('event-pin-cancel-action')
+ onAccepted: {
+ pinEvent.eventIds = [eventId];
+ pinEvent.call();
+ }
+
+ EventViewWrapper {
+ Layout.fillWidth: true
+ event: room.messageById(eventId)
+ compactMode: true
+ }
+ }
+ }
+
+ property var unpinEventPopupComp: Component {
+ Kazv.ConfirmationOverlay {
+ objectName: 'unpinEventPopup'
+ property var eventId
+ shouldSelfDestroy: true
+ title: l10n.get('event-unpin-confirmation-title')
+ message: l10n.get('event-unpin-confirmation')
+ confirmActionText: l10n.get('event-unpin-confirm-action')
+ cancelActionText: l10n.get('event-unpin-cancel-action')
+ onAccepted: {
+ unpinEvent.eventIds = [eventId];
+ unpinEvent.call();
+ }
+
+ EventViewWrapper {
+ Layout.fillWidth: true
+ event: room.messageById(eventId)
+ compactMode: true
+ }
+ }
+ }
+
spacing: 0
model: timeline
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
@@ -197,6 +197,20 @@
event-read-indicator-more = +{ $rest }
event-read-indicator-list-title = { $numUsers } 个用户已读
event-edited-indicator = (编辑过了)
+event-pin-action = 在房间置顶
+event-unpin-action = 从房间取消置顶
+event-pin-confirmation-title = 在房间置顶
+event-pin-confirmation = 确定要置顶这条消息吗?
+event-pin-confirm-action = 置顶
+event-pin-cancel-action = 不置顶
+event-pin-success-prompt = 消息在房间置顶了。
+event-pin-failed-prompt = 无法置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
+event-unpin-confirmation-title = 从房间取消置顶
+event-unpin-confirmation = 确定要取消置顶这条消息吗?
+event-unpin-confirm-action = 取消置顶
+event-unpin-cancel-action = 不取消置顶
+event-unpin-success-prompt = 消息从房间取消置顶了。
+event-unpin-failed-prompt = 无法取消置顶消息。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
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
@@ -219,6 +219,20 @@
*[other] { $numUsers } users
}
event-edited-indicator = (edited)
+event-pin-action = Pin to room
+event-unpin-action = Unpin from room
+event-pin-confirmation-title = Pin to room
+event-pin-confirmation = Are you sure you want to pin this message?
+event-pin-confirm-action = Pin
+event-pin-cancel-action = Do not pin
+event-pin-success-prompt = Message pinned to room.
+event-pin-failed-prompt = Unable to pin message. Error code: { $errorCode }. Error message: { $errorMsg }.
+event-unpin-confirmation-title = Unpin from room
+event-unpin-confirmation = Are you sure you want to unpin this message?
+event-unpin-confirm-action = Unpin
+event-unpin-cancel-action = Do not unpin
+event-unpin-success-prompt = Message unpinned from room.
+event-unpin-failed-prompt = Unable to unpin message. Error code: { $errorCode }. Error message: { $errorMsg }.
media-file-menu-option-view = View
media-file-menu-option-save-as = Save as
diff --git a/src/matrix-room-pinned-events-timeline.hpp b/src/matrix-room-pinned-events-timeline.hpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-room-pinned-events-timeline.hpp
@@ -0,0 +1,24 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+#include <QObject>
+#include <QQmlEngine>
+#include "matrix-room-timeline.hpp"
+
+class MatrixRoomPinnedEventsTimeline : public MatrixRoomTimeline
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit MatrixRoomPinnedEventsTimeline(Kazv::Room room, QObject *parent = 0);
+ ~MatrixRoomPinnedEventsTimeline() override;
+
+ LAGER_QT_READER(QSet<QString>, eventIds);
+};
diff --git a/src/matrix-room-pinned-events-timeline.cpp b/src/matrix-room-pinned-events-timeline.cpp
new file mode 100644
--- /dev/null
+++ b/src/matrix-room-pinned-events-timeline.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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 "matrix-room-pinned-events-timeline.hpp"
+
+using namespace Kazv;
+
+MatrixRoomPinnedEventsTimeline::MatrixRoomPinnedEventsTimeline(Kazv::Room room, QObject *parent)
+ : MatrixRoomTimeline(
+ room.pinnedEvents(),
+ room.messagesMap(),
+ lager::make_constant(immer::flex_vector<LocalEchoDesc>()),
+ lager::make_constant(immer::map<std::string, std::string>()),
+ room,
+ parent
+ )
+ , LAGER_QT(eventIds)(room.pinnedEvents().map([](const auto &ids) {
+ QSet<QString> res;
+ for (const auto &id : ids) {
+ res.insert(QString::fromStdString(id));
+ }
+ return res;
+ }))
+{
+}
+
+MatrixRoomPinnedEventsTimeline::~MatrixRoomPinnedEventsTimeline() = default;
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -15,8 +15,11 @@
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
+Q_MOC_INCLUDE("matrix-room-timeline.hpp")
+Q_MOC_INCLUDE("matrix-room-pinned-events-timeline.hpp")
class MatrixRoomTimeline;
+class MatrixRoomPinnedEventsTimeline;
class MatrixRoomMember;
class MatrixPromise;
class MatrixRoomMemberListModel;
@@ -68,7 +71,11 @@
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
- Q_INVOKABLE MatrixRoomTimeline *pinnedEventsTimeline() const;
+ Q_INVOKABLE MatrixRoomPinnedEventsTimeline *pinnedEventsTimeline() const;
+
+ Q_INVOKABLE MatrixPromise *pinEvents(const QStringList &eventIds) const;
+
+ Q_INVOKABLE MatrixPromise *unpinEvents(const QStringList &eventIds) const;
Q_INVOKABLE MatrixEvent *messageById(QString eventId) const;
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -16,6 +16,7 @@
#include "kazv-log.hpp"
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
+#include "matrix-room-pinned-events-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-promise.hpp"
@@ -105,15 +106,25 @@
return new MatrixRoomTimeline(m_room);
}
-MatrixRoomTimeline *MatrixRoom::pinnedEventsTimeline() const
+MatrixRoomPinnedEventsTimeline *MatrixRoom::pinnedEventsTimeline() const
{
- return new MatrixRoomTimeline(
- m_room.pinnedEvents(),
- m_room.messagesMap(),
- lager::make_constant(immer::flex_vector<LocalEchoDesc>()),
- lager::make_constant(immer::map<std::string, std::string>()),
- m_room
- );
+ return new MatrixRoomPinnedEventsTimeline(m_room);
+}
+
+MatrixPromise *MatrixRoom::pinEvents(const QStringList &eventIds) const
+{
+ return new MatrixPromise(m_room.pinEvents(intoImmer(
+ immer::flex_vector<std::string>(),
+ qStringToStd,
+ eventIds)));
+}
+
+MatrixPromise *MatrixRoom::unpinEvents(const QStringList &eventIds) const
+{
+ return new MatrixPromise(m_room.unpinEvents(intoImmer(
+ immer::flex_vector<std::string>(),
+ qStringToStd,
+ eventIds)));
}
static void maybeAddRelations(nlohmann::json &msg, const QString &relType, const QString &relatedTo)
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
@@ -24,10 +24,18 @@
property var activateUserPage: mockHelper.noop()
property var paginateBackRequested: mockHelper.noop()
property var viewEventHistoryRequested: mockHelper.noop()
+ property var pinEventRequested: mockHelper.noop()
+ property var unpinEventRequested: mockHelper.noop()
property var timeline: ({
gaps: [],
})
+
+ property var pinnedEvents: ({
+ count: 2,
+ eventIds: ['$123', '$456'],
+ })
+
property var room: ({
resendMessage: mockHelper.promise(),
removeLocalEcho: mockHelper.promise(),
@@ -243,6 +251,28 @@
isEdited: true,
})
+ property var pinnedEvent: ({
+ eventId: '$123',
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ formattedTime: '4:06 P.M.',
+ })
+
+ property var notPinnedEvent: ({
+ eventId: '$789',
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ formattedTime: '4:06 P.M.',
+ })
+
property var sender: ({
membership: 'join',
userId: '@foo:tusooa.xyz',
@@ -418,6 +448,20 @@
event: item.editedEvent
sender: item.sender
}
+
+ Kazv.EventView {
+ Layout.fillWidth: true
+ id: eventViewPinned
+ event: item.pinnedEvent
+ sender: item.sender
+ }
+
+ Kazv.EventView {
+ Layout.fillWidth: true
+ id: eventViewNotPinned
+ event: item.notPinnedEvent
+ sender: item.sender
+ }
}
TestCase {
@@ -637,5 +681,34 @@
compare(viewEventHistoryRequested.lastArgs()[0], '!some-event-id');
}
}
+
+ function test_pinned() {
+ {
+ const menuComp = findChild(eventViewPinned, 'bubble').menuComp;
+ const menu = menuComp.createObject(menuComp.parent);
+ const action = findChild(menu, 'pinUnpinMenuItem');
+ verify(action.enabled);
+ action.trigger();
+ compare(unpinEventRequested.calledTimes(), 1);
+ compare(unpinEventRequested.lastArgs()[0], '$123');
+ compare(pinEventRequested.calledTimes(), 0);
+ }
+ {
+ const menuComp = findChild(eventViewNotPinned, 'bubble').menuComp;
+ const menu = menuComp.createObject(menuComp.parent);
+ const action = findChild(menu, 'pinUnpinMenuItem');
+ verify(action.enabled);
+ action.trigger();
+ compare(pinEventRequested.calledTimes(), 1);
+ compare(pinEventRequested.lastArgs()[0], '$789');
+ compare(pinEventRequested.calledTimes(), 1);
+ }
+ {
+ const menuComp = findChild(eventView, 'bubble').menuComp;
+ const menu = menuComp.createObject(menuComp.parent);
+ const action = findChild(menu, 'pinUnpinMenuItem');
+ verify(!action.enabled);
+ }
+ }
}
}
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
@@ -6,6 +6,8 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
+import QtQuick.Window
+import QtQuick.Controls
import QtTest 1.0
import moe.kazv.mxc.kazv 0.0 as MK
@@ -20,6 +22,8 @@
height: 600
property var mockHelper: TestHelpers.MockHelper {}
+ property var l10n: Helpers.fluentMock
+ property var matrixSdk: TestHelpers.MatrixSdkMock {}
property var makeLocalEcho: (i) => ({
eventId: '',
@@ -51,6 +55,16 @@
formattedTime: '4:06 P.M.',
})
+ property var room: ({
+ pinEvents: mockHelper.promise(),
+ unpinEvents: mockHelper.promise(),
+ messageById(id) {
+ return { eventId: id };
+ },
+ })
+
+ property var showPassiveNotification: mockHelper.noop()
+
Kazv.RoomTimelineView {
anchors.fill: parent
id: roomTimelineView
@@ -75,6 +89,10 @@
name: 'RoomTimelineViewTest'
when: windowShown
+ function init() {
+ mockHelper.clearAll();
+ }
+
function test_selected() {
tryVerify(() => roomTimelineView.itemAtIndex(0));
tryVerify(() => roomTimelineView.itemAtIndex(1));
@@ -88,5 +106,65 @@
roomTimelineView.selectedEventId = '$1';
verify(sentMessage.isSelected);
}
+
+ function test_pinSuccess() {
+ roomTimelineView.pinEventRequested('$2');
+ const popup = findChild(Overlay.overlay, 'pinEventPopup');
+ verify(popup);
+ tryVerify(() => popup.opened);
+ mouseClick(findChild(popup, 'confirmButton'));
+ verify(room.pinEvents.calledTimes() === 1);
+ verify(Helpers.deepEqual(room.pinEvents.lastArgs()[0], ['$2']));
+ tryVerify(() => !popup.opened);
+ tryVerify(() => !findChild(Overlay.overlay, 'pinEventPopup'));
+ room.pinEvents.lastRetVal().resolve(true, {});
+ compare(showPassiveNotification.calledTimes(), 1);
+ compare(showPassiveNotification.lastArgs()[0], l10n.get('event-pin-success-prompt'));
+ }
+
+ function test_pinFailure() {
+ roomTimelineView.pinEventRequested('$2');
+ const popup = findChild(Overlay.overlay, 'pinEventPopup');
+ verify(popup);
+ tryVerify(() => popup.opened);
+ mouseClick(findChild(popup, 'confirmButton'));
+ verify(room.pinEvents.calledTimes() === 1);
+ verify(Helpers.deepEqual(room.pinEvents.lastArgs()[0], ['$2']));
+ tryVerify(() => !popup.opened);
+ tryVerify(() => !findChild(Overlay.overlay, 'pinEventPopup'));
+ room.pinEvents.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-pin-failed-prompt', { errorCode: 'M_FORBIDDEN', errorMsg: 'You are not allowed to do so' }));
+ }
+
+ function test_unpinSuccess() {
+ roomTimelineView.unpinEventRequested('$2');
+ const popup = findChild(Overlay.overlay, 'unpinEventPopup');
+ verify(popup);
+ tryVerify(() => popup.opened);
+ mouseClick(findChild(popup, 'confirmButton'));
+ verify(room.unpinEvents.calledTimes() === 1);
+ verify(Helpers.deepEqual(room.unpinEvents.lastArgs()[0], ['$2']));
+ tryVerify(() => !popup.opened);
+ tryVerify(() => !findChild(Overlay.overlay, 'unpinEventPopup'));
+ room.unpinEvents.lastRetVal().resolve(true, {});
+ compare(showPassiveNotification.calledTimes(), 1);
+ compare(showPassiveNotification.lastArgs()[0], l10n.get('event-unpin-success-prompt'));
+ }
+
+ function test_unpinFailure() {
+ roomTimelineView.unpinEventRequested('$2');
+ const popup = findChild(Overlay.overlay, 'unpinEventPopup');
+ verify(popup);
+ tryVerify(() => popup.opened);
+ mouseClick(findChild(popup, 'confirmButton'));
+ verify(room.unpinEvents.calledTimes() === 1);
+ verify(Helpers.deepEqual(room.unpinEvents.lastArgs()[0], ['$2']));
+ tryVerify(() => !popup.opened);
+ tryVerify(() => !findChild(Overlay.overlay, 'unpinEventPopup'));
+ room.unpinEvents.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-unpin-failed-prompt', { errorCode: 'M_FORBIDDEN', errorMsg: 'You are not allowed to do so' }));
+ }
}
}

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 25, 6:54 AM (11 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40230
Default Alt Text
D131.1732546459.diff (19 KB)

Event Timeline