Page MenuHomePhorge

No OneTemporary

Size
39 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sun, Jan 19, 1:21 PM (21 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55196
Default Alt Text
(39 KB)

Event Timeline