Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F116263
D88.1732954918.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D88.1732954918.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 30, 12:21 AM (13 h, 43 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41562
Default Alt Text
D88.1732954918.diff (14 KB)
Attached To
Mode
D88: Autocomplete mentions
Attached
Detach File
Event Timeline
Log In to Comment