diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55b58da1..049ed8a3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -541,32 +541,33 @@ qt5_wrap_cpp(MOC_HEADERS
src/AvatarProvider.h
src/BlurhashProvider.h
- src/Cache_p.h
src/CacheCryptoStructs.h
+ src/Cache_p.h
src/CallDevices.h
src/CallManager.h
src/ChatPage.h
src/Clipboard.h
+ src/CombinedImagePackModel.h
src/CompletionProxyModel.h
src/DeviceVerificationFlow.h
+ src/ImagePackListModel.h
src/InviteesModel.h
src/LoginPage.h
src/MainWindow.h
src/MemberList.h
src/MxcImageProvider.h
- src/ReadReceiptsModel.h
+ src/Olm.h
src/RegisterPage.h
+ src/RoomsModel.h
src/SSOHandler.h
- src/CombinedImagePackModel.h
src/SingleImagePackModel.h
- src/ImagePackListModel.h
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h
- src/RoomsModel.h
src/WebRTCSession.h
src/WelcomePage.h
- )
+ src/ReadReceiptsModel.h
+)
#
# Bundle translations.
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index f3e15d84..79cbd700 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -349,6 +349,7 @@ ScrollView {
required property string callType
required property var reactions
required property int trustlevel
+ required property int encryptionError
required property var timestamp
required property int status
required property int index
@@ -456,6 +457,7 @@ ScrollView {
callType: wrapper.callType
reactions: wrapper.reactions
trustlevel: wrapper.trustlevel
+ encryptionError: wrapper.encryptionError
timestamp: wrapper.timestamp
status: wrapper.status
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 6345f44c..c612479a 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -38,6 +38,7 @@ Item {
required property string callType
required property var reactions
required property int trustlevel
+ required property int encryptionError
required property var timestamp
required property int status
required property int relatedEventCacheBuster
@@ -110,6 +111,7 @@ Item {
roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
+ encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
}
@@ -136,6 +138,7 @@ Item {
roomTopic: r.roomTopic
roomName: r.roomName
callType: r.callType
+ encryptionError: r.encryptionError
relatedEventCacheBuster: r.relatedEventCacheBuster
isReply: false
}
diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml
new file mode 100644
index 00000000..cd00a9d4
--- /dev/null
+++ b/resources/qml/delegates/Encrypted.qml
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+ColumnLayout {
+ id: r
+
+ required property int encryptionError
+ required property string eventId
+
+ width: parent ? parent.width : undefined
+
+ MatrixText {
+ text: {
+ switch (encryptionError) {
+ case Olm.MissingSession:
+ return qsTr("There is no key to unlock this message. We requested the key automatically, but you can try requesting it again if you are impatient.");
+ case Olm.MissingSessionIndex:
+ return qsTr("This message couldn't be decrypted, because we only have a key for newer messages. You can try requesting access to this message.");
+ case Olm.DbError:
+ return qsTr("There was an internal error reading the decryption key from the database.");
+ case Olm.DecryptionFailed:
+ return qsTr("There was an error decrypting this message.");
+ case Olm.ParsingFailed:
+ return qsTr("The message couldn't be parsed.");
+ case Olm.ReplayAttack:
+ return qsTr("The encryption key was reused! Someone is possibly trying to insert false messages into this chat!");
+ default:
+ return qsTr("Unknown decryption error");
+ }
+ }
+ color: Nheko.colors.buttonText
+ width: r ? r.width : undefined
+ }
+
+ Button {
+ palette: Nheko.colors
+ visible: encryptionError == Olm.MissingSession || encryptionError == Olm.MissingSessionIndex
+ text: qsTr("Request key")
+ onClicked: room.requestKeyForEvent(eventId)
+ }
+
+}
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index a98c2a8b..a8bdf183 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -29,6 +29,7 @@ Item {
required property string roomTopic
required property string roomName
required property string callType
+ required property int encryptionError
required property int relatedEventCacheBuster
height: chooser.childrenRect.height
@@ -189,6 +190,16 @@ Item {
}
+ DelegateChoice {
+ roleValue: MtxEvent.Encrypted
+
+ Encrypted {
+ encryptionError: d.encryptionError
+ eventId: d.eventId
+ }
+
+ }
+
DelegateChoice {
roleValue: MtxEvent.Name
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 3e02a940..8bbce10e 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -30,6 +30,7 @@ Item {
property string roomTopic
property string roomName
property string callType
+ property int encryptionError
property int relatedEventCacheBuster
width: parent.width
@@ -97,6 +98,7 @@ Item {
roomName: r.roomName
callType: r.callType
relatedEventCacheBuster: r.relatedEventCacheBuster
+ encryptionError: r.encryptionError
enabled: false
width: parent.width
isReply: true
diff --git a/resources/res.qrc b/resources/res.qrc
index d7187f42..f50265ca 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -143,14 +143,15 @@
qml/emoji/StickerPicker.qml
qml/UserProfile.qml
qml/delegates/MessageDelegate.qml
- qml/delegates/TextMessage.qml
- qml/delegates/NoticeMessage.qml
- qml/delegates/ImageMessage.qml
- qml/delegates/PlayableMediaMessage.qml
+ qml/delegates/Encrypted.qml
qml/delegates/FileMessage.qml
+ qml/delegates/ImageMessage.qml
+ qml/delegates/NoticeMessage.qml
qml/delegates/Pill.qml
qml/delegates/Placeholder.qml
+ qml/delegates/PlayableMediaMessage.qml
qml/delegates/Reply.qml
+ qml/delegates/TextMessage.qml
qml/device-verification/Waiting.qml
qml/device-verification/DeviceVerification.qml
qml/device-verification/DigitVerification.qml
diff --git a/src/Olm.cpp b/src/Olm.cpp
index 048a6c0f..293b12de 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -1069,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index,
mtx::events::collections::TimelineEvent te;
mtx::events::collections::from_json(body, te);
- return {std::nullopt, std::nullopt, std::move(te.data)};
+ return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
} catch (std::exception &e) {
return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
}
diff --git a/src/Olm.h b/src/Olm.h
index a18cbbfb..ac1a1617 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -14,9 +14,11 @@
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
namespace olm {
+Q_NAMESPACE
-enum class DecryptionErrorCode
+enum DecryptionErrorCode
{
+ NoError,
MissingSession, // Session was not found, retrieve from backup or request from other devices
// and try again
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
@@ -25,14 +27,13 @@ enum class DecryptionErrorCode
DecryptionFailed, // libolm error
ParsingFailed, // Failed to parse the actual event
ReplayAttack, // Megolm index reused
- UnknownFingerprint, // Unknown device Fingerprint
};
+Q_ENUM_NS(DecryptionErrorCode)
struct DecryptionResult
{
- std::optional error;
+ DecryptionErrorCode error;
std::optional error_message;
-
std::optional event;
};
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 9a91ff79..742f8dbb 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -20,8 +20,7 @@
Q_DECLARE_METATYPE(Reaction)
-QCache EventStore::decryptedEvents_{
- 1000};
+QCache EventStore::decryptedEvents_{1000};
QCache EventStore::events_by_id_{
1000};
QCache EventStore::events_{1000};
@@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *)
mtx::events::msg::Encrypted>) {
auto event =
decryptEvent({room_id_, e.event_id}, e);
- if (auto dec =
- std::get_if>(event)) {
- emit updateFlowEventId(
- event_id.event_id.to_string());
+ if (event->event) {
+ if (auto dec = std::get_if<
+ mtx::events::RoomEvent<
+ mtx::events::msg::
+ KeyVerificationRequest>>(
+ &event->event.value())) {
+ emit updateFlowEventId(
+ event_id.event_id
+ .to_string());
+ }
}
}
});
@@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
if (auto encrypted =
std::get_if>(
&event)) {
- mtx::events::collections::TimelineEvents *d_event =
- decryptEvent({room_id_, encrypted->event_id}, *encrypted);
- if (std::visit(
+ auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ if (d_event->event &&
+ std::visit(
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
- *d_event)) {
- handle_room_verification(*d_event);
+ *d_event->event)) {
+ handle_room_verification(*d_event->event);
}
}
}
@@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
events_.insert(index, event_ptr);
}
- if (decrypt)
+ if (decrypt) {
if (auto encrypted =
std::get_if>(
- event_ptr))
- return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ event_ptr)) {
+ auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+ if (decrypted->event)
+ return &*decrypted->event;
+ }
+ }
return event_ptr;
}
@@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
}
-mtx::events::collections::TimelineEvents *
+olm::DecryptionResult *
EventStore::decryptEvent(const IdIndex &idx,
const mtx::events::EncryptedEvent &e)
{
@@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id = e.content.session_id;
index.sender_key = e.content.sender_key;
- auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
- auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
+ auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
+ auto event_ptr = new olm::DecryptionResult(std::move(event));
decryptedEvents_.insert(idx, event_ptr);
return event_ptr;
};
auto decryptionResult = olm::decryptEvent(index, e);
- mtx::events::RoomEvent dummy;
- dummy.origin_server_ts = e.origin_server_ts;
- dummy.event_id = e.event_id;
- dummy.sender = e.sender;
-
if (decryptionResult.error) {
- switch (*decryptionResult.error) {
+ switch (decryptionResult.error) {
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,
e.sender);
- // we may not want to request keys during initial sync and such
- if (suppressKeyRequests)
- break;
- // TODO: Check if this actually works and look in key backup
- auto copy = e;
- copy.room_id = room_id_;
- if (pending_key_requests.count(e.content.session_id)) {
- pending_key_requests.at(e.content.session_id)
- .events.push_back(copy);
- } else {
- PendingKeyRequests request;
- request.request_id =
- "key_request." + http::client()->generate_txn_id();
- request.events.push_back(copy);
- olm::send_key_request_for(copy, request.request_id);
- pending_key_requests[e.content.session_id] = request;
- }
+
+ requestSession(e, false);
break;
}
case olm::DecryptionErrorCode::DbError:
@@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
- dummy.content.body =
- tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
- "Placeholder, when the message can't be decrypted, because the DB "
- "access "
- "failed.")
- .toStdString();
break;
case olm::DecryptionErrorCode::DecryptionFailed:
nhlog::crypto()->critical(
@@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
- dummy.content.body =
- tr("-- Decryption Error (%1) --",
- "Placeholder, when the message can't be decrypted. In this case, the "
- "Olm "
- "decrytion returned an error, which is passed as %1.")
- .arg(
- QString::fromStdString(decryptionResult.error_message.value_or("")))
- .toStdString();
break;
case olm::DecryptionErrorCode::ParsingFailed:
- dummy.content.body =
- tr("-- Encrypted Event (Unknown event type) --",
- "Placeholder, when the message was decrypted, but we couldn't parse "
- "it, because "
- "Nheko/mtxclient don't support that event type yet.")
- .toStdString();
break;
case olm::DecryptionErrorCode::ReplayAttack:
nhlog::crypto()->critical(
@@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
e.event_id,
room_id_,
index.sender_key);
- dummy.content.body =
- tr("-- Replay attack! This message index was reused! --").toStdString();
break;
- case olm::DecryptionErrorCode::UnknownFingerprint:
- // TODO: don't fail, just show in UI.
- nhlog::crypto()->critical("Message by unverified fingerprint {}",
- index.sender_key);
- dummy.content.body =
- tr("-- Message by unverified device! --").toStdString();
+ case olm::DecryptionErrorCode::NoError:
+ // unreachable
break;
}
- return asCacheEntry(std::move(dummy));
- }
-
- std::string msg_str;
- try {
- auto session = cache::client()->getInboundMegolmSession(index);
- auto res =
- olm::client()->decrypt_group_message(session.get(), e.content.ciphertext);
- msg_str = std::string((char *)res.data.data(), res.data.size());
- } catch (const lmdb::error &e) {
- nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
- "Placeholder, when the message can't be decrypted, because the DB "
- "access "
- "failed.")
- .toStdString();
- return asCacheEntry(std::move(dummy));
- } catch (const mtx::crypto::olm_exception &e) {
- nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
- index.room_id,
- index.session_id,
- index.sender_key,
- e.what());
- dummy.content.body =
- tr("-- Decryption Error (%1) --",
- "Placeholder, when the message can't be decrypted. In this case, the "
- "Olm "
- "decrytion returned an error, which is passed as %1.")
- .arg(e.what())
- .toStdString();
- return asCacheEntry(std::move(dummy));
- }
-
- // Add missing fields for the event.
- json body = json::parse(msg_str);
- body["event_id"] = e.event_id;
- body["sender"] = e.sender;
- body["origin_server_ts"] = e.origin_server_ts;
- body["unsigned"] = e.unsigned_data;
-
- // relations are unencrypted in content...
- mtx::common::add_relations(body["content"], e.content.relations);
-
- json event_array = json::array();
- event_array.push_back(body);
-
- std::vector temp_events;
- mtx::responses::utils::parse_timeline_events(event_array, temp_events);
-
- if (temp_events.size() == 1) {
- auto encInfo = mtx::accessors::file(temp_events[0]);
-
- if (encInfo)
- emit newEncryptedImage(encInfo.value());
-
- return asCacheEntry(std::move(temp_events[0]));
+ return asCacheEntry(std::move(decryptionResult));
}
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
if (encInfo)
emit newEncryptedImage(encInfo.value());
- return asCacheEntry(std::move(decryptionResult.event.value()));
+ return asCacheEntry(std::move(decryptionResult));
+}
+
+void
+EventStore::requestSession(const mtx::events::EncryptedEvent &ev,
+ bool manual)
+{
+ // we may not want to request keys during initial sync and such
+ if (suppressKeyRequests)
+ return;
+
+ // TODO: Look in key backup
+ auto copy = ev;
+ copy.room_id = room_id_;
+ if (pending_key_requests.count(ev.content.session_id)) {
+ auto &r = pending_key_requests.at(ev.content.session_id);
+ r.events.push_back(copy);
+
+ // automatically request once every 10 min, manually every 1 min
+ qint64 delay = manual ? 60 : (60 * 10);
+ if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
+ r.requested_at = QDateTime::currentSecsSinceEpoch();
+ olm::send_key_request_for(copy, r.request_id);
+ }
+ } else {
+ PendingKeyRequests request;
+ request.request_id = "key_request." + http::client()->generate_txn_id();
+ request.requested_at = QDateTime::currentSecsSinceEpoch();
+ request.events.push_back(copy);
+ olm::send_key_request_for(copy, request.request_id);
+ pending_key_requests[ev.content.session_id] = request;
+ }
}
void
@@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool
events_by_id_.insert(index, event_ptr);
}
- if (decrypt)
+ if (decrypt) {
if (auto encrypted =
std::get_if>(
- event_ptr))
- return decryptEvent(index, *encrypted);
+ event_ptr)) {
+ auto decrypted = decryptEvent(index, *encrypted);
+ if (decrypted->event)
+ return &*decrypted->event;
+ }
+ }
return event_ptr;
}
+olm::DecryptionErrorCode
+EventStore::decryptionError(std::string id)
+{
+ if (this->thread() != QThread::currentThread())
+ nhlog::db()->warn("{} called from a different thread!", __func__);
+
+ if (id.empty())
+ return olm::DecryptionErrorCode::NoError;
+
+ IdIndex index{room_id_, std::move(id)};
+ auto edits_ = edits(index.id);
+ if (!edits_.empty()) {
+ index.id = mtx::accessors::event_id(edits_.back());
+ auto event_ptr =
+ new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
+ events_by_id_.insert(index, event_ptr);
+ }
+
+ auto event_ptr = events_by_id_.object(index);
+ if (!event_ptr) {
+ auto event = cache::client()->getEvent(room_id_, index.id);
+ if (!event) {
+ return olm::DecryptionErrorCode::NoError;
+ }
+ event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
+ events_by_id_.insert(index, event_ptr);
+ }
+
+ if (auto encrypted =
+ std::get_if>(event_ptr)) {
+ auto decrypted = decryptEvent(index, *encrypted);
+ return decrypted->error;
+ }
+
+ return olm::DecryptionErrorCode::NoError;
+}
+
void
EventStore::fetchMore()
{
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 7c404102..59c1c7c0 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -15,6 +15,7 @@
#include
#include
+#include "Olm.h"
#include "Reaction.h"
class EventStore : public QObject
@@ -78,6 +79,9 @@ public:
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
QVariantList reactions(const std::string &event_id);
+ olm::DecryptionErrorCode decryptionError(std::string id);
+ void requestSession(const mtx::events::EncryptedEvent &ev,
+ bool manual);
int size() const
{
@@ -119,7 +123,7 @@ public slots:
private:
std::vector edits(const std::string &event_id);
- mtx::events::collections::TimelineEvents *decryptEvent(
+ olm::DecryptionResult *decryptEvent(
const IdIndex &idx,
const mtx::events::EncryptedEvent &e);
void handle_room_verification(mtx::events::collections::TimelineEvents event);
@@ -129,7 +133,7 @@ private:
uint64_t first = std::numeric_limits::max(),
last = std::numeric_limits::max();
- static QCache decryptedEvents_;
+ static QCache decryptedEvents_;
static QCache events_;
static QCache events_by_id_;
@@ -137,6 +141,7 @@ private:
{
std::string request_id;
std::vector> events;
+ qint64 requested_at;
};
std::map pending_key_requests;
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 10d9788d..99e00a67 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -452,6 +452,7 @@ TimelineModel::roleNames() const
{IsEditable, "isEditable"},
{IsEncrypted, "isEncrypted"},
{Trustlevel, "trustlevel"},
+ {EncryptionError, "encryptionError"},
{ReplyTo, "replyTo"},
{Reactions, "reactions"},
{RoomId, "roomId"},
@@ -639,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return crypto::Trust::Unverified;
}
+ case EncryptionError:
+ return events.decryptionError(event_id(event));
+
case ReplyTo:
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
case Reactions: {
@@ -690,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
m.insert(names[RoomName], data(event, static_cast(RoomName)));
m.insert(names[RoomTopic], data(event, static_cast(RoomTopic)));
m.insert(names[CallType], data(event, static_cast(CallType)));
+ m.insert(names[EncryptionError], data(event, static_cast(EncryptionError)));
return QVariant(m);
}
@@ -1551,6 +1556,17 @@ TimelineModel::scrollTimerEvent()
}
}
+void
+TimelineModel::requestKeyForEvent(QString id)
+{
+ auto encrypted_event = events.get(id.toStdString(), "", false);
+ if (encrypted_event) {
+ if (auto ev = std::get_if>(
+ encrypted_event))
+ events.requestSession(*ev, true);
+ }
+}
+
void
TimelineModel::copyLinkToEvent(QString eventId) const
{
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index b5c8ca37..ad7cfbbb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -212,6 +212,7 @@ public:
IsEditable,
IsEncrypted,
Trustlevel,
+ EncryptionError,
ReplyTo,
Reactions,
RoomId,
@@ -264,6 +265,8 @@ public:
endResetModel();
}
+ Q_INVOKABLE void requestKeyForEvent(QString id);
+
std::vector<::Reaction> reactions(const std::string &event_id)
{
auto list = events.reactions(event_id);
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 76bc127e..b23ed278 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -157,6 +157,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"MtxEvent",
"Can't instantiate enum!");
+ qmlRegisterUncreatableMetaObject(
+ olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,