Page MenuHomePhorge

D88.1726914607.diff
No OneTemporary

D88.1726914607.diff

diff --git a/src/contents/ui/Completion.qml b/src/contents/ui/Completion.qml
new file mode 100644
--- /dev/null
+++ b/src/contents/ui/Completion.qml
@@ -0,0 +1,70 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+import org.kde.kirigami 2.13 as Kirigami
+import moe.kazv.mxc.kazv 0.0 as MK
+
+import '.' as Kazv
+
+ColumnLayout {
+ id: upper
+ spacing: 0
+ width: parent.width
+ property var maxHeight: Kirigami.Units.gridUnit * 10
+ implicitHeight: Math.min(listView.contentHeight, upper.maxHeight)
+ property var members
+
+ signal mentionUserRequested(string userId)
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.preferredHeight: contentHeight
+ Layout.maximumHeight: upper.maxHeight
+
+ ListView {
+ id: listView
+ objectName: 'completionListView'
+ anchors.fill: parent
+ model: members
+ delegate: ItemDelegate {
+ objectName: `completionItem${index}`
+ property var member: members.at(index)
+ property var nameProvider: Kazv.UserNameProvider {
+ user: member
+ }
+ height: completionItemLayout.implicitHeight
+ width: ListView.view.width
+
+ onClicked: mentionUserRequested(member.userId)
+
+ RowLayout {
+ id: completionItemLayout
+ Kazv.AvatarAdapter {
+ Layout.preferredWidth: Kirigami.Units.iconSizes.medium
+ Layout.preferredHeight: Kirigami.Units.iconSizes.medium
+ name: nameProvider.name
+ source: member.avatarMxcUri ? matrixSdk.mxcUriToHttp(member.avatarMxcUri) : ''
+ }
+ ColumnLayout {
+ Label {
+ Layout.fillWidth: true
+ text: nameProvider.name
+ }
+ Label {
+ objectName: 'userIdLabel'
+ Layout.fillWidth: true
+ text: member.userId
+ }
+ }
+ }
+ }
+ }
+ }
+}
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
@@ -25,6 +25,7 @@
property var roomTimeline: room.timeline()
property var lastReceiptableEventId: getLastReceiptableEventId(roomTimeline, roomTimeline.count)
property var paginationRequests: ({})
+ property var refreshRoomStateRequest: null
signal mentionUserRequested(string userId)
signal replaceDraftRequested(string newDraft)
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -20,6 +20,7 @@
property var draftRelType: ''
property var draftRelatedTo: ''
property var timeline: room.timeline()
+ property var members: room.members()
function getRelationPrompt(draftRelType) {
if (draftRelType === 'm.in_reply_to') {
@@ -45,6 +46,23 @@
textArea.changeText(room.localDraft, true);
}
+ Popup {
+ id: completionPopup
+ objectName: 'completionPopup'
+ x: 1
+ y: -height
+ width: parent.width - 1
+ contentItem: Kazv.Completion {
+ id: completion
+ members: sendMessageBox.members
+
+ onMentionUserRequested: (userId) => {
+ textArea.removePartialMention();
+ sendMessageBox.mentionUser(userId);
+ }
+ }
+ }
+
ColumnLayout {
visible: !!sendMessageBox.draftRelatedTo
@@ -86,6 +104,7 @@
property var shortcutList: ["Ctrl+Return", "Ctrl+Enter"]
property var inhibitTyping: false
placeholderText: l10n.get('send-message-box-input-placeholder')
+ property string filter: getFilter()
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
onTextChanged: {
@@ -107,6 +126,32 @@
}
}
+ function getFilter() {
+ const atPos = text.lastIndexOf('@', cursorPosition);
+ if (atPos < 0) {
+ return '';
+ }
+
+ const textInBetween = text.slice(atPos, cursorPosition);
+ const hasSpaces = /\s/.test(textInBetween);
+ return hasSpaces ? '' : textInBetween;
+ }
+
+ function removePartialMention() {
+ const atPos = text.lastIndexOf('@', cursorPosition);
+ text = text.slice(0, atPos) + text.slice(cursorPosition);
+ cursorPosition = atPos;
+ }
+
+ onFilterChanged: {
+ sendMessageBox.members.filter = filter.slice(1);
+ if (filter && sendMessageBox.members.count > 0) {
+ completionPopup.open();
+ } else {
+ completionPopup.close();
+ }
+ }
+
// Shortcut keys for sending messages (TODO: Shortcut keys customized by the user)
Shortcut {
sequences: textArea.shortcutList
@@ -150,13 +195,19 @@
if (prefix && prefix.slice(prefix.length - 1).trim() !== '') {
insertion = ' ' + insertion;
}
+ let postPadding = 0;
// If we are at the end or the next char is not a space,
// insert a space there.
if (!suffix || suffix.slice(0, 1).trim() !== '') {
insertion = insertion + ' ';
+ } else if (suffix) {
+ // If there is a suffix, and space is not inserted
+ // this means the next character is a space,
+ // move the cursor to that space.
+ postPadding = 1;
}
textArea.text = prefix + insertion + suffix;
- textArea.cursorPosition = prefix.length + insertion.length;
+ textArea.cursorPosition = prefix.length + insertion.length + postPadding;
}
function focusInput() {
diff --git a/src/matrix-event-reader-list-model.cpp b/src/matrix-event-reader-list-model.cpp
--- a/src/matrix-event-reader-list-model.cpp
+++ b/src/matrix-event-reader-list-model.cpp
@@ -16,6 +16,7 @@
readers.xform(containerMap(EventList{}, zug::map([](const auto &item) {
return std::get<0>(item);
}))),
+ QString(),
parent
)
, m_readers(readers)
diff --git a/src/matrix-room-member-list-model.hpp b/src/matrix-room-member-list-model.hpp
--- a/src/matrix-room-member-list-model.hpp
+++ b/src/matrix-room-member-list-model.hpp
@@ -12,7 +12,7 @@
#include <QAbstractListModel>
#include <lager/extra/qt.hpp>
-
+#include <lager/state.hpp>
#include <client/room/room.hpp>
class MatrixRoomMember;
@@ -23,13 +23,15 @@
QML_ELEMENT
QML_UNCREATABLE("")
+ lager::state<std::string, lager::automatic_tag> m_filter;
lager::reader<Kazv::EventList> m_members;
int m_internalCount;
public:
- explicit MatrixRoomMemberListModel(lager::reader<Kazv::EventList> members, QObject *parent = 0);
+ explicit MatrixRoomMemberListModel(lager::reader<Kazv::EventList> members, QString filter = QString(), QObject *parent = 0);
~MatrixRoomMemberListModel() override;
+ LAGER_QT_CURSOR(QString, filter);
LAGER_QT_READER(int, count);
int rowCount(const QModelIndex &index) const override;
diff --git a/src/matrix-room-member-list-model.cpp b/src/matrix-room-member-list-model.cpp
--- a/src/matrix-room-member-list-model.cpp
+++ b/src/matrix-room-member-list-model.cpp
@@ -11,16 +11,36 @@
#include <lager/lenses/optional.hpp>
#include <cursorutil.hpp>
-
+#include "helper.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-room-member.hpp"
using namespace Kazv;
-MatrixRoomMemberListModel::MatrixRoomMemberListModel(lager::reader<Kazv::EventList> members, QObject *parent)
+MatrixRoomMemberListModel::MatrixRoomMemberListModel(lager::reader<Kazv::EventList> members, QString filter, QObject *parent)
: QAbstractListModel(parent)
- , m_members(members)
+ , m_filter(lager::make_state(filter.toStdString(), lager::automatic_tag{}))
+ , m_members(lager::with(members, m_filter).map([](const auto &members, const auto &filter) {
+ if (filter.empty()) {
+ return members;
+ }
+
+ return intoImmer(EventList{}, zug::filter([filter](const auto &e) {
+ auto userId = e.stateKey();
+ if (userId.find(filter) != std::string::npos) {
+ return true;
+ }
+ auto content = e.content().get();
+ if (content.contains("displayname")
+ && content["displayname"].is_string()) {
+ auto name = content["displayname"].template get<std::string>();
+ return name.find(filter) != std::string::npos;
+ }
+ return false;
+ }), members);
+ }))
, m_internalCount(0)
+ , LAGER_QT(filter)(m_filter.xform(strToQt, qStringToStd))
, LAGER_QT(count)(m_members.map([](const auto &m) -> int { return m.size(); }))
{
m_internalCount = count();
diff --git a/src/resources.qrc b/src/resources.qrc
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -51,6 +51,7 @@
<file alias="RoomMemberListViewItemDelegate.qml">contents/ui/RoomMemberListViewItemDelegate.qml</file>
<file alias="UserNameProvider.qml">contents/ui/UserNameProvider.qml</file>
<file alias="RoomNameProvider.qml">contents/ui/RoomNameProvider.qml</file>
+ <file alias="Completion.qml">contents/ui/Completion.qml</file>
<file alias="AsyncHandler.qml">contents/ui/AsyncHandler.qml</file>
<file alias="UploadFileHelper.qml">contents/ui/UploadFileHelper.qml</file>
diff --git a/src/tests/matrix-room-member-list-model-test.cpp b/src/tests/matrix-room-member-list-model-test.cpp
--- a/src/tests/matrix-room-member-list-model-test.cpp
+++ b/src/tests/matrix-room-member-list-model-test.cpp
@@ -26,10 +26,11 @@
private Q_SLOTS:
void testMembers();
+ void testFilter();
};
static auto data = Kazv::EventList{
- makeRoomMember("@t1:tusooa.xyz", "t1"),
+ makeRoomMember("@t1:tusooa.xyz", "user 1"),
makeRoomMember("@t2:tusooa.xyz"),
};
@@ -40,7 +41,7 @@
QCOMPARE(model.count(), 2);
auto first = toUniquePtr(model.at(0));
- QCOMPARE(first->name(), "t1");
+ QCOMPARE(first->name(), "user 1");
QCOMPARE(first->userId(), "@t1:tusooa.xyz");
auto second = toUniquePtr(model.at(1));
QCOMPARE(second->name(), QStringLiteral());
@@ -48,6 +49,30 @@
}
+void MatrixRoomMemberListModelTest::testFilter()
+{
+ MatrixRoomMemberListModel model(lager::make_constant(data));
+
+ QCOMPARE(model.count(), 2);
+
+ model.setfilter(QString("@t"));
+ QCOMPARE(model.count(), 2);
+
+ {
+ model.setfilter(QString("user 1"));
+ QCOMPARE(model.count(), 1);
+ auto first = toUniquePtr(model.at(0));
+ QCOMPARE(first->userId(), QString("@t1:tusooa.xyz"));
+ }
+
+ {
+ model.setfilter(QString("t2"));
+ QCOMPARE(model.count(), 1);
+ auto first = toUniquePtr(model.at(0));
+ QCOMPARE(first->userId(), QString("@t2:tusooa.xyz"));
+ }
+}
+
QTEST_MAIN(MatrixRoomMemberListModelTest)
#include "matrix-room-member-list-model-test.moc"
diff --git a/src/tests/quick-tests/tst_SendMessageBox.qml b/src/tests/quick-tests/tst_SendMessageBox.qml
--- a/src/tests/quick-tests/tst_SendMessageBox.qml
+++ b/src/tests/quick-tests/tst_SendMessageBox.qml
@@ -38,16 +38,30 @@
},
updateLocalDraftNow () {
},
+ members () {
+ return membersModel;
+ },
};
}
property var room: makeRoom()
+ property var membersModel: ListModel {
+ ListElement { userId: '@foo:example.com' }
+ ListElement { userId: '@bar:example.com' }
+ property string filter
+ property var count: 2
+ function at(index) {
+ return get(index);
+ }
+ }
property var l10n: Helpers.fluentMock
property var matrixSdk: TestHelpers.MatrixSdkMock {}
property var kazvIOManager: TestHelpers.KazvIOManagerMock {}
+ property var sdkVars: ({})
Kazv.SendMessageBox {
id: sendMessageBox
+ y: 400
room: item.room
}
@@ -57,6 +71,7 @@
when: windowShown
function init() {
+ membersModel.count = 2;
findChild(sendMessageBox, 'draftMessage').text = '';
}
@@ -100,7 +115,7 @@
textArea.cursorPosition = 0;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com\nsome test');
- verify(textArea.cursorPosition === '@foo:example.com'.length);
+ verify(textArea.cursorPosition === '@foo:example.com\n'.length);
}
function test_cursorInMiddleNoSpacing() {
@@ -127,7 +142,7 @@
textArea.cursorPosition = 4;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some @foo:example.com\ntest');
- verify(textArea.cursorPosition === 'some @foo:example.com'.length);
+ verify(textArea.cursorPosition === 'some @foo:example.com\n'.length);
}
function test_cursorInMiddleSpacingAround() {
@@ -136,7 +151,58 @@
textArea.cursorPosition = 5;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some\n@foo:example.com\ntest');
- verify(textArea.cursorPosition === 'some\n@foo:example.com'.length);
+ verify(textArea.cursorPosition === 'some\n@foo:example.com\n'.length);
+ }
+
+ function test_filter() {
+ const data = [
+ ['some\n\ntest', 5, ''],
+ ['some\n\n@test\n', 11, '@test'],
+ ['some\n\n@test\n', 10, '@tes'],
+ ['some\n\n@test喵 ', 12, '@test喵'],
+ // separated by a whitespace
+ ['some\n\n@test\n', 12, ''],
+ ['some\n\n@test\t', 12, ''],
+ ['some\n\n@test ', 12, ''],
+ // multiple @s
+ ['@test@233', 9, '@233'],
+ ];
+ const textArea = findChild(sendMessageBox, 'draftMessage');
+ data.forEach(([text, pos, expected]) => {
+ textArea.text = text;
+ textArea.cursorPosition = pos;
+ compare(textArea.filter, expected);
+ });
+ }
+
+ function test_completion() {
+ const textArea = findChild(sendMessageBox, 'draftMessage');
+ textArea.text = '@ex';
+ textArea.cursorPosition = 3;
+ const completionPopup = findChild(sendMessageBox, 'completionPopup');
+ tryVerify(() => completionPopup.opened);
+ const listView = findChild(completionPopup, 'completionListView');
+ tryVerify(() => listView);
+ tryVerify(() => findChild(listView, 'completionItem0'));
+ tryVerify(() => findChild(listView, 'completionItem1'));
+ const first = findChild(listView, 'completionItem0');
+ const second = findChild(listView, 'completionItem1');
+ verify(findChild(first, 'userIdLabel').text === '@foo:example.com');
+ verify(findChild(second, 'userIdLabel').text === '@bar:example.com');
+ mouseClick(second);
+ tryVerify(() => !completionPopup.opened);
+ const expected = '@bar:example.com ';
+ tryCompare(textArea, 'text', expected);
+ compare(textArea.cursorPosition, expected.length);
+ }
+
+ function test_zeroCompletion() {
+ membersModel.count = 0;
+ const textArea = findChild(sendMessageBox, 'draftMessage');
+ textArea.text = '@ex';
+ textArea.cursorPosition = 3;
+ const completionPopup = findChild(sendMessageBox, 'completionPopup');
+ tryVerify(() => !completionPopup.opened);
}
}
}

File Metadata

Mime Type
text/plain
Expires
Sat, Sep 21, 3:30 AM (21 h, 43 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
17003
Default Alt Text
D88.1726914607.diff (14 KB)

Event Timeline