diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 41001081..be4bc09e 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -229,6 +229,9 @@ EventStore::clearTimeline()
}
nhlog::ui()->info("Range {} {}", this->last, this->first);
+ decryptedEvents_.clear();
+ events_.clear();
+
emit endResetModel();
}
@@ -239,9 +242,12 @@ EventStore::receivedSessionKey(const std::string &session_id)
return;
auto request = pending_key_requests.at(session_id);
- pending_key_requests.erase(session_id);
- olm::send_key_request_for(request.events.front(), request.request_id, true);
+ // Don't request keys again until Nheko is restarted (for now)
+ pending_key_requests[session_id].events.clear();
+
+ if (!request.events.empty())
+ olm::send_key_request_for(request.events.front(), request.request_id, true);
for (const auto &e : request.events) {
auto idx = idToIndex(e.event_id);
@@ -265,6 +271,9 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
emit beginResetModel();
this->first = std::numeric_limits<uint64_t>::max();
this->last = std::numeric_limits<uint64_t>::max();
+
+ decryptedEvents_.clear();
+ events_.clear();
emit endResetModel();
return;
}
@@ -273,6 +282,9 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
emit beginResetModel();
this->last = range->last;
this->first = range->first;
+
+ decryptedEvents_.clear();
+ events_.clear();
emit endResetModel();
} else if (range->last > this->last) {
emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last));
@@ -543,12 +555,21 @@ EventStore::decryptEvent(const IdIndex &idx,
if (decryptionResult.error) {
switch (*decryptionResult.error) {
- case olm::DecryptionErrorCode::MissingSession: {
- dummy.content.body =
- tr("-- Encrypted Event (No keys found for decryption) --",
- "Placeholder, when the message was not decrypted yet or can't be "
- "decrypted.")
- .toStdString();
+ case olm::DecryptionErrorCode::MissingSession:
+ case olm::DecryptionErrorCode::MissingSessionIndex: {
+ if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession)
+ dummy.content.body =
+ tr("-- Encrypted Event (No keys found for decryption) --",
+ "Placeholder, when the message was not decrypted yet or can't "
+ "be "
+ "decrypted.")
+ .toStdString();
+ else
+ dummy.content.body =
+ tr("-- Encrypted Event (Key not valid for this index) --",
+ "Placeholder, when the message can't be decrypted with this "
+ "key since it is not valid for this index ")
+ .toStdString();
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
index.room_id,
index.session_id,
@@ -760,7 +781,8 @@ EventStore::fetchMore()
if (cache::client()->previousBatchToken(room_id_) != opts.from) {
nhlog::net()->warn("Cache cleared while fetching more messages, dropping "
"/messages response");
- emit fetchedMore();
+ if (!opts.to.empty())
+ emit fetchedMore();
return;
}
if (err) {
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 3cddd613..b31c1f76 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -251,12 +251,14 @@ InputBar::openFileSelection()
}
void
-InputBar::message(QString msg)
+InputBar::message(QString msg, MarkdownOverride useMarkdown)
{
mtx::events::msg::Text text = {};
text.body = msg.trimmed().toStdString();
- if (ChatPage::instance()->userSettings()->markdown()) {
+ if ((ChatPage::instance()->userSettings()->markdown() &&
+ useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
+ useMarkdown == MarkdownOverride::ON) {
text.formatted_body = utils::markdownToHtml(msg).toStdString();
// Don't send formatted_body, when we don't need to
@@ -477,6 +479,10 @@ InputBar::command(QString command, QString args)
room->clearTimeline();
} else if (command == "rotate-megolm-session") {
cache::dropOutboundMegolmSession(room->roomId().toStdString());
+ } else if (command == "md") {
+ message(args, MarkdownOverride::ON);
+ } else if (command == "plain") {
+ message(args, MarkdownOverride::OFF);
}
}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index c729a6fc..f173bbc0 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -12,6 +12,13 @@ class QMimeData;
class QDropEvent;
class QStringList;
+enum class MarkdownOverride
+{
+ NOT_SPECIFIED, // no override set
+ ON,
+ OFF,
+};
+
class InputBar : public QObject
{
Q_OBJECT
@@ -41,7 +48,7 @@ public slots:
void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
void openFileSelection();
bool uploading() const { return uploading_; }
- void message(QString body);
+ void message(QString body, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED);
QObject *completerFor(QString completerName);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 5db6f0c2..79cf5184 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -5,6 +5,7 @@
#include <type_traits>
#include <QCache>
+#include <QDesktopServices>
#include <QFileDialog>
#include <QMimeDatabase>
#include <QRegularExpression>
@@ -353,7 +354,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return QVariant(emojiCount);
}
case Body:
- return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
+ return QVariant(
+ utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped()));
case FormattedBody: {
const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
@@ -797,9 +799,9 @@ TimelineModel::viewDecryptedRawMessage(QString id) const
}
void
-TimelineModel::openUserProfile(QString userid)
+TimelineModel::openUserProfile(QString userid, bool global)
{
- emit openProfile(new UserProfile(room_id_, userid, manager_, this));
+ emit openProfile(new UserProfile(global ? "" : room_id_, userid, manager_, this));
}
void
@@ -1072,6 +1074,14 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
std::visit(SendMessageVisitor{this}, event);
}
+void
+TimelineModel::openMedia(QString eventId)
+{
+ cacheMedia(eventId, [](QString filename) {
+ QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
+ });
+}
+
bool
TimelineModel::saveMedia(QString eventId) const
{
@@ -1148,7 +1158,7 @@ TimelineModel::saveMedia(QString eventId) const
}
void
-TimelineModel::cacheMedia(QString eventId)
+TimelineModel::cacheMedia(QString eventId, std::function<void(const QString)> callback)
{
mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), "");
if (!event)
@@ -1168,12 +1178,13 @@ TimelineModel::cacheMedia(QString eventId)
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
- const auto url = mxcUrl.toStdString();
+ const auto url = mxcUrl.toStdString();
+ const auto name = QString(mxcUrl).remove("mxc://");
QFileInfo filename(QString("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
- .arg(QString(mxcUrl).remove("mxc://"))
+ .arg(name)
.arg(suffix));
- if (QDir::cleanPath(filename.path()) != filename.path()) {
+ if (QDir::cleanPath(name) != name) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
@@ -1182,15 +1193,18 @@ TimelineModel::cacheMedia(QString eventId)
if (filename.isReadable()) {
emit mediaCached(mxcUrl, filename.filePath());
+ if (callback) {
+ callback(filename.filePath());
+ }
return;
}
http::client()->download(
url,
- [this, mxcUrl, filename, url, encryptionInfo](const std::string &data,
- const std::string &,
- const std::string &,
- mtx::http::RequestErr err) {
+ [this, callback, mxcUrl, filename, url, encryptionInfo](const std::string &data,
+ const std::string &,
+ const std::string &,
+ mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
@@ -1212,6 +1226,10 @@ TimelineModel::cacheMedia(QString eventId)
file.write(QByteArray(temp.data(), (int)temp.size()));
file.close();
+
+ if (callback) {
+ callback(filename.filePath());
+ }
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
@@ -1220,6 +1238,12 @@ TimelineModel::cacheMedia(QString eventId)
});
}
+void
+TimelineModel::cacheMedia(QString eventId)
+{
+ cacheMedia(eventId, NULL);
+}
+
QString
TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg)
{
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index b6b3b5ae..51b8049e 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -212,14 +212,16 @@ public:
Q_INVOKABLE void viewRawMessage(QString id) const;
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
- Q_INVOKABLE void openUserProfile(QString userid);
+ Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
Q_INVOKABLE void replyAction(QString id);
Q_INVOKABLE void readReceiptsAction(QString id) const;
Q_INVOKABLE void redactEvent(QString id);
Q_INVOKABLE int idToIndex(QString id) const;
Q_INVOKABLE QString indexToId(int index) const;
+ Q_INVOKABLE void openMedia(QString eventId);
Q_INVOKABLE void cacheMedia(QString eventId);
Q_INVOKABLE bool saveMedia(QString eventId) const;
+ void cacheMedia(QString eventId, std::function<void(const QString filename)> callback);
std::vector<::Reaction> reactions(const std::string &event_id)
{
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 97af0065..93451976 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -128,6 +128,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"UserProfile needs to be instantiated on the C++ side");
static auto self = this;
+ qmlRegisterSingletonType<MainWindow>(
+ "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * {
+ return MainWindow::instance();
+ });
qmlRegisterSingletonType<TimelineViewManager>(
"im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
return self;
|