Page MenuHomePhorge

No OneTemporary

Size
71 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index b8a315a..f9c8138 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,383 +1,388 @@
/*
* 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);
}
}
}
Control {
visible: !!sendMessageBox.draftRelatedTo
Layout.fillWidth: true
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
}
contentItem: RowLayout {
spacing: 0
ColumnLayout {
Layout.fillWidth: true
Label {
id: replyLabel
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
text: getRelationPrompt(sendMessageBox.draftRelType)
}
Item {
id: replyItem
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
}
}
}
ToolButton {
Layout.alignment: Qt.AlignTop
action: removeRelatedToAction
display: AbstractButton.IconOnly
}
}
}
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
persistentSelection: true
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);
}
}
}
function handlePaste() {
if (MK.KazvClipboard.hasUrls) {
confirmUploadPopup.call(MK.KazvClipboard.urls());
} else if (MK.KazvClipboard.hasImage) {
confirmUploadPopup.call([ MK.KazvClipboard.saveClipboardImage() ]);
} else {
textArea.paste();
}
}
Keys.onPressed: (event) => {
if (event.matches(StandardKey.Paste)) {
textArea.handlePaste();
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
preventStealing: true
cursorShape: Qt.IBeamCursor
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton)
textAreaMenu.popup();
}
Menu {
id: textAreaMenu
MenuItem {
text: l10n.get("action-undo")
icon.name: "edit-undo"
onTriggered: textArea.undo()
enabled: textArea.canUndo
}
MenuItem {
text: l10n.get("action-redo")
icon.name: "edit-redo"
onTriggered: textArea.redo()
enabled: textArea.canRedo
}
MenuSeparator {}
MenuItem {
text: l10n.get("action-cut")
icon.name: "edit-cut"
onTriggered: textArea.cut()
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-copy")
icon.name: "edit-copy"
onTriggered: textArea.copy()
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-paste")
icon.name: "edit-paste"
onTriggered: textArea.handlePaste()
enabled: textArea.canPaste || MK.KazvClipboard.hasImage || MK.KazvClipboard.hasUrls
}
MenuItem {
text: l10n.get("action-delete")
icon.name: "edit-delete"
onTriggered: textArea.remove(textArea.selectionStart, textArea.selectionEnd)
enabled: !!textArea.selectedText
}
MenuItem {
text: l10n.get("action-select-all")
icon.name: "edit-select-all"
onTriggered: textArea.selectAll()
}
}
}
}
ToolButton {
action: sendMediaFileAction
+ objectName: 'sendMediaFileButton'
display: AbstractButton.IconOnly
}
ToolButton {
action: stickersAction
display: AbstractButton.IconOnly
}
ToolButton {
objectName: 'sendButton'
action: sendAction
display: AbstractButton.IconOnly
}
}
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 + 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 = '';
}
enabled: textArea.text !== ''
}
Kirigami.Action {
id: sendMediaFileAction
icon.name: "document-open-data"
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 {
+ objectName: 'fileDialog'
onAccepted: {
sendMessageBox.uploadFile(fileUrl);
}
}
function uploadFile(fileUrl) {
kazvIOManager.startNewUploadJob(
matrixSdk.serverUrl, fileUrl, matrixSdk.token,
- room.roomId, sdkVars.roomList, room.encrypted
+ room.roomId, sdkVars.roomList, room.encrypted,
+ draftRelType, draftRelatedTo
);
+ sendMessageBox.draftRelType = '';
+ sendMessageBox.draftRelatedTo = '';
}
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/kazv-io-job.cpp b/src/kazv-io-job.cpp
index a79da43..ee675ff 100644
--- a/src/kazv-io-job.cpp
+++ b/src/kazv-io-job.cpp
@@ -1,344 +1,353 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "kazv-log.hpp"
#include <QObject>
#include <QPointer>
#include <QSharedPointer>
#include <QSaveFile>
#include <QFile>
#include <QString>
#include <QMimeDatabase>
#include <QCryptographicHash>
#include <QLoggingCategory>
#include <KJob>
#include <KIO/TransferJob>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
using namespace Qt::Literals::StringLiterals;
struct KazvIOBaseJobPrivate
{
QPointer<KIO::TransferJob> job;
std::optional<KazvIOBaseJob::ErrorCode> result = std::nullopt;
std::optional<Kazv::AES256CTRDesc> aes = std::nullopt;
};
KazvIOBaseJob::KazvIOBaseJob(std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
: QObject(parent)
, m_d(new KazvIOBaseJobPrivate)
{
/**
* For unknown reasons, if a new AES256CTRDesc is not constructed here,
* it will cause the program to crash.
*/
if (aes != std::nullopt) {
m_d->aes = Kazv::AES256CTRDesc{aes.value().key(), aes.value().iv()};
}
// m_d->aes = aes;
connect(this, &KazvIOBaseJob::jobChanged, this, &KazvIOBaseJob::connectJob);
}
KazvIOBaseJob::~KazvIOBaseJob() = default;
float KazvIOBaseJob::progress() {
if (m_d->job.isNull()) {
return 0;
}
return static_cast<float>(m_d->job->percent()) / 100;
}
void KazvIOBaseJob::suspend()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->suspend();
}
void KazvIOBaseJob::resume()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->resume();
}
void KazvIOBaseJob::cancel()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
emitResult(UserCancel);
m_d->job->kill();
}
bool KazvIOBaseJob::isSuspended()
{
if (m_d->job.isNull()) {
return false;
}
return m_d->job->isSuspended();
}
bool KazvIOBaseJob::isResulted()
{
return m_d->result.has_value();
}
KazvIOBaseJob::ErrorCode KazvIOBaseJob::error()
{
if (m_d->result.has_value()) {
return m_d->result.value();
}
// Shouldn't call this function before result are emited
return NoError;
}
void KazvIOBaseJob::connectJob()
{
connect(m_d->job, &KJob::result, this, [this](KJob *job) {
if (job->error()) {
emitResult(KIOError);
}
});
connect(m_d->job, &KJob::percentChanged, this,
[this](KJob * /* job */, unsigned long /* percent */) { Q_EMIT progressChanged(); });
}
QPointer<KIO::TransferJob> KazvIOBaseJob::job()
{
return m_d->job;
}
void KazvIOBaseJob::setJob(QPointer<KIO::TransferJob> job)
{
m_d->job = job;
Q_EMIT jobChanged();
}
void KazvIOBaseJob::emitResult(ErrorCode ec, QString data)
{
if (m_d->result.has_value()) {
return;
}
m_d->result = ec;
Q_EMIT result(m_d->result.value(), data);
}
std::optional<Kazv::AES256CTRDesc> KazvIOBaseJob::aes()
{
return m_d->aes;
}
void KazvIOBaseJob::setAes(Kazv::AES256CTRDesc aes)
{
m_d->aes = aes;
}
struct KazvIODownloadJobPrivate
{
QSharedPointer<KazvSaveFile> file;
QString hash;
};
KazvIODownloadJob::KazvIODownloadJob(const QString &hash, std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIODownloadJobPrivate)
{
m_d->hash = hash;
}
KazvIODownloadJob::KazvIODownloadJob(const QString &fileName, const QUrl &serverUrl,
const QString &hash, std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIODownloadJobPrivate)
{
if (setFile(fileName)) {
setJob(KIO::get(serverUrl));
}
m_d->hash = hash;
}
KazvIODownloadJob::~KazvIODownloadJob() = default;
QString KazvIODownloadJob::fileName() {
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIODownloadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this, &KazvIODownloadJob::writeFile);
connect(this->job(), &KJob::result, this, &KazvIODownloadJob::closeFile);
}
bool KazvIODownloadJob::setFile(QString fileName)
{
m_d->file.reset(new KazvSaveFile(fileName, this->aes()));
if (m_d->file->open(QIODevice::WriteOnly)) {
return true;
}
emitResult(KazvIOBaseJob::OpenFileError);
return false;
}
void KazvIODownloadJob::writeFile(KJob *job, const QByteArray &data)
{
if (m_d->file.isNull() || !m_d->file->isOpen()) {
emitResult(KazvError);
job->kill();
return;
}
auto len = m_d->file->write(data);
if (len == -1) {
KazvIOBaseJob::emitResult(WriteFileError);
job->kill();
}
}
void KazvIODownloadJob::closeFile(KJob *job)
{
if (m_d->file.isNull()) {
emitResult(KazvError);
return;
}
if (job->error()) {
m_d->file->cancelWriting();
return;
}
if (!m_d->hash.isEmpty() && QString::fromUtf8(m_d->file->hash()) != m_d->hash) {
emitResult(HashError);
return;
}
m_d->file->commit();
emitResult(NoError);
}
struct KazvIOUploadJobPrivate
{
QSharedPointer<KazvFile> file;
QString response;
QSharedPointer<MatrixRoom> room;
QString mimeType;
QString mxcUri;
+ QString relType;
+ QString relatedTo;
};
-KazvIOUploadJob::KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
+KazvIOUploadJob::KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes,
+ const QString &relType, const QString &relatedTo, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIOUploadJobPrivate)
{
+ m_d->relType = relType;
+ m_d->relatedTo = relatedTo;
}
KazvIOUploadJob::KazvIOUploadJob(const QString fileName, const QUrl serverUrl,
MatrixRoomList *roomList, const QString &roomId, const QString token,
- std::optional<Kazv::AES256CTRDesc> aes, QObject *parent)
+ std::optional<Kazv::AES256CTRDesc> aes,
+ const QString &relType, const QString &relatedTo, QObject *parent)
: KazvIOBaseJob(aes, parent)
, m_d(new KazvIOUploadJobPrivate)
{
+ m_d->relType = relType;
+ m_d->relatedTo = relatedTo;
if (setFile(fileName)) {
auto kazvUploadJob = Kazv::Api::UploadContentJob(serverUrl.toString().toStdString(),
token.toStdString(), Kazv::FileDesc(std::string()));
auto job = KIO::http_post(QUrl(QString::fromStdString(kazvUploadJob.url())), m_d->file.data());
job->addMetaData(u"customHTTPHeader"_s, QStringLiteral("Authorization: ")
.append(QString::fromStdString(kazvUploadJob.requestHeader()->at("Authorization"))));
job->addMetaData(u"PropagateHttpHeader"_s, u"true"_s);
setJob(job);
if (roomList) {
m_d->room.reset(roomList->room(roomId));
}
}
}
KazvIOUploadJob::~KazvIOUploadJob() = default;
QString KazvIOUploadJob::fileName()
{
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIOUploadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this,
[this](KJob * /* job */, const QByteArray &data) { m_d->response.append(QString::fromUtf8(data)); });
connect(this->job(), &KJob::result, this, &KazvIOUploadJob::handleResult);
connect(this->job(), &KIO::TransferJob::mimeTypeFound, this,
[this](KJob * /* job */, const QString &mimeType) { m_d->mimeType = mimeType; });
}
void KazvIOUploadJob::handleResult(KJob *job)
{
if (m_d->file.isNull()) {
emitResult(KazvError);
return;
}
m_d->file->close();
if (job->error()) {
return;
}
nlohmann::json j;
try {
j = nlohmann::json::parse(m_d->response.toStdString());
} catch(...) {
emitResult(ResponseError);
qCWarning(kazvLog) << "Get a invaild response from server.";
return;
}
QString mxcUri{};
if (j.contains("content_uri") && j["content_uri"].is_string()) {
mxcUri = QString::fromStdString(j["content_uri"].template get<std::string>());
} else {
emitResult(ResponseError);
qCWarning(kazvLog) << "There is no \"content_uri\" field in response.";
return;
}
auto shouldSendMessage = !!m_d->room;
if (shouldSendMessage) {
auto aes = this->aes();
if (aes.has_value()) {
m_d->room->sendEncryptedFileMessage(QUrl::fromLocalFile(m_d->file->fileName()).fileName(),
QMimeDatabase().mimeTypeForFile(m_d->file.data()->fileName()).name(),
m_d->file->size(), mxcUri,
QString::fromStdString(aes.value().key()), QString::fromStdString(aes.value().iv()),
- m_d->file->hash());
+ m_d->file->hash(), m_d->relType, m_d->relatedTo);
} else {
m_d->room->sendMediaFileMessage(QUrl::fromLocalFile(m_d->file->fileName()).fileName(),
- QMimeDatabase().mimeTypeForData(m_d->file.data()).name(), m_d->file->size(), mxcUri);
+ QMimeDatabase().mimeTypeForData(m_d->file.data()).name(),
+ m_d->file->size(), mxcUri, m_d->relType, m_d->relatedTo);
}
}
KazvIOBaseJob::emitResult(NoError, mxcUri);
}
bool KazvIOUploadJob::setFile(const QString fileName)
{
m_d->file.reset(new KazvFile(fileName, this->aes()));
if (m_d->file->open(QIODevice::ReadOnly)) {
return true;
}
emitResult(KazvIOBaseJob::OpenFileError);
return false;
}
diff --git a/src/kazv-io-job.hpp b/src/kazv-io-job.hpp
index 568b82d..bb42234 100644
--- a/src/kazv-io-job.hpp
+++ b/src/kazv-io-job.hpp
@@ -1,160 +1,164 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "matrix-room.hpp"
#include "matrix-room-list.hpp"
#include "kazv-file.hpp"
#include <aes-256-ctr.hpp>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QUrl>
#include <KIO/TransferJob>
#include <memory>
#include <optional>
struct KazvIOBaseJobPrivate;
class KazvIOBaseJob : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Q_PROPERTY(float progress READ progress NOTIFY progressChanged)
std::unique_ptr<KazvIOBaseJobPrivate> m_d;
public:
enum ErrorCode {
/**
* No Error or the signal result has not been emitted yet.
*/
NoError = 0,
/**
* The user actively canceled the job.
*/
UserCancel,
/**
* Open File Error.
*/
OpenFileError,
/**
* Write File Error
*/
WriteFileError,
/**
* Errors caused by KIO, such network error.
*/
KIOError,
/**
* Error caused by hash error
*/
HashError,
/**
* Error caused by invalid response.
* KIO reported no error, but got an invalid JSON response.
*/
ResponseError,
/**
* Errors caused by bad code, means the bug is somewhere.
*/
KazvError
};
Q_ENUM(ErrorCode)
KazvIOBaseJob(std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
~KazvIOBaseJob();
float progress();
Q_INVOKABLE void suspend();
Q_INVOKABLE void resume();
Q_INVOKABLE void cancel();
Q_INVOKABLE bool isSuspended();
Q_INVOKABLE bool isResulted();
/**
* Only use it after emitted the result signal
*/
Q_INVOKABLE ErrorCode error();
Q_SIGNALS:
void jobChanged();
void progressChanged();
/**
* @param data the mxcUri if this is an upload job.
*/
void result(ErrorCode ec, QString data);
protected Q_SLOTS:
virtual void connectJob();
protected:
QPointer<KIO::TransferJob> job();
void setJob(QPointer<KIO::TransferJob> job);
std::optional<Kazv::AES256CTRDesc> aes();
void setAes(Kazv::AES256CTRDesc);
void emitResult(ErrorCode ec, QString data = QString());
};
struct KazvIODownloadJobPrivate;
class KazvIODownloadJob : public KazvIOBaseJob
{
Q_OBJECT
std::unique_ptr<KazvIODownloadJobPrivate> m_d;
bool setFile(QString fileName);
public:
KazvIODownloadJob(const QString &hash, std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
KazvIODownloadJob(const QString &fileName, const QUrl &serverUrl, const QString &hash,
std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
~KazvIODownloadJob();
Q_INVOKABLE QString fileName();
private Q_SLOTS:
void writeFile(KJob *job, const QByteArray &data);
void closeFile(KJob *job);
protected Q_SLOTS:
void connectJob() override;
};
struct KazvIOUploadJobPrivate;
class KazvIOUploadJob : public KazvIOBaseJob
{
Q_OBJECT
std::unique_ptr<KazvIOUploadJobPrivate> m_d;
bool setFile(const QString fileName);
public:
- KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
+ KazvIOUploadJob(std::optional<Kazv::AES256CTRDesc> aes = std::nullopt,
+ const QString &relType = QStringLiteral(""),
+ const QString &relatedTo = QStringLiteral(""), QObject *parent = 0);
KazvIOUploadJob(const QString fileName, const QUrl serverUrl, MatrixRoomList *roomList,
const QString &roomId, const QString token,
- std::optional<Kazv::AES256CTRDesc> aes = std::nullopt, QObject *parent = 0);
+ std::optional<Kazv::AES256CTRDesc> aes = std::nullopt,
+ const QString &relType = QStringLiteral(""),
+ const QString &relatedTo = QStringLiteral(""), QObject *parent = 0);
~KazvIOUploadJob();
Q_INVOKABLE QString fileName();
private Q_SLOTS:
void handleResult(KJob *job);
protected Q_SLOTS:
void connectJob() override;
};
diff --git a/src/kazv-io-manager.cpp b/src/kazv-io-manager.cpp
index 3b2eefc..a6c25ce 100644
--- a/src/kazv-io-manager.cpp
+++ b/src/kazv-io-manager.cpp
@@ -1,211 +1,215 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-manager.hpp"
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "qt-rand-adapter.hpp"
#include <api/csapi/content-repo.hpp>
#include <file-desc.hpp>
#include <QObject>
#include <QIODevice>
#include <QFile>
#include <QSaveFile>
#include <QPointer>
#include <QSharedPointer>
#include <QUrl>
#include <QMap>
#include <QMultiMap>
#include <QTemporaryDir>
#include <string>
#include <algorithm>
using namespace Qt::Literals::StringLiterals;
static const QString ROOMLESS = QStringLiteral("not-a-room");
struct KazvIOManagerPrivate
{
/**
* Used to store various temporary files.
* If the user does not specify a cache directory,
* this directory is also used as a cache directory.
*/
QTemporaryDir tmpDir = QTemporaryDir{};
/**
* Stores files that need to be previewed (e.g. in a timeline)
* and are not downloaded by the user, such as images, audio, video covers, and videos.
*/
QDir cacheDir{tmpDir.path()};
Kazv::RandomInterface randomGenerator = QtRandAdapter{};
QMap<QString, QSharedPointer<KazvIODownloadJob>> cacheJobs;
QMap<QString, QSharedPointer<KazvIODownloadJob>> downloadJobs;
QMap<QString, QSharedPointer<UploadJobModel>> uploadJobs;
};
KazvIOManager::KazvIOManager(QObject *parent)
: QObject(parent)
, m_d(new KazvIOManagerPrivate)
{
}
KazvIOManager::~KazvIOManager() = default;
QPointer<KazvIODownloadJob> KazvIOManager::makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl, const QString &hash,
const QString &key, const QString &iv) const
{
QPointer<KazvIODownloadJob> job{};
if (key.isEmpty()) {
job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl, hash, std::nullopt};
} else {
auto aes = Kazv::AES256CTRDesc{key.toStdString(), iv.toStdString()};
job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl, hash, aes};
}
return job;
}
QUrl KazvIOManager::cacheFile(const QUrl &fileUrl, const QString &id, const QString &hash,
const QString &key, const QString &iv)
{
const QString filePath = m_d->cacheDir.filePath(id);
QDir dir(m_d->cacheDir.path());
if (!dir.exists(id) && this->getCacheJob(id) == nullptr) {
auto job = this->makeDownloadJob(fileUrl, QUrl::fromLocalFile(filePath), hash, key, iv);
m_d->cacheJobs[id].reset(job);
connect(job, &KazvIOBaseJob::result, this, [this, id](auto /* result */) {
m_d->cacheJobs.remove(id);
});
}
return QUrl::fromLocalFile(filePath);
}
KazvIOBaseJob *KazvIOManager::startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileUrl,
const QString &jobId, const QString &hash, const QString &key, const QString &iv)
{
auto downloadJob = this->makeDownloadJob(serverUrl, localFileUrl, hash, key, iv);
m_d->downloadJobs[jobId].reset(downloadJob);
QQmlEngine::setObjectOwnership(downloadJob.data(), QQmlEngine::CppOwnership);
return downloadJob.data();
}
-KazvIOBaseJob *KazvIOManager::startNewUploadJob(const QUrl &serverUrl, const QUrl &localFileUrl,
- const QString &token, const QString &roomId, MatrixRoomList *roomList, const bool encrypted)
+KazvIOBaseJob *KazvIOManager::startNewUploadJob(
+ const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token,
+ const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
+ const QString &relType, const QString &relatedTo)
{
auto uploadJob = QPointer<KazvIOUploadJob>();
if (encrypted) {
auto aes = Kazv::AES256CTRDesc::fromRandom(
m_d->randomGenerator.generateRange<Kazv::RandomData>(Kazv::AES256CTRDesc::randomSize));
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
- roomList, roomId, token, aes);
+ roomList, roomId, token, aes, relType, relatedTo);
} else {
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
- roomList, roomId, token, std::nullopt);
+ roomList, roomId, token, std::nullopt, relType, relatedTo);
}
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId].reset(new UploadJobModel());
}
m_d->uploadJobs[roomId]->addJob(uploadJob);
QQmlEngine::setObjectOwnership(uploadJob.data(), QQmlEngine::CppOwnership);
return uploadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token)
{
- return startNewUploadJob(serverUrl, localFileUrl, token, ROOMLESS, 0, false);
+ return startNewUploadJob(
+ serverUrl, localFileUrl, token, ROOMLESS, 0, false,
+ QStringLiteral(""), QStringLiteral(""));
}
KazvIOBaseJob *KazvIOManager::getCacheJob(const QString &jobId) const
{
auto targetJob = m_d->cacheJobs.find(jobId);
if (targetJob == m_d->cacheJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
KazvIOBaseJob *KazvIOManager::getDownloadJob(const QString &jobId)
{
auto targetJob = m_d->downloadJobs.find(jobId);
if (targetJob == m_d->downloadJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
void KazvIOManager::deleteDownloadJob(const QString &jobId)
{
m_d->downloadJobs.remove(jobId);
}
UploadJobModel *KazvIOManager::getUploadJobs(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId].reset(new UploadJobModel());
}
QQmlEngine::setObjectOwnership(m_d->uploadJobs[roomId].data(), QQmlEngine::CppOwnership);
return m_d->uploadJobs[roomId].data();
}
void KazvIOManager::deleteUploadJob(const QString &roomId, KazvIOBaseJob *job)
{
m_d->uploadJobs[roomId]->removeJob(QPointer<KazvIOUploadJob>(qobject_cast<KazvIOUploadJob*>(job)));
}
void KazvIOManager::deleteRoomlessUploadJob(KazvIOBaseJob *job)
{
deleteUploadJob(ROOMLESS, job);
}
void KazvIOManager::deleteModelIfEmpty(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId)) {
return;
}
if (m_d->uploadJobs[roomId]->rowCount() == 0) {
m_d->uploadJobs.remove(roomId);
}
}
void KazvIOManager::clearJobs()
{
std::for_each(m_d->downloadJobs.begin(), m_d->downloadJobs.end(), [this](auto job){
job->cancel();
});
std::for_each(m_d->uploadJobs.begin(), m_d->uploadJobs.end(), [this](auto room) {
room->clearJobs();
});
m_d->downloadJobs.clear();
m_d->uploadJobs.clear();
}
QString KazvIOManager::cacheDirectory() const
{
return m_d->cacheDir.path();
}
void KazvIOManager::setCacheDirectory(QString cacheDirectory)
{
if (cacheDirectory.isEmpty()) {
m_d->cacheDir = QDir{m_d->tmpDir.path()};
} else {
auto cacheDir = QDir{QUrl{cacheDirectory}.toLocalFile()};
if (!cacheDir.mkpath(u"."_s)) {
return;
}
m_d->cacheDir = cacheDir;
}
m_d->downloadJobs.clear();
Q_EMIT cacheDirectoryChanged();
}
diff --git a/src/kazv-io-manager.hpp b/src/kazv-io-manager.hpp
index 217048c..f0e6c4f 100644
--- a/src/kazv-io-manager.hpp
+++ b/src/kazv-io-manager.hpp
@@ -1,71 +1,73 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "matrix-room-list.hpp"
#include "matrix-sdk.hpp"
#include <QObject>
#include <QtQml>
#include <QUrl>
#include <QString>
#include <KIO/TransferJob>
#include <QPointer>
#include <memory>
struct KazvIOManagerPrivate;
class KazvIOManager : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<KazvIOManagerPrivate> m_d;
Q_PROPERTY(QString cacheDirectory READ cacheDirectory WRITE setCacheDirectory NOTIFY cacheDirectoryChanged)
QPointer<KazvIODownloadJob> makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl, const QString &hash,
const QString &key = QStringLiteral(""),
const QString &iv = QStringLiteral("")) const;
public:
explicit KazvIOManager(QObject *parent = 0);
~KazvIOManager() override;
/**
* Cache the file specified by fileUrl into a temporary folder
* Multiple calls do not repeat downloads
* Returns the local url of the cached file.
*/
Q_INVOKABLE QUrl cacheFile(const QUrl &fileUrl, const QString &id, const QString &hash = QStringLiteral(""),
const QString &key = QStringLiteral(""), const QString &iv = QStringLiteral(""));
Q_INVOKABLE KazvIOBaseJob *startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileName,
const QString &jobId, const QString &hash,
const QString &key = QStringLiteral(""), const QString &iv = QStringLiteral(""));
- Q_INVOKABLE KazvIOBaseJob *startNewUploadJob(const QUrl &serverUrl, const QUrl &localFileName,
- const QString &token, const QString &roomId, MatrixRoomList *roomList, const bool encrypted);
+ Q_INVOKABLE KazvIOBaseJob *startNewUploadJob(
+ const QUrl &serverUrl, const QUrl &localFileName, const QString &token,
+ const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
+ const QString &relType, const QString &relatedTo);
Q_INVOKABLE KazvIOBaseJob *startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileName, const QString &token);
Q_INVOKABLE KazvIOBaseJob *getCacheJob(const QString &jobId) const;
Q_INVOKABLE KazvIOBaseJob *getDownloadJob(const QString &jobId);
Q_INVOKABLE void deleteDownloadJob(const QString &jobId);
Q_INVOKABLE UploadJobModel *getUploadJobs(const QString &roomId);
Q_INVOKABLE void deleteUploadJob(const QString &roomId, KazvIOBaseJob *job);
Q_INVOKABLE void deleteRoomlessUploadJob(KazvIOBaseJob *job);
Q_INVOKABLE void deleteModelIfEmpty(const QString &roomId);
Q_INVOKABLE void clearJobs();
QString cacheDirectory() const;
void setCacheDirectory(QString cacheDirectory);
Q_SIGNALS:
void cacheDirectoryChanged();
};
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
index 973e4d6..daca168 100644
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -1,551 +1,564 @@
/*
* 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 <lager/setter.hpp>
#include <nlohmann/json.hpp>
#include <event.hpp>
#include <QFile>
#include <QMimeDatabase>
#include "kazv-log.hpp"
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
#include "matrix-room-pinned-events-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-room-member-list-model.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "kazv-markdown.hpp"
#include "qt-json.hpp"
#include "qfunctionutils.hpp"
#include "helper.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const int typingDebounceMs = 500;
static const int saveDraftDebounceMs = 5000;
static const int typingTimeoutMs = 1000;
MatrixRoom::MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, lager::reader<Kazv::Event> userGivenNicknameEvent, QObject *parent)
: QObject(parent)
, m_room(room)
, m_userGivenNicknameEvent(userGivenNicknameEvent)
, m_selfUserId(selfUserId)
, m_memberNames(m_room.members())
, m_powerLevels(m_room.powerLevels())
, LAGER_QT(roomId)(m_room.roomId().xform(strToQt))
, LAGER_QT(name)(m_room.nameOpt()[lager::lenses::or_default].xform(strToQt))
, LAGER_QT(topic)(m_room.topic().xform(strToQt))
, LAGER_QT(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
, LAGER_QT(heroEvents)(m_room.heroMemberEvents()
.map([](const auto &events) {
auto res = QList<QVariant>{};
res.reserve(events.size());
std::transform(events.begin(), events.end(),
std::back_inserter(res),
[](const auto &event) -> QVariant {
return event.originalJson().get().template get<QJsonObject>();
});
return QVariant(res);
}))
, LAGER_QT(avatarMxcUri)(m_room.avatarMxcUri().xform(strToQt))
, LAGER_QT(roomOrHeroAvatarMxcUri)(lager::with(m_room.heroMemberEvents(), m_room.avatarMxcUri())
.map([](const auto &heroes, const auto &roomAvatar) {
if (!roomAvatar.empty()) {
return roomAvatar;
}
if (heroes.size() == 1) {
return zug::reduce(roomMemberToAvatar(zug::last), std::string(), heroes);
}
return std::string();
})
.xform(strToQt))
, LAGER_QT(localDraft)(m_room.localDraft().xform(strToQt))
, LAGER_QT(encrypted)(m_room.encrypted())
, LAGER_QT(memberNames)(m_memberNames.xform(strListToQt))
, LAGER_QT(tagIds)(m_room.tags().map([](const auto &tagsMap) {
return zug::into(
QStringList(),
zug::map([](const auto pair) {
return QString::fromStdString(pair.first);
}),
tagsMap);
}))
, LAGER_QT(membership)(m_room.membership().map([](const auto &membership) { return static_cast<Membership>(membership); }))
, LAGER_QT(unreadNotificationCount)(m_room.unreadNotificationEventIds().map([](const auto &cont) -> int { return cont.size(); }))
, m_setTypingThrottled(QFunctionUtils::Throttle([self=QPointer<MatrixRoom>(this)]() {
if (self) {
self->setTypingImpl();
}
}, typingDebounceMs))
, m_updateLocalDraftDebounced(QFunctionUtils::Debounce([self=QPointer<MatrixRoom>(this)]() {
if (self) {
self->updateLocalDraftNow();
}
}, saveDraftDebounceMs))
, m_internalLocalDraft(std::nullopt)
{
lager::watch(m_powerLevels, [this](auto &&) {
powerLevelsChanged();
});
}
MatrixRoom::~MatrixRoom() {
updateLocalDraftNow();
}
MatrixRoomTimeline *MatrixRoom::timeline() const
{
return new MatrixRoomTimeline(m_room);
}
MatrixRoomPinnedEventsTimeline *MatrixRoom::pinnedEventsTimeline() const
{
return new MatrixRoomPinnedEventsTimeline(m_room);
}
MatrixPromise *MatrixRoom::pinEvents(const QStringList &eventIds) const
{
return new MatrixPromise(m_room.pinEvents(intoImmer(
immer::flex_vector<std::string>(),
qStringToStd,
eventIds)));
}
MatrixPromise *MatrixRoom::unpinEvents(const QStringList &eventIds) const
{
return new MatrixPromise(m_room.unpinEvents(intoImmer(
immer::flex_vector<std::string>(),
qStringToStd,
eventIds)));
}
static void maybeAddRelations(nlohmann::json &msg, const QString &relType, const QString &relatedTo)
{
if (relatedTo.isEmpty()) {
return;
}
if (relType == u"m.in_reply_to"_s) {
msg["content"]["m.relates_to"] = nlohmann::json{
{"m.in_reply_to", {
{"event_id", relatedTo},
}},
};
} else {
msg["content"]["m.relates_to"] = nlohmann::json{
{"rel_type", relType},
{"event_id", relatedTo},
};
}
}
static const std::string HTML_FORMAT = "org.matrix.custom.html";
nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Event replyToEvent)
{
auto msg = nlohmann::json{
{"type", "m.room.message"},
{"content", {
{"msgtype", "m.text"},
{"body", text},
}},
};
std::string replyToBody;
if (relType == u"m.in_reply_to"_s && !relatedTo.isEmpty()) {
auto replyToContent = replyToEvent.content().get();
if (replyToContent.contains("format")
&& replyToContent["format"] == HTML_FORMAT
&& replyToContent.contains("formatted_body")
&& replyToContent["formatted_body"].is_string()) {
replyToBody = replyToContent["formatted_body"].template get<std::string>();
auto replyPos = replyToBody.find("</mx-reply>");
if (replyPos != std::string::npos) {
replyToBody.erase(0, replyPos + std::string("</mx-reply>").size());
}
} else if (replyToContent.contains("body") && replyToContent["body"].is_string()) {
replyToBody = markdownToHtml(replyToContent["body"].template get<std::string>()).html;
}
if (!replyToBody.empty()) {
replyToBody = "<mx-reply><blockquote>" + replyToBody + "</blockquote></mx-reply>";
}
}
auto richText = markdownToHtml(text.toStdString());
msg["content"]["format"] = HTML_FORMAT;
msg["content"]["formatted_body"] = replyToBody + richText.html;
auto mentions = richText.mentions;
if (!replyToEvent.sender().empty()
&& std::find(mentions.begin(), mentions.end(), replyToEvent.sender()) == mentions.end()) {
mentions = std::move(mentions).push_back(replyToEvent.sender());
}
if (!mentions.empty()) {
msg["content"]["m.mentions"] = {
{"user_ids", mentions},
};
}
if (relType == u"m.replace"_s && !relatedTo.isEmpty()) {
msg["content"]["m.new_content"] = msg["content"];
}
maybeAddRelations(msg, relType, relatedTo);
return msg;
}
void MatrixRoom::sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const
{
auto msg = nlohmann::json(eventJson);
maybeAddRelations(msg, relType, relatedTo);
m_room.sendMessage(Event(msg));
}
void MatrixRoom::sendTextMessage(QString text, const QString &relType, QString relatedTo) const
{
Event replyToEvent = (relType == u"m.in_reply_to"_s && !relatedTo.isEmpty())
? m_room.message(lager::make_constant(relatedTo.toStdString())).make().get()
: Event();
auto j = makeTextMessageJson(text, relType, relatedTo, replyToEvent);
m_room.sendMessage(Event(j));
}
-void MatrixRoom::sendMediaFileMessage(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) const
+void MatrixRoom::sendMediaFileMessage(
+ QString fileName, QString mimeType, qint64 fileSize, QString mxcUri,
+ const QString &relType, const QString &relatedTo) const
{
- auto j = makeMediaFileMessageJson(fileName, mimeType, fileSize, mxcUri);
+ auto j = makeMediaFileMessageJson(
+ fileName, mimeType, fileSize, mxcUri, relType, relatedTo);
Kazv::Event e{j};
m_room.sendMessage(e);
}
void MatrixRoom::sendEncryptedFileMessage(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
- const QString &key, const QString &iv, const QByteArray &hash) const
+ const QString &key, const QString &iv, const QByteArray &hash,
+ const QString &relType, const QString &relatedTo) const
{
- auto j = makeEncryptedFileMessageJson(fileName, mimeType, fileSize, mxcUri, key, iv, hash);
+ auto j = makeEncryptedFileMessageJson(
+ fileName, mimeType, fileSize, mxcUri,
+ key, iv, hash, relType, relatedTo);
Kazv::Event e{j};
m_room.sendMessage(e);
}
static nlohmann::json makeReactionJson(QString text, QString relatedTo)
{
auto msg = nlohmann::json{
{"type", "m.reaction"},
{"content", {
{"m.relates_to", {
{"rel_type", "m.annotation"},
{"event_id", relatedTo.toStdString()},
{"key", text.toStdString()},
}},
}},
};
return msg;
}
void MatrixRoom::sendReaction(QString text, QString relatedTo) const
{
auto j = makeReactionJson(text, relatedTo);
m_room.sendMessage(Event(j));
}
void MatrixRoom::resendMessage(QString txnId) const
{
m_room.resendMessage(txnId.toStdString());
}
MatrixPromise *MatrixRoom::sendStateEvent(const QJsonObject &eventJson) const
{
return new MatrixPromise(m_room.sendStateEvent(Event(json(eventJson))));
}
MatrixPromise *MatrixRoom::setSelfName(const QString &name) const
{
auto eventJson = m_room.state({"m.room.member", m_selfUserId.get()}).make().get().originalJson().get();
eventJson.merge_patch(json{
{"content", {
{"displayname", name.toStdString()},
}},
});
return new MatrixPromise(m_room.sendStateEvent(Event(json(eventJson))));
}
-nlohmann::json MatrixRoom::makeMediaFileMessageJson(QString fileName, QString mimeType, qint64 fileSize, QString mxcUri) const
+nlohmann::json MatrixRoom::makeMediaFileMessageJson(
+ QString fileName, QString mimeType, qint64 fileSize, QString mxcUri,
+ const QString &relType, const QString &relatedTo) const
{
static auto available_msgtype = std::array<std::string, 3>{"m.audio", "m.video", "m.image"};
auto try_msgtype = std::find(available_msgtype.begin(), available_msgtype.end(),
u"m."_s.append(mimeType.split(u'/')[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
- return nlohmann::json {
+ auto msg = nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"url", mxcUri.toStdString()},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
+ maybeAddRelations(msg, relType, relatedTo);
+ return msg;
}
nlohmann::json MatrixRoom::makeEncryptedFileMessageJson(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
- const QString &key, const QString &iv, const QByteArray &hash) const
+ const QString &key, const QString &iv, const QByteArray &hash,
+ const QString &relType, const QString &relatedTo) const
{
static auto available_msgtype = std::array<std::string, 3>{"m.audio", "m.video", "m.image"};
auto try_msgtype = std::find(available_msgtype.begin(), available_msgtype.end(),
u"m."_s.append(mimeType.split(u'/')[0]).toStdString());
std::string msgtype;
if (try_msgtype == available_msgtype.end()) {
msgtype = "m.file";
} else {
msgtype = *try_msgtype;
}
- return nlohmann::json {
+ auto msg = nlohmann::json {
{"type", "m.room.message"},
{"content", {
{"msgtype", msgtype},
{"body", fileName.toStdString()},
{"file", {
{"url", mxcUri.toStdString()},
{"key", {
{"kty", "oct"},
{"key_ops", nlohmann::json::array({"encrypt", "decrypt"})},
{"alg", "A256CTR"},
{"k", key.toStdString()},
{"ext", true}
}},
{"iv", iv.toStdString()},
{"hashes", {
{"sha256", hash.toStdString()}
}},
{"v", "v2"}
}},
{"info", {
{"size", fileSize},
{"mimetype", mimeType.toStdString()}
}}
}}
};
+ maybeAddRelations(msg, relType, relatedTo);
+ return msg;
}
MatrixPromise *MatrixRoom::redactEvent(QString eventId, QString reason) const
{
qCInfo(kazvLog) << "redactEvent(" << eventId << ", " << reason << ")";
return new MatrixPromise(m_room.redactEvent(
eventId.toStdString(),
reason.isEmpty() ? std::nullopt : std::optional<std::string>(reason.toStdString())
));
}
MatrixPromise *MatrixRoom::removeLocalEcho(QString txnId) const
{
return new MatrixPromise(m_room.removeLocalEcho(txnId.toStdString()));
}
MatrixPromise *MatrixRoom::addOrSetTag(QString tagId) const
{
return new MatrixPromise(m_room.addOrSetTag(tagId.toStdString()));
}
MatrixPromise *MatrixRoom::removeTag(QString tagId) const
{
return new MatrixPromise(m_room.removeTag(tagId.toStdString()));
}
MatrixPromise *MatrixRoom::paginateBackFrom(QString eventId) const
{
return new MatrixPromise(m_room.paginateBackFromEvent(eventId.toStdString()));
}
MatrixPromise *MatrixRoom::leaveRoom() const
{
return new MatrixPromise(m_room.leave());
}
MatrixPromise *MatrixRoom::forgetRoom() const
{
return new MatrixPromise(m_room.forget());
}
MatrixRoomMember *MatrixRoom::memberAt(int index) const
{
return new MatrixRoomMember(m_room.memberEventByCursor(
m_memberNames[index][lager::lenses::or_default]));
}
MatrixRoomMember *MatrixRoom::member(QString userId) const
{
return new MatrixRoomMember(m_room.memberEventFor(userId.toStdString()));
}
MatrixEvent *MatrixRoom::messageById(QString eventId) const
{
return new MatrixEvent(
m_room.message(lager::make_constant(eventId.toStdString()))
.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
m_room
);
}
MatrixEvent *MatrixRoom::localEchoById(QString txnId) const
{
return new MatrixEvent(
m_room.localEcho(lager::make_constant(txnId.toStdString()))
.map([](const auto &e) -> std::variant<Kazv::Event, Kazv::LocalEchoDesc> { return e; }),
m_room
);
}
MatrixRoomMemberListModel *MatrixRoom::typingUsers() const
{
return new MatrixRoomMemberListModel(
m_room.typingMemberEvents()
.xform(containerMap(EventList{}, zug::filter([self=m_selfUserId.make().get()](const auto &e) {
return e.stateKey() != self;
})))
);
}
void MatrixRoom::setTyping(bool typing)
{
if (typing) {
m_setTypingThrottled();
} else {
m_room.setTyping(false, std::nullopt);
}
}
void MatrixRoom::setTypingImpl()
{
m_room.setTyping(true, typingTimeoutMs);
}
void MatrixRoom::setLocalDraft(QString localDraft)
{
// To avoid heavy computations when updating the local draft again
// and again, we only set our internal state. **Assume only
// one MatrixRoom can deal with the local draft** at the same time,
// it is safe to update it debounced and when we are destructed.
// See also the destructor.
m_internalLocalDraft = localDraft;
m_updateLocalDraftDebounced();
}
void MatrixRoom::updateLocalDraftNow()
{
if (m_internalLocalDraft.has_value()) {
m_room.setLocalDraft(m_internalLocalDraft.value().toStdString());
m_internalLocalDraft = std::nullopt;
}
}
MatrixRoomMemberListModel *MatrixRoom::members() const
{
return new MatrixRoomMemberListModel(
m_room.joinedMemberEvents(),
QString(),
m_userGivenNicknameEvent
);
}
MatrixRoomMemberListModel *MatrixRoom::bannedMembers() const
{
return new MatrixRoomMemberListModel(
m_room.bannedMemberEvents(),
QString(),
m_userGivenNicknameEvent
);
}
qint64 MatrixRoom::userPowerLevel(const QString &userId) const
{
auto e = m_powerLevels.get().normalizedEvent().content().get();
auto userIdStd = userId.toStdString();
return e.at("users").contains(userIdStd)
? e.at("users")[userIdStd].template get<Kazv::PowerLevel>()
: e.at("users_default").template get<Kazv::PowerLevel>();
}
MatrixPromise *MatrixRoom::setUserPowerLevel(const QString &userId, qint64 powerLevel) const
{
auto next = m_powerLevels.get().setUser(userId.toStdString(), powerLevel);
return new MatrixPromise(m_room.sendStateEvent(next.originalEvent()));
}
MatrixPromise *MatrixRoom::unsetUserPowerLevel(const QString &userId) const
{
auto next = m_powerLevels.get().setUser(userId.toStdString(), std::nullopt);
return new MatrixPromise(m_room.sendStateEvent(next.originalEvent()));
}
MatrixPromise *MatrixRoom::getStateEvent(const QString &type, const QString &stateKey) const
{
return new MatrixPromise(m_room.getStateEvent(
type.toStdString(), stateKey.toStdString()
));
}
MatrixPromise *MatrixRoom::ensureStateEvent(const QString &type, const QString &stateKey) const
{
if (m_room
.stateOpt({type.toStdString(), stateKey.toStdString()})
.make()
.get()
.has_value()) {
return MatrixPromise::createResolved(true, QJsonObject());
} else {
return getStateEvent(type, stateKey);
}
}
MatrixPromise *MatrixRoom::refreshState() const
{
return new MatrixPromise(m_room.refreshRoomState());
}
MatrixPromise *MatrixRoom::kickUser(const QString &userId, const QString &reason) const
{
return new MatrixPromise(m_room.kick(userId.toStdString(), reason.toStdString()));
}
MatrixPromise *MatrixRoom::banUser(const QString &userId, const QString &reason) const
{
return new MatrixPromise(m_room.ban(userId.toStdString(), reason.toStdString()));
}
MatrixPromise *MatrixRoom::unbanUser(const QString &userId) const
{
return new MatrixPromise(m_room.unban(userId.toStdString()));
}
MatrixPromise *MatrixRoom::inviteUser(const QString &userId) const
{
return new MatrixPromise(m_room.invite(userId.toStdString()));
}
MatrixPromise *MatrixRoom::postReadReceipt(const QString &eventId) const
{
return new MatrixPromise(m_room.postReceipt(eventId.toStdString()));
}
MatrixPromise *MatrixRoom::setTopic(const QString &newTopic) const
{
return new MatrixPromise(m_room.setTopic(newTopic.toStdString()));
}
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
index e328167..506ec55 100644
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -1,170 +1,174 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
#include <QTimer>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
Q_MOC_INCLUDE("matrix-room-timeline.hpp")
Q_MOC_INCLUDE("matrix-room-pinned-events-timeline.hpp")
class MatrixRoomTimeline;
class MatrixRoomPinnedEventsTimeline;
class MatrixRoomMember;
class MatrixPromise;
class MatrixRoomMemberListModel;
class MatrixEvent;
nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Kazv::Event replyToEvent);
class MatrixRoom : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Room m_room;
lager::reader<Kazv::Event> m_userGivenNicknameEvent;
lager::reader<std::string> m_selfUserId;
lager::reader<immer::flex_vector<std::string>> m_memberNames;
lager::reader<Kazv::PowerLevelsDesc> m_powerLevels;
public:
enum Membership {
Invite = Kazv::RoomMembership::Invite,
Join = Kazv::RoomMembership::Join,
Leave = Kazv::RoomMembership::Leave,
};
Q_ENUM(Membership);
explicit MatrixRoom(Kazv::Room room, lager::reader<std::string> selfUserId, lager::reader<Kazv::Event> userGivenNicknameEvent = lager::make_constant(Kazv::Event()), QObject *parent = 0);
~MatrixRoom() override;
LAGER_QT_READER(QString, roomId);
LAGER_QT_READER(QString, name);
LAGER_QT_READER(QString, topic);
LAGER_QT_READER(QStringList, heroNames);
LAGER_QT_READER(QVariant, heroEvents);
LAGER_QT_READER(QString, avatarMxcUri);
LAGER_QT_READER(QString, roomOrHeroAvatarMxcUri);
LAGER_QT_READER(QString, localDraft);
LAGER_QT_READER(bool, encrypted);
LAGER_QT_READER(QStringList, memberNames);
LAGER_QT_READER(QStringList, tagIds);
LAGER_QT_READER(Membership, membership);
LAGER_QT_READER(int, unreadNotificationCount);
Q_INVOKABLE MatrixRoomMember *memberAt(int index) const;
Q_INVOKABLE MatrixRoomMember *member(QString userId) const;
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
Q_INVOKABLE MatrixRoomPinnedEventsTimeline *pinnedEventsTimeline() const;
Q_INVOKABLE MatrixPromise *pinEvents(const QStringList &eventIds) const;
Q_INVOKABLE MatrixPromise *unpinEvents(const QStringList &eventIds) const;
Q_INVOKABLE MatrixEvent *messageById(QString eventId) const;
Q_INVOKABLE MatrixEvent *localEchoById(QString txnId) const;
Q_INVOKABLE void sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendTextMessage(QString text, const QString &relType, QString relatedTo) const;
Q_INVOKABLE void sendMediaFileMessage(QString fileName, QString mimeType,
- qint64 fileSize, QString mxcUri) const;
+ qint64 fileSize, QString mxcUri,
+ const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendEncryptedFileMessage(const QString &fileName, const QString& mimeType,
const qint64 fileSize, const QString& mxcUri,
- const QString &key, const QString &iv, const QByteArray &hash) const;
+ const QString &key, const QString &iv, const QByteArray &hash,
+ const QString &relType, const QString &relatedTo) const;
Q_INVOKABLE void sendReaction(QString text, QString relatedTo) const;
Q_INVOKABLE void resendMessage(QString txnId) const;
Q_INVOKABLE MatrixPromise *sendStateEvent(const QJsonObject &eventJson) const;
Q_INVOKABLE MatrixPromise *setSelfName(const QString &name) const;
Q_INVOKABLE MatrixPromise *redactEvent(QString eventId, QString reason) const;
Q_INVOKABLE MatrixPromise *removeLocalEcho(QString txnId) const;
Q_INVOKABLE MatrixRoomMemberListModel *typingUsers() const;
Q_INVOKABLE void setTyping(bool typing);
Q_INVOKABLE void setLocalDraft(QString localDraft);
Q_INVOKABLE void updateLocalDraftNow();
Q_INVOKABLE MatrixPromise *addOrSetTag(QString tagId) const;
Q_INVOKABLE MatrixPromise *removeTag(QString tagId) const;
Q_INVOKABLE MatrixPromise *paginateBackFrom(QString eventId) const;
Q_INVOKABLE MatrixPromise *leaveRoom() const;
Q_INVOKABLE MatrixPromise *forgetRoom() const;
Q_INVOKABLE MatrixRoomMemberListModel *members() const;
Q_INVOKABLE MatrixRoomMemberListModel *bannedMembers() const;
Q_INVOKABLE qint64 userPowerLevel(const QString &userId) const;
Q_INVOKABLE MatrixPromise *setUserPowerLevel(const QString &userId, qint64 powerLevel) const;
Q_INVOKABLE MatrixPromise *unsetUserPowerLevel(const QString &userId) const;
Q_INVOKABLE MatrixPromise *getStateEvent(const QString &type, const QString &stateKey) const;
Q_INVOKABLE MatrixPromise *ensureStateEvent(const QString &type, const QString &stateKey) const;
Q_INVOKABLE MatrixPromise *refreshState() const;
Q_INVOKABLE MatrixPromise *kickUser(const QString &userId, const QString &reason) const;
Q_INVOKABLE MatrixPromise *banUser(const QString &userId, const QString &reason = QStringLiteral()) const;
Q_INVOKABLE MatrixPromise *unbanUser(const QString &userId) const;
Q_INVOKABLE MatrixPromise *inviteUser(const QString &userId) const;
Q_INVOKABLE MatrixPromise *postReadReceipt(const QString &eventId) const;
Q_INVOKABLE MatrixPromise *setTopic(const QString &newTopic) const;
Q_SIGNALS:
void powerLevelsChanged();
protected:
nlohmann::json makeMediaFileMessageJson(QString fileName, QString mimeType,
- qint64 fileSize, QString mxcUri) const;
+ qint64 fileSize, QString mxcUri,
+ const QString &relType, const QString &relatedTo) const;
nlohmann::json makeEncryptedFileMessageJson(const QString &fileName, const QString &mimeType,
const qint64 fileSize, const QString &mxcUri,
- const QString &key, const QString &iv, const QByteArray &hash) const;
+ const QString &key, const QString &iv, const QByteArray &hash,
+ const QString &relType, const QString &relatedTo) const;
private Q_SLOTS:
void setTypingImpl();
private:
std::function<void()> m_setTypingThrottled;
std::function<void()> m_updateLocalDraftDebounced;
std::optional<QString> m_internalLocalDraft;
};
diff --git a/src/tests/quick-tests/tst_SendMessageBox.qml b/src/tests/quick-tests/tst_SendMessageBox.qml
index 97598e0..6521081 100644
--- a/src/tests/quick-tests/tst_SendMessageBox.qml
+++ b/src/tests/quick-tests/tst_SendMessageBox.qml
@@ -1,225 +1,269 @@
/*
* 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
property var mockHelper: TestHelpers.MockHelper {}
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;
},
setTyping: mockHelper.noop(),
sendTextMessage: mockHelper.noop(),
+ roomId: '!some-event-id',
+ encrypted: true,
};
}
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: ({})
+ property var matrixSdk: TestHelpers.MatrixSdkMock {
+ property var serverUrl: "https://example.com"
+ property var token: "token"
+ }
+ property var kazvIOManager: TestHelpers.KazvIOManagerMock {
+ property var startNewUploadJob: mockHelper.noop([
+ 'serverUrl',
+ 'fileUrl',
+ 'token',
+ 'roomId',
+ 'roomList',
+ 'encrypted',
+ 'draftRelType',
+ 'draftRelatedTo'
+ ])
+ }
+ property var sdkVars: ({
+ roomList: {}
+ })
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 = '';
mockHelper.clearAll();
}
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\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\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\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);
}
function test_emptySend() {
mouseClick(findChild(sendMessageBox, 'sendButton'));
verify(room.sendTextMessage.calledTimes() === 0);
const textArea = findChild(sendMessageBox, 'draftMessage');
mouseClick(textArea);
tryVerify(() => textArea.activeFocus);
keySequence('Ctrl+Enter');
verify(room.sendTextMessage.calledTimes() === 0);
keySequence('Ctrl+Return');
verify(room.sendTextMessage.calledTimes() === 0);
}
+
+ function test_replyWithFile() {
+ sendMessageBox.draftRelType = 'm.in_reply_to';
+ sendMessageBox.draftRelatedTo = '!some-event-id';
+ const sendFileButton = findChild(sendMessageBox, 'sendMediaFileButton');
+ const fileDialog = findChild(sendMessageBox, 'fileDialog');
+
+ verify(sendFileButton.enabled);
+ verify(!fileDialog.visible);
+ mouseClick(sendFileButton);
+ verify(fileDialog.visible);
+ fileDialog.accept();
+ verify(!fileDialog.visible);
+
+ compare(kazvIOManager.startNewUploadJob.calledTimes(), 1);
+ verify(Helpers.deepEqual(kazvIOManager.startNewUploadJob.lastArgs(), {
+ serverUrl: matrixSdk.serverUrl,
+ fileUrl: Qt.url(''),
+ token: matrixSdk.token,
+ roomId: room.roomId,
+ roomList: sdkVars.roomList,
+ encrypted: room.encrypted,
+ draftRelType: 'm.in_reply_to',
+ draftRelatedTo: '!some-event-id'
+ }));
+ }
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 3:01 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785017
Default Alt Text
(71 KB)

Event Timeline