2021-03-05 02:35:15 +03:00
|
|
|
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2020-11-01 01:24:07 +03:00
|
|
|
#include "InputBar.h"
|
|
|
|
|
|
|
|
#include <QClipboard>
|
2020-11-25 19:02:23 +03:00
|
|
|
#include <QDropEvent>
|
2020-11-15 06:52:49 +03:00
|
|
|
#include <QFileDialog>
|
2020-11-01 01:24:07 +03:00
|
|
|
#include <QGuiApplication>
|
|
|
|
#include <QMimeData>
|
2020-11-15 06:52:49 +03:00
|
|
|
#include <QMimeDatabase>
|
|
|
|
#include <QStandardPaths>
|
|
|
|
#include <QUrl>
|
2020-11-01 01:24:07 +03:00
|
|
|
|
2021-03-28 18:37:36 +03:00
|
|
|
#include <QRegularExpression>
|
2020-11-09 05:12:37 +03:00
|
|
|
#include <mtx/responses/common.hpp>
|
2020-11-15 06:52:49 +03:00
|
|
|
#include <mtx/responses/media.hpp>
|
2020-11-09 05:12:37 +03:00
|
|
|
|
|
|
|
#include "Cache.h"
|
|
|
|
#include "ChatPage.h"
|
2020-11-20 04:38:08 +03:00
|
|
|
#include "CompletionProxyModel.h"
|
2020-11-01 01:24:07 +03:00
|
|
|
#include "Logging.h"
|
2020-11-16 01:14:47 +03:00
|
|
|
#include "MainWindow.h"
|
2020-11-09 05:12:37 +03:00
|
|
|
#include "MatrixClient.h"
|
2020-11-15 06:52:49 +03:00
|
|
|
#include "Olm.h"
|
2021-02-17 17:21:35 +03:00
|
|
|
#include "RoomsModel.h"
|
2020-11-09 05:12:37 +03:00
|
|
|
#include "TimelineModel.h"
|
2020-12-13 18:23:04 +03:00
|
|
|
#include "TimelineViewManager.h"
|
2020-11-09 05:12:37 +03:00
|
|
|
#include "UserSettingsPage.h"
|
2020-11-20 04:38:08 +03:00
|
|
|
#include "UsersModel.h"
|
2020-11-09 05:12:37 +03:00
|
|
|
#include "Utils.h"
|
2020-11-15 06:52:49 +03:00
|
|
|
#include "dialogs/PreviewUploadOverlay.h"
|
2020-11-20 06:33:11 +03:00
|
|
|
#include "emoji/EmojiModel.h"
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
#include "blurhash.hpp"
|
2020-11-09 05:12:37 +03:00
|
|
|
|
|
|
|
static constexpr size_t INPUT_HISTORY_SIZE = 10;
|
2020-11-01 01:24:07 +03:00
|
|
|
|
2020-11-09 05:12:37 +03:00
|
|
|
void
|
2020-11-01 01:24:07 +03:00
|
|
|
InputBar::paste(bool fromMouse)
|
|
|
|
{
|
|
|
|
const QMimeData *md = nullptr;
|
|
|
|
|
|
|
|
if (fromMouse) {
|
|
|
|
if (QGuiApplication::clipboard()->supportsSelection()) {
|
|
|
|
md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard);
|
|
|
|
}
|
|
|
|
|
2020-11-25 19:02:23 +03:00
|
|
|
if (md)
|
|
|
|
insertMimeData(md);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::insertMimeData(const QMimeData *md)
|
|
|
|
{
|
2020-11-01 01:24:07 +03:00
|
|
|
if (!md)
|
2020-11-09 05:12:37 +03:00
|
|
|
return;
|
2020-11-01 01:24:07 +03:00
|
|
|
|
2020-11-15 06:52:49 +03:00
|
|
|
nhlog::ui()->debug("Got mime formats: {}", md->formats().join(", ").toStdString());
|
|
|
|
const auto formats = md->formats().filter("/");
|
|
|
|
const auto image = formats.filter("image/", Qt::CaseInsensitive);
|
|
|
|
const auto audio = formats.filter("audio/", Qt::CaseInsensitive);
|
|
|
|
const auto video = formats.filter("video/", Qt::CaseInsensitive);
|
|
|
|
|
|
|
|
if (!image.empty() && md->hasImage()) {
|
|
|
|
showPreview(*md, "", image);
|
|
|
|
} else if (!audio.empty()) {
|
|
|
|
showPreview(*md, "", audio);
|
|
|
|
} else if (!video.empty()) {
|
|
|
|
showPreview(*md, "", video);
|
|
|
|
} else if (md->hasUrls()) {
|
|
|
|
// Generic file path for any platform.
|
|
|
|
QString path;
|
|
|
|
for (auto &&u : md->urls()) {
|
|
|
|
if (u.isLocalFile()) {
|
|
|
|
path = u.toLocalFile();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!path.isEmpty() && QFileInfo{path}.exists()) {
|
|
|
|
showPreview(*md, path, formats);
|
|
|
|
} else {
|
|
|
|
nhlog::ui()->warn("Clipboard does not contain any valid file paths.");
|
|
|
|
}
|
|
|
|
} else if (md->hasFormat("x-special/gnome-copied-files")) {
|
|
|
|
// Special case for X11 users. See "Notes for X11 Users" in md.
|
|
|
|
// Source: http://doc.qt.io/qt-5/qclipboard.html
|
|
|
|
|
|
|
|
// This MIME type returns a string with multiple lines separated by '\n'. The first
|
|
|
|
// line is the command to perform with the clipboard (not useful to us). The
|
|
|
|
// following lines are the file URIs.
|
|
|
|
//
|
|
|
|
// Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function
|
|
|
|
// nautilus_clipboard_get_uri_list_from_selection_data()
|
|
|
|
// https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c
|
|
|
|
|
|
|
|
auto data = md->data("x-special/gnome-copied-files").split('\n');
|
|
|
|
if (data.size() < 2) {
|
|
|
|
nhlog::ui()->warn("MIME format is malformed, cannot perform paste.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString path;
|
|
|
|
for (int i = 1; i < data.size(); ++i) {
|
|
|
|
QUrl url{data[i]};
|
|
|
|
if (url.isLocalFile()) {
|
|
|
|
path = url.toLocalFile();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!path.isEmpty()) {
|
|
|
|
showPreview(*md, path, formats);
|
|
|
|
} else {
|
|
|
|
nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}",
|
|
|
|
data.join(", ").toStdString());
|
|
|
|
}
|
2020-11-09 05:12:37 +03:00
|
|
|
} else if (md->hasText()) {
|
|
|
|
emit insertText(md->text());
|
2020-11-01 01:24:07 +03:00
|
|
|
} else {
|
|
|
|
nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-25 02:50:17 +03:00
|
|
|
void
|
|
|
|
InputBar::setText(QString newText)
|
|
|
|
{
|
|
|
|
if (history_.empty())
|
|
|
|
history_.push_front(newText);
|
|
|
|
else
|
|
|
|
history_.front() = newText;
|
|
|
|
history_index_ = 0;
|
|
|
|
|
|
|
|
if (history_.size() == INPUT_HISTORY_SIZE)
|
|
|
|
history_.pop_back();
|
|
|
|
|
|
|
|
emit textChanged(newText);
|
|
|
|
}
|
2020-11-01 01:24:07 +03:00
|
|
|
void
|
|
|
|
InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
|
|
|
|
{
|
2020-11-17 04:37:43 +03:00
|
|
|
if (text_.isEmpty())
|
|
|
|
stopTyping();
|
|
|
|
else
|
|
|
|
startTyping();
|
|
|
|
|
2020-11-17 15:25:16 +03:00
|
|
|
if (text_ != text()) {
|
|
|
|
if (history_.empty())
|
|
|
|
history_.push_front(text_);
|
|
|
|
else
|
|
|
|
history_.front() = text_;
|
|
|
|
history_index_ = 0;
|
|
|
|
}
|
|
|
|
|
2020-11-01 01:24:07 +03:00
|
|
|
selectionStart = selectionStart_;
|
|
|
|
selectionEnd = selectionEnd_;
|
|
|
|
cursorPosition = cursorPosition_;
|
2020-11-17 15:25:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
QString
|
|
|
|
InputBar::text() const
|
|
|
|
{
|
|
|
|
if (history_index_ < history_.size())
|
|
|
|
return history_.at(history_index_);
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
QString
|
|
|
|
InputBar::previousText()
|
|
|
|
{
|
|
|
|
history_index_++;
|
|
|
|
if (history_index_ >= INPUT_HISTORY_SIZE)
|
|
|
|
history_index_ = INPUT_HISTORY_SIZE;
|
|
|
|
else if (text().isEmpty())
|
|
|
|
history_index_--;
|
|
|
|
|
|
|
|
return text();
|
|
|
|
}
|
|
|
|
|
|
|
|
QString
|
|
|
|
InputBar::nextText()
|
|
|
|
{
|
|
|
|
history_index_--;
|
|
|
|
if (history_index_ >= INPUT_HISTORY_SIZE)
|
|
|
|
history_index_ = 0;
|
|
|
|
|
|
|
|
return text();
|
2020-11-01 01:24:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::send()
|
|
|
|
{
|
2020-11-17 15:25:16 +03:00
|
|
|
if (text().trimmed().isEmpty())
|
2020-11-09 05:12:37 +03:00
|
|
|
return;
|
|
|
|
|
2021-02-25 02:50:17 +03:00
|
|
|
nhlog::ui()->debug("Send: {}", text().toStdString());
|
|
|
|
|
|
|
|
auto wasEdit = !room->edit().isEmpty();
|
|
|
|
|
2020-11-17 15:25:16 +03:00
|
|
|
if (text().startsWith('/')) {
|
2021-03-28 16:59:47 +03:00
|
|
|
int command_end = text().indexOf(QRegularExpression("\\s"));
|
2020-11-09 05:12:37 +03:00
|
|
|
if (command_end == -1)
|
2020-11-17 15:25:16 +03:00
|
|
|
command_end = text().size();
|
|
|
|
auto name = text().mid(1, command_end - 1);
|
|
|
|
auto args = text().mid(command_end + 1);
|
2020-11-09 05:12:37 +03:00
|
|
|
if (name.isEmpty() || name == "/") {
|
|
|
|
message(args);
|
|
|
|
} else {
|
|
|
|
command(name, args);
|
|
|
|
}
|
|
|
|
} else {
|
2020-11-17 15:25:16 +03:00
|
|
|
message(text());
|
2020-11-09 05:12:37 +03:00
|
|
|
}
|
|
|
|
|
2021-02-25 02:50:17 +03:00
|
|
|
if (!wasEdit) {
|
|
|
|
history_.push_front("");
|
|
|
|
setText("");
|
|
|
|
}
|
2020-11-01 01:24:07 +03:00
|
|
|
}
|
2020-11-09 05:12:37 +03:00
|
|
|
|
2020-11-15 06:52:49 +03:00
|
|
|
void
|
|
|
|
InputBar::openFileSelection()
|
|
|
|
{
|
|
|
|
const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
|
|
|
const auto fileName = QFileDialog::getOpenFileName(
|
|
|
|
ChatPage::instance(), tr("Select a file"), homeFolder, tr("All Files (*)"));
|
|
|
|
|
|
|
|
if (fileName.isEmpty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
QMimeDatabase db;
|
|
|
|
QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
|
|
|
|
|
|
|
|
QFile file{fileName};
|
|
|
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
emit ChatPage::instance()->showNotification(
|
|
|
|
QString("Error while reading media: %1").arg(file.errorString()));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setUploading(true);
|
|
|
|
|
|
|
|
auto bin = file.readAll();
|
|
|
|
|
|
|
|
QMimeData data;
|
|
|
|
data.setData(mime.name(), bin);
|
|
|
|
|
|
|
|
showPreview(data, fileName, QStringList{mime.name()});
|
|
|
|
}
|
|
|
|
|
2020-11-09 05:12:37 +03:00
|
|
|
void
|
2021-03-26 02:42:46 +03:00
|
|
|
InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify)
|
2020-11-09 05:12:37 +03:00
|
|
|
{
|
|
|
|
mtx::events::msg::Text text = {};
|
|
|
|
text.body = msg.trimmed().toStdString();
|
|
|
|
|
2021-01-21 01:47:57 +03:00
|
|
|
if ((ChatPage::instance()->userSettings()->markdown() &&
|
2021-01-23 03:51:29 +03:00
|
|
|
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
|
|
|
|
useMarkdown == MarkdownOverride::ON) {
|
2021-03-26 02:42:46 +03:00
|
|
|
text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
|
2021-03-15 22:59:18 +03:00
|
|
|
// Remove markdown links by completer
|
|
|
|
text.body =
|
|
|
|
msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
|
2020-11-09 05:12:37 +03:00
|
|
|
|
|
|
|
// Don't send formatted_body, when we don't need to
|
|
|
|
if (text.formatted_body.find("<") == std::string::npos)
|
|
|
|
text.formatted_body = "";
|
|
|
|
else
|
|
|
|
text.format = "org.matrix.custom.html";
|
|
|
|
}
|
|
|
|
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
if (!room->reply().isEmpty()) {
|
|
|
|
text.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
|
|
|
}
|
|
|
|
|
|
|
|
text.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
|
|
|
|
} else if (!room->reply().isEmpty()) {
|
2020-11-09 05:12:37 +03:00
|
|
|
auto related = room->relatedInfo(room->reply());
|
|
|
|
|
|
|
|
QString body;
|
|
|
|
bool firstLine = true;
|
|
|
|
for (const auto &line : related.quoted_body.split("\n")) {
|
|
|
|
if (firstLine) {
|
|
|
|
firstLine = false;
|
|
|
|
body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
|
|
|
|
} else {
|
|
|
|
body = QString("%1\n> %2\n").arg(body).arg(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
text.body = QString("%1\n%2").arg(body).arg(msg).toStdString();
|
|
|
|
|
|
|
|
// NOTE(Nico): rich replies always need a formatted_body!
|
|
|
|
text.format = "org.matrix.custom.html";
|
2021-02-14 02:18:08 +03:00
|
|
|
if ((ChatPage::instance()->userSettings()->markdown() &&
|
2021-02-16 02:20:47 +03:00
|
|
|
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
|
|
|
|
useMarkdown == MarkdownOverride::ON)
|
2021-04-01 18:51:10 +03:00
|
|
|
text.formatted_body = utils::getFormattedQuoteBody(
|
|
|
|
related, utils::markdownToHtml(msg, rainbowify))
|
|
|
|
.toStdString();
|
2020-11-09 05:12:37 +03:00
|
|
|
else
|
|
|
|
text.formatted_body =
|
|
|
|
utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
|
|
|
|
|
2021-01-27 00:36:35 +03:00
|
|
|
text.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, related.related_event});
|
2020-11-09 05:12:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2021-04-01 18:51:10 +03:00
|
|
|
InputBar::emote(QString msg, bool rainbowify)
|
2020-11-09 05:12:37 +03:00
|
|
|
{
|
2021-04-01 18:51:10 +03:00
|
|
|
auto html = utils::markdownToHtml(msg, rainbowify);
|
2020-11-09 05:12:37 +03:00
|
|
|
|
|
|
|
mtx::events::msg::Emote emote;
|
|
|
|
emote.body = msg.trimmed().toStdString();
|
|
|
|
|
|
|
|
if (html != msg.trimmed().toHtmlEscaped() &&
|
|
|
|
ChatPage::instance()->userSettings()->markdown()) {
|
|
|
|
emote.formatted_body = html.toStdString();
|
|
|
|
emote.format = "org.matrix.custom.html";
|
2021-03-15 22:59:18 +03:00
|
|
|
// Remove markdown links by completer
|
|
|
|
emote.body =
|
|
|
|
msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
|
2020-11-09 05:12:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
2021-01-27 00:36:35 +03:00
|
|
|
emote.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
2020-11-09 05:12:37 +03:00
|
|
|
}
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
emote.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
2020-11-09 05:12:37 +03:00
|
|
|
|
|
|
|
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
2021-04-11 22:47:20 +03:00
|
|
|
void
|
|
|
|
InputBar::notice(QString msg, bool rainbowify)
|
|
|
|
{
|
|
|
|
auto html = utils::markdownToHtml(msg, rainbowify);
|
|
|
|
|
|
|
|
mtx::events::msg::Notice notice;
|
|
|
|
notice.body = msg.trimmed().toStdString();
|
|
|
|
|
|
|
|
if (html != msg.trimmed().toHtmlEscaped() &&
|
|
|
|
ChatPage::instance()->userSettings()->markdown()) {
|
|
|
|
notice.formatted_body = html.toStdString();
|
|
|
|
notice.format = "org.matrix.custom.html";
|
|
|
|
// Remove markdown links by completer
|
|
|
|
notice.body =
|
|
|
|
msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
|
|
|
notice.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
|
|
|
}
|
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
notice.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
|
|
|
|
|
|
|
room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
2020-11-15 06:52:49 +03:00
|
|
|
void
|
|
|
|
InputBar::image(const QString &filename,
|
|
|
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
|
|
|
const QString &url,
|
|
|
|
const QString &mime,
|
|
|
|
uint64_t dsize,
|
|
|
|
const QSize &dimensions,
|
|
|
|
const QString &blurhash)
|
|
|
|
{
|
|
|
|
mtx::events::msg::Image image;
|
|
|
|
image.info.mimetype = mime.toStdString();
|
|
|
|
image.info.size = dsize;
|
|
|
|
image.info.blurhash = blurhash.toStdString();
|
|
|
|
image.body = filename.toStdString();
|
|
|
|
image.info.h = dimensions.height();
|
|
|
|
image.info.w = dimensions.width();
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
image.file = file;
|
|
|
|
else
|
|
|
|
image.url = url.toStdString();
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
2021-01-27 00:36:35 +03:00
|
|
|
image.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
image.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::file(const QString &filename,
|
|
|
|
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
|
|
|
|
const QString &url,
|
|
|
|
const QString &mime,
|
|
|
|
uint64_t dsize)
|
|
|
|
{
|
|
|
|
mtx::events::msg::File file;
|
|
|
|
file.info.mimetype = mime.toStdString();
|
|
|
|
file.info.size = dsize;
|
|
|
|
file.body = filename.toStdString();
|
|
|
|
|
|
|
|
if (encryptedFile)
|
|
|
|
file.file = encryptedFile;
|
|
|
|
else
|
|
|
|
file.url = url.toStdString();
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
2021-01-27 00:36:35 +03:00
|
|
|
file.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
file.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::audio(const QString &filename,
|
|
|
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
|
|
|
const QString &url,
|
|
|
|
const QString &mime,
|
|
|
|
uint64_t dsize)
|
|
|
|
{
|
|
|
|
mtx::events::msg::Audio audio;
|
|
|
|
audio.info.mimetype = mime.toStdString();
|
|
|
|
audio.info.size = dsize;
|
|
|
|
audio.body = filename.toStdString();
|
|
|
|
audio.url = url.toStdString();
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
audio.file = file;
|
|
|
|
else
|
|
|
|
audio.url = url.toStdString();
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
2021-01-27 00:36:35 +03:00
|
|
|
audio.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
audio.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::video(const QString &filename,
|
|
|
|
const std::optional<mtx::crypto::EncryptedFile> &file,
|
|
|
|
const QString &url,
|
|
|
|
const QString &mime,
|
|
|
|
uint64_t dsize)
|
|
|
|
{
|
|
|
|
mtx::events::msg::Video video;
|
|
|
|
video.info.mimetype = mime.toStdString();
|
|
|
|
video.info.size = dsize;
|
|
|
|
video.body = filename.toStdString();
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
video.file = file;
|
|
|
|
else
|
|
|
|
video.url = url.toStdString();
|
|
|
|
|
|
|
|
if (!room->reply().isEmpty()) {
|
2021-01-27 00:36:35 +03:00
|
|
|
video.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
2021-02-01 04:22:53 +03:00
|
|
|
if (!room->edit().isEmpty()) {
|
|
|
|
video.relations.relations.push_back(
|
|
|
|
{mtx::common::RelationType::Replace, room->edit().toStdString()});
|
|
|
|
}
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
|
|
|
}
|
|
|
|
|
2020-11-09 05:12:37 +03:00
|
|
|
void
|
|
|
|
InputBar::command(QString command, QString args)
|
|
|
|
{
|
|
|
|
if (command == "me") {
|
2021-04-01 18:51:10 +03:00
|
|
|
emote(args, false);
|
2020-12-13 18:23:04 +03:00
|
|
|
} else if (command == "react") {
|
|
|
|
auto eventId = room->reply();
|
|
|
|
if (!eventId.isEmpty())
|
2021-05-28 23:14:59 +03:00
|
|
|
reaction(eventId, args.trimmed());
|
2020-11-09 05:12:37 +03:00
|
|
|
} else if (command == "join") {
|
|
|
|
ChatPage::instance()->joinRoom(args);
|
2021-03-26 03:19:48 +03:00
|
|
|
} else if (command == "part" || command == "leave") {
|
|
|
|
MainWindow::instance()->openLeaveRoomDialog(room->roomId());
|
2020-11-09 05:12:37 +03:00
|
|
|
} else if (command == "invite") {
|
|
|
|
ChatPage::instance()->inviteUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
|
|
|
|
} else if (command == "kick") {
|
|
|
|
ChatPage::instance()->kickUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
|
|
|
|
} else if (command == "ban") {
|
|
|
|
ChatPage::instance()->banUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
|
|
|
|
} else if (command == "unban") {
|
|
|
|
ChatPage::instance()->unbanUser(args.section(' ', 0, 0), args.section(' ', 1, -1));
|
|
|
|
} else if (command == "roomnick") {
|
|
|
|
mtx::events::state::Member member;
|
|
|
|
member.display_name = args.toStdString();
|
|
|
|
member.avatar_url =
|
|
|
|
cache::avatarUrl(room->roomId(),
|
|
|
|
QString::fromStdString(http::client()->user_id().to_string()))
|
|
|
|
.toStdString();
|
|
|
|
member.membership = mtx::events::state::Membership::Join;
|
|
|
|
|
|
|
|
http::client()->send_state_event(
|
|
|
|
room->roomId().toStdString(),
|
|
|
|
http::client()->user_id().to_string(),
|
|
|
|
member,
|
|
|
|
[](mtx::responses::EventId, mtx::http::RequestErr err) {
|
|
|
|
if (err)
|
|
|
|
nhlog::net()->error("Failed to set room displayname: {}",
|
|
|
|
err->matrix_error.error);
|
|
|
|
});
|
|
|
|
} else if (command == "shrug") {
|
|
|
|
message("¯\\_(ツ)_/¯" + (args.isEmpty() ? "" : " " + args));
|
|
|
|
} else if (command == "fliptable") {
|
|
|
|
message("(╯°□°)╯︵ ┻━┻");
|
|
|
|
} else if (command == "unfliptable") {
|
|
|
|
message(" ┯━┯╭( º _ º╭)");
|
|
|
|
} else if (command == "sovietflip") {
|
|
|
|
message("ノ┬─┬ノ ︵ ( \\o°o)\\");
|
|
|
|
} else if (command == "clear-timeline") {
|
|
|
|
room->clearTimeline();
|
|
|
|
} else if (command == "rotate-megolm-session") {
|
|
|
|
cache::dropOutboundMegolmSession(room->roomId().toStdString());
|
2021-01-21 01:44:11 +03:00
|
|
|
} else if (command == "md") {
|
2021-01-21 01:47:57 +03:00
|
|
|
message(args, MarkdownOverride::ON);
|
2021-01-21 01:44:11 +03:00
|
|
|
} else if (command == "plain") {
|
2021-01-21 01:47:57 +03:00
|
|
|
message(args, MarkdownOverride::OFF);
|
2021-03-26 02:42:46 +03:00
|
|
|
} else if (command == "rainbow") {
|
2021-03-28 15:00:35 +03:00
|
|
|
message(args, MarkdownOverride::ON, true);
|
2021-04-01 18:51:10 +03:00
|
|
|
} else if (command == "rainbowme") {
|
|
|
|
emote(args, true);
|
2021-04-11 22:47:20 +03:00
|
|
|
} else if (command == "notice") {
|
|
|
|
notice(args, false);
|
|
|
|
} else if (command == "rainbownotice") {
|
|
|
|
notice(args, true);
|
2020-11-09 05:12:37 +03:00
|
|
|
}
|
|
|
|
}
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::showPreview(const QMimeData &source, QString path, const QStringList &formats)
|
|
|
|
{
|
|
|
|
dialogs::PreviewUploadOverlay *previewDialog_ =
|
|
|
|
new dialogs::PreviewUploadOverlay(ChatPage::instance());
|
|
|
|
previewDialog_->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
|
|
|
if (source.hasImage())
|
|
|
|
previewDialog_->setPreview(qvariant_cast<QImage>(source.imageData()),
|
|
|
|
formats.front());
|
|
|
|
else if (!path.isEmpty())
|
|
|
|
previewDialog_->setPreview(path);
|
|
|
|
else if (!formats.isEmpty()) {
|
|
|
|
auto mime = formats.first();
|
|
|
|
previewDialog_->setPreview(source.data(mime), mime);
|
|
|
|
} else {
|
|
|
|
setUploading(false);
|
|
|
|
previewDialog_->deleteLater();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
connect(previewDialog_, &dialogs::PreviewUploadOverlay::aborted, this, [this]() {
|
|
|
|
setUploading(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
connect(
|
|
|
|
previewDialog_,
|
|
|
|
&dialogs::PreviewUploadOverlay::confirmUpload,
|
|
|
|
this,
|
|
|
|
[this](const QByteArray data, const QString &mime, const QString &fn) {
|
|
|
|
setUploading(true);
|
|
|
|
|
2021-02-01 04:22:53 +03:00
|
|
|
setText("");
|
|
|
|
|
2020-11-15 06:52:49 +03:00
|
|
|
auto payload = std::string(data.data(), data.size());
|
|
|
|
std::optional<mtx::crypto::EncryptedFile> encryptedFile;
|
|
|
|
if (cache::isRoomEncrypted(room->roomId().toStdString())) {
|
|
|
|
mtx::crypto::BinaryBuf buf;
|
|
|
|
std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
|
|
|
|
payload = mtx::crypto::to_string(buf);
|
|
|
|
}
|
|
|
|
|
|
|
|
QSize dimensions;
|
|
|
|
QString blurhash;
|
|
|
|
auto mimeClass = mime.split("/")[0];
|
2020-11-23 20:19:24 +03:00
|
|
|
nhlog::ui()->debug("Mime: {}", mime.toStdString());
|
2020-11-15 06:52:49 +03:00
|
|
|
if (mimeClass == "image") {
|
2021-03-02 03:45:16 +03:00
|
|
|
QImage img = utils::readImage(data);
|
2020-11-15 06:52:49 +03:00
|
|
|
|
|
|
|
dimensions = img.size();
|
|
|
|
if (img.height() > 200 && img.width() > 360)
|
|
|
|
img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding);
|
2020-11-26 18:09:53 +03:00
|
|
|
std::vector<unsigned char> data_;
|
2020-11-15 06:52:49 +03:00
|
|
|
for (int y = 0; y < img.height(); y++) {
|
|
|
|
for (int x = 0; x < img.width(); x++) {
|
|
|
|
auto p = img.pixel(x, y);
|
2020-11-26 18:09:53 +03:00
|
|
|
data_.push_back(static_cast<unsigned char>(qRed(p)));
|
|
|
|
data_.push_back(static_cast<unsigned char>(qGreen(p)));
|
|
|
|
data_.push_back(static_cast<unsigned char>(qBlue(p)));
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
blurhash = QString::fromStdString(
|
2020-11-26 18:09:53 +03:00
|
|
|
blurhash::encode(data_.data(), img.width(), img.height(), 4, 3));
|
2020-11-15 06:52:49 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
http::client()->upload(
|
|
|
|
payload,
|
|
|
|
encryptedFile ? "application/octet-stream" : mime.toStdString(),
|
|
|
|
QFileInfo(fn).fileName().toStdString(),
|
|
|
|
[this,
|
|
|
|
filename = fn,
|
|
|
|
encryptedFile = std::move(encryptedFile),
|
|
|
|
mimeClass,
|
|
|
|
mime,
|
|
|
|
size = payload.size(),
|
|
|
|
dimensions,
|
|
|
|
blurhash](const mtx::responses::ContentURI &res,
|
|
|
|
mtx::http::RequestErr err) mutable {
|
|
|
|
if (err) {
|
|
|
|
emit ChatPage::instance()->showNotification(
|
|
|
|
tr("Failed to upload media. Please try again."));
|
|
|
|
nhlog::net()->warn("failed to upload media: {} {} ({})",
|
|
|
|
err->matrix_error.error,
|
|
|
|
to_string(err->matrix_error.errcode),
|
|
|
|
static_cast<int>(err->status_code));
|
|
|
|
setUploading(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto url = QString::fromStdString(res.content_uri);
|
|
|
|
if (encryptedFile)
|
|
|
|
encryptedFile->url = res.content_uri;
|
|
|
|
|
|
|
|
if (mimeClass == "image")
|
|
|
|
image(filename,
|
|
|
|
encryptedFile,
|
|
|
|
url,
|
|
|
|
mime,
|
|
|
|
size,
|
|
|
|
dimensions,
|
|
|
|
blurhash);
|
|
|
|
else if (mimeClass == "audio")
|
|
|
|
audio(filename, encryptedFile, url, mime, size);
|
|
|
|
else if (mimeClass == "video")
|
|
|
|
video(filename, encryptedFile, url, mime, size);
|
|
|
|
else
|
|
|
|
file(filename, encryptedFile, url, mime, size);
|
|
|
|
|
|
|
|
setUploading(false);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2020-11-16 01:14:47 +03:00
|
|
|
|
2020-11-17 04:37:43 +03:00
|
|
|
void
|
|
|
|
InputBar::startTyping()
|
|
|
|
{
|
|
|
|
if (!typingRefresh_.isActive()) {
|
|
|
|
typingRefresh_.start();
|
|
|
|
|
|
|
|
if (ChatPage::instance()->userSettings()->typingNotifications()) {
|
|
|
|
http::client()->start_typing(
|
|
|
|
room->roomId().toStdString(), 10'000, [](mtx::http::RequestErr err) {
|
|
|
|
if (err) {
|
|
|
|
nhlog::net()->warn(
|
|
|
|
"failed to send typing notification: {}",
|
|
|
|
err->matrix_error.error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
typingTimeout_.start();
|
|
|
|
}
|
|
|
|
void
|
|
|
|
InputBar::stopTyping()
|
|
|
|
{
|
|
|
|
typingRefresh_.stop();
|
|
|
|
typingTimeout_.stop();
|
|
|
|
|
|
|
|
if (!ChatPage::instance()->userSettings()->typingNotifications())
|
|
|
|
return;
|
|
|
|
|
|
|
|
http::client()->stop_typing(room->roomId().toStdString(), [](mtx::http::RequestErr err) {
|
|
|
|
if (err) {
|
|
|
|
nhlog::net()->warn("failed to stop typing notifications: {}",
|
|
|
|
err->matrix_error.error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-05-28 23:14:59 +03:00
|
|
|
|
|
|
|
void
|
|
|
|
InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
|
|
|
|
{
|
|
|
|
auto reactions = room->reactions(reactedEvent.toStdString());
|
|
|
|
|
|
|
|
QString selfReactedEvent;
|
|
|
|
for (const auto &reaction : reactions) {
|
|
|
|
if (reactionKey == reaction.key_) {
|
|
|
|
selfReactedEvent = reaction.selfReactedEvent_;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selfReactedEvent.startsWith("m"))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// If selfReactedEvent is empty, that means we haven't previously reacted
|
|
|
|
if (selfReactedEvent.isEmpty()) {
|
|
|
|
mtx::events::msg::Reaction reaction;
|
|
|
|
mtx::common::Relation rel;
|
|
|
|
rel.rel_type = mtx::common::RelationType::Annotation;
|
|
|
|
rel.event_id = reactedEvent.toStdString();
|
|
|
|
rel.key = reactionKey.toStdString();
|
|
|
|
reaction.relations.relations.push_back(rel);
|
|
|
|
|
|
|
|
room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
|
|
|
|
// Otherwise, we have previously reacted and the reaction should be redacted
|
|
|
|
} else {
|
|
|
|
room->redactEvent(selfReactedEvent);
|
|
|
|
}
|
|
|
|
}
|