summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-11-08 18:32:19 -0500
committerJoseph Donofry <joedonofry@gmail.com>2021-11-08 18:32:19 -0500
commitc93a7b24353a98b09949baf3ca4d8de8439cfa4a (patch)
tree53adf948d663566e198d00ad179acc4465e329c3 /src
parentFix syntax issue (diff)
parentRemove appimage (diff)
downloadnheko-c93a7b24353a98b09949baf3ca4d8de8439cfa4a.tar.xz
Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp327
-rw-r--r--src/CacheCryptoStructs.h8
-rw-r--r--src/Cache_p.h5
-rw-r--r--src/ChatPage.cpp122
-rw-r--r--src/UserSettingsPage.cpp4
-rw-r--r--src/encryption/SelfVerificationStatus.cpp28
-rw-r--r--src/main.cpp7
-rw-r--r--src/timeline/EventStore.cpp3
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/TimelineModel.cpp36
-rw-r--r--src/timeline/TimelineModel.h2
-rw-r--r--src/timeline/TimelineViewManager.cpp7
-rw-r--r--src/ui/UserProfile.cpp2
13 files changed, 333 insertions, 220 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 58eb2630..0fdf8dd3 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -199,7 +199,6 @@ Cache::Cache(const QString &userId, QObject *parent) , env_{nullptr} , localUserId_{userId} { - setup(); connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); connect( this, @@ -212,6 +211,7 @@ Cache::Cache(const QString &userId, QObject *parent) } }, Qt::QueuedConnection); + setup(); } void @@ -308,7 +308,178 @@ Cache::setup() txn.commit(); - databaseReady_ = true; + loadSecrets({ + {mtx::secret_storage::secrets::cross_signing_master, false}, + {mtx::secret_storage::secrets::cross_signing_self_signing, false}, + {mtx::secret_storage::secrets::cross_signing_user_signing, false}, + {mtx::secret_storage::secrets::megolm_backup_v1, false}, + {"pickle_secret", true}, + }); +} + +static void +fatalSecretError() +{ + QMessageBox::critical( + ChatPage::instance(), + QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"), + QCoreApplication::translate( + "SecretStorage", + "Nheko could not connect to the secure storage to save encryption secrets to. This can " + "have multiple reasons. Check if your D-Bus service is running and you have configured a " + "service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If " + "you are having trouble, feel free to open an issue here: " + "https://github.com/Nheko-Reborn/nheko/issues")); + + QCoreApplication::exit(1); + exit(1); +} + +static QString +secretName(std::string name, bool internal) +{ + auto settings = UserSettings::instance(); + return (internal ? "nheko." : "matrix.") + + QString( + QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256) + .toBase64()) + + "." + QString::fromStdString(name); +} + +void +Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad) +{ + if (toLoad.empty()) { + this->databaseReady_ = true; + emit databaseReady(); + return; + } + + auto [name_, internal] = toLoad.front(); + + auto job = new QKeychain::ReadPasswordJob(QCoreApplication::applicationName()); + job->setAutoDelete(true); + job->setInsecureFallback(true); + job->setSettings(UserSettings::instance()->qsettings()); + auto name = secretName(name_, internal); + job->setKey(name); + + connect(job, + &QKeychain::ReadPasswordJob::finished, + this, + [this, name, toLoad, job](QKeychain::Job *) mutable { + const QString secret = job->textData(); + if (job->error() && job->error() != QKeychain::Error::EntryNotFound) { + nhlog::db()->error("Restoring secret '{}' failed ({}): {}", + name.toStdString(), + job->error(), + job->errorString().toStdString()); + + fatalSecretError(); + } + if (secret.isEmpty()) { + nhlog::db()->debug("Restored empty secret '{}'.", name.toStdString()); + } else { + std::unique_lock lock(secret_storage.mtx); + secret_storage.secrets[name.toStdString()] = secret.toStdString(); + } + + // load next secret + toLoad.erase(toLoad.begin()); + + // You can't start a job from the finish signal of a job. + QTimer::singleShot(0, [this, toLoad] { loadSecrets(toLoad); }); + }); + job->start(); +} + +std::optional<std::string> +Cache::secret(const std::string name_, bool internal) +{ + auto name = secretName(name_, internal); + std::unique_lock lock(secret_storage.mtx); + if (auto secret = secret_storage.secrets.find(name.toStdString()); + secret != secret_storage.secrets.end()) + return secret->second; + else + return std::nullopt; +} + +void +Cache::storeSecret(const std::string name_, const std::string secret, bool internal) +{ + auto name = secretName(name_, internal); + { + std::unique_lock lock(secret_storage.mtx); + secret_storage.secrets[name.toStdString()] = secret; + } + + auto settings = UserSettings::instance(); + auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName()); + job->setAutoDelete(true); + job->setInsecureFallback(true); + job->setSettings(UserSettings::instance()->qsettings()); + + job->setKey(name); + + job->setTextData(QString::fromStdString(secret)); + + QObject::connect( + job, + &QKeychain::WritePasswordJob::finished, + this, + [name_, this](QKeychain::Job *job) { + if (job->error()) { + nhlog::db()->warn( + "Storing secret '{}' failed: {}", name_, job->errorString().toStdString()); + fatalSecretError(); + } else { + // if we emit the signal directly, qtkeychain breaks and won't execute new + // jobs. You can't start a job from the finish signal of a job. + QTimer::singleShot(0, [this, name_] { emit secretChanged(name_); }); + nhlog::db()->info("Storing secret '{}' successful", name_); + } + }, + Qt::ConnectionType::DirectConnection); + job->start(); +} + +void +Cache::deleteSecret(const std::string name, bool internal) +{ + auto name_ = secretName(name, internal); + { + std::unique_lock lock(secret_storage.mtx); + secret_storage.secrets.erase(name_.toStdString()); + } + + auto settings = UserSettings::instance(); + auto job = new QKeychain::DeletePasswordJob(QCoreApplication::applicationName()); + job->setAutoDelete(true); + job->setInsecureFallback(true); + job->setSettings(UserSettings::instance()->qsettings()); + + job->setKey(name_); + + job->connect( + job, &QKeychain::Job::finished, this, [this, name]() { emit secretChanged(name); }); + job->start(); +} + +std::string +Cache::pickleSecret() +{ + if (pickle_secret_.empty()) { + auto s = secret("pickle_secret", true); + if (!s) { + this->pickle_secret_ = mtx::client::utils::random_token(64, true); + storeSecret("pickle_secret", pickle_secret_, true); + } else { + this->pickle_secret_ = *s; + } + } + + return pickle_secret_; } void @@ -758,144 +929,6 @@ Cache::backupVersion() } } -static void -fatalSecretError() -{ - QMessageBox::critical( - ChatPage::instance(), - QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"), - QCoreApplication::translate( - "SecretStorage", - "Nheko could not connect to the secure storage to save encryption secrets to. This can " - "have multiple reasons. Check if your D-Bus service is running and you have configured a " - "service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If " - "you are having trouble, feel free to open an issue here: " - "https://github.com/Nheko-Reborn/nheko/issues")); - - QCoreApplication::exit(1); - exit(1); -} - -void -Cache::storeSecret(const std::string name, const std::string secret, bool internal) -{ - auto settings = UserSettings::instance(); - auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName()); - job->setAutoDelete(true); - job->setInsecureFallback(true); - job->setSettings(UserSettings::instance()->qsettings()); - - job->setKey( - (internal ? "nheko." : "matrix.") + - QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256) - .toBase64()) + - "." + QString::fromStdString(name)); - - job->setTextData(QString::fromStdString(secret)); - - QObject::connect( - job, - &QKeychain::WritePasswordJob::finished, - this, - [name, this](QKeychain::Job *job) { - if (job->error()) { - nhlog::db()->warn( - "Storing secret '{}' failed: {}", name, job->errorString().toStdString()); - fatalSecretError(); - } else { - // if we emit the signal directly, qtkeychain breaks and won't execute new - // jobs. You can't start a job from the finish signal of a job. - QTimer::singleShot(100, [this, name] { emit secretChanged(name); }); - nhlog::db()->info("Storing secret '{}' successful", name); - } - }, - Qt::ConnectionType::DirectConnection); - job->start(); -} - -void -Cache::deleteSecret(const std::string name, bool internal) -{ - auto settings = UserSettings::instance(); - QKeychain::DeletePasswordJob job(QCoreApplication::applicationName()); - job.setAutoDelete(false); - job.setInsecureFallback(true); - job.setSettings(UserSettings::instance()->qsettings()); - - job.setKey( - (internal ? "nheko." : "matrix.") + - QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256) - .toBase64()) + - "." + QString::fromStdString(name)); - - // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean - // time! - QEventLoop loop; - job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); - job.start(); - loop.exec(); - - emit secretChanged(name); -} - -std::optional<std::string> -Cache::secret(const std::string name, bool internal) -{ - auto settings = UserSettings::instance(); - QKeychain::ReadPasswordJob job(QCoreApplication::applicationName()); - job.setAutoDelete(false); - job.setInsecureFallback(true); - job.setSettings(UserSettings::instance()->qsettings()); - - job.setKey( - (internal ? "nheko." : "matrix.") + - QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256) - .toBase64()) + - "." + QString::fromStdString(name)); - - // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean - // time! - QEventLoop loop; - job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); - job.start(); - loop.exec(); - - const QString secret = job.textData(); - if (job.error()) { - if (job.error() == QKeychain::Error::EntryNotFound) - return std::nullopt; - nhlog::db()->error("Restoring secret '{}' failed ({}): {}", - name, - job.error(), - job.errorString().toStdString()); - - fatalSecretError(); - return std::nullopt; - } - if (secret.isEmpty()) { - nhlog::db()->debug("Restored empty secret '{}'.", name); - return std::nullopt; - } - - return secret.toStdString(); -} - -std::string -Cache::pickleSecret() -{ - if (pickle_secret_.empty()) { - auto s = secret("pickle_secret", true); - if (!s) { - this->pickle_secret_ = mtx::client::utils::random_token(64, true); - storeSecret("pickle_secret", pickle_secret_, true); - } else { - this->pickle_secret_ = *s; - } - } - - return pickle_secret_; -} - void Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) { @@ -3182,9 +3215,11 @@ Cache::clearTimeline(const std::string &room_id) break; } - do { - lmdb::cursor_del(msgCursor); - } while (msgCursor.get(indexVal, val, MDB_PREV)); + if (!start) { + do { + lmdb::cursor_del(msgCursor); + } while (msgCursor.get(indexVal, val, MDB_PREV)); + } cursor.close(); msgCursor.close(); @@ -4066,8 +4101,9 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query (void)status; emit verificationStatusChanged(user); } + } else { + emit verificationStatusChanged(user_id); } - emit verificationStatusChanged(user_id); } } @@ -4276,8 +4312,9 @@ Cache::markDeviceVerified(const std::string &user_id, const std::string &key) (void)status; emit verificationStatusChanged(user); } + } else { + emit verificationStatusChanged(user_id); } - emit verificationStatusChanged(user_id); } void diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index b7461848..f56c8685 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h
@@ -141,6 +141,14 @@ struct VerificationStorage std::mutex verification_storage_mtx; }; +//! In memory cache of verification status +struct SecretsStorage +{ + //! secret name -> secret + std::map<std::string, std::string> secrets; + std::mutex mtx; +}; + // this will store the keys of the user with whom a encrypted room is shared with struct UserKeyCache { diff --git a/src/Cache_p.h b/src/Cache_p.h
index 651d73d7..a529bc37 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -304,6 +304,7 @@ public: return get_skey(a).compare(get_skey(b)); } + signals: void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void roomReadStatus(const std::map<QString, bool> &status); @@ -312,8 +313,11 @@ signals: void verificationStatusChanged(const std::string &userid); void selfVerificationStatusChanged(); void secretChanged(const std::string name); + void databaseReady(); private: + void loadSecrets(std::vector<std::pair<std::string, bool>> toLoad); + //! Save an invited room. void saveInvite(lmdb::txn &txn, lmdb::dbi &statesdb, @@ -684,6 +688,7 @@ private: std::string pickle_secret_; VerificationStorage verification_storage; + SecretsStorage secret_storage; bool databaseReady_ = false; }; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index d262387c..77a8edcf 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -316,6 +316,66 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) try { cache::init(userid); + connect(cache::client(), &Cache::databaseReady, this, [this]() { + nhlog::db()->info("database ready"); + + const bool isInitialized = cache::isInitialized(); + const auto cacheVersion = cache::formatVersion(); + + try { + if (!isInitialized) { + cache::setCurrentFormat(); + } else { + if (cacheVersion == cache::CacheVersion::Current) { + loadStateFromCache(); + return; + } else if (cacheVersion == cache::CacheVersion::Older) { + if (!cache::runMigrations()) { + QMessageBox::critical( + this, + tr("Cache migration failed!"), + tr("Migrating the cache to the current version failed. " + "This can have different reasons. Please open an " + "issue and try to use an older version in the mean " + "time. Alternatively you can try deleting the cache " + "manually.")); + QCoreApplication::quit(); + } + loadStateFromCache(); + return; + } else if (cacheVersion == cache::CacheVersion::Newer) { + QMessageBox::critical( + this, + tr("Incompatible cache version"), + tr("The cache on your disk is newer than this version of Nheko " + "supports. Please update Nheko or clear your cache.")); + QCoreApplication::quit(); + return; + } + } + + // It's the first time syncing with this device + // There isn't a saved olm account to restore. + nhlog::crypto()->info("creating new olm account"); + olm::client()->create_new_account(); + cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create new olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } + + getProfileInfo(); + getBackupVersion(); + tryInitialSync(); + callManager_->refreshTurnServer(); + emit MainWindow::instance()->reload(); + }); + connect(cache::client(), &Cache::newReadReceipts, view_manager_, @@ -326,66 +386,10 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) &notificationsManager, &NotificationsManager::removeNotification); - const bool isInitialized = cache::isInitialized(); - const auto cacheVersion = cache::formatVersion(); - - callManager_->refreshTurnServer(); - - if (!isInitialized) { - cache::setCurrentFormat(); - } else { - if (cacheVersion == cache::CacheVersion::Current) { - loadStateFromCache(); - return; - } else if (cacheVersion == cache::CacheVersion::Older) { - if (!cache::runMigrations()) { - QMessageBox::critical(this, - tr("Cache migration failed!"), - tr("Migrating the cache to the current version failed. " - "This can have different reasons. Please open an " - "issue and try to use an older version in the mean " - "time. Alternatively you can try deleting the cache " - "manually.")); - QCoreApplication::quit(); - } - loadStateFromCache(); - return; - } else if (cacheVersion == cache::CacheVersion::Newer) { - QMessageBox::critical( - this, - tr("Incompatible cache version"), - tr("The cache on your disk is newer than this version of Nheko " - "supports. Please update or clear your cache.")); - QCoreApplication::quit(); - return; - } - } - } catch (const lmdb::error &e) { nhlog::db()->critical("failure during boot: {}", e.what()); - cache::deleteData(); - nhlog::net()->info("falling back to initial sync"); + emit dropToLoginPageCb(tr("Failed to open database, logging out!")); } - - try { - // It's the first time syncing with this device - // There isn't a saved olm account to restore. - nhlog::crypto()->info("creating new olm account"); - olm::client()->create_new_account(); - cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save olm account {}", e.what()); - emit dropToLoginPageCb(QString::fromStdString(e.what())); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to create new olm account {}", e.what()); - emit dropToLoginPageCb(QString::fromStdString(e.what())); - return; - } - - getProfileInfo(); - getBackupVersion(); - tryInitialSync(); } void @@ -522,6 +526,8 @@ ChatPage::tryInitialSync() for (const auto &entry : res.one_time_key_counts) nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first); + cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()}); + startInitialSync(); }); } @@ -1139,7 +1145,7 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio if (!decrypted.empty()) { cache::storeSecret(secretName, decrypted); - if (deviceKeys && + if (deviceKeys && deviceKeys->device_keys.count(http::client()->device_id()) && secretName == mtx::secret_storage::secrets::cross_signing_self_signing) { auto myKey = deviceKeys->device_keys.at(http::client()->device_id()); if (myKey.user_id == http::client()->user_id().to_string() && diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 340709a6..52567289 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -950,7 +950,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle.")); boxWrap(tr("Use identicons"), useIdenticon_, - tr("Display an identicon instead of a letter when a user has not set an avatar.")); + tr("Display an identicon instead of a letter when no avatar is set.")); boxWrap(tr("Group's sidebar"), groupViewToggle_, tr("Show a column containing groups and tags next to the room list.")); @@ -1098,7 +1098,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge backupSecretCached, tr("The key to decrypt online key backups. If it is cached, you can enable online " "key backup to store encryption keys securely encrypted on the server.")); - updateSecretStatus(); + // updateSecretStatus(); auto scrollArea_ = new QScrollArea{this}; scrollArea_->setFrameShape(QFrame::NoFrame); diff --git a/src/encryption/SelfVerificationStatus.cpp b/src/encryption/SelfVerificationStatus.cpp
index ebb6b548..c4f4f196 100644 --- a/src/encryption/SelfVerificationStatus.cpp +++ b/src/encryption/SelfVerificationStatus.cpp
@@ -4,6 +4,8 @@ #include "SelfVerificationStatus.h" +#include <QApplication> + #include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" @@ -18,13 +20,13 @@ SelfVerificationStatus::SelfVerificationStatus(QObject *o) : QObject(o) { - connect(MainWindow::instance(), &MainWindow::reload, this, [this] { + connect(ChatPage::instance(), &ChatPage::contentLoaded, this, [this] { connect(cache::client(), &Cache::selfVerificationStatusChanged, this, &SelfVerificationStatus::invalidate, Qt::UniqueConnection); - invalidate(); + cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()}); }); } @@ -82,7 +84,7 @@ SelfVerificationStatus::setupCrosssigning(bool useSSSS, QString password, bool u ssss = olm::client()->create_ssss_key(password.toStdString()); if (!ssss) { nhlog::crypto()->critical("Failed to setup secure server side secret storage!"); - emit setupFailed(tr("Failed to create keys secure server side secret storage!")); + emit setupFailed(tr("Failed to create keys for secure server side secret storage!")); return; } @@ -259,15 +261,29 @@ SelfVerificationStatus::invalidate() using namespace mtx::secret_storage; nhlog::db()->info("Invalidating self verification status"); + if (!cache::isInitialized()) { + nhlog::db()->warn("SelfVerificationStatus: cache not initialized"); + return; + } + this->hasSSSS_ = false; emit hasSSSSChanged(); auto keys = cache::client()->userKeys(http::client()->user_id().to_string()); if (!keys || keys->device_keys.find(http::client()->device_id()) == keys->device_keys.end()) { + if (keys && (keys->seen_device_ids.count(http::client()->device_id()) || + keys->seen_device_keys.count(olm::client()->identity_keys().curve25519))) { + emit ChatPage::instance()->dropToLoginPageCb( + tr("Identity key changed. This breaks E2EE, so logging out.")); + return; + } + cache::client()->markUserKeysOutOfDate({http::client()->user_id().to_string()}); - cache::client()->query_keys(http::client()->user_id().to_string(), - [](const UserKeyCache &, mtx::http::RequestErr) {}); - return; + + QTimer::singleShot(1'000, [] { + cache::client()->query_keys(http::client()->user_id().to_string(), + [](const UserKeyCache &, mtx::http::RequestErr) {}); + }); } if (keys->master_keys.keys.empty()) { diff --git a/src/main.cpp b/src/main.cpp
index f6373d2a..f2364c59 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -174,7 +174,7 @@ main(int argc, char *argv[]) SingleApplication::Mode::ExcludeAppVersion | SingleApplication::Mode::SecondaryNotification, 100, - userdata); + userdata == "default" ? "" : userdata); QCommandLineParser parser; parser.addHelpOption(); @@ -199,8 +199,9 @@ main(int argc, char *argv[]) // This check needs to happen _after_ process(), so that we actually print help for --help when // Nheko is already running. if (app.isSecondary()) { - nhlog::ui()->info("Sending Matrix URL to main application: {}", matrixUri.toStdString()); - // open uri in main instance + std::cout << "Sending Matrix URL to main application: " << matrixUri.toStdString() + << std::endl; + // open uri in main instance app.sendMessage(matrixUri.toUtf8()); return 0; } diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index a1f4c67f..7bb985f8 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -447,7 +447,8 @@ EventStore::edits(const std::string &event_id) auto event_ids = cache::client()->relatedEvents(room_id_, event_id); auto original_event = get(event_id, "", false, false); - if (!original_event) + if (!original_event || + std::holds_alternative<mtx::events::RoomEvent<mtx::events::msg::Redacted>>(*original_event)) return {}; auto original_sender = mtx::accessors::sender(*original_event); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 9b857dcf..1730f9fd 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -77,6 +77,7 @@ public: mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); QVariantList reactions(const std::string &event_id); + std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id); olm::DecryptionErrorCode decryptionError(std::string id); void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev, bool manual); @@ -120,7 +121,6 @@ public slots: void enableKeyRequests(bool suppressKeyRequests_); private: - std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id); olm::DecryptionResult *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 0e5ce510..aa7a68f3 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -344,6 +344,19 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }, Qt::QueuedConnection); + connect(this, &TimelineModel::dataAtIdChanged, this, [this](QString id) { + relatedEventCacheBuster++; + + auto idx = idToIndex(id); + if (idx != -1) { + auto pos = index(idx); + nhlog::ui()->debug("data changed at {}", id.toStdString()); + emit dataChanged(pos, pos); + } else { + nhlog::ui()->debug("id not found {}", id.toStdString()); + } + }); + connect(this, &TimelineModel::newMessageToSend, this, @@ -1095,7 +1108,8 @@ TimelineModel::showReadReceipts(QString id) void TimelineModel::redactEvent(QString id) { - if (!id.isEmpty()) + if (!id.isEmpty()) { + auto edits = events.edits(id.toStdString()); http::client()->redact_event( room_id_.toStdString(), id.toStdString(), @@ -1106,8 +1120,26 @@ TimelineModel::redactEvent(QString id) return; } - emit eventRedacted(id); + emit dataAtIdChanged(id); }); + + // redact all edits to prevent leaks + for (const auto &e : edits) { + auto id_ = mtx::accessors::event_id(e); + http::client()->redact_event( + room_id_.toStdString(), + id_, + [this, id, id_](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed(tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit dataAtIdChanged(id); + }); + } + } } int diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index f16529e2..af067476 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -367,9 +367,9 @@ private slots: void scrollTimerEvent(); signals: + void dataAtIdChanged(QString id); void currentIndexChanged(int index); void redactionFailed(QString id); - void eventRedacted(QString id); void mediaCached(QString mxcUrl, QString cacheUrl); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void typingUsersChanged(std::vector<QString> users); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 94e6a0d7..84aa2c90 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -248,7 +248,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_); qmlRegisterSingletonType<SelfVerificationStatus>( "im.nheko", 1, 0, "SelfVerificationStatus", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new SelfVerificationStatus(); + auto ptr = new SelfVerificationStatus(); + QObject::connect(ChatPage::instance(), + &ChatPage::initializeEmptyViews, + ptr, + &SelfVerificationStatus::invalidate); + return ptr; }); qRegisterMetaType<mtx::events::collections::TimelineEvents>(); diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index b5a16f43..72b1d8e6 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp
@@ -188,6 +188,7 @@ UserProfile::fetchDeviceList(const QString &userID) nhlog::net()->warn("failed to query device keys: {},{}", mtx::errors::to_string(err->matrix_error.errcode), static_cast<int>(err->status_code)); + return; } // Ensure local key cache is up to date @@ -201,6 +202,7 @@ UserProfile::fetchDeviceList(const QString &userID) nhlog::net()->warn("failed to query device keys: {},{}", mtx::errors::to_string(err->matrix_error.errcode), static_cast<int>(err->status_code)); + return; } emit verificationStatiChanged();