Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F140130
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
39 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/contents/ui/Completion.qml b/src/contents/ui/Completion.qml
new file mode 100644
index 0000000..6caf225
--- /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
index a0204a4..284f7cd 100644
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -1,224 +1,225 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
import 'matrix-helpers.js' as Helpers
Kazv.ScrollablePageAdapter {
id: roomPage
property string roomId: ''
property var room: sdkVars.roomList.room(roomId)
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomPage.room
}
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)
signal paginateBackRequested(string eventId)
title: roomNameProvider.name
property var isInvite: room.membership === MK.MatrixRoom.Invite
property var isJoin: room.membership === MK.MatrixRoom.Join
contextualActions: [
Kirigami.Action {
id: acceptInviteAction
icon.name: 'checkmark'
text: l10n.get('room-invite-accept-action')
visible: isInvite
onTriggered: joinRoomHandler.call()
},
Kirigami.Action {
id: rejectInviteAction
icon.name: 'im-ban-kick-user'
text: l10n.get('room-invite-reject-action')
visible: isInvite
onTriggered: leaveRoomHandler.call()
},
Kirigami.Action {
id: roomSettingsAction
icon.name: 'settings-configure'
text: l10n.get('room-settings-action')
onTriggered: activateRoomSettingsPage(room)
}
]
property var inviteOverlay: Kirigami.OverlaySheet {
id: inviteOverlay
title: l10n.get('room-invite-popup-title')
parent: roomPage.overlay
property var self: room.member(matrixSdk.userId)
property var inviteEvent: self.toEvent()
property var inviter: room.member(inviteEvent.sender)
property var inviterName: Helpers.userNameWithId(inviter, l10n)
ColumnLayout {
Label {
text: {
inviteOverlay.inviterName
? l10n.get('room-invite-popup-text-with-inviter', { inviterName: inviteOverlay.inviterName })
: l10n.get('room-invite-popup-text')
}
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Button {
action: acceptInviteAction
}
Button {
action: rejectInviteAction
}
}
}
}
Component.onCompleted: {
if (isInvite) {
inviteOverlay.open();
}
}
RoomTimelineView {
timeline: roomPage.roomTimeline
}
onRoomChanged: {
kazvIOManager.deleteModelIfEmpty(roomId)
uploadList.model = kazvIOManager.getUploadJobs(roomId)
}
footer: Item {
height: childrenRect.height
width: parent.width
ListView {
id: uploadList
width: parent.width
height: contentHeight
model: kazvIOManager.getUploadJobs(roomId)
delegate: Kazv.KazvIOMenu {
width: parent.width
jobId: roomId
isUpload: true
}
}
Kazv.TypingIndicator {
id: typingIndicator
typingUsers: roomPage.room.typingUsers()
width: parent.width
anchors.top: uploadList.bottom
}
Kazv.SendMessageBox {
id: sendMessageBox
objectName: 'sendMessageBox'
width: parent.width
room: roomPage.room
anchors.top: typingIndicator.bottom
enabled: isJoin
}
}
function setDraftRelation(relType, eventId) {
sendMessageBox.draftRelType = relType;
sendMessageBox.draftRelatedTo = eventId;
}
property var joinRoomHandler: Kazv.AsyncHandler {
trigger: () => matrixSdk.joinRoom(roomId, [])
onResolved: {
if (success) {
showPassiveNotification(l10n.get('join-room-page-success-prompt', { room: roomId }));
inviteOverlay.close();
} else {
showPassiveNotification(l10n.get('join-room-page-failed-prompt', { room: roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
property var leaveRoomHandler: Kazv.AsyncHandler {
trigger: () => room.leaveRoom()
onResolved: {
if (success) {
showPassiveNotification(l10n.get('leave-room-success-prompt', { room: roomId }));
inviteOverlay.close();
} else {
showPassiveNotification(l10n.get('leave-room-failed-prompt', { room: roomId, errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
onMentionUserRequested: (userId) => {
sendMessageBox.mentionUser(userId);
sendMessageBox.focusInput();
}
onReplaceDraftRequested: (newDraft) => {
sendMessageBox.replaceDraft(newDraft);
}
function getLastReceiptableEventId(timeline, timelineCount) {
for (let i = 0; i < timelineCount; ++i) {
const event = timeline.at(i);
if (!event.isLocalEcho && event.sender !== matrixSdk.userId) {
return event.eventId;
}
}
}
onLastReceiptableEventIdChanged: maybePostReadReceipt()
Window.onActiveChanged: maybePostReadReceipt()
onVisibleChanged: maybePostReadReceipt()
function maybePostReadReceipt() {
if (!Window.active || !roomPage.visible) {
return;
}
const eventId = roomPage.lastReceiptableEventId;
if (!eventId) {
return;
}
const event = roomPage.room.messageById(eventId);
const readByMe = Helpers.isEventReadBy(event, matrixSdk.userId);
if (!readByMe) {
roomPage.room.postReadReceipt(eventId);
}
}
onPaginateBackRequested: (eventId) => {
if (!paginationRequests[eventId]) {
console.debug('paginating back from', eventId);
paginationRequests[eventId] = room.paginateBackFrom(eventId);
paginationRequests[eventId].resolved.connect(() => {
console.debug('finished paginating back from', eventId);
delete paginationRequests[eventId];
});
} else {
console.debug('pagination request of', eventId, 'is already under way');
}
}
}
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index 7d00e2d..f0b640b 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,242 +1,293 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 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 QtQuick.Window 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
ColumnLayout {
id: sendMessageBox
property var room
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') {
return l10n.get('send-message-box-reply-to');
} else if (draftRelType === 'm.replace') {
return l10n.get('send-message-box-edit');
}
}
function getCancelRelationPrompt(draftRelType) {
if (draftRelType === 'm.in_reply_to') {
return l10n.get('send-message-box-remove-reply-to-action');
} else if (draftRelType === 'm.replace') {
return l10n.get('send-message-box-remove-replace-action');
}
}
function replaceDraft(newDraft) {
textArea.changeText(newDraft, /* inhibitTyping = */ true);
}
onRoomChanged: {
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
RowLayout {
Label {
Layout.fillWidth: true
text: getRelationPrompt(sendMessageBox.draftRelType)
}
ToolButton {
action: removeRelatedToAction
display: AbstractButton.IconOnly
}
}
Item {
Layout.minimumHeight: Math.min(replyToEventView.implicitHeight, Kirigami.Units.gridUnit * 5)
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
Layout.fillWidth: true
clip: true
Kazv.EventViewWrapper {
id: replyToEventView
anchors {
top: parent.top
left: parent.left
right: parent.right
}
event: room.messageById(sendMessageBox.draftRelatedTo)
compactMode: true
}
}
}
RowLayout {
TextArea {
id: textArea
objectName: 'draftMessage'
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: {
room.setLocalDraft(text);
if (!inhibitTyping) {
room.setTyping(true);
}
inhibitTyping = false;
}
function changeText(newText, inhibitTyping) {
textArea.inhibitTyping = inhibitTyping;
textArea.text = newText;
}
onVisibleChanged: {
if (!visible) {
room.updateLocalDraftNow();
}
}
+ 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
onActivated: {
sendAction.trigger()
}
}
DropArea {
anchors.fill: parent
onDropped: (drag) => {
if (drag.hasUrls) {
confirmUploadPopup.call(drag.urls);
}
}
}
}
ToolButton {
icon.name: "document-open-data"
onClicked: sendMediaFileAction.trigger()
}
ToolButton {
action: stickersAction
display: AbstractButton.IconOnly
}
ToolButton {
icon.name: "document-send"
onClicked: sendAction.trigger()
}
}
function mentionUser(userId) {
const prefix = textArea.text.slice(0, textArea.cursorPosition);
const suffix = textArea.text.slice(textArea.cursorPosition);
let insertion = userId;
// If we are not at the beginning and the prev char is not a space,
// insert a space there.
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() {
textArea.forceActiveFocus();
}
Kirigami.Action {
id: removeRelatedToAction
icon.name: 'window-close-symbolic'
text: getCancelRelationPrompt(sendMessageBox.draftRelType)
onTriggered: {
sendMessageBox.draftRelType = '';
sendMessageBox.draftRelatedTo = '';
}
}
Kirigami.Action {
id: sendAction
icon.name: "document-send"
text: l10n.get("send-message-box-send")
onTriggered: {
room.setTyping(false);
room.sendTextMessage(textArea.text, draftRelType, draftRelatedTo);
textArea.changeText("", true);
sendMessageBox.draftRelType = '';
sendMessageBox.draftRelatedTo = '';
}
}
Kirigami.Action {
id:sendMediaFileAction
icon.name: "mail-attachment-symbolic"
text: l10n.get("send-message-box-send-file")
onTriggered: {
fileDialog.open()
}
}
property var confirmUploadPopup: ConfirmUploadPopup {
id: confirmUploadPopup
parent: applicationWindow().overlay
onUploadRequested: (url) => {
sendMessageBox.uploadFile(url);
}
}
property var fileDialog: Kazv.FileDialogAdapter {
onAccepted: {
sendMessageBox.uploadFile(fileUrl);
}
}
function uploadFile(fileUrl) {
kazvIOManager.startNewUploadJob(
matrixSdk.serverUrl, fileUrl, matrixSdk.token,
room.roomId, sdkVars.roomList, room.encrypted
);
}
property var stickerPopup: Kirigami.OverlaySheet {
id: stickerPopup
parent: applicationWindow().overlay
title: l10n.get('send-message-box-stickers-popup-title')
Kazv.StickerPicker {
Layout.preferredWidth: Math.min(Kirigami.Units.gridUnit * 40, Window.width)
stickerPackList: matrixSdk.stickerPackList()
onSendMessageRequested: eventJson => {
console.log(JSON.stringify(eventJson));
room.sendMessage(eventJson, draftRelType, draftRelatedTo);
draftRelType = '';
draftRelatedTo = '';
stickerPopup.close();
}
}
}
Kirigami.Action {
id: stickersAction
icon.name: 'smiley'
text: l10n.get('send-message-box-stickers')
onTriggered: stickerPopup.open()
}
}
diff --git a/src/matrix-event-reader-list-model.cpp b/src/matrix-event-reader-list-model.cpp
index eebbcf8..f10341f 100644
--- a/src/matrix-event-reader-list-model.cpp
+++ b/src/matrix-event-reader-list-model.cpp
@@ -1,37 +1,38 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "matrix-event-reader-list-model.hpp"
#include "matrix-event-reader.hpp"
using namespace Kazv;
MatrixEventReaderListModel::MatrixEventReaderListModel(lager::reader<immer::flex_vector<std::tuple<Kazv::Event, Kazv::Timestamp>>> readers, QObject *parent)
: MatrixRoomMemberListModel(
readers.xform(containerMap(EventList{}, zug::map([](const auto &item) {
return std::get<0>(item);
}))),
+ QString(),
parent
)
, m_readers(readers)
{
}
MatrixEventReaderListModel::~MatrixEventReaderListModel() = default;
MatrixEventReader *MatrixEventReaderListModel::at(int index) const
{
auto reader = m_readers[index][lager::lenses::or_default].make();
return new MatrixEventReader(
reader.map([](const auto &r) {
return std::get<0>(r);
}),
reader.map([](const auto &r) {
return std::get<1>(r);
}));
}
diff --git a/src/matrix-room-member-list-model.cpp b/src/matrix-room-member-list-model.cpp
index 17ecd35..2f3ab83 100644
--- a/src/matrix-room-member-list-model.cpp
+++ b/src/matrix-room-member-list-model.cpp
@@ -1,68 +1,88 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <cmath>
#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();
connect(this, &MatrixRoomMemberListModel::countChanged,
this, &MatrixRoomMemberListModel::updateInternalCount);
}
MatrixRoomMemberListModel::~MatrixRoomMemberListModel() = default;
MatrixRoomMember *MatrixRoomMemberListModel::at(int index) const
{
return new MatrixRoomMember(m_members[index][lager::lenses::or_default]);
}
int MatrixRoomMemberListModel::rowCount(const QModelIndex &index) const
{
if (index.isValid()) {
return 0;
} else {
return count();
}
}
QVariant MatrixRoomMemberListModel::data(const QModelIndex &, int) const
{
return QVariant();
}
void MatrixRoomMemberListModel::updateInternalCount()
{
auto curCount = count();
auto oldCount = m_internalCount;
auto diff = std::abs(curCount - oldCount);
if (curCount > oldCount) {
beginInsertRows(QModelIndex(), 0, diff - 1);
m_internalCount = curCount;
endInsertRows();
} else if (curCount < oldCount) {
beginRemoveRows(QModelIndex(), 0, diff - 1);
m_internalCount = curCount;
endRemoveRows();
}
}
diff --git a/src/matrix-room-member-list-model.hpp b/src/matrix-room-member-list-model.hpp
index 3989915..869f437 100644
--- a/src/matrix-room-member-list-model.hpp
+++ b/src/matrix-room-member-list-model.hpp
@@ -1,42 +1,44 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <QAbstractListModel>
#include <lager/extra/qt.hpp>
-
+#include <lager/state.hpp>
#include <client/room/room.hpp>
class MatrixRoomMember;
class MatrixRoomMemberListModel : public QAbstractListModel
{
Q_OBJECT
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;
QVariant data(const QModelIndex &index, int role) const override;
Q_INVOKABLE MatrixRoomMember *at(int index) const;
private Q_SLOTS:
void updateInternalCount();
};
diff --git a/src/resources.qrc b/src/resources.qrc
index 196cce5..1be3ba5 100644
--- a/src/resources.qrc
+++ b/src/resources.qrc
@@ -1,91 +1,92 @@
<RCC>
<qresource prefix="/">
<file alias="main.qml">contents/ui/main.qml</file>
<file alias="PageManager.qml">contents/ui/PageManager.qml</file>
<file alias="LoginPage.qml">contents/ui/LoginPage.qml</file>
<file alias="MainPage.qml">contents/ui/MainPage.qml</file>
<file alias="TabView.qml">contents/ui/TabView.qml</file>
<file alias="Tab.qml">contents/ui/Tab.qml</file>
<file alias="ClosableScrollablePage.qml">contents/ui/ClosableScrollablePage.qml</file>
<file alias="SelfDestroyableOverlaySheet.qml">contents/ui/SelfDestroyableOverlaySheet.qml</file>
<file alias="RoomListView.qml">contents/ui/RoomListView.qml</file>
<file alias="RoomListViewItemDelegate.qml">contents/ui/RoomListViewItemDelegate.qml</file>
<file alias="RoomPage.qml">contents/ui/RoomPage.qml</file>
<file alias="RoomTimelineView.qml">contents/ui/RoomTimelineView.qml</file>
<file alias="SendMessageBox.qml">contents/ui/SendMessageBox.qml</file>
<file alias="EventView.qml">contents/ui/EventView.qml</file>
<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="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>
<file alias="ConfirmUploadPopup.qml">contents/ui/ConfirmUploadPopup.qml</file>
<file alias="StickerPicker.qml">contents/ui/StickerPicker.qml</file>
<file alias="AddStickerPopup.qml">contents/ui/AddStickerPopup.qml</file>
<file alias="ConfirmationOverlay.qml">contents/ui/ConfirmationOverlay.qml</file>
<file alias="event-types/Simple.qml">contents/ui/event-types/Simple.qml</file>
<file alias="event-types/Text.qml">contents/ui/event-types/Text.qml</file>
<file alias="event-types/Emote.qml">contents/ui/event-types/Emote.qml</file>
<file alias="event-types/Notice.qml">contents/ui/event-types/Notice.qml</file>
<file alias="event-types/State.qml">contents/ui/event-types/State.qml</file>
<file alias="event-types/TextTemplate.qml">contents/ui/event-types/TextTemplate.qml</file>
<file alias="event-types/Image.qml">contents/ui/event-types/Image.qml</file>
<file alias="event-types/File.qml">contents/ui/event-types/File.qml</file>
<file alias="event-types/Video.qml">contents/ui/event-types/Video.qml</file>
<file alias="event-types/Audio.qml">contents/ui/event-types/Audio.qml</file>
<file alias="event-types/MediaBubble.qml">contents/ui/event-types/MediaBubble.qml</file>
<file alias="event-types/Redacted.qml">contents/ui/event-types/Redacted.qml</file>
<file alias="event-types/Reaction.qml">contents/ui/event-types/Reaction.qml</file>
<file alias="event-types/Fallback.qml">contents/ui/event-types/Fallback.qml</file>
<file alias="TypingIndicator.qml">contents/ui/TypingIndicator.qml</file>
<file alias="EventReadIndicator.qml">contents/ui/EventReadIndicator.qml</file>
<file alias="SelectableText.qml">contents/ui/SelectableText.qml</file>
<file alias="ReactToEventPopup.qml">contents/ui/ReactToEventPopup.qml</file>
<file alias="FileHandler.qml">contents/ui/FileHandler.qml</file>
<file alias="ActionSettingsPage.qml">contents/ui/ActionSettingsPage.qml</file>
<file alias="CreateRoomPage.qml">contents/ui/CreateRoomPage.qml</file>
<file alias="JoinRoomPage.qml">contents/ui/JoinRoomPage.qml</file>
<file alias="RoomMemberListView.qml">contents/ui/RoomMemberListView.qml</file>
<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>
<file alias="SettingsPage.qml">contents/ui/SettingsPage.qml</file>
<file alias="settings/ProfileSettings.qml">contents/ui/settings/ProfileSettings.qml</file>
<file alias="settings/CacheSettings.qml">contents/ui/settings/CacheSettings.qml</file>
<file alias="room-settings/RoomSettingsPage.qml">contents/ui/room-settings/RoomSettingsPage.qml</file>
<file alias="room-settings/RoomTagHandler.qml">contents/ui/room-settings/RoomTagHandler.qml</file>
<file alias="room-settings/RoomMemberListPage.qml">contents/ui/room-settings/RoomMemberListPage.qml</file>
<file alias="room-settings/RoomInvitePage.qml">contents/ui/room-settings/RoomInvitePage.qml</file>
<file alias="device-mgmt/Device.qml">contents/ui/device-mgmt/Device.qml</file>
<file alias="device-mgmt/DeviceList.qml">contents/ui/device-mgmt/DeviceList.qml</file>
<file alias="UserPage.qml">contents/ui/UserPage.qml</file>
<file alias="shortcuts/ActionCollection.qml">contents/ui/shortcuts/ActionCollection.qml</file>
<file alias="shortcuts/ActionItem.qml">contents/ui/shortcuts/ActionItem.qml</file>
<file alias="shortcuts/ActionSettings.qml">contents/ui/shortcuts/ActionSettings.qml</file>
<file alias="shortcuts/ShortcutInput.qml">contents/ui/shortcuts/ShortcutInput.qml</file>
<file alias="MessageNotification.qml">contents/ui/MessageNotification.qml</file>
<file alias="Notifier.qml">contents/ui/Notifier.qml</file>
<file alias="NotificationAction.qml">contents/ui/NotificationAction.qml</file>
<file alias="AvatarAdapter.qml">contents/ui/AvatarAdapter.qml</file>
<file alias="FileDialogAdapter.qml">contents/ui/FileDialogAdapter.qml</file>
<file alias="FolderDialogAdapter.qml">contents/ui/FolderDialogAdapter.qml</file>
<file alias="ScrollablePageAdapter.qml">contents/ui/ScrollablePageAdapter.qml</file>
<file alias="l10n.js">js/l10n.js</file>
<file alias="fluent-bundle.js">js/transformed-libs/fluent-bundle.js</file>
<file alias="fluent-sequence.js">js/transformed-libs/fluent-sequence.js</file>
<file alias="fluent-langneg.js">js/transformed-libs/fluent-langneg.js</file>
<file alias="bundled-deps.js">js/transformed-libs/bundled-deps.js</file>
<file alias="global-this.js">js/global-this.js</file>
<file alias="matrix-helpers.js">js/matrix-helpers.js</file>
</qresource>
</RCC>
diff --git a/src/tests/matrix-room-member-list-model-test.cpp b/src/tests/matrix-room-member-list-model-test.cpp
index 2c0de2d..e6dc209 100644
--- a/src/tests/matrix-room-member-list-model-test.cpp
+++ b/src/tests/matrix-room-member-list-model-test.cpp
@@ -1,53 +1,78 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <lager/constant.hpp>
#include <matrix-room-member-list-model.hpp>
#include <matrix-room-member.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
using namespace Kazv;
class MatrixRoomMemberListModelTest : public QObject
{
Q_OBJECT
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"),
};
void MatrixRoomMemberListModelTest::testMembers()
{
MatrixRoomMemberListModel model(lager::make_constant(data));
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());
QCOMPARE(second->userId(), "@t2:tusooa.xyz");
}
+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
index 2145b0c..559c9e6 100644
--- a/src/tests/quick-tests/tst_SendMessageBox.qml
+++ b/src/tests/quick-tests/tst_SendMessageBox.qml
@@ -1,142 +1,208 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-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 QtTest 1.0
import org.kde.kirigami 2.13 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers.js' as Helpers
import 'test-helpers' as TestHelpers
Item {
id: item
width: 800
height: 600
function makeRoom () {
return {
timeline () {
return {
count: 0,
eventIds: [],
gaps: [],
at () { return {}; },
};
},
messageById () { return {}; },
member () { return {}; },
localDraft: '',
setLocalDraft (localDraft) {
this.localDraft = localDraft;
},
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
}
TestCase {
id: sendMessageBoxTest
name: 'SendMessageBoxTest'
when: windowShown
function init() {
+ membersModel.count = 2;
findChild(sendMessageBox, 'draftMessage').text = '';
}
function test_empty() {
const textArea = findChild(sendMessageBox, 'draftMessage');
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtEnd() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test';
textArea.cursorPosition = textArea.text.length;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some test @foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtEndSpacingBefore() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test\n';
textArea.cursorPosition = textArea.text.length;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some test\n@foo:example.com ');
verify(textArea.cursorPosition === textArea.text.length);
}
function test_cursorAtBeginning() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some test';
textArea.cursorPosition = 0;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === '@foo:example.com some test');
verify(textArea.cursorPosition === '@foo:example.com '.length);
}
function test_cursorAtBeginningSpacingAfter() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = '\nsome test';
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() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
textArea.cursorPosition = 1;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 's @foo:example.com ome\ntest');
verify(textArea.cursorPosition === 's @foo:example.com '.length);
}
function test_cursorInMiddleSpacingBefore() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
textArea.cursorPosition = 5;
sendMessageBox.mentionUser('@foo:example.com');
tryVerify(() => textArea.text === 'some\n@foo:example.com test');
verify(textArea.cursorPosition === 'some\n@foo:example.com '.length);
}
function test_cursorInMiddleSpacingAfter() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\ntest';
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() {
const textArea = findChild(sendMessageBox, 'draftMessage');
textArea.text = 'some\n\ntest';
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/x-diff
Expires
Sun, Jan 19, 1:21 PM (18 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55196
Default Alt Text
(39 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment