diff options
author | Nicolas Werner <nicolas.werner@hotmail.de> | 2021-09-18 00:22:33 +0200 |
---|---|---|
committer | Nicolas Werner <nicolas.werner@hotmail.de> | 2021-09-18 00:45:50 +0200 |
commit | cfca7157b98c9dc8e0852fe6484bc3f75008af7d (patch) | |
tree | 32b92340908a9374214ec7b84c1fac7ea338f56d | |
parent | Merge pull request #728 from Thulinma/goto (diff) | |
download | nheko-cfca7157b98c9dc8e0852fe6484bc3f75008af7d.tar.xz |
Change indentation to 4 spaces
165 files changed, 24152 insertions, 24981 deletions
diff --git a/.clang-format b/.clang-format index 059aee19..b5e2f017 100644 --- a/.clang-format +++ b/.clang-format @@ -1,14 +1,14 @@ --- Language: Cpp -Standard: Cpp11 -AccessModifierOffset: -8 +Standard: c++17 +AccessModifierOffset: -4 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: true AllowShortFunctionsOnASingleLine: true BasedOnStyle: Mozilla ColumnLimit: 100 IndentCaseLabels: false -IndentWidth: 8 +IndentWidth: 4 KeepEmptyLinesAtTheStartOfBlocks: false PointerAlignment: Right Cpp11BracedListStyle: true diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index b9962cef..177bf903 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -22,45 +22,44 @@ namespace AvatarProvider { void resolve(QString avatarUrl, int size, QObject *receiver, AvatarCallback callback) { - const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); + const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); - QPixmap pixmap; - if (avatarUrl.isEmpty()) { - callback(pixmap); - return; - } + QPixmap pixmap; + if (avatarUrl.isEmpty()) { + callback(pixmap); + return; + } - if (avatar_cache.find(cacheKey, &pixmap)) { - callback(pixmap); - return; - } + if (avatar_cache.find(cacheKey, &pixmap)) { + callback(pixmap); + return; + } - MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")), - QSize(size, size), - [callback, cacheKey, recv = QPointer<QObject>(receiver)]( - QString, QSize, QImage img, QString) { - if (!recv) - return; + MxcImageProvider::download(avatarUrl.remove(QStringLiteral("mxc://")), + QSize(size, size), + [callback, cacheKey, recv = QPointer<QObject>(receiver)]( + QString, QSize, QImage img, QString) { + if (!recv) + return; - auto proxy = std::make_shared<AvatarProxy>(); - QObject::connect(proxy.get(), - &AvatarProxy::avatarDownloaded, - recv, - [callback, cacheKey](QPixmap pm) { - if (!pm.isNull()) - avatar_cache.insert( - cacheKey, pm); - callback(pm); - }); + auto proxy = std::make_shared<AvatarProxy>(); + QObject::connect(proxy.get(), + &AvatarProxy::avatarDownloaded, + recv, + [callback, cacheKey](QPixmap pm) { + if (!pm.isNull()) + avatar_cache.insert(cacheKey, pm); + callback(pm); + }); - if (img.isNull()) { - emit proxy->avatarDownloaded(QPixmap{}); - return; - } + if (img.isNull()) { + emit proxy->avatarDownloaded(QPixmap{}); + return; + } - auto pm = QPixmap::fromImage(std::move(img)); - emit proxy->avatarDownloaded(pm); - }); + auto pm = QPixmap::fromImage(std::move(img)); + emit proxy->avatarDownloaded(pm); + }); } void @@ -70,8 +69,8 @@ resolve(const QString &room_id, QObject *receiver, AvatarCallback callback) { - auto avatarUrl = cache::avatarUrl(room_id, user_id); + auto avatarUrl = cache::avatarUrl(room_id, user_id); - resolve(std::move(avatarUrl), size, receiver, callback); + resolve(std::move(avatarUrl), size, receiver, callback); } } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h index 173a2fba..efd0f330 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h @@ -12,10 +12,10 @@ using AvatarCallback = std::function<void(QPixmap)>; class AvatarProxy : public QObject { - Q_OBJECT + Q_OBJECT signals: - void avatarDownloaded(QPixmap pm); + void avatarDownloaded(QPixmap pm); }; namespace AvatarProvider { diff --git a/src/BlurhashProvider.cpp b/src/BlurhashProvider.cpp index aef618a2..e905474a 100644 --- a/src/BlurhashProvider.cpp +++ b/src/BlurhashProvider.cpp @@ -13,33 +13,33 @@ void BlurhashResponse::run() { - if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) { - m_error = QStringLiteral("Blurhash needs size request"); - emit finished(); - return; - } - if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) { - m_image = QImage(m_requestedSize, QImage::Format_RGB32); - m_image.fill(QColor(0, 0, 0)); - emit finished(); - return; - } - - auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(), - m_requestedSize.width(), - m_requestedSize.height()); - if (decoded.image.empty()) { - m_error = QStringLiteral("Failed decode!"); - emit finished(); - return; - } - - QImage image(decoded.image.data(), - (int)decoded.width, - (int)decoded.height, - (int)decoded.width * 3, - QImage::Format_RGB888); - - m_image = image.copy(); + if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) { + m_error = QStringLiteral("Blurhash needs size request"); emit finished(); + return; + } + if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) { + m_image = QImage(m_requestedSize, QImage::Format_RGB32); + m_image.fill(QColor(0, 0, 0)); + emit finished(); + return; + } + + auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(), + m_requestedSize.width(), + m_requestedSize.height()); + if (decoded.image.empty()) { + m_error = QStringLiteral("Failed decode!"); + emit finished(); + return; + } + + QImage image(decoded.image.data(), + (int)decoded.width, + (int)decoded.height, + (int)decoded.width * 3, + QImage::Format_RGB888); + + m_image = image.copy(); + emit finished(); } diff --git a/src/BlurhashProvider.h b/src/BlurhashProvider.h index ee89302c..1c8351f2 100644 --- a/src/BlurhashProvider.h +++ b/src/BlurhashProvider.h @@ -15,41 +15,41 @@ class BlurhashResponse , public QRunnable { public: - BlurhashResponse(const QString &id, const QSize &requestedSize) + BlurhashResponse(const QString &id, const QSize &requestedSize) - : m_id(id) - , m_requestedSize(requestedSize) - { - setAutoDelete(false); - } + : m_id(id) + , m_requestedSize(requestedSize) + { + setAutoDelete(false); + } - QQuickTextureFactory *textureFactory() const override - { - return QQuickTextureFactory::textureFactoryForImage(m_image); - } - QString errorString() const override { return m_error; } + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } - void run() override; + void run() override; - QString m_id, m_error; - QSize m_requestedSize; - QImage m_image; + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; }; class BlurhashProvider : public QObject , public QQuickAsyncImageProvider { - Q_OBJECT + Q_OBJECT public slots: - QQuickImageResponse *requestImageResponse(const QString &id, - const QSize &requestedSize) override - { - BlurhashResponse *response = new BlurhashResponse(id, requestedSize); - pool.start(response); - return response; - } + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + BlurhashResponse *response = new BlurhashResponse(id, requestedSize); + pool.start(response); + return response; + } private: - QThreadPool pool; + QThreadPool pool; }; diff --git a/src/Cache.cpp b/src/Cache.cpp index 9ebc61b9..b124fe5e 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -97,55 +97,55 @@ std::unique_ptr<Cache> instance_ = nullptr; struct RO_txn { - ~RO_txn() { txn.reset(); } - operator MDB_txn *() const noexcept { return txn.handle(); } - operator lmdb::txn &() noexcept { return txn; } + ~RO_txn() { txn.reset(); } + operator MDB_txn *() const noexcept { return txn.handle(); } + operator lmdb::txn &() noexcept { return txn; } - lmdb::txn &txn; + lmdb::txn &txn; }; RO_txn ro_txn(lmdb::env &env) { - thread_local lmdb::txn txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); - thread_local int reuse_counter = 0; + thread_local lmdb::txn txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); + thread_local int reuse_counter = 0; - if (reuse_counter >= 100 || txn.env() != env.handle()) { - txn.abort(); - txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); - reuse_counter = 0; - } else if (reuse_counter > 0) { - try { - txn.renew(); - } catch (...) { - txn.abort(); - txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); - reuse_counter = 0; - } + if (reuse_counter >= 100 || txn.env() != env.handle()) { + txn.abort(); + txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); + reuse_counter = 0; + } else if (reuse_counter > 0) { + try { + txn.renew(); + } catch (...) { + txn.abort(); + txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); + reuse_counter = 0; } - reuse_counter++; + } + reuse_counter++; - return RO_txn{txn}; + return RO_txn{txn}; } template<class T> bool containsStateUpdates(const T &e) { - return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e); + return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e); } bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - return std::holds_alternative<StrippedEvent<state::Avatar>>(e) || - std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) || - std::holds_alternative<StrippedEvent<Name>>(e) || - std::holds_alternative<StrippedEvent<Member>>(e) || - std::holds_alternative<StrippedEvent<Topic>>(e); + return std::holds_alternative<StrippedEvent<state::Avatar>>(e) || + std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) || + std::holds_alternative<StrippedEvent<Name>>(e) || + std::holds_alternative<StrippedEvent<Member>>(e) || + std::holds_alternative<StrippedEvent<Topic>>(e); } bool @@ -153,45 +153,45 @@ Cache::isHiddenEvent(lmdb::txn &txn, mtx::events::collections::TimelineEvents e, const std::string &room_id) { - using namespace mtx::events; - - // Always hide edits - if (mtx::accessors::relations(e).replaces()) - return true; - - if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { - MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = encryptedEvent->content.session_id; - index.sender_key = encryptedEvent->content.sender_key; + using namespace mtx::events; - auto result = olm::decryptEvent(index, *encryptedEvent, true); - if (!result.error) - e = result.event.value(); - } + // Always hide edits + if (mtx::accessors::relations(e).replaces()) + return true; - mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents; - hiddenEvents.hidden_event_types = { - EventType::Reaction, EventType::CallCandidates, EventType::Unsupported}; - - if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) - hiddenEvents = - std::move(std::get<mtx::events::AccountDataEvent< - mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp) - .content); - if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id)) - hiddenEvents = - std::move(std::get<mtx::events::AccountDataEvent< - mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp) - .content); - - return std::visit( - [hiddenEvents](const auto &ev) { - return std::any_of(hiddenEvents.hidden_event_types.begin(), - hiddenEvents.hidden_event_types.end(), - [ev](EventType type) { return type == ev.type; }); - }, - e); + if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = encryptedEvent->content.session_id; + index.sender_key = encryptedEvent->content.sender_key; + + auto result = olm::decryptEvent(index, *encryptedEvent, true); + if (!result.error) + e = result.event.value(); + } + + mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents; + hiddenEvents.hidden_event_types = { + EventType::Reaction, EventType::CallCandidates, EventType::Unsupported}; + + if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) + hiddenEvents = + std::move(std::get<mtx::events::AccountDataEvent< + mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp) + .content); + if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id)) + hiddenEvents = + std::move(std::get<mtx::events::AccountDataEvent< + mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp) + .content); + + return std::visit( + [hiddenEvents](const auto &ev) { + return std::any_of(hiddenEvents.hidden_event_types.begin(), + hiddenEvents.hidden_event_types.end(), + [ev](EventType type) { return type == ev.type; }); + }, + e); } Cache::Cache(const QString &userId, QObject *parent) @@ -199,218 +199,210 @@ Cache::Cache(const QString &userId, QObject *parent) , env_{nullptr} , localUserId_{userId} { - setup(); - connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); + setup(); + connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); } void Cache::setup() { - auto settings = UserSettings::instance(); + auto settings = UserSettings::instance(); - nhlog::db()->debug("setting up cache"); + nhlog::db()->debug("setting up cache"); - // Previous location of the cache directory - auto oldCache = QString("%1/%2%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())) - .arg(QString::fromUtf8(settings->profile().toUtf8().toHex())); + // Previous location of the cache directory + auto oldCache = QString("%1/%2%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())) + .arg(QString::fromUtf8(settings->profile().toUtf8().toHex())); - cacheDirectory_ = QString("%1/%2%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)) - .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())) - .arg(QString::fromUtf8(settings->profile().toUtf8().toHex())); + cacheDirectory_ = QString("%1/%2%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)) + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())) + .arg(QString::fromUtf8(settings->profile().toUtf8().toHex())); - bool isInitial = !QFile::exists(cacheDirectory_); + bool isInitial = !QFile::exists(cacheDirectory_); - // NOTE: If both cache directories exist it's better to do nothing: it - // could mean a previous migration failed or was interrupted. - bool needsMigration = isInitial && QFile::exists(oldCache); + // NOTE: If both cache directories exist it's better to do nothing: it + // could mean a previous migration failed or was interrupted. + bool needsMigration = isInitial && QFile::exists(oldCache); - if (needsMigration) { - nhlog::db()->info("found old state directory, migrating"); - if (!QDir().rename(oldCache, cacheDirectory_)) { - throw std::runtime_error(("Unable to migrate the old state directory (" + - oldCache + ") to the new location (" + - cacheDirectory_ + ")") - .toStdString() - .c_str()); - } - nhlog::db()->info("completed state migration"); + if (needsMigration) { + nhlog::db()->info("found old state directory, migrating"); + if (!QDir().rename(oldCache, cacheDirectory_)) { + throw std::runtime_error(("Unable to migrate the old state directory (" + oldCache + + ") to the new location (" + cacheDirectory_ + ")") + .toStdString() + .c_str()); } + nhlog::db()->info("completed state migration"); + } - env_ = lmdb::env::create(); - env_.set_mapsize(DB_SIZE); - env_.set_max_dbs(MAX_DBS); + env_ = lmdb::env::create(); + env_.set_mapsize(DB_SIZE); + env_.set_max_dbs(MAX_DBS); - if (isInitial) { - nhlog::db()->info("initializing LMDB"); + if (isInitial) { + nhlog::db()->info("initializing LMDB"); - if (!QDir().mkpath(cacheDirectory_)) { - throw std::runtime_error( - ("Unable to create state directory:" + cacheDirectory_) - .toStdString() - .c_str()); - } + if (!QDir().mkpath(cacheDirectory_)) { + throw std::runtime_error( + ("Unable to create state directory:" + cacheDirectory_).toStdString().c_str()); } + } - try { - // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but - // it can really mess up our database, so we shouldn't. For now, hopefully - // NOMETASYNC is fast enough. - env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC); - } catch (const lmdb::error &e) { - if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { - throw std::runtime_error("LMDB initialization failed" + - std::string(e.what())); - } + try { + // NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but + // it can really mess up our database, so we shouldn't. For now, hopefully + // NOMETASYNC is fast enough. + env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC); + } catch (const lmdb::error &e) { + if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { + throw std::runtime_error("LMDB initialization failed" + std::string(e.what())); + } - nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); + nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); - QDir stateDir(cacheDirectory_); + QDir stateDir(cacheDirectory_); - for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { - if (!stateDir.remove(file)) - throw std::runtime_error( - ("Unable to delete file " + file).toStdString().c_str()); - } - env_.open(cacheDirectory_.toStdString().c_str()); + for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { + if (!stateDir.remove(file)) + throw std::runtime_error(("Unable to delete file " + file).toStdString().c_str()); } + env_.open(cacheDirectory_.toStdString().c_str()); + } - auto txn = lmdb::txn::begin(env_); - syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); - roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); - spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT); - spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT); - invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); - notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); - - // Device management - devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); - deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); - - // Session management - inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); - outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); - megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); - - // What rooms are encrypted - encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - [[maybe_unused]] auto verificationDb = getVerificationDb(txn); - [[maybe_unused]] auto userKeysDb = getUserKeysDb(txn); + auto txn = lmdb::txn::begin(env_); + syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); + roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); + spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT); + spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT); + invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); + notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); - txn.commit(); + // Device management + devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); + deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); + + // Session management + inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); + + // What rooms are encrypted + encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + [[maybe_unused]] auto verificationDb = getVerificationDb(txn); + [[maybe_unused]] auto userKeysDb = getUserKeysDb(txn); - databaseReady_ = true; + txn.commit(); + + databaseReady_ = true; } void Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { - nhlog::db()->info("mark room {} as encrypted", room_id); + nhlog::db()->info("mark room {} as encrypted", room_id); - encryptedRooms_.put(txn, room_id, "0"); + encryptedRooms_.put(txn, room_id, "0"); } bool Cache::isRoomEncrypted(const std::string &room_id) { - std::string_view unused; + std::string_view unused; - auto txn = ro_txn(env_); - auto res = encryptedRooms_.get(txn, room_id, unused); + auto txn = ro_txn(env_); + auto res = encryptedRooms_.get(txn, room_id, unused); - return res; + return res; } std::optional<mtx::events::state::Encryption> Cache::roomEncryptionSettings(const std::string &room_id) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - try { - auto txn = ro_txn(env_); - auto statesdb = getStatesDb(txn, room_id); - std::string_view event; - bool res = - statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event); - - if (res) { - try { - StateEvent<Encryption> msg = json::parse(event); - - return msg.content; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.encryption event: {}", - e.what()); - return Encryption{}; - } - } - } catch (lmdb::error &) { + try { + auto txn = ro_txn(env_); + auto statesdb = getStatesDb(txn, room_id); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event); + + if (res) { + try { + StateEvent<Encryption> msg = json::parse(event); + + return msg.content; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.encryption event: {}", e.what()); + return Encryption{}; + } } + } catch (lmdb::error &) { + } - return std::nullopt; + return std::nullopt; } mtx::crypto::ExportedSessionKeys Cache::exportSessionKeys() { - using namespace mtx::crypto; + using namespace mtx::crypto; - ExportedSessionKeys keys; + ExportedSessionKeys keys; - auto txn = ro_txn(env_); - auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); + auto txn = ro_txn(env_); + auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); - std::string_view key, value; - while (cursor.get(key, value, MDB_NEXT)) { - ExportedSession exported; - MegolmSessionIndex index; + std::string_view key, value; + while (cursor.get(key, value, MDB_NEXT)) { + ExportedSession exported; + MegolmSessionIndex index; - auto saved_session = - unpickle<InboundSessionObject>(std::string(value), pickle_secret_); + auto saved_session = unpickle<InboundSessionObject>(std::string(value), pickle_secret_); - try { - index = nlohmann::json::parse(key).get<MegolmSessionIndex>(); - } catch (const nlohmann::json::exception &e) { - nhlog::db()->critical("failed to export megolm session: {}", e.what()); - continue; - } + try { + index = nlohmann::json::parse(key).get<MegolmSessionIndex>(); + } catch (const nlohmann::json::exception &e) { + nhlog::db()->critical("failed to export megolm session: {}", e.what()); + continue; + } - exported.room_id = index.room_id; - exported.sender_key = index.sender_key; - exported.session_id = index.session_id; - exported.session_key = export_session(saved_session.get(), -1); + exported.room_id = index.room_id; + exported.sender_key = index.sender_key; + exported.session_id = index.session_id; + exported.session_key = export_session(saved_session.get(), -1); - keys.sessions.push_back(exported); - } + keys.sessions.push_back(exported); + } - cursor.close(); + cursor.close(); - return keys; + return keys; } void Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) { - for (const auto &s : keys.sessions) { - MegolmSessionIndex index; - index.room_id = s.room_id; - index.session_id = s.session_id; - index.sender_key = s.sender_key; + for (const auto &s : keys.sessions) { + MegolmSessionIndex index; + index.room_id = s.room_id; + index.session_id = s.session_id; + index.sender_key = s.sender_key; - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain; - if (s.sender_claimed_keys.count("ed25519")) - data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519"); + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain; + if (s.sender_claimed_keys.count("ed25519")) + data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519"); - auto exported_session = mtx::crypto::import_session(s.session_key); + auto exported_session = mtx::crypto::import_session(s.session_key); - saveInboundMegolmSession(index, std::move(exported_session), data); - ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); - } + saveInboundMegolmSession(index, std::move(exported_session), data); + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); + } } // @@ -422,67 +414,64 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session, const GroupSessionData &data) { - using namespace mtx::crypto; - const auto key = json(index).dump(); - const auto pickled = pickle<InboundSessionObject>(session.get(), pickle_secret_); + using namespace mtx::crypto; + const auto key = json(index).dump(); + const auto pickled = pickle<InboundSessionObject>(session.get(), pickle_secret_); - auto txn = lmdb::txn::begin(env_); + auto txn = lmdb::txn::begin(env_); - std::string_view value; - if (inboundMegolmSessionDb_.get(txn, key, value)) { - auto oldSession = - unpickle<InboundSessionObject>(std::string(value), pickle_secret_); - if (olm_inbound_group_session_first_known_index(session.get()) > - olm_inbound_group_session_first_known_index(oldSession.get())) { - nhlog::crypto()->warn( - "Not storing inbound session with newer first known index"); - return; - } + std::string_view value; + if (inboundMegolmSessionDb_.get(txn, key, value)) { + auto oldSession = unpickle<InboundSessionObject>(std::string(value), pickle_secret_); + if (olm_inbound_group_session_first_known_index(session.get()) > + olm_inbound_group_session_first_known_index(oldSession.get())) { + nhlog::crypto()->warn("Not storing inbound session with newer first known index"); + return; } + } - inboundMegolmSessionDb_.put(txn, key, pickled); - megolmSessionDataDb_.put(txn, key, json(data).dump()); - txn.commit(); + inboundMegolmSessionDb_.put(txn, key, pickled); + megolmSessionDataDb_.put(txn, key, json(data).dump()); + txn.commit(); } mtx::crypto::InboundGroupSessionPtr Cache::getInboundMegolmSession(const MegolmSessionIndex &index) { - using namespace mtx::crypto; + using namespace mtx::crypto; - try { - auto txn = ro_txn(env_); - std::string key = json(index).dump(); - std::string_view value; - - if (inboundMegolmSessionDb_.get(txn, key, value)) { - auto session = - unpickle<InboundSessionObject>(std::string(value), pickle_secret_); - return session; - } - } catch (std::exception &e) { - nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); + try { + auto txn = ro_txn(env_); + std::string key = json(index).dump(); + std::string_view value; + + if (inboundMegolmSessionDb_.get(txn, key, value)) { + auto session = unpickle<InboundSessionObject>(std::string(value), pickle_secret_); + return session; } + } catch (std::exception &e) { + nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); + } - return nullptr; + return nullptr; } bool Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) { - using namespace mtx::crypto; + using namespace mtx::crypto; - try { - auto txn = ro_txn(env_); - std::string key = json(index).dump(); - std::string_view value; + try { + auto txn = ro_txn(env_); + std::string key = json(index).dump(); + std::string_view value; - return inboundMegolmSessionDb_.get(txn, key, value); - } catch (std::exception &e) { - nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); - } + return inboundMegolmSessionDb_.get(txn, key, value); + } catch (std::exception &e) { + nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); + } - return false; + return false; } void @@ -490,42 +479,42 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &ptr) { - using namespace mtx::crypto; + using namespace mtx::crypto; - if (!outboundMegolmSessionExists(room_id)) - return; + if (!outboundMegolmSessionExists(room_id)) + return; - GroupSessionData data = data_; - data.message_index = olm_outbound_group_session_message_index(ptr.get()); - MegolmSessionIndex index; - index.room_id = room_id; - index.sender_key = olm::client()->identity_keys().ed25519; - index.session_id = mtx::crypto::session_id(ptr.get()); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(ptr.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ptr.get()); - // Save the updated pickled data for the session. - json j; - j["session"] = pickle<OutboundSessionObject>(ptr.get(), pickle_secret_); + // Save the updated pickled data for the session. + json j; + j["session"] = pickle<OutboundSessionObject>(ptr.get(), pickle_secret_); - auto txn = lmdb::txn::begin(env_); - outboundMegolmSessionDb_.put(txn, room_id, j.dump()); - megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); + txn.commit(); } void Cache::dropOutboundMegolmSession(const std::string &room_id) { - using namespace mtx::crypto; + using namespace mtx::crypto; - if (!outboundMegolmSessionExists(room_id)) - return; + if (!outboundMegolmSessionExists(room_id)) + return; - { - auto txn = lmdb::txn::begin(env_); - outboundMegolmSessionDb_.del(txn, room_id); - // don't delete session data, so that we can still share the session. - txn.commit(); - } + { + auto txn = lmdb::txn::begin(env_); + outboundMegolmSessionDb_.del(txn, room_id); + // don't delete session data, so that we can still share the session. + txn.commit(); + } } void @@ -533,86 +522,86 @@ Cache::saveOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &session) { - using namespace mtx::crypto; - const auto pickled = pickle<OutboundSessionObject>(session.get(), pickle_secret_); + using namespace mtx::crypto; + const auto pickled = pickle<OutboundSessionObject>(session.get(), pickle_secret_); - GroupSessionData data = data_; - data.message_index = olm_outbound_group_session_message_index(session.get()); - MegolmSessionIndex index; - index.room_id = room_id; - index.sender_key = olm::client()->identity_keys().ed25519; - index.session_id = mtx::crypto::session_id(session.get()); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(session.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(session.get()); - json j; - j["session"] = pickled; + json j; + j["session"] = pickled; - auto txn = lmdb::txn::begin(env_); - outboundMegolmSessionDb_.put(txn, room_id, j.dump()); - megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); + txn.commit(); } bool Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept { - try { - auto txn = ro_txn(env_); - std::string_view value; - return outboundMegolmSessionDb_.get(txn, room_id, value); - } catch (std::exception &e) { - nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); - return false; - } + try { + auto txn = ro_txn(env_); + std::string_view value; + return outboundMegolmSessionDb_.get(txn, room_id, value); + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); + return false; + } } OutboundGroupSessionDataRef Cache::getOutboundMegolmSession(const std::string &room_id) { - try { - using namespace mtx::crypto; - - auto txn = ro_txn(env_); - std::string_view value; - outboundMegolmSessionDb_.get(txn, room_id, value); - auto obj = json::parse(value); + try { + using namespace mtx::crypto; - OutboundGroupSessionDataRef ref{}; - ref.session = unpickle<OutboundSessionObject>(obj.at("session"), pickle_secret_); + auto txn = ro_txn(env_); + std::string_view value; + outboundMegolmSessionDb_.get(txn, room_id, value); + auto obj = json::parse(value); - MegolmSessionIndex index; - index.room_id = room_id; - index.sender_key = olm::client()->identity_keys().ed25519; - index.session_id = mtx::crypto::session_id(ref.session.get()); + OutboundGroupSessionDataRef ref{}; + ref.session = unpickle<OutboundSessionObject>(obj.at("session"), pickle_secret_); - if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { - ref.data = nlohmann::json::parse(value).get<GroupSessionData>(); - } + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ref.session.get()); - return ref; - } catch (std::exception &e) { - nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); - return {}; + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + ref.data = nlohmann::json::parse(value).get<GroupSessionData>(); } + + return ref; + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); + return {}; + } } std::optional<GroupSessionData> Cache::getMegolmSessionData(const MegolmSessionIndex &index) { - try { - using namespace mtx::crypto; - - auto txn = ro_txn(env_); + try { + using namespace mtx::crypto; - std::string_view value; - if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { - return nlohmann::json::parse(value).get<GroupSessionData>(); - } + auto txn = ro_txn(env_); - return std::nullopt; - } catch (std::exception &e) { - nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); - return std::nullopt; + std::string_view value; + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + return nlohmann::json::parse(value).get<GroupSessionData>(); } + + return std::nullopt; + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); + return std::nullopt; + } } // // OLM sessions. @@ -623,1013 +612,972 @@ Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session, uint64_t timestamp) { - using namespace mtx::crypto; + using namespace mtx::crypto; - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); - const auto pickled = pickle<SessionObject>(session.get(), pickle_secret_); - const auto session_id = mtx::crypto::session_id(session.get()); + const auto pickled = pickle<SessionObject>(session.get(), pickle_secret_); + const auto session_id = mtx::crypto::session_id(session.get()); - StoredOlmSession stored_session; - stored_session.pickled_session = pickled; - stored_session.last_message_ts = timestamp; + StoredOlmSession stored_session; + stored_session.pickled_session = pickled; + stored_session.last_message_ts = timestamp; - db.put(txn, session_id, json(stored_session).dump()); + db.put(txn, session_id, json(stored_session).dump()); - txn.commit(); + txn.commit(); } std::optional<mtx::crypto::OlmSessionPtr> Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) { - using namespace mtx::crypto; + using namespace mtx::crypto; - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); - std::string_view pickled; - bool found = db.get(txn, session_id, pickled); + std::string_view pickled; + bool found = db.get(txn, session_id, pickled); - txn.commit(); + txn.commit(); - if (found) { - auto data = json::parse(pickled).get<StoredOlmSession>(); - return unpickle<SessionObject>(data.pickled_session, pickle_secret_); - } + if (found) { + auto data = json::parse(pickled).get<StoredOlmSession>(); + return unpickle<SessionObject>(data.pickled_session, pickle_secret_); + } - return std::nullopt; + return std::nullopt; } std::optional<mtx::crypto::OlmSessionPtr> Cache::getLatestOlmSession(const std::string &curve25519) { - using namespace mtx::crypto; + using namespace mtx::crypto; - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); - std::string_view session_id, pickled_session; + std::string_view session_id, pickled_session; - std::optional<StoredOlmSession> currentNewest; + std::optional<StoredOlmSession> currentNewest; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(session_id, pickled_session, MDB_NEXT)) { - auto data = json::parse(pickled_session).get<StoredOlmSession>(); - if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts) - currentNewest = data; - } - cursor.close(); + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(session_id, pickled_session, MDB_NEXT)) { + auto data = json::parse(pickled_session).get<StoredOlmSession>(); + if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts) + currentNewest = data; + } + cursor.close(); - txn.commit(); + txn.commit(); - return currentNewest ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session, - pickle_secret_)) - : std::nullopt; + return currentNewest ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session, + pickle_secret_)) + : std::nullopt; } std::vector<std::string> Cache::getOlmSessions(const std::string &curve25519) { - using namespace mtx::crypto; + using namespace mtx::crypto; - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); - std::string_view session_id, unused; - std::vector<std::string> res; + std::string_view session_id, unused; + std::vector<std::string> res; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(session_id, unused, MDB_NEXT)) - res.emplace_back(session_id); - cursor.close(); + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(session_id, unused, MDB_NEXT)) + res.emplace_back(session_id); + cursor.close(); - txn.commit(); + txn.commit(); - return res; + return res; } void Cache::saveOlmAccount(const std::string &data) { - auto txn = lmdb::txn::begin(env_); - syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data); + txn.commit(); } std::string Cache::restoreOlmAccount() { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::string_view pickled; - syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled); + std::string_view pickled; + syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled); - return std::string(pickled.data(), pickled.size()); + return std::string(pickled.data(), pickled.size()); } void Cache::saveBackupVersion(const OnlineBackupVersion &data) { - auto txn = lmdb::txn::begin(env_); - syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump()); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump()); + txn.commit(); } void Cache::deleteBackupVersion() { - auto txn = lmdb::txn::begin(env_); - syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION); + txn.commit(); } std::optional<OnlineBackupVersion> Cache::backupVersion() { - try { - auto txn = ro_txn(env_); - std::string_view v; - syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v); + try { + auto txn = ro_txn(env_); + std::string_view v; + syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v); - return nlohmann::json::parse(v).get<OnlineBackupVersion>(); - } catch (...) { - return std::nullopt; - } + return nlohmann::json::parse(v).get<OnlineBackupVersion>(); + } catch (...) { + return std::nullopt; + } } 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 Secrets 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")); + 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 Secrets 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); + 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(); + 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()); + 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)); + 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(); + // 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); + 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; - } + 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(); + 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; - } + 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_; + return pickle_secret_; } void Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) { - invitesDb_.del(txn, room_id); - getInviteStatesDb(txn, room_id).drop(txn, true); - getInviteMembersDb(txn, room_id).drop(txn, true); + invitesDb_.del(txn, room_id); + getInviteStatesDb(txn, room_id).drop(txn, true); + getInviteMembersDb(txn, room_id).drop(txn, true); } void Cache::removeInvite(const std::string &room_id) { - auto txn = lmdb::txn::begin(env_); - removeInvite(txn, room_id); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + removeInvite(txn, room_id); + txn.commit(); } void Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) { - roomsDb_.del(txn, roomid); - getStatesDb(txn, roomid).drop(txn, true); - getAccountDataDb(txn, roomid).drop(txn, true); - getMembersDb(txn, roomid).drop(txn, true); + roomsDb_.del(txn, roomid); + getStatesDb(txn, roomid).drop(txn, true); + getAccountDataDb(txn, roomid).drop(txn, true); + getMembersDb(txn, roomid).drop(txn, true); } void Cache::removeRoom(const std::string &roomid) { - auto txn = lmdb::txn::begin(env_, nullptr, 0); - roomsDb_.del(txn, roomid); - txn.commit(); + auto txn = lmdb::txn::begin(env_, nullptr, 0); + roomsDb_.del(txn, roomid); + txn.commit(); } void Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) { - syncStateDb_.put(txn, NEXT_BATCH_KEY, token); + syncStateDb_.put(txn, NEXT_BATCH_KEY, token); } bool Cache::isInitialized() { - if (!env_.handle()) - return false; + if (!env_.handle()) + return false; - auto txn = ro_txn(env_); - std::string_view token; + auto txn = ro_txn(env_); + std::string_view token; - bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); + bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); - return res; + return res; } std::string Cache::nextBatchToken() { - if (!env_.handle()) - throw lmdb::error("Env already closed", MDB_INVALID); + if (!env_.handle()) + throw lmdb::error("Env already closed", MDB_INVALID); - auto txn = ro_txn(env_); - std::string_view token; + auto txn = ro_txn(env_); + std::string_view token; - bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); + bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token); - if (result) - return std::string(token.data(), token.size()); - else - return ""; + if (result) + return std::string(token.data(), token.size()); + else + return ""; } void Cache::deleteData() { - this->databaseReady_ = false; - // TODO: We need to remove the env_ while not accepting new requests. - lmdb::dbi_close(env_, syncStateDb_); - lmdb::dbi_close(env_, roomsDb_); - lmdb::dbi_close(env_, invitesDb_); - lmdb::dbi_close(env_, readReceiptsDb_); - lmdb::dbi_close(env_, notificationsDb_); + this->databaseReady_ = false; + // TODO: We need to remove the env_ while not accepting new requests. + lmdb::dbi_close(env_, syncStateDb_); + lmdb::dbi_close(env_, roomsDb_); + lmdb::dbi_close(env_, invitesDb_); + lmdb::dbi_close(env_, readReceiptsDb_); + lmdb::dbi_close(env_, notificationsDb_); - lmdb::dbi_close(env_, devicesDb_); - lmdb::dbi_close(env_, deviceKeysDb_); + lmdb::dbi_close(env_, devicesDb_); + lmdb::dbi_close(env_, deviceKeysDb_); - lmdb::dbi_close(env_, inboundMegolmSessionDb_); - lmdb::dbi_close(env_, outboundMegolmSessionDb_); - lmdb::dbi_close(env_, megolmSessionDataDb_); + lmdb::dbi_close(env_, inboundMegolmSessionDb_); + lmdb::dbi_close(env_, outboundMegolmSessionDb_); + lmdb::dbi_close(env_, megolmSessionDataDb_); - env_.close(); + env_.close(); - verification_storage.status.clear(); + verification_storage.status.clear(); - if (!cacheDirectory_.isEmpty()) { - QDir(cacheDirectory_).removeRecursively(); - nhlog::db()->info("deleted cache files from disk"); - } + if (!cacheDirectory_.isEmpty()) { + QDir(cacheDirectory_).removeRecursively(); + nhlog::db()->info("deleted cache files from disk"); + } - deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1); - deleteSecret(mtx::secret_storage::secrets::cross_signing_master); - deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing); - deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing); - deleteSecret("pickle_secret", true); + deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1); + deleteSecret(mtx::secret_storage::secrets::cross_signing_master); + deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing); + deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing); + deleteSecret("pickle_secret", true); } //! migrates db to the current format bool Cache::runMigrations() { - std::string stored_version; - { - auto txn = ro_txn(env_); - - std::string_view current_version; - bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); - - if (!res) - return false; - - stored_version = std::string(current_version); - } + std::string stored_version; + { + auto txn = ro_txn(env_); - std::vector<std::pair<std::string, std::function<bool()>>> migrations{ - {"2020.05.01", - [this]() { - try { - auto txn = lmdb::txn::begin(env_, nullptr); - auto pending_receipts = - lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); - lmdb::dbi_drop(txn, pending_receipts, true); - txn.commit(); - } catch (const lmdb::error &) { - nhlog::db()->critical( - "Failed to delete pending_receipts database in migration!"); - return false; - } + std::string_view current_version; + bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); - nhlog::db()->info("Successfully deleted pending receipts database."); - return true; - }}, - {"2020.07.05", - [this]() { + if (!res) + return false; + + stored_version = std::string(current_version); + } + + std::vector<std::pair<std::string, std::function<bool()>>> migrations{ + {"2020.05.01", + [this]() { + try { + auto txn = lmdb::txn::begin(env_, nullptr); + auto pending_receipts = lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); + lmdb::dbi_drop(txn, pending_receipts, true); + txn.commit(); + } catch (const lmdb::error &) { + nhlog::db()->critical("Failed to delete pending_receipts database in migration!"); + return false; + } + + nhlog::db()->info("Successfully deleted pending receipts database."); + return true; + }}, + {"2020.07.05", + [this]() { + try { + auto txn = lmdb::txn::begin(env_, nullptr); + auto room_ids = getRoomIds(txn); + + for (const auto &room_id : room_ids) { try { - auto txn = lmdb::txn::begin(env_, nullptr); - auto room_ids = getRoomIds(txn); - - for (const auto &room_id : room_ids) { - try { - auto messagesDb = lmdb::dbi::open( - txn, std::string(room_id + "/messages").c_str()); - - // keep some old messages and batch token - { - auto roomsCursor = - lmdb::cursor::open(txn, messagesDb); - std::string_view ts, stored_message; - bool start = true; - mtx::responses::Timeline oldMessages; - while (roomsCursor.get(ts, - stored_message, - start ? MDB_FIRST - : MDB_NEXT)) { - start = false; - - auto j = json::parse(std::string_view( - stored_message.data(), - stored_message.size())); - - if (oldMessages.prev_batch.empty()) - oldMessages.prev_batch = - j["token"].get<std::string>(); - else if (j["token"] != - oldMessages.prev_batch) - break; - - mtx::events::collections::TimelineEvent - te; - mtx::events::collections::from_json( - j["event"], te); - oldMessages.events.push_back(te.data); - } - // messages were stored in reverse order, so we - // need to reverse them - std::reverse(oldMessages.events.begin(), - oldMessages.events.end()); - // save messages using the new method - auto eventsDb = getEventsDb(txn, room_id); - saveTimelineMessages( - txn, eventsDb, room_id, oldMessages); - } - - // delete old messages db - lmdb::dbi_drop(txn, messagesDb, true); - } catch (std::exception &e) { - nhlog::db()->error( - "While migrating messages from {}, ignoring error {}", - room_id, - e.what()); - } + auto messagesDb = + lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str()); + + // keep some old messages and batch token + { + auto roomsCursor = lmdb::cursor::open(txn, messagesDb); + std::string_view ts, stored_message; + bool start = true; + mtx::responses::Timeline oldMessages; + while ( + roomsCursor.get(ts, stored_message, start ? MDB_FIRST : MDB_NEXT)) { + start = false; + + auto j = json::parse( + std::string_view(stored_message.data(), stored_message.size())); + + if (oldMessages.prev_batch.empty()) + oldMessages.prev_batch = j["token"].get<std::string>(); + else if (j["token"] != oldMessages.prev_batch) + break; + + mtx::events::collections::TimelineEvent te; + mtx::events::collections::from_json(j["event"], te); + oldMessages.events.push_back(te.data); } - txn.commit(); - } catch (const lmdb::error &) { - nhlog::db()->critical( - "Failed to delete messages database in migration!"); - return false; + // messages were stored in reverse order, so we + // need to reverse them + std::reverse(oldMessages.events.begin(), oldMessages.events.end()); + // save messages using the new method + auto eventsDb = getEventsDb(txn, room_id); + saveTimelineMessages(txn, eventsDb, room_id, oldMessages); + } + + // delete old messages db + lmdb::dbi_drop(txn, messagesDb, true); + } catch (std::exception &e) { + nhlog::db()->error( + "While migrating messages from {}, ignoring error {}", room_id, e.what()); } + } + txn.commit(); + } catch (const lmdb::error &) { + nhlog::db()->critical("Failed to delete messages database in migration!"); + return false; + } + + nhlog::db()->info("Successfully deleted pending receipts database."); + return true; + }}, + {"2020.10.20", + [this]() { + try { + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + + auto mainDb = lmdb::dbi::open(txn, nullptr); + + std::string_view dbName, ignored; + auto olmDbCursor = lmdb::cursor::open(txn, mainDb); + while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) { + // skip every db but olm session dbs + nhlog::db()->debug("Db {}", dbName); + if (dbName.find("olm_sessions/") != 0) + continue; + + nhlog::db()->debug("Migrating {}", dbName); + + auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str()); + + std::string_view session_id, session_value; + + std::vector<std::pair<std::string, StoredOlmSession>> sessions; + + auto cursor = lmdb::cursor::open(txn, olmDb); + while (cursor.get(session_id, session_value, MDB_NEXT)) { + nhlog::db()->debug( + "session_id {}, session_value {}", session_id, session_value); + StoredOlmSession session; + bool invalid = false; + for (auto c : session_value) + if (!isprint(c)) { + invalid = true; + break; + } + if (invalid) + continue; - nhlog::db()->info("Successfully deleted pending receipts database."); - return true; - }}, - {"2020.10.20", - [this]() { - try { - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_); - - auto mainDb = lmdb::dbi::open(txn, nullptr); - - std::string_view dbName, ignored; - auto olmDbCursor = lmdb::cursor::open(txn, mainDb); - while (olmDbCursor.get(dbName, ignored, MDB_NEXT)) { - // skip every db but olm session dbs - nhlog::db()->debug("Db {}", dbName); - if (dbName.find("olm_sessions/") != 0) - continue; - - nhlog::db()->debug("Migrating {}", dbName); - - auto olmDb = lmdb::dbi::open(txn, std::string(dbName).c_str()); - - std::string_view session_id, session_value; - - std::vector<std::pair<std::string, StoredOlmSession>> sessions; - - auto cursor = lmdb::cursor::open(txn, olmDb); - while (cursor.get(session_id, session_value, MDB_NEXT)) { - nhlog::db()->debug("session_id {}, session_value {}", - session_id, - session_value); - StoredOlmSession session; - bool invalid = false; - for (auto c : session_value) - if (!isprint(c)) { - invalid = true; - break; - } - if (invalid) - continue; - - nhlog::db()->debug("Not skipped"); - - session.pickled_session = session_value; - sessions.emplace_back(session_id, session); - } - cursor.close(); + nhlog::db()->debug("Not skipped"); - olmDb.drop(txn, true); + session.pickled_session = session_value; + sessions.emplace_back(session_id, session); + } + cursor.close(); - auto newDbName = std::string(dbName); - newDbName.erase(0, sizeof("olm_sessions") - 1); - newDbName = "olm_sessions.v2" + newDbName; + olmDb.drop(txn, true); - auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE); + auto newDbName = std::string(dbName); + newDbName.erase(0, sizeof("olm_sessions") - 1); + newDbName = "olm_sessions.v2" + newDbName; - for (const auto &[key, value] : sessions) { - // nhlog::db()->debug("{}\n{}", key, json(value).dump()); - newDb.put(txn, key, json(value).dump()); - } - } - olmDbCursor.close(); + auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE); - txn.commit(); - } catch (const lmdb::error &) { - nhlog::db()->critical("Failed to migrate olm sessions,"); - return false; + for (const auto &[key, value] : sessions) { + // nhlog::db()->debug("{}\n{}", key, json(value).dump()); + newDb.put(txn, key, json(value).dump()); } - - nhlog::db()->info("Successfully migrated olm sessions."); - return true; - }}, - {"2021.08.22", - [this]() { + } + olmDbCursor.close(); + + txn.commit(); + } catch (const lmdb::error &) { + nhlog::db()->critical("Failed to migrate olm sessions,"); + return false; + } + + nhlog::db()->info("Successfully migrated olm sessions."); + return true; + }}, + {"2021.08.22", + [this]() { + try { + auto txn = lmdb::txn::begin(env_, nullptr); + auto try_drop = [&txn](const std::string &dbName) { try { - auto txn = lmdb::txn::begin(env_, nullptr); - auto try_drop = [&txn](const std::string &dbName) { - try { - auto db = lmdb::dbi::open(txn, dbName.c_str()); - db.drop(txn, true); - } catch (std::exception &e) { - nhlog::db()->warn( - "Failed to drop '{}': {}", dbName, e.what()); - } - }; - - auto room_ids = getRoomIds(txn); - - for (const auto &room : room_ids) { - try_drop(room + "/state"); - try_drop(room + "/state_by_key"); - try_drop(room + "/account_data"); - try_drop(room + "/members"); - try_drop(room + "/mentions"); - try_drop(room + "/events"); - try_drop(room + "/event_order"); - try_drop(room + "/event2order"); - try_drop(room + "/msg2order"); - try_drop(room + "/order2msg"); - try_drop(room + "/pending"); - try_drop(room + "/related"); - } - - // clear db, don't delete - roomsDb_.drop(txn, false); - setNextBatchToken(txn, ""); - - txn.commit(); - } catch (const lmdb::error &) { - nhlog::db()->critical("Failed to clear cache!"); - return false; + auto db = lmdb::dbi::open(txn, dbName.c_str()); + db.drop(txn, true); + } catch (std::exception &e) { + nhlog::db()->warn("Failed to drop '{}': {}", dbName, e.what()); } + }; + + auto room_ids = getRoomIds(txn); + + for (const auto &room : room_ids) { + try_drop(room + "/state"); + try_drop(room + "/state_by_key"); + try_drop(room + "/account_data"); + try_drop(room + "/members"); + try_drop(room + "/mentions"); + try_drop(room + "/events"); + try_drop(room + "/event_order"); + try_drop(room + "/event2order"); + try_drop(room + "/msg2order"); + try_drop(room + "/order2msg"); + try_drop(room + "/pending"); + try_drop(room + "/related"); + } + + // clear db, don't delete + roomsDb_.drop(txn, false); + setNextBatchToken(txn, ""); + + txn.commit(); + } catch (const lmdb::error &) { + nhlog::db()->critical("Failed to clear cache!"); + return false; + } + + nhlog::db()->info("Successfully cleared the cache. Will do a clean sync after startup."); + return true; + }}, + {"2021.08.31", + [this]() { + storeSecret("pickle_secret", "secret", true); + return true; + }}, + }; + + nhlog::db()->info("Running migrations, this may take a while!"); + for (const auto &[target_version, migration] : migrations) { + if (target_version > stored_version) + if (!migration()) { + nhlog::db()->critical("migration failure!"); + return false; + } + } + nhlog::db()->info("Migrations finished."); - nhlog::db()->info( - "Successfully cleared the cache. Will do a clean sync after startup."); - return true; - }}, - {"2021.08.31", - [this]() { - storeSecret("pickle_secret", "secret", true); - return true; - }}, - }; - - nhlog::db()->info("Running migrations, this may take a while!"); - for (const auto &[target_version, migration] : migrations) { - if (target_version > stored_version) - if (!migration()) { - nhlog::db()->critical("migration failure!"); - return false; - } - } - nhlog::db()->info("Migrations finished."); - - setCurrentFormat(); - return true; + setCurrentFormat(); + return true; } cache::CacheVersion Cache::formatVersion() { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::string_view current_version; - bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); + std::string_view current_version; + bool res = syncStateDb_.get(txn, CACHE_FORMAT_VERSION_KEY, current_version); - if (!res) - return cache::CacheVersion::Older; + if (!res) + return cache::CacheVersion::Older; - std::string stored_version(current_version.data(), current_version.size()); + std::string stored_version(current_version.data(), current_version.size()); - if (stored_version < CURRENT_CACHE_FORMAT_VERSION) - return cache::CacheVersion::Older; - else if (stored_version > CURRENT_CACHE_FORMAT_VERSION) - return cache::CacheVersion::Older; - else - return cache::CacheVersion::Current; + if (stored_version < CURRENT_CACHE_FORMAT_VERSION) + return cache::CacheVersion::Older; + else if (stored_version > CURRENT_CACHE_FORMAT_VERSION) + return cache::CacheVersion::Older; + else + return cache::CacheVersion::Current; } void Cache::setCurrentFormat() { - auto txn = lmdb::txn::begin(env_); + auto txn = lmdb::txn::begin(env_); - syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION); + syncStateDb_.put(txn, CACHE_FORMAT_VERSION_KEY, CURRENT_CACHE_FORMAT_VERSION); - txn.commit(); + txn.commit(); } CachedReceipts Cache::readReceipts(const QString &event_id, const QString &room_id) { - CachedReceipts receipts; + CachedReceipts receipts; - ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; - nlohmann::json json_key = receipt_key; - - try { - auto txn = ro_txn(env_); - auto key = json_key.dump(); + ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; + nlohmann::json json_key = receipt_key; - std::string_view value; + try { + auto txn = ro_txn(env_); + auto key = json_key.dump(); - bool res = readReceiptsDb_.get(txn, key, value); + std::string_view value; - if (res) { - auto json_response = - json::parse(std::string_view(value.data(), value.size())); - auto values = json_response.get<std::map<std::string, uint64_t>>(); + bool res = readReceiptsDb_.get(txn, key, value); - for (const auto &v : values) - // timestamp, user_id - receipts.emplace(v.second, v.first); - } + if (res) { + auto json_response = json::parse(std::string_view(value.data(), value.size())); + auto values = json_response.get<std::map<std::string, uint64_t>>(); - } catch (const lmdb::error &e) { - nhlog::db()->critical("readReceipts: {}", e.what()); + for (const auto &v : values) + // timestamp, user_id + receipts.emplace(v.second, v.first); } - return receipts; + } catch (const lmdb::error &e) { + nhlog::db()->critical("readReceipts: {}", e.what()); + } + + return receipts; } void Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) { - auto user_id = this->localUserId_.toStdString(); - for (const auto &receipt : receipts) { - const auto event_id = receipt.first; - auto event_receipts = receipt.second; + auto user_id = this->localUserId_.toStdString(); + for (const auto &receipt : receipts) { + const auto event_id = receipt.first; + auto event_receipts = receipt.second; - ReadReceiptKey receipt_key{event_id, room_id}; - nlohmann::json json_key = receipt_key; + ReadReceiptKey receipt_key{event_id, room_id}; + nlohmann::json json_key = receipt_key; - try { - const auto key = json_key.dump(); + try { + const auto key = json_key.dump(); - std::string_view prev_value; + std::string_view prev_value; - bool exists = readReceiptsDb_.get(txn, key, prev_value); + bool exists = readReceiptsDb_.get(txn, key, prev_value); - std::map<std::string, uint64_t> saved_receipts; + std::map<std::string, uint64_t> saved_receipts; - // If an entry for the event id already exists, we would - // merge the existing receipts with the new ones. - if (exists) { - auto json_value = json::parse( - std::string_view(prev_value.data(), prev_value.size())); + // If an entry for the event id already exists, we would + // merge the existing receipts with the new ones. + if (exists) { + auto json_value = + json::parse(std::string_view(prev_value.data(), prev_value.size())); - // Retrieve the saved receipts. - saved_receipts = json_value.get<std::map<std::string, uint64_t>>(); - } + // Retrieve the saved receipts. + saved_receipts = json_value.get<std::map<std::string, uint64_t>>(); + } - // Append the new ones. - for (const auto &[read_by, timestamp] : event_receipts) { - if (read_by == user_id) { - emit removeNotification(QString::fromStdString(room_id), - QString::fromStdString(event_id)); - } - saved_receipts.emplace(read_by, timestamp); - } + // Append the new ones. + for (const auto &[read_by, timestamp] : event_receipts) { + if (read_by == user_id) { + emit removeNotification(QString::fromStdString(room_id), + QString::fromStdString(event_id)); + } + saved_receipts.emplace(read_by, timestamp); + } - // Save back the merged (or only the new) receipts. - nlohmann::json json_updated_value = saved_receipts; - std::string merged_receipts = json_updated_value.dump(); + // Save back the merged (or only the new) receipts. + nlohmann::json json_updated_value = saved_receipts; + std::string merged_receipts = json_updated_value.dump(); - readReceiptsDb_.put(txn, key, merged_receipts); + readReceiptsDb_.put(txn, key, merged_receipts); - } catch (const lmdb::error &e) { - nhlog::db()->critical("updateReadReceipts: {}", e.what()); - } + } catch (const lmdb::error &e) { + nhlog::db()->critical("updateReadReceipts: {}", e.what()); } + } } void Cache::calculateRoomReadStatus() { - const auto joined_rooms = joinedRooms(); + const auto joined_rooms = joinedRooms(); - std::map<QString, bool> readStatus; + std::map<QString, bool> readStatus; - for (const auto &room : joined_rooms) - readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room)); + for (const auto &room : joined_rooms) + readStatus.emplace(QString::fromStdString(room), calculateRoomReadStatus(room)); - emit roomReadStatus(readStatus); + emit roomReadStatus(readStatus); } bool Cache::calculateRoomReadStatus(const std::string &room_id) { - std::string last_event_id_, fullyReadEventId_; - { - auto txn = ro_txn(env_); - - // Get last event id on the room. - const auto last_event_id = getLastEventId(txn, room_id); - const auto localUser = utils::localUser().toStdString(); - - std::string fullyReadEventId; - if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { - if (auto fr = std::get_if< - mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>( - &ev.value())) { - fullyReadEventId = fr->content.event_id; - } - } - - if (last_event_id.empty() || fullyReadEventId.empty()) - return true; + std::string last_event_id_, fullyReadEventId_; + { + auto txn = ro_txn(env_); - if (last_event_id == fullyReadEventId) - return false; + // Get last event id on the room. + const auto last_event_id = getLastEventId(txn, room_id); + const auto localUser = utils::localUser().toStdString(); - last_event_id_ = std::string(last_event_id); - fullyReadEventId_ = std::string(fullyReadEventId); + std::string fullyReadEventId; + if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { + if (auto fr = + std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>( + &ev.value())) { + fullyReadEventId = fr->content.event_id; + } } - // Retrieve all read receipts for that event. - return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_); + if (last_event_id.empty() || fullyReadEventId.empty()) + return true; + + if (last_event_id == fullyReadEventId) + return false; + + last_event_id_ = std::string(last_event_id); + fullyReadEventId_ = std::string(fullyReadEventId); + } + + // Retrieve all read receipts for that event. + return getEventIndex(room_id, last_event_id_) > getEventIndex(room_id, fullyReadEventId_); } void Cache::saveState(const mtx::responses::Sync &res) { - using namespace mtx::events; - auto local_user_id = this->localUserId_.toStdString(); + using namespace mtx::events; + auto local_user_id = this->localUserId_.toStdString(); - auto currentBatchToken = nextBatchToken(); + auto currentBatchToken = nextBatchToken(); - auto txn = lmdb::txn::begin(env_); + auto txn = lmdb::txn::begin(env_); - setNextBatchToken(txn, res.next_batch); - - if (!res.account_data.events.empty()) { - auto accountDataDb = getAccountDataDb(txn, ""); - for (const auto &ev : res.account_data.events) - std::visit( - [&txn, &accountDataDb](const auto &event) { - auto j = json(event); - accountDataDb.put(txn, j["type"].get<std::string>(), j.dump()); - }, - ev); - } + setNextBatchToken(txn, res.next_batch); - auto userKeyCacheDb = getUserKeysDb(txn); - - std::set<std::string> spaces_with_updates; - std::set<std::string> rooms_with_space_updates; - - // Save joined rooms - for (const auto &room : res.rooms.join) { - auto statesdb = getStatesDb(txn, room.first); - auto stateskeydb = getStatesKeyDb(txn, room.first); - auto membersdb = getMembersDb(txn, room.first); - auto eventsDb = getEventsDb(txn, room.first); - - saveStateEvents(txn, - statesdb, - stateskeydb, - membersdb, - eventsDb, - room.first, - room.second.state.events); - saveStateEvents(txn, - statesdb, - stateskeydb, - membersdb, - eventsDb, - room.first, - room.second.timeline.events); - - saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline); - - RoomInfo updatedInfo; - updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); - updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); - updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); - updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); - updatedInfo.is_space = getRoomIsSpace(txn, statesdb); - - if (updatedInfo.is_space) { - bool space_updates = false; - for (const auto &e : room.second.state.events) - if (std::holds_alternative<StateEvent<state::space::Child>>(e) || - std::holds_alternative<StateEvent<state::PowerLevels>>(e)) - space_updates = true; - for (const auto &e : room.second.timeline.events) - if (std::holds_alternative<StateEvent<state::space::Child>>(e) || - std::holds_alternative<StateEvent<state::PowerLevels>>(e)) - space_updates = true; - - if (space_updates) - spaces_with_updates.insert(room.first); - } + if (!res.account_data.events.empty()) { + auto accountDataDb = getAccountDataDb(txn, ""); + for (const auto &ev : res.account_data.events) + std::visit( + [&txn, &accountDataDb](const auto &event) { + auto j = json(event); + accountDataDb.put(txn, j["type"].get<std::string>(), j.dump()); + }, + ev); + } - { - bool room_has_space_update = false; - for (const auto &e : room.second.state.events) { - if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { - spaces_with_updates.insert(se->state_key); - room_has_space_update = true; - } - } - for (const auto &e : room.second.timeline.events) { - if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { - spaces_with_updates.insert(se->state_key); - room_has_space_update = true; - } - } + auto userKeyCacheDb = getUserKeysDb(txn); - if (room_has_space_update) - rooms_with_space_updates.insert(room.first); - } + std::set<std::string> spaces_with_updates; + std::set<std::string> rooms_with_space_updates; - bool has_new_tags = false; - // Process the account_data associated with this room - if (!room.second.account_data.events.empty()) { - auto accountDataDb = getAccountDataDb(txn, room.first); - - for (const auto &evt : room.second.account_data.events) { - std::visit( - [&txn, &accountDataDb](const auto &event) { - auto j = json(event); - accountDataDb.put( - txn, j["type"].get<std::string>(), j.dump()); - }, - evt); - - // for tag events - if (std::holds_alternative<AccountDataEvent<account_data::Tags>>( - evt)) { - auto tags_evt = - std::get<AccountDataEvent<account_data::Tags>>(evt); - has_new_tags = true; - for (const auto &tag : tags_evt.content.tags) { - updatedInfo.tags.push_back(tag.first); - } - } - if (auto fr = std::get_if<mtx::events::AccountDataEvent< - mtx::events::account_data::FullyRead>>(&evt)) { - nhlog::db()->debug("Fully read: {}", fr->content.event_id); - } - } + // Save joined rooms + for (const auto &room : res.rooms.join) { + auto statesdb = getStatesDb(txn, room.first); + auto stateskeydb = getStatesKeyDb(txn, room.first); + auto membersdb = getMembersDb(txn, room.first); + auto eventsDb = getEventsDb(txn, room.first); + + saveStateEvents( + txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.state.events); + saveStateEvents( + txn, statesdb, stateskeydb, membersdb, eventsDb, room.first, room.second.timeline.events); + + saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline); + + RoomInfo updatedInfo; + updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); + updatedInfo.is_space = getRoomIsSpace(txn, statesdb); + + if (updatedInfo.is_space) { + bool space_updates = false; + for (const auto &e : room.second.state.events) + if (std::holds_alternative<StateEvent<state::space::Child>>(e) || + std::holds_alternative<StateEvent<state::PowerLevels>>(e)) + space_updates = true; + for (const auto &e : room.second.timeline.events) + if (std::holds_alternative<StateEvent<state::space::Child>>(e) || + std::holds_alternative<StateEvent<state::PowerLevels>>(e)) + space_updates = true; + + if (space_updates) + spaces_with_updates.insert(room.first); + } + + { + bool room_has_space_update = false; + for (const auto &e : room.second.state.events) { + if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { + spaces_with_updates.insert(se->state_key); + room_has_space_update = true; } - if (!has_new_tags) { - // retrieve the old tags, they haven't changed - std::string_view data; - if (roomsDb_.get(txn, room.first, data)) { - try { - RoomInfo tmp = - json::parse(std::string_view(data.data(), data.size())); - updatedInfo.tags = tmp.tags; - } catch (const json::exception &e) { - nhlog::db()->warn( - "failed to parse room info: room_id ({}), {}: {}", - room.first, - std::string(data.data(), data.size()), - e.what()); - } - } + } + for (const auto &e : room.second.timeline.events) { + if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) { + spaces_with_updates.insert(se->state_key); + room_has_space_update = true; + } + } + + if (room_has_space_update) + rooms_with_space_updates.insert(room.first); + } + + bool has_new_tags = false; + // Process the account_data associated with this room + if (!room.second.account_data.events.empty()) { + auto accountDataDb = getAccountDataDb(txn, room.first); + + for (const auto &evt : room.second.account_data.events) { + std::visit( + [&txn, &accountDataDb](const auto &event) { + auto j = json(event); + accountDataDb.put(txn, j["type"].get<std::string>(), j.dump()); + }, + evt); + + // for tag events + if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) { + auto tags_evt = std::get<AccountDataEvent<account_data::Tags>>(evt); + has_new_tags = true; + for (const auto &tag : tags_evt.content.tags) { + updatedInfo.tags.push_back(tag.first); + } + } + if (auto fr = std::get_if< + mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(&evt)) { + nhlog::db()->debug("Fully read: {}", fr->content.event_id); + } + } + } + if (!has_new_tags) { + // retrieve the old tags, they haven't changed + std::string_view data; + if (roomsDb_.get(txn, room.first, data)) { + try { + RoomInfo tmp = json::parse(std::string_view(data.data(), data.size())); + updatedInfo.tags = tmp.tags; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", + room.first, + std::string(data.data(), data.size()), + e.what()); } + } + } - roomsDb_.put(txn, room.first, json(updatedInfo).dump()); + roomsDb_.put(txn, room.first, json(updatedInfo).dump()); - for (const auto &e : room.second.ephemeral.events) { - if (auto receiptsEv = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) { - Receipts receipts; + for (const auto &e : room.second.ephemeral.events) { + if (auto receiptsEv = + std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) { + Receipts receipts; - for (const auto &[event_id, userReceipts] : - receiptsEv->content.receipts) { - for (const auto &[user_id, receipt] : userReceipts.users) { - receipts[event_id][user_id] = receipt.ts; - } - } - updateReadReceipt(txn, room.first, receipts); - } + for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) { + for (const auto &[user_id, receipt] : userReceipts.users) { + receipts[event_id][user_id] = receipt.ts; + } } - - // Clean up non-valid invites. - removeInvite(txn, room.first); + updateReadReceipt(txn, room.first, receipts); + } } - saveInvites(txn, res.rooms.invite); + // Clean up non-valid invites. + removeInvite(txn, room.first); + } - savePresence(txn, res.presence); + saveInvites(txn, res.rooms.invite); - markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken); + savePresence(txn, res.presence); - removeLeftRooms(txn, res.rooms.leave); + markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken); - updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates)); + removeLeftRooms(txn, res.rooms.leave); - txn.commit(); + updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates)); + + txn.commit(); - std::map<QString, bool> readStatus; - - for (const auto &room : res.rooms.join) { - for (const auto &e : room.second.ephemeral.events) { - if (auto receiptsEv = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) { - std::vector<QString> receipts; - - for (const auto &[event_id, userReceipts] : - receiptsEv->content.receipts) { - for (const auto &[user_id, receipt] : userReceipts.users) { - (void)receipt; - - if (user_id != local_user_id) { - receipts.push_back( - QString::fromStdString(event_id)); - break; - } - } - } - if (!receipts.empty()) - emit newReadReceipts(QString::fromStdString(room.first), - receipts); + std::map<QString, bool> readStatus; + + for (const auto &room : res.rooms.join) { + for (const auto &e : room.second.ephemeral.events) { + if (auto receiptsEv = + std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Receipt>>(&e)) { + std::vector<QString> receipts; + + for (const auto &[event_id, userReceipts] : receiptsEv->content.receipts) { + for (const auto &[user_id, receipt] : userReceipts.users) { + (void)receipt; + + if (user_id != local_user_id) { + receipts.push_back(QString::fromStdString(event_id)); + break; } + } } - readStatus.emplace(QString::fromStdString(room.first), - calculateRoomReadStatus(room.first)); + if (!receipts.empty()) + emit newReadReceipts(QString::fromStdString(room.first), receipts); + } } + readStatus.emplace(QString::fromStdString(room.first), calculateRoomReadStatus(room.first)); + } - emit roomReadStatus(readStatus); + emit roomReadStatus(readStatus); } void Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms) { - for (const auto &room : rooms) { - auto statesdb = getInviteStatesDb(txn, room.first); - auto membersdb = getInviteMembersDb(txn, room.first); + for (const auto &room : rooms) { + auto statesdb = getInviteStatesDb(txn, room.first); + auto membersdb = getInviteMembersDb(txn, room.first); - saveInvite(txn, statesdb, membersdb, room.second); + saveInvite(txn, statesdb, membersdb, room.second); - RoomInfo updatedInfo; - updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); - updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); - updatedInfo.avatar_url = - getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); - updatedInfo.is_space = getInviteRoomIsSpace(txn, statesdb); - updatedInfo.is_invite = true; + RoomInfo updatedInfo; + updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.is_space = getInviteRoomIsSpace(txn, statesdb); + updatedInfo.is_invite = true; - invitesDb_.put(txn, room.first, json(updatedInfo).dump()); - } + invitesDb_.put(txn, room.first, json(updatedInfo).dump()); + } } void @@ -1638,32 +1586,29 @@ Cache::saveInvite(lmdb::txn &txn, lmdb::dbi &membersdb, const mtx::responses::InvitedRoom &room) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - for (const auto &e : room.invite_state) { - if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) { - auto display_name = msg->content.display_name.empty() - ? msg->state_key - : msg->content.display_name; + for (const auto &e : room.invite_state) { + if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) { + auto display_name = + msg->content.display_name.empty() ? msg->state_key : msg->content.display_name; - MemberInfo tmp{display_name, msg->content.avatar_url}; + MemberInfo tmp{display_name, msg->content.avatar_url}; - membersdb.put(txn, msg->state_key, json(tmp).dump()); - } else { - std::visit( - [&txn, &statesdb](auto msg) { - auto j = json(msg); - bool res = - statesdb.put(txn, j["type"].get<std::string>(), j.dump()); - - if (!res) - nhlog::db()->warn("couldn't save data: {}", - json(msg).dump()); - }, - e); - } + membersdb.put(txn, msg->state_key, json(tmp).dump()); + } else { + std::visit( + [&txn, &statesdb](auto msg) { + auto j = json(msg); + bool res = statesdb.put(txn, j["type"].get<std::string>(), j.dump()); + + if (!res) + nhlog::db()->warn("couldn't save data: {}", json(msg).dump()); + }, + e); } + } } void @@ -1671,281 +1616,279 @@ Cache::savePresence( lmdb::txn &txn, const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates) { - for (const auto &update : presenceUpdates) { - auto presenceDb = getPresenceDb(txn); + for (const auto &update : presenceUpdates) { + auto presenceDb = getPresenceDb(txn); - presenceDb.put(txn, update.sender, json(update.content).dump()); - } + presenceDb.put(txn, update.sender, json(update.content).dump()); + } } std::vector<std::string> Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) { - std::vector<std::string> rooms; - for (const auto &room : res.rooms.join) { - bool hasUpdates = false; - for (const auto &s : room.second.state.events) { - if (containsStateUpdates(s)) { - hasUpdates = true; - break; - } - } - - for (const auto &s : room.second.timeline.events) { - if (containsStateUpdates(s)) { - hasUpdates = true; - break; - } - } + std::vector<std::string> rooms; + for (const auto &room : res.rooms.join) { + bool hasUpdates = false; + for (const auto &s : room.second.state.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } - if (hasUpdates) - rooms.emplace_back(room.first); + for (const auto &s : room.second.timeline.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } } - for (const auto &room : res.rooms.invite) { - for (const auto &s : room.second.invite_state) { - if (containsStateUpdates(s)) { - rooms.emplace_back(room.first); - break; - } - } + if (hasUpdates) + rooms.emplace_back(room.first); + } + + for (const auto &room : res.rooms.invite) { + for (const auto &s : room.second.invite_state) { + if (containsStateUpdates(s)) { + rooms.emplace_back(room.first); + break; + } } + } - return rooms; + return rooms; } RoomInfo Cache::singleRoomInfo(const std::string &room_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - try { - auto statesdb = getStatesDb(txn, room_id); - - std::string_view data; - - // Check if the room is joined. - if (roomsDb_.get(txn, room_id, data)) { - try { - RoomInfo tmp = json::parse(data); - tmp.member_count = getMembersDb(txn, room_id).size(txn); - tmp.join_rule = getRoomJoinRule(txn, statesdb); - tmp.guest_access = getRoomGuestAccess(txn, statesdb); - - return tmp; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", - room_id, - std::string(data.data(), data.size()), - e.what()); - } - } - } catch (const lmdb::error &e) { - nhlog::db()->warn( - "failed to read room info from db: room_id ({}), {}", room_id, e.what()); + try { + auto statesdb = getStatesDb(txn, room_id); + + std::string_view data; + + // Check if the room is joined. + if (roomsDb_.get(txn, room_id, data)) { + try { + RoomInfo tmp = json::parse(data); + tmp.member_count = getMembersDb(txn, room_id).size(txn); + tmp.join_rule = getRoomJoinRule(txn, statesdb); + tmp.guest_access = getRoomGuestAccess(txn, statesdb); + + return tmp; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", + room_id, + std::string(data.data(), data.size()), + e.what()); + } } + } catch (const lmdb::error &e) { + nhlog::db()->warn("failed to read room info from db: room_id ({}), {}", room_id, e.what()); + } - return RoomInfo(); + return RoomInfo(); } std::map<QString, RoomInfo> Cache::getRoomInfo(const std::vector<std::string> &rooms) { - std::map<QString, RoomInfo> room_info; + std::map<QString, RoomInfo> room_info; - // TODO This should be read only. - auto txn = lmdb::txn::begin(env_); + // TODO This should be read only. + auto txn = lmdb::txn::begin(env_); - for (const auto &room : rooms) { - std::string_view data; - auto statesdb = getStatesDb(txn, room); - - // Check if the room is joined. - if (roomsDb_.get(txn, room, data)) { - try { - RoomInfo tmp = json::parse(data); - tmp.member_count = getMembersDb(txn, room).size(txn); - tmp.join_rule = getRoomJoinRule(txn, statesdb); - tmp.guest_access = getRoomGuestAccess(txn, statesdb); - - room_info.emplace(QString::fromStdString(room), std::move(tmp)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", - room, - std::string(data.data(), data.size()), - e.what()); - } - } else { - // Check if the room is an invite. - if (invitesDb_.get(txn, room, data)) { - try { - RoomInfo tmp = json::parse(std::string_view(data)); - tmp.member_count = getInviteMembersDb(txn, room).size(txn); - - room_info.emplace(QString::fromStdString(room), - std::move(tmp)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info for invite: " - "room_id ({}), {}: {}", - room, - std::string(data.data(), data.size()), - e.what()); - } - } + for (const auto &room : rooms) { + std::string_view data; + auto statesdb = getStatesDb(txn, room); + + // Check if the room is joined. + if (roomsDb_.get(txn, room, data)) { + try { + RoomInfo tmp = json::parse(data); + tmp.member_count = getMembersDb(txn, room).size(txn); + tmp.join_rule = getRoomJoinRule(txn, statesdb); + tmp.guest_access = getRoomGuestAccess(txn, statesdb); + + room_info.emplace(QString::fromStdString(room), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}", + room, + std::string(data.data(), data.size()), + e.what()); + } + } else { + // Check if the room is an invite. + if (invitesDb_.get(txn, room, data)) { + try { + RoomInfo tmp = json::parse(std::string_view(data)); + tmp.member_count = getInviteMembersDb(txn, room).size(txn); + + room_info.emplace(QString::fromStdString(room), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + room, + std::string(data.data(), data.size()), + e.what()); } + } } + } - txn.commit(); + txn.commit(); - return room_info; + return room_info; } std::vector<QString> Cache::roomIds() { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::vector<QString> rooms; - std::string_view room_id, unused; + std::vector<QString> rooms; + std::string_view room_id, unused; - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - while (roomsCursor.get(room_id, unused, MDB_NEXT)) - rooms.push_back(QString::fromStdString(std::string(room_id))); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + while (roomsCursor.get(room_id, unused, MDB_NEXT)) + rooms.push_back(QString::fromStdString(std::string(room_id))); - roomsCursor.close(); + roomsCursor.close(); - return rooms; + return rooms; } QMap<QString, mtx::responses::Notifications> Cache::getTimelineMentions() { - // TODO: Should be read-only, but getMentionsDb will attempt to create a DB - // if it doesn't exist, throwing an error. - auto txn = lmdb::txn::begin(env_, nullptr); + // TODO: Should be read-only, but getMentionsDb will attempt to create a DB + // if it doesn't exist, throwing an error. + auto txn = lmdb::txn::begin(env_, nullptr); - QMap<QString, mtx::responses::Notifications> notifs; + QMap<QString, mtx::responses::Notifications> notifs; - auto room_ids = getRoomIds(txn); + auto room_ids = getRoomIds(txn); - for (const auto &room_id : room_ids) { - auto roomNotifs = getTimelineMentionsForRoom(txn, room_id); - notifs[QString::fromStdString(room_id)] = roomNotifs; - } + for (const auto &room_id : room_ids) { + auto roomNotifs = getTimelineMentionsForRoom(txn, room_id); + notifs[QString::fromStdString(room_id)] = roomNotifs; + } - txn.commit(); + txn.commit(); - return notifs; + return notifs; } std::string Cache::previousBatchToken(const std::string &room_id) { - auto txn = lmdb::txn::begin(env_, nullptr); - auto orderDb = getEventOrderDb(txn, room_id); + auto txn = lmdb::txn::begin(env_, nullptr); + auto orderDb = getEventOrderDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, orderDb); - std::string_view indexVal, val; - if (!cursor.get(indexVal, val, MDB_FIRST)) { - return ""; - } + auto cursor = lmdb::cursor::open(txn, orderDb); + std::string_view indexVal, val; + if (!cursor.get(indexVal, val, MDB_FIRST)) { + return ""; + } - auto j = json::parse(val); + auto j = json::parse(val); - return j.value("prev_batch", ""); + return j.value("prev_batch", ""); } Cache::Messages Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t index, bool forward) { - // TODO(nico): Limit the messages returned by this maybe? - auto orderDb = getOrderToMessageDb(txn, room_id); - auto eventsDb = getEventsDb(txn, room_id); + // TODO(nico): Limit the messages returned by this maybe? + auto orderDb = getOrderToMessageDb(txn, room_id); + auto eventsDb = getEventsDb(txn, room_id); - Messages messages{}; + Messages messages{}; - std::string_view indexVal, event_id; + std::string_view indexVal, event_id; - auto cursor = lmdb::cursor::open(txn, orderDb); - if (index == std::numeric_limits<uint64_t>::max()) { - if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) { - index = lmdb::from_sv<uint64_t>(indexVal); - } else { - messages.end_of_cache = true; - return messages; - } + auto cursor = lmdb::cursor::open(txn, orderDb); + if (index == std::numeric_limits<uint64_t>::max()) { + if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) { + index = lmdb::from_sv<uint64_t>(indexVal); } else { - if (cursor.get(indexVal, event_id, MDB_SET)) { - index = lmdb::from_sv<uint64_t>(indexVal); - } else { - messages.end_of_cache = true; - return messages; - } + messages.end_of_cache = true; + return messages; } + } else { + if (cursor.get(indexVal, event_id, MDB_SET)) { + index = lmdb::from_sv<uint64_t>(indexVal); + } else { + messages.end_of_cache = true; + return messages; + } + } - int counter = 0; - - bool ret; - while ((ret = cursor.get(indexVal, - event_id, - counter == 0 ? (forward ? MDB_FIRST : MDB_LAST) - : (forward ? MDB_NEXT : MDB_PREV))) && - counter++ < BATCH_SIZE) { - std::string_view event; - bool success = eventsDb.get(txn, event_id, event); - if (!success) - continue; + int counter = 0; - mtx::events::collections::TimelineEvent te; - try { - mtx::events::collections::from_json(json::parse(event), te); - } catch (std::exception &e) { - nhlog::db()->error("Failed to parse message from cache {}", e.what()); - continue; - } + bool ret; + while ((ret = cursor.get(indexVal, + event_id, + counter == 0 ? (forward ? MDB_FIRST : MDB_LAST) + : (forward ? MDB_NEXT : MDB_PREV))) && + counter++ < BATCH_SIZE) { + std::string_view event; + bool success = eventsDb.get(txn, event_id, event); + if (!success) + continue; - messages.timeline.events.push_back(std::move(te.data)); + mtx::events::collections::TimelineEvent te; + try { + mtx::events::collections::from_json(json::parse(event), te); + } catch (std::exception &e) { + nhlog::db()->error("Failed to parse message from cache {}", e.what()); + continue; } - cursor.close(); - // std::reverse(timeline.events.begin(), timeline.events.end()); - messages.next_index = lmdb::from_sv<uint64_t>(indexVal); - messages.end_of_cache = !ret; + messages.timeline.events.push_back(std::move(te.data)); + } + cursor.close(); + + // std::reverse(timeline.events.begin(), timeline.events.end()); + messages.next_index = lmdb::from_sv<uint64_t>(indexVal); + messages.end_of_cache = !ret; - return messages; + return messages; } std::optional<mtx::events::collections::TimelineEvent> Cache::getEvent(const std::string &room_id, const std::string &event_id) { - auto txn = ro_txn(env_); - auto eventsDb = getEventsDb(txn, room_id); + auto txn = ro_txn(env_); + auto eventsDb = getEventsDb(txn, room_id); - std::string_view event{}; - bool success = eventsDb.get(txn, event_id, event); - if (!success) - return {}; + std::string_view event{}; + bool success = eventsDb.get(txn, event_id, event); + if (!success) + return {}; - mtx::events::collections::TimelineEvent te; - try { - mtx::events::collections::from_json(json::parse(event), te); - } catch (std::exception &e) { - nhlog::db()->error("Failed to parse message from cache {}", e.what()); - return std::nullopt; - } + mtx::events::collections::TimelineEvent te; + try { + mtx::events::collections::from_json(json::parse(event), te); + } catch (std::exception &e) { + nhlog::db()->error("Failed to parse message from cache {}", e.what()); + return std::nullopt; + } - return te; + return te; } void Cache::storeEvent(const std::string &room_id, const std::string &event_id, const mtx::events::collections::TimelineEvent &event) { - auto txn = lmdb::txn::begin(env_); - auto eventsDb = getEventsDb(txn, room_id); - auto event_json = mtx::accessors::serialize_event(event.data); - eventsDb.put(txn, event_id, event_json.dump()); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto event_json = mtx::accessors::serialize_event(event.data); + eventsDb.put(txn, event_id, event_json.dump()); + txn.commit(); } void @@ -1953,962 +1896,944 @@ Cache::replaceEvent(const std::string &room_id, const std::string &event_id, const mtx::events::collections::TimelineEvent &event) { - auto txn = lmdb::txn::begin(env_); - auto eventsDb = getEventsDb(txn, room_id); - auto relationsDb = getRelationsDb(txn, room_id); - auto event_json = mtx::accessors::serialize_event(event.data).dump(); + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); + auto event_json = mtx::accessors::serialize_event(event.data).dump(); - { - eventsDb.del(txn, event_id); - eventsDb.put(txn, event_id, event_json); - for (auto relation : mtx::accessors::relations(event.data).relations) { - relationsDb.put(txn, relation.event_id, event_id); - } + { + eventsDb.del(txn, event_id); + eventsDb.put(txn, event_id, event_json); + for (auto relation : mtx::accessors::relations(event.data).relations) { + relationsDb.put(txn, relation.event_id, event_id); } + } - txn.commit(); + txn.commit(); } std::vector<std::string> Cache::relatedEvents(const std::string &room_id, const std::string &event_id) { - auto txn = ro_txn(env_); - auto relationsDb = getRelationsDb(txn, room_id); + auto txn = ro_txn(env_); + auto relationsDb = getRelationsDb(txn, room_id); - std::vector<std::string> related_ids; + std::vector<std::string> related_ids; - auto related_cursor = lmdb::cursor::open(txn, relationsDb); - std::string_view related_to = event_id, related_event; - bool first = true; + auto related_cursor = lmdb::cursor::open(txn, relationsDb); + std::string_view related_to = event_id, related_event; + bool first = true; - try { - if (!related_cursor.get(related_to, related_event, MDB_SET)) - return {}; + try { + if (!related_cursor.get(related_to, related_event, MDB_SET)) + return {}; - while (related_cursor.get( - related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { - first = false; - if (event_id != std::string_view(related_to.data(), related_to.size())) - break; + while ( + related_cursor.get(related_to, related_event, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + if (event_id != std::string_view(related_to.data(), related_to.size())) + break; - related_ids.emplace_back(related_event.data(), related_event.size()); - } - } catch (const lmdb::error &e) { - nhlog::db()->error("related events error: {}", e.what()); + related_ids.emplace_back(related_event.data(), related_event.size()); } + } catch (const lmdb::error &e) { + nhlog::db()->error("related events error: {}", e.what()); + } - return related_ids; + return related_ids; } size_t Cache::memberCount(const std::string &room_id) { - auto txn = ro_txn(env_); - return getMembersDb(txn, room_id).size(txn); + auto txn = ro_txn(env_); + return getMembersDb(txn, room_id).size(txn); } QMap<QString, RoomInfo> Cache::roomInfo(bool withInvites) { - QMap<QString, RoomInfo> result; + QMap<QString, RoomInfo> result; - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::string_view room_id; - std::string_view room_data; + std::string_view room_id; + std::string_view room_data; - // Gather info about the joined rooms. - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(std::move(room_data)); - tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn); - result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); - } - roomsCursor.close(); - - if (withInvites) { - // Gather info about the invites. - auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); - while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(room_data); - tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); - result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); - } - invitesCursor.close(); + // Gather info about the joined rooms. + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(std::move(room_data)); + tmp.member_count = getMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); + } + roomsCursor.close(); + + if (withInvites) { + // Gather info about the invites. + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); } + invitesCursor.close(); + } - return result; + return result; } std::string Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id) { - lmdb::dbi orderDb; - try { - orderDb = getOrderToMessageDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + lmdb::dbi orderDb; + try { + orderDb = getOrderToMessageDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view indexVal, val; + std::string_view indexVal, val; - auto cursor = lmdb::cursor::open(txn, orderDb); - if (!cursor.get(indexVal, val, MDB_LAST)) { - return {}; - } + auto cursor = lmdb::cursor::open(txn, orderDb); + if (!cursor.get(indexVal, val, MDB_LAST)) { + return {}; + } - return std::string(val.data(), val.size()); + return std::string(val.data(), val.size()); } std::optional<Cache::TimelineRange> Cache::getTimelineRange(const std::string &room_id) { - auto txn = ro_txn(env_); - lmdb::dbi orderDb; - try { - orderDb = getOrderToMessageDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + auto txn = ro_txn(env_); + lmdb::dbi orderDb; + try { + orderDb = getOrderToMessageDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view indexVal, val; + std::string_view indexVal, val; - auto cursor = lmdb::cursor::open(txn, orderDb); - if (!cursor.get(indexVal, val, MDB_LAST)) { - return {}; - } + auto cursor = lmdb::cursor::open(txn, orderDb); + if (!cursor.get(indexVal, val, MDB_LAST)) { + return {}; + } - TimelineRange range{}; - range.last = lmdb::from_sv<uint64_t>(indexVal); + TimelineRange range{}; + range.last = lmdb::from_sv<uint64_t>(indexVal); - if (!cursor.get(indexVal, val, MDB_FIRST)) { - return {}; - } - range.first = lmdb::from_sv<uint64_t>(indexVal); + if (!cursor.get(indexVal, val, MDB_FIRST)) { + return {}; + } + range.first = lmdb::from_sv<uint64_t>(indexVal); - return range; + return range; } std::optional<uint64_t> Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) { - if (event_id.empty() || room_id.empty()) - return {}; + if (event_id.empty() || room_id.empty()) + return {}; - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - lmdb::dbi orderDb; - try { - orderDb = getMessageToOrderDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + lmdb::dbi orderDb; + try { + orderDb = getMessageToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view indexVal{event_id.data(), event_id.size()}, val; + std::string_view indexVal{event_id.data(), event_id.size()}, val; - bool success = orderDb.get(txn, indexVal, val); - if (!success) { - return {}; - } + bool success = orderDb.get(txn, indexVal, val); + if (!success) { + return {}; + } - return lmdb::from_sv<uint64_t>(val); + return lmdb::from_sv<uint64_t>(val); } std::optional<uint64_t> Cache::getEventIndex(const std::string &room_id, std::string_view event_id) { - if (room_id.empty() || event_id.empty()) - return {}; + if (room_id.empty() || event_id.empty()) + return {}; - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - lmdb::dbi orderDb; - try { - orderDb = getEventToOrderDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + lmdb::dbi orderDb; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view val; + std::string_view val; - bool success = orderDb.get(txn, event_id, val); - if (!success) { - return {}; - } + bool success = orderDb.get(txn, event_id, val); + if (!success) { + return {}; + } - return lmdb::from_sv<uint64_t>(val); + return lmdb::from_sv<uint64_t>(val); } std::optional<std::pair<uint64_t, std::string>> Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) { - if (room_id.empty() || event_id.empty()) - return {}; - - auto txn = ro_txn(env_); - - lmdb::dbi orderDb; - lmdb::dbi eventOrderDb; - lmdb::dbi timelineDb; - try { - orderDb = getEventToOrderDb(txn, room_id); - eventOrderDb = getEventOrderDb(txn, room_id); - timelineDb = getMessageToOrderDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } - - std::string_view indexVal; - - bool success = orderDb.get(txn, event_id, indexVal); - if (!success) { - return {}; - } - - try { - uint64_t prevIdx = lmdb::from_sv<uint64_t>(indexVal); - std::string prevId{event_id}; - - auto cursor = lmdb::cursor::open(txn, eventOrderDb); - cursor.get(indexVal, MDB_SET); - while (cursor.get(indexVal, event_id, MDB_NEXT)) { - std::string evId = json::parse(event_id)["event_id"].get<std::string>(); - std::string_view temp; - if (timelineDb.get(txn, evId, temp)) { - return std::pair{prevIdx, std::string(prevId)}; - } else { - prevIdx = lmdb::from_sv<uint64_t>(indexVal); - prevId = std::move(evId); - } - } - + if (room_id.empty() || event_id.empty()) + return {}; + + auto txn = ro_txn(env_); + + lmdb::dbi orderDb; + lmdb::dbi eventOrderDb; + lmdb::dbi timelineDb; + try { + orderDb = getEventToOrderDb(txn, room_id); + eventOrderDb = getEventOrderDb(txn, room_id); + timelineDb = getMessageToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } + + std::string_view indexVal; + + bool success = orderDb.get(txn, event_id, indexVal); + if (!success) { + return {}; + } + + try { + uint64_t prevIdx = lmdb::from_sv<uint64_t>(indexVal); + std::string prevId{event_id}; + + auto cursor = lmdb::cursor::open(txn, eventOrderDb); + cursor.get(indexVal, MDB_SET); + while (cursor.get(indexVal, event_id, MDB_NEXT)) { + std::string evId = json::parse(event_id)["event_id"].get<std::string>(); + std::string_view temp; + if (timelineDb.get(txn, evId, temp)) { return std::pair{prevIdx, std::string(prevId)}; - } catch (lmdb::runtime_error &e) { - nhlog::db()->error( - "Failed to get last invisible event after {}", event_id, e.what()); - return {}; + } else { + prevIdx = lmdb::from_sv<uint64_t>(indexVal); + prevId = std::move(evId); + } } + + return std::pair{prevIdx, std::string(prevId)}; + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Failed to get last invisible event after {}", event_id, e.what()); + return {}; + } } std::optional<uint64_t> Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - lmdb::dbi orderDb; - try { - orderDb = getEventToOrderDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + lmdb::dbi orderDb; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view val; + std::string_view val; - bool success = orderDb.get(txn, event_id, val); - if (!success) { - return {}; - } + bool success = orderDb.get(txn, event_id, val); + if (!success) { + return {}; + } - return lmdb::from_sv<uint64_t>(val); + return lmdb::from_sv<uint64_t>(val); } std::optional<std::string> Cache::getTimelineEventId(const std::string &room_id, uint64_t index) { - auto txn = ro_txn(env_); - lmdb::dbi orderDb; - try { - orderDb = getOrderToMessageDb(txn, room_id); - } catch (lmdb::runtime_error &e) { - nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", - room_id, - e.what()); - return {}; - } + auto txn = ro_txn(env_); + lmdb::dbi orderDb; + try { + orderDb = getOrderToMessageDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error( + "Can't open db for room '{}', probably doesn't exist yet. ({})", room_id, e.what()); + return {}; + } - std::string_view val; + std::string_view val; - bool success = orderDb.get(txn, lmdb::to_sv(index), val); - if (!success) { - return {}; - } + bool success = orderDb.get(txn, lmdb::to_sv(index), val); + if (!success) { + return {}; + } - return std::string(val); + return std::string(val); } QHash<QString, RoomInfo> Cache::invites() { - QHash<QString, RoomInfo> result; + QHash<QString, RoomInfo> result; - auto txn = ro_txn(env_); - auto cursor = lmdb::cursor::open(txn, invitesDb_); + auto txn = ro_txn(env_); + auto cursor = lmdb::cursor::open(txn, invitesDb_); - std::string_view room_id, room_data; + std::string_view room_id, room_data; - while (cursor.get(room_id, room_data, MDB_NEXT)) { - try { - RoomInfo tmp = json::parse(room_data); - tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); - result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info for invite: " - "room_id ({}), {}: {}", - room_id, - std::string(room_data), - e.what()); - } + while (cursor.get(room_id, room_data, MDB_NEXT)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + room_id, + std::string(room_data), + e.what()); } + } - cursor.close(); + cursor.close(); - return result; + return result; } std::optional<RoomInfo> Cache::invite(std::string_view roomid) { - std::optional<RoomInfo> result; + std::optional<RoomInfo> result; - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::string_view room_data; + std::string_view room_data; - if (invitesDb_.get(txn, roomid, room_data)) { - try { - RoomInfo tmp = json::parse(room_data); - tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); - result = std::move(tmp); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info for invite: " - "room_id ({}), {}: {}", - roomid, - std::string(room_data), - e.what()); - } + if (invitesDb_.get(txn, roomid, room_data)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); + result = std::move(tmp); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + roomid, + std::string(room_data), + e.what()); } + } - return result; + return result; } QString Cache::getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); - if (res) { - try { - StateEvent<Avatar> msg = - json::parse(std::string_view(event.data(), event.size())); + if (res) { + try { + StateEvent<Avatar> msg = json::parse(std::string_view(event.data(), event.size())); - if (!msg.content.url.empty()) - return QString::fromStdString(msg.content.url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); - } + if (!msg.content.url.empty()) + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } + } - // We don't use an avatar for group chats. - if (membersdb.size(txn) > 2) - return QString(); + // We don't use an avatar for group chats. + if (membersdb.size(txn) > 2) + return QString(); - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string_view user_id; - std::string_view member_data; - std::string fallback_url; + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string_view user_id; + std::string_view member_data; + std::string fallback_url; - // Resolve avatar for 1-1 chats. - while (cursor.get(user_id, member_data, MDB_NEXT)) { - try { - MemberInfo m = json::parse(member_data); - if (user_id == localUserId_.toStdString()) { - fallback_url = m.avatar_url; - continue; - } + // Resolve avatar for 1-1 chats. + while (cursor.get(user_id, member_data, MDB_NEXT)) { + try { + MemberInfo m = json::parse(member_data); + if (user_id == localUserId_.toStdString()) { + fallback_url = m.avatar_url; + continue; + } - cursor.close(); - return QString::fromStdString(m.avatar_url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } + cursor.close(); + return QString::fromStdString(m.avatar_url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); } + } - cursor.close(); + cursor.close(); - // Default case when there is only one member. - return QString::fromStdString(fallback_url); + // Default case when there is only one member. + return QString::fromStdString(fallback_url); } QString Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); - if (res) { - try { - StateEvent<Name> msg = - json::parse(std::string_view(event.data(), event.size())); + if (res) { + try { + StateEvent<Name> msg = json::parse(std::string_view(event.data(), event.size())); - if (!msg.content.name.empty()) - return QString::fromStdString(msg.content.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); - } + if (!msg.content.name.empty()) + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } + } - res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event); + res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event); - if (res) { - try { - StateEvent<CanonicalAlias> msg = - json::parse(std::string_view(event.data(), event.size())); + if (res) { + try { + StateEvent<CanonicalAlias> msg = + json::parse(std::string_view(event.data(), event.size())); - if (!msg.content.alias.empty()) - return QString::fromStdString(msg.content.alias); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", - e.what()); - } + if (!msg.content.alias.empty()) + return QString::fromStdString(msg.content.alias); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what()); } + } - auto cursor = lmdb::cursor::open(txn, membersdb); - const auto total = membersdb.size(txn); + auto cursor = lmdb::cursor::open(txn, membersdb); + const auto total = membersdb.size(txn); - std::size_t ii = 0; - std::string_view user_id; - std::string_view member_data; - std::map<std::string, MemberInfo> members; + std::size_t ii = 0; + std::string_view user_id; + std::string_view member_data; + std::map<std::string, MemberInfo> members; - while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { - try { - members.emplace(user_id, json::parse(member_data)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } - - ii++; + while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { + try { + members.emplace(user_id, json::parse(member_data)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); } - cursor.close(); + ii++; + } - if (total == 1 && !members.empty()) - return QString::fromStdString(members.begin()->second.name); + cursor.close(); - auto first_member = [&members, this]() { - for (const auto &m : members) { - if (m.first != localUserId_.toStdString()) - return QString::fromStdString(m.second.name); - } + if (total == 1 && !members.empty()) + return QString::fromStdString(members.begin()->second.name); + + auto first_member = [&members, this]() { + for (const auto &m : members) { + if (m.first != localUserId_.toStdString()) + return QString::fromStdString(m.second.name); + } - return localUserId_; - }(); + return localUserId_; + }(); - if (total == 2) - return first_member; - else if (total > 2) - return QString("%1 and %2 others").arg(first_member).arg(total - 1); + if (total == 2) + return first_member; + else if (total > 2) + return QString("%1 and %2 others").arg(first_member).arg(total - 1); - return "Empty Room"; + return "Empty Room"; } mtx::events::state::JoinRule Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomJoinRules), event); - if (res) { - try { - StateEvent<state::JoinRules> msg = json::parse(event); - return msg.content.join_rule; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); - } + if (res) { + try { + StateEvent<state::JoinRules> msg = json::parse(event); + return msg.content.join_rule; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } - return state::JoinRule::Knock; + } + return state::JoinRule::Knock; } bool Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomGuestAccess), event); - if (res) { - try { - StateEvent<GuestAccess> msg = json::parse(event); - return msg.content.guest_access == AccessState::CanJoin; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.guest_access event: {}", - e.what()); - } + if (res) { + try { + StateEvent<GuestAccess> msg = json::parse(event); + return msg.content.guest_access == AccessState::CanJoin; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.guest_access event: {}", e.what()); } - return false; + } + return false; } QString Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomTopic), event); - if (res) { - try { - StateEvent<Topic> msg = json::parse(event); + if (res) { + try { + StateEvent<Topic> msg = json::parse(event); - if (!msg.content.topic.empty()) - return QString::fromStdString(msg.content.topic); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); - } + if (!msg.content.topic.empty()) + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } + } - return QString(); + return QString(); } QString Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); - if (res) { - try { - StateEvent<Create> msg = json::parse(event); + if (res) { + try { + StateEvent<Create> msg = json::parse(event); - if (!msg.content.room_version.empty()) - return QString::fromStdString(msg.content.room_version); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); - } + if (!msg.content.room_version.empty()) + return QString::fromStdString(msg.content.room_version); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); } + } - nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); - return QString("1"); + nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); + return QString("1"); } bool Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event); - if (res) { - try { - StateEvent<Create> msg = json::parse(event); + if (res) { + try { + StateEvent<Create> msg = json::parse(event); - return msg.content.type == mtx::events::state::room_type::space; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); - } + return msg.content.type == mtx::events::state::room_type::space; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); } + } - nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); - return false; + nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); + return false; } std::optional<mtx::events::state::CanonicalAlias> Cache::getRoomAliases(const std::string &roomid) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - auto txn = ro_txn(env_); - auto statesdb = getStatesDb(txn, roomid); + auto txn = ro_txn(env_); + auto statesdb = getStatesDb(txn, roomid); - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCanonicalAlias), event); - if (res) { - try { - StateEvent<CanonicalAlias> msg = json::parse(event); + if (res) { + try { + StateEvent<CanonicalAlias> msg = json::parse(event); - return msg.content; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", - e.what()); - } + return msg.content; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", e.what()); } + } - return std::nullopt; + return std::nullopt; } QString Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomName), event); - if (res) { - try { - StrippedEvent<state::Name> msg = json::parse(event); - return QString::fromStdString(msg.content.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); - } + if (res) { + try { + StrippedEvent<state::Name> msg = json::parse(event); + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); } + } - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string_view user_id, member_data; + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string_view user_id, member_data; - while (cursor.get(user_id, member_data, MDB_NEXT)) { - if (user_id == localUserId_.toStdString()) - continue; + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; - try { - MemberInfo tmp = json::parse(member_data); - cursor.close(); + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); - return QString::fromStdString(tmp.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } + return QString::fromStdString(tmp.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); } + } - cursor.close(); + cursor.close(); - return QString("Empty Room"); + return QString("Empty Room"); } QString Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); + std::string_view event; + bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomAvatar), event); - if (res) { - try { - StrippedEvent<state::Avatar> msg = json::parse(event); - return QString::fromStdString(msg.content.url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); - } + if (res) { + try { + StrippedEvent<state::Avatar> msg = json::parse(event); + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); } + } - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string_view user_id, member_data; + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string_view user_id, member_data; - while (cursor.get(user_id, member_data, MDB_NEXT)) { - if (user_id == localUserId_.toStdString()) - continue; + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; - try { - MemberInfo tmp = json::parse(member_data); - cursor.close(); + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); - return QString::fromStdString(tmp.avatar_url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } + return QString::fromStdString(tmp.avatar_url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); } + } - cursor.close(); + cursor.close(); - return QString(); + return QString(); } QString Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event); + std::string_view event; + bool res = db.get(txn, to_string(mtx::events::EventType::RoomTopic), event); - if (res) { - try { - StrippedEvent<Topic> msg = json::parse(event); - return QString::fromStdString(msg.content.topic); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); - } + if (res) { + try { + StrippedEvent<Topic> msg = json::parse(event); + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } + } - return QString(); + return QString(); } bool Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - std::string_view event; - bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event); + std::string_view event; + bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event); - if (res) { - try { - StrippedEvent<Create> msg = json::parse(event); - return msg.content.type == mtx::events::state::room_type::space; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); - } + if (res) { + try { + StrippedEvent<Create> msg = json::parse(event); + return msg.content.type == mtx::events::state::room_type::space; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); } + } - return false; + return false; } std::vector<std::string> Cache::joinedRooms() { - auto txn = ro_txn(env_); - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + auto txn = ro_txn(env_); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - std::string_view id, data; - std::vector<std::string> room_ids; + std::string_view id, data; + std::vector<std::string> room_ids; - // Gather the room ids for the joined rooms. - while (roomsCursor.get(id, data, MDB_NEXT)) - room_ids.emplace_back(id); + // Gather the room ids for the joined rooms. + while (roomsCursor.get(id, data, MDB_NEXT)) + room_ids.emplace_back(id); - roomsCursor.close(); + roomsCursor.close(); - return room_ids; + return room_ids; } std::optional<MemberInfo> Cache::getMember(const std::string &room_id, const std::string &user_id) { - if (user_id.empty() || !env_.handle()) - return std::nullopt; + if (user_id.empty() || !env_.handle()) + return std::nullopt; - try { - auto txn = ro_txn(env_); + try { + auto txn = ro_txn(env_); - auto membersdb = getMembersDb(txn, room_id); + auto membersdb = getMembersDb(txn, room_id); - std::string_view info; - if (membersdb.get(txn, user_id, info)) { - MemberInfo m = json::parse(info); - return m; - } - } catch (std::exception &e) { - nhlog::db()->warn( - "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); + std::string_view info; + if (membersdb.get(txn, user_id, info)) { + MemberInfo m = json::parse(info); + return m; } - return std::nullopt; + } catch (std::exception &e) { + nhlog::db()->warn( + "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); + } + return std::nullopt; } std::vector<RoomMember> Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { - auto txn = ro_txn(env_); - auto db = getMembersDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, db); - - std::size_t currentIndex = 0; + auto txn = ro_txn(env_); + auto db = getMembersDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, db); - const auto endIndex = std::min(startIndex + len, db.size(txn)); + std::size_t currentIndex = 0; - std::vector<RoomMember> members; + const auto endIndex = std::min(startIndex + len, db.size(txn)); - std::string_view user_id, user_data; - while (cursor.get(user_id, user_data, MDB_NEXT)) { - if (currentIndex < startIndex) { - currentIndex += 1; - continue; - } + std::vector<RoomMember> members; - if (currentIndex >= endIndex) - break; + std::string_view user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + if (currentIndex < startIndex) { + currentIndex += 1; + continue; + } - try { - MemberInfo tmp = json::parse(user_data); - members.emplace_back( - RoomMember{QString::fromStdString(std::string(user_id)), - QString::fromStdString(tmp.name)}); - } catch (const json::exception &e) { - nhlog::db()->warn("{}", e.what()); - } + if (currentIndex >= endIndex) + break; - currentIndex += 1; + try { + MemberInfo tmp = json::parse(user_data); + members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)), + QString::fromStdString(tmp.name)}); + } catch (const json::exception &e) { + nhlog::db()->warn("{}", e.what()); } - cursor.close(); + currentIndex += 1; + } - return members; + cursor.close(); + + return members; } std::vector<RoomMember> Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) { - auto txn = ro_txn(env_); - auto db = getInviteMembersDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, db); + auto txn = ro_txn(env_); + auto db = getInviteMembersDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, db); - std::size_t currentIndex = 0; + std::size_t currentIndex = 0; - const auto endIndex = std::min(startIndex + len, db.size(txn)); + const auto endIndex = std::min(startIndex + len, db.size(txn)); - std::vector<RoomMember> members; + std::vector<RoomMember> members; - std::string_view user_id, user_data; - while (cursor.get(user_id, user_data, MDB_NEXT)) { - if (currentIndex < startIndex) { - currentIndex += 1; - continue; - } - - if (currentIndex >= endIndex) - break; + std::string_view user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + if (currentIndex < startIndex) { + currentIndex += 1; + continue; + } - try { - MemberInfo tmp = json::parse(user_data); - members.emplace_back( - RoomMember{QString::fromStdString(std::string(user_id)), - QString::fromStdString(tmp.name)}); - } catch (const json::exception &e) { - nhlog::db()->warn("{}", e.what()); - } + if (currentIndex >= endIndex) + break; - currentIndex += 1; + try { + MemberInfo tmp = json::parse(user_data); + members.emplace_back(RoomMember{QString::fromStdString(std::string(user_id)), + QString::fromStdString(tmp.name)}); + } catch (const json::exception &e) { + nhlog::db()->warn("{}", e.what()); } - cursor.close(); + currentIndex += 1; + } - return members; + cursor.close(); + + return members; } bool Cache::isRoomMember(const std::string &user_id, const std::string &room_id) { - try { - auto txn = ro_txn(env_); - auto db = getMembersDb(txn, room_id); + try { + auto txn = ro_txn(env_); + auto db = getMembersDb(txn, room_id); - std::string_view value; - bool res = db.get(txn, user_id, value); + std::string_view value; + bool res = db.get(txn, user_id, value); - return res; - } catch (std::exception &e) { - nhlog::db()->warn("Failed to read member membership ({}) in room ({}): {}", - user_id, - room_id, - e.what()); - } - return false; + return res; + } catch (std::exception &e) { + nhlog::db()->warn( + "Failed to read member membership ({}) in room ({}): {}", user_id, room_id, e.what()); + } + return false; } void Cache::savePendingMessage(const std::string &room_id, const mtx::events::collections::TimelineEvent &message) { - auto txn = lmdb::txn::begin(env_); - auto eventsDb = getEventsDb(txn, room_id); + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); - mtx::responses::Timeline timeline; - timeline.events.push_back(message.data); - saveTimelineMessages(txn, eventsDb, room_id, timeline); + mtx::responses::Timeline timeline; + timeline.events.push_back(message.data); + saveTimelineMessages(txn, eventsDb, room_id, timeline); - auto pending = getPendingMessagesDb(txn, room_id); + auto pending = getPendingMessagesDb(txn, room_id); - int64_t now = QDateTime::currentMSecsSinceEpoch(); - pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data)); + int64_t now = QDateTime::currentMSecsSinceEpoch(); + pending.put(txn, lmdb::to_sv(now), mtx::accessors::event_id(message.data)); - txn.commit(); + txn.commit(); } std::optional<mtx::events::collections::TimelineEvent> Cache::firstPendingMessage(const std::string &room_id) { - auto txn = lmdb::txn::begin(env_); - auto pending = getPendingMessagesDb(txn, room_id); - - { - auto pendingCursor = lmdb::cursor::open(txn, pending); - std::string_view tsIgnored, pendingTxn; - while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { - auto eventsDb = getEventsDb(txn, room_id); - std::string_view event; - if (!eventsDb.get(txn, pendingTxn, event)) { - pending.del(txn, tsIgnored, pendingTxn); - continue; - } + auto txn = lmdb::txn::begin(env_); + auto pending = getPendingMessagesDb(txn, room_id); + + { + auto pendingCursor = lmdb::cursor::open(txn, pending); + std::string_view tsIgnored, pendingTxn; + while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { + auto eventsDb = getEventsDb(txn, room_id); + std::string_view event; + if (!eventsDb.get(txn, pendingTxn, event)) { + pending.del(txn, tsIgnored, pendingTxn); + continue; + } + + try { + mtx::events::collections::TimelineEvent te; + mtx::events::collections::from_json(json::parse(event), te); - try { - mtx::events::collections::TimelineEvent te; - mtx::events::collections::from_json(json::parse(event), te); - - pendingCursor.close(); - txn.commit(); - return te; - } catch (std::exception &e) { - nhlog::db()->error("Failed to parse message from cache {}", - e.what()); - pending.del(txn, tsIgnored, pendingTxn); - continue; - } - } + pendingCursor.close(); + txn.commit(); + return te; + } catch (std::exception &e) { + nhlog::db()->error("Failed to parse message from cache {}", e.what()); + pending.del(txn, tsIgnored, pendingTxn); + continue; + } } + } - txn.commit(); + txn.commit(); - return std::nullopt; + return std::nullopt; } void Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id) { - auto txn = lmdb::txn::begin(env_); - auto pending = getPendingMessagesDb(txn, room_id); + auto txn = lmdb::txn::begin(env_); + auto pending = getPendingMessagesDb(txn, room_id); - { - auto pendingCursor = lmdb::cursor::open(txn, pending); - std::string_view tsIgnored, pendingTxn; - while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { - if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id) - lmdb::cursor_del(pendingCursor); - } + { + auto pendingCursor = lmdb::cursor::open(txn, pending); + std::string_view tsIgnored, pendingTxn; + while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { + if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id) + lmdb::cursor_del(pendingCursor); } + } - txn.commit(); + txn.commit(); } void @@ -2917,403 +2842,398 @@ Cache::saveTimelineMessages(lmdb::txn &txn, const std::string &room_id, const mtx::responses::Timeline &res) { - if (res.events.empty()) - return; - - auto relationsDb = getRelationsDb(txn, room_id); - - auto orderDb = getEventOrderDb(txn, room_id); - auto evToOrderDb = getEventToOrderDb(txn, room_id); - auto msg2orderDb = getMessageToOrderDb(txn, room_id); - auto order2msgDb = getOrderToMessageDb(txn, room_id); - auto pending = getPendingMessagesDb(txn, room_id); - - if (res.limited) { - lmdb::dbi_drop(txn, orderDb, false); - lmdb::dbi_drop(txn, evToOrderDb, false); - lmdb::dbi_drop(txn, msg2orderDb, false); - lmdb::dbi_drop(txn, order2msgDb, false); - lmdb::dbi_drop(txn, pending, true); - } - - using namespace mtx::events; - using namespace mtx::events::state; - - std::string_view indexVal, val; - uint64_t index = std::numeric_limits<uint64_t>::max() / 2; - auto cursor = lmdb::cursor::open(txn, orderDb); - if (cursor.get(indexVal, val, MDB_LAST)) { - index = lmdb::from_sv<uint64_t>(indexVal); - } - - uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2; - auto msgCursor = lmdb::cursor::open(txn, order2msgDb); - if (msgCursor.get(indexVal, val, MDB_LAST)) { - msgIndex = lmdb::from_sv<uint64_t>(indexVal); - } - - bool first = true; - for (const auto &e : res.events) { - auto event = mtx::accessors::serialize_event(e); - auto txn_id = mtx::accessors::transaction_id(e); - - std::string event_id_val = event.value("event_id", ""); - if (event_id_val.empty()) { - nhlog::db()->error("Event without id!"); - continue; + if (res.events.empty()) + return; + + auto relationsDb = getRelationsDb(txn, room_id); + + auto orderDb = getEventOrderDb(txn, room_id); + auto evToOrderDb = getEventToOrderDb(txn, room_id); + auto msg2orderDb = getMessageToOrderDb(txn, room_id); + auto order2msgDb = getOrderToMessageDb(txn, room_id); + auto pending = getPendingMessagesDb(txn, room_id); + + if (res.limited) { + lmdb::dbi_drop(txn, orderDb, false); + lmdb::dbi_drop(txn, evToOrderDb, false); + lmdb::dbi_drop(txn, msg2orderDb, false); + lmdb::dbi_drop(txn, order2msgDb, false); + lmdb::dbi_drop(txn, pending, true); + } + + using namespace mtx::events; + using namespace mtx::events::state; + + std::string_view indexVal, val; + uint64_t index = std::numeric_limits<uint64_t>::max() / 2; + auto cursor = lmdb::cursor::open(txn, orderDb); + if (cursor.get(indexVal, val, MDB_LAST)) { + index = lmdb::from_sv<uint64_t>(indexVal); + } + + uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2; + auto msgCursor = lmdb::cursor::open(txn, order2msgDb); + if (msgCursor.get(indexVal, val, MDB_LAST)) { + msgIndex = lmdb::from_sv<uint64_t>(indexVal); + } + + bool first = true; + for (const auto &e : res.events) { + auto event = mtx::accessors::serialize_event(e); + auto txn_id = mtx::accessors::transaction_id(e); + + std::string event_id_val = event.value("event_id", ""); + if (event_id_val.empty()) { + nhlog::db()->error("Event without id!"); + continue; + } + + std::string_view event_id = event_id_val; + + json orderEntry = json::object(); + orderEntry["event_id"] = event_id_val; + if (first && !res.prev_batch.empty()) + orderEntry["prev_batch"] = res.prev_batch; + + std::string_view txn_order; + if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) { + eventsDb.put(txn, event_id, event.dump()); + eventsDb.del(txn, txn_id); + + std::string_view msg_txn_order; + if (msg2orderDb.get(txn, txn_id, msg_txn_order)) { + order2msgDb.put(txn, msg_txn_order, event_id); + msg2orderDb.put(txn, event_id, msg_txn_order); + msg2orderDb.del(txn, txn_id); + } + + orderDb.put(txn, txn_order, orderEntry.dump()); + evToOrderDb.put(txn, event_id, txn_order); + evToOrderDb.del(txn, txn_id); + + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + relationsDb.del(txn, r.event_id, txn_id); + relationsDb.put(txn, r.event_id, event_id); + } } + } + + auto pendingCursor = lmdb::cursor::open(txn, pending); + std::string_view tsIgnored, pendingTxn; + while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { + if (std::string_view(pendingTxn.data(), pendingTxn.size()) == txn_id) + lmdb::cursor_del(pendingCursor); + } + } else if (auto redaction = + std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) { + if (redaction->redacts.empty()) + continue; + + std::string_view oldEvent; + bool success = eventsDb.get(txn, redaction->redacts, oldEvent); + if (!success) + continue; + + mtx::events::collections::TimelineEvent te; + try { + mtx::events::collections::from_json( + json::parse(std::string_view(oldEvent.data(), oldEvent.size())), te); + // overwrite the content and add redation data + std::visit( + [redaction](auto &ev) { + ev.unsigned_data.redacted_because = *redaction; + ev.unsigned_data.redacted_by = redaction->event_id; + }, + te.data); + event = mtx::accessors::serialize_event(te.data); + event["content"].clear(); + + } catch (std::exception &e) { + nhlog::db()->error("Failed to parse message from cache {}", e.what()); + continue; + } - std::string_view event_id = event_id_val; - - json orderEntry = json::object(); - orderEntry["event_id"] = event_id_val; - if (first && !res.prev_batch.empty()) - orderEntry["prev_batch"] = res.prev_batch; - - std::string_view txn_order; - if (!txn_id.empty() && evToOrderDb.get(txn, txn_id, txn_order)) { - eventsDb.put(txn, event_id, event.dump()); - eventsDb.del(txn, txn_id); - - std::string_view msg_txn_order; - if (msg2orderDb.get(txn, txn_id, msg_txn_order)) { - order2msgDb.put(txn, msg_txn_order, event_id); - msg2orderDb.put(txn, event_id, msg_txn_order); - msg2orderDb.del(txn, txn_id); - } - - orderDb.put(txn, txn_order, orderEntry.dump()); - evToOrderDb.put(txn, event_id, txn_order); - evToOrderDb.del(txn, txn_id); - - auto relations = mtx::accessors::relations(e); - if (!relations.relations.empty()) { - for (const auto &r : relations.relations) { - if (!r.event_id.empty()) { - relationsDb.del(txn, r.event_id, txn_id); - relationsDb.put(txn, r.event_id, event_id); - } - } - } - - auto pendingCursor = lmdb::cursor::open(txn, pending); - std::string_view tsIgnored, pendingTxn; - while (pendingCursor.get(tsIgnored, pendingTxn, MDB_NEXT)) { - if (std::string_view(pendingTxn.data(), pendingTxn.size()) == - txn_id) - lmdb::cursor_del(pendingCursor); - } - } else if (auto redaction = - std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>( - &e)) { - if (redaction->redacts.empty()) - continue; - - std::string_view oldEvent; - bool success = eventsDb.get(txn, redaction->redacts, oldEvent); - if (!success) - continue; - - mtx::events::collections::TimelineEvent te; - try { - mtx::events::collections::from_json( - json::parse(std::string_view(oldEvent.data(), oldEvent.size())), - te); - // overwrite the content and add redation data - std::visit( - [redaction](auto &ev) { - ev.unsigned_data.redacted_because = *redaction; - ev.unsigned_data.redacted_by = redaction->event_id; - }, - te.data); - event = mtx::accessors::serialize_event(te.data); - event["content"].clear(); - - } catch (std::exception &e) { - nhlog::db()->error("Failed to parse message from cache {}", - e.what()); - continue; - } - - eventsDb.put(txn, redaction->redacts, event.dump()); - eventsDb.put(txn, redaction->event_id, json(*redaction).dump()); - } else { - first = false; + eventsDb.put(txn, redaction->redacts, event.dump()); + eventsDb.put(txn, redaction->event_id, json(*redaction).dump()); + } else { + first = false; - // This check protects against duplicates in the timeline. If the event_id - // is already in the DB, we skip putting it (again) in ordered DBs, and only - // update the event itself and its relations. - std::string_view unused_read; - if (!evToOrderDb.get(txn, event_id, unused_read)) { - ++index; + // This check protects against duplicates in the timeline. If the event_id + // is already in the DB, we skip putting it (again) in ordered DBs, and only + // update the event itself and its relations. + std::string_view unused_read; + if (!evToOrderDb.get(txn, event_id, unused_read)) { + ++index; - nhlog::db()->debug("saving '{}'", orderEntry.dump()); + nhlog::db()->debug("saving '{}'", orderEntry.dump()); - cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND); - evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); + cursor.put(lmdb::to_sv(index), orderEntry.dump(), MDB_APPEND); + evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); - // TODO(Nico): Allow blacklisting more event types in UI - if (!isHiddenEvent(txn, e, room_id)) { - ++msgIndex; - msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND); + // TODO(Nico): Allow blacklisting more event types in UI + if (!isHiddenEvent(txn, e, room_id)) { + ++msgIndex; + msgCursor.put(lmdb::to_sv(msgIndex), event_id, MDB_APPEND); - msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); - } - } else { - nhlog::db()->warn("duplicate event '{}'", orderEntry.dump()); - } - eventsDb.put(txn, event_id, event.dump()); - - auto relations = mtx::accessors::relations(e); - if (!relations.relations.empty()) { - for (const auto &r : relations.relations) { - if (!r.event_id.empty()) { - relationsDb.put(txn, r.event_id, event_id); - } - } - } + msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); + } + } else { + nhlog::db()->warn("duplicate event '{}'", orderEntry.dump()); + } + eventsDb.put(txn, event_id, event.dump()); + + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + relationsDb.put(txn, r.event_id, event_id); + } } + } } + } } uint64_t Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res) { - auto txn = lmdb::txn::begin(env_); - auto eventsDb = getEventsDb(txn, room_id); - auto relationsDb = getRelationsDb(txn, room_id); - - auto orderDb = getEventOrderDb(txn, room_id); - auto evToOrderDb = getEventToOrderDb(txn, room_id); - auto msg2orderDb = getMessageToOrderDb(txn, room_id); - auto order2msgDb = getOrderToMessageDb(txn, room_id); - - std::string_view indexVal, val; - uint64_t index = std::numeric_limits<uint64_t>::max() / 2; - { - auto cursor = lmdb::cursor::open(txn, orderDb); - if (cursor.get(indexVal, val, MDB_FIRST)) { - index = lmdb::from_sv<uint64_t>(indexVal); - } - } + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); - uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2; - { - auto msgCursor = lmdb::cursor::open(txn, order2msgDb); - if (msgCursor.get(indexVal, val, MDB_FIRST)) { - msgIndex = lmdb::from_sv<uint64_t>(indexVal); - } - } + auto orderDb = getEventOrderDb(txn, room_id); + auto evToOrderDb = getEventToOrderDb(txn, room_id); + auto msg2orderDb = getMessageToOrderDb(txn, room_id); + auto order2msgDb = getOrderToMessageDb(txn, room_id); - if (res.chunk.empty()) { - if (orderDb.get(txn, lmdb::to_sv(index), val)) { - auto orderEntry = json::parse(val); - orderEntry["prev_batch"] = res.end; - orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); - txn.commit(); - } - return index; + std::string_view indexVal, val; + uint64_t index = std::numeric_limits<uint64_t>::max() / 2; + { + auto cursor = lmdb::cursor::open(txn, orderDb); + if (cursor.get(indexVal, val, MDB_FIRST)) { + index = lmdb::from_sv<uint64_t>(indexVal); } + } - std::string event_id_val; - for (const auto &e : res.chunk) { - if (std::holds_alternative< - mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e)) - continue; - - auto event = mtx::accessors::serialize_event(e); - event_id_val = event["event_id"].get<std::string>(); - std::string_view event_id = event_id_val; - - // This check protects against duplicates in the timeline. If the event_id is - // already in the DB, we skip putting it (again) in ordered DBs, and only update the - // event itself and its relations. - std::string_view unused_read; - if (!evToOrderDb.get(txn, event_id, unused_read)) { - --index; - - json orderEntry = json::object(); - orderEntry["event_id"] = event_id_val; - - orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); - evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); - - // TODO(Nico): Allow blacklisting more event types in UI - if (!isHiddenEvent(txn, e, room_id)) { - --msgIndex; - order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id); - - msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); - } - } - eventsDb.put(txn, event_id, event.dump()); - - auto relations = mtx::accessors::relations(e); - if (!relations.relations.empty()) { - for (const auto &r : relations.relations) { - if (!r.event_id.empty()) { - relationsDb.put(txn, r.event_id, event_id); - } - } + uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2; + { + auto msgCursor = lmdb::cursor::open(txn, order2msgDb); + if (msgCursor.get(indexVal, val, MDB_FIRST)) { + msgIndex = lmdb::from_sv<uint64_t>(indexVal); + } + } + + if (res.chunk.empty()) { + if (orderDb.get(txn, lmdb::to_sv(index), val)) { + auto orderEntry = json::parse(val); + orderEntry["prev_batch"] = res.end; + orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); + txn.commit(); + } + return index; + } + + std::string event_id_val; + for (const auto &e : res.chunk) { + if (std::holds_alternative<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e)) + continue; + + auto event = mtx::accessors::serialize_event(e); + event_id_val = event["event_id"].get<std::string>(); + std::string_view event_id = event_id_val; + + // This check protects against duplicates in the timeline. If the event_id is + // already in the DB, we skip putting it (again) in ordered DBs, and only update the + // event itself and its relations. + std::string_view unused_read; + if (!evToOrderDb.get(txn, event_id, unused_read)) { + --index; + + json orderEntry = json::object(); + orderEntry["event_id"] = event_id_val; + + orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); + evToOrderDb.put(txn, event_id, lmdb::to_sv(index)); + + // TODO(Nico): Allow blacklisting more event types in UI + if (!isHiddenEvent(txn, e, room_id)) { + --msgIndex; + order2msgDb.put(txn, lmdb::to_sv(msgIndex), event_id); + + msg2orderDb.put(txn, event_id, lmdb::to_sv(msgIndex)); + } + } + eventsDb.put(txn, event_id, event.dump()); + + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + relationsDb.put(txn, r.event_id, event_id); } + } } + } - json orderEntry = json::object(); - orderEntry["event_id"] = event_id_val; - orderEntry["prev_batch"] = res.end; - orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); + json orderEntry = json::object(); + orderEntry["event_id"] = event_id_val; + orderEntry["prev_batch"] = res.end; + orderDb.put(txn, lmdb::to_sv(index), orderEntry.dump()); - txn.commit(); + txn.commit(); - return msgIndex; + return msgIndex; } void Cache::clearTimeline(const std::string &room_id) { - auto txn = lmdb::txn::begin(env_); - auto eventsDb = getEventsDb(txn, room_id); - auto relationsDb = getRelationsDb(txn, room_id); - - auto orderDb = getEventOrderDb(txn, room_id); - auto evToOrderDb = getEventToOrderDb(txn, room_id); - auto msg2orderDb = getMessageToOrderDb(txn, room_id); - auto order2msgDb = getOrderToMessageDb(txn, room_id); + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); - std::string_view indexVal, val; - auto cursor = lmdb::cursor::open(txn, orderDb); + auto orderDb = getEventOrderDb(txn, room_id); + auto evToOrderDb = getEventToOrderDb(txn, room_id); + auto msg2orderDb = getMessageToOrderDb(txn, room_id); + auto order2msgDb = getOrderToMessageDb(txn, room_id); - bool start = true; - bool passed_pagination_token = false; - while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) { - start = false; - json obj; + std::string_view indexVal, val; + auto cursor = lmdb::cursor::open(txn, orderDb); - try { - obj = json::parse(std::string_view(val.data(), val.size())); - } catch (std::exception &) { - // workaround bug in the initial db format, where we sometimes didn't store - // json... - obj = {{"event_id", std::string(val.data(), val.size())}}; - } + bool start = true; + bool passed_pagination_token = false; + while (cursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) { + start = false; + json obj; - if (passed_pagination_token) { - if (obj.count("event_id") != 0) { - std::string event_id = obj["event_id"].get<std::string>(); - - if (!event_id.empty()) { - evToOrderDb.del(txn, event_id); - eventsDb.del(txn, event_id); - relationsDb.del(txn, event_id); - - std::string_view order{}; - bool exists = msg2orderDb.get(txn, event_id, order); - if (exists) { - order2msgDb.del(txn, order); - msg2orderDb.del(txn, event_id); - } - } - } - lmdb::cursor_del(cursor); - } else { - if (obj.count("prev_batch") != 0) - passed_pagination_token = true; + try { + obj = json::parse(std::string_view(val.data(), val.size())); + } catch (std::exception &) { + // workaround bug in the initial db format, where we sometimes didn't store + // json... + obj = {{"event_id", std::string(val.data(), val.size())}}; + } + + if (passed_pagination_token) { + if (obj.count("event_id") != 0) { + std::string event_id = obj["event_id"].get<std::string>(); + + if (!event_id.empty()) { + evToOrderDb.del(txn, event_id); + eventsDb.del(txn, event_id); + relationsDb.del(txn, event_id); + + std::string_view order{}; + bool exists = msg2orderDb.get(txn, event_id, order); + if (exists) { + order2msgDb.del(txn, order); + msg2orderDb.del(txn, event_id); + } } + } + lmdb::cursor_del(cursor); + } else { + if (obj.count("prev_batch") != 0) + passed_pagination_token = true; } + } - auto msgCursor = lmdb::cursor::open(txn, order2msgDb); - start = true; - while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) { - start = false; - - std::string_view eventId; - bool innerStart = true; - bool found = false; - while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) { - innerStart = false; - - json obj; - try { - obj = json::parse(std::string_view(eventId.data(), eventId.size())); - } catch (std::exception &) { - obj = {{"event_id", std::string(eventId.data(), eventId.size())}}; - } + auto msgCursor = lmdb::cursor::open(txn, order2msgDb); + start = true; + while (msgCursor.get(indexVal, val, start ? MDB_LAST : MDB_PREV)) { + start = false; - if (obj["event_id"] == std::string(val.data(), val.size())) { - found = true; - break; - } - } + std::string_view eventId; + bool innerStart = true; + bool found = false; + while (cursor.get(indexVal, eventId, innerStart ? MDB_LAST : MDB_PREV)) { + innerStart = false; - if (!found) - break; + json obj; + try { + obj = json::parse(std::string_view(eventId.data(), eventId.size())); + } catch (std::exception &) { + obj = {{"event_id", std::string(eventId.data(), eventId.size())}}; + } + + if (obj["event_id"] == std::string(val.data(), val.size())) { + found = true; + break; + } } - do { - lmdb::cursor_del(msgCursor); - } while (msgCursor.get(indexVal, val, MDB_PREV)); + if (!found) + break; + } - cursor.close(); - msgCursor.close(); - txn.commit(); + do { + lmdb::cursor_del(msgCursor); + } while (msgCursor.get(indexVal, val, MDB_PREV)); + + cursor.close(); + msgCursor.close(); + txn.commit(); } mtx::responses::Notifications Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id) { - auto db = getMentionsDb(txn, room_id); + auto db = getMentionsDb(txn, room_id); - if (db.size(txn) == 0) { - return mtx::responses::Notifications{}; - } + if (db.size(txn) == 0) { + return mtx::responses::Notifications{}; + } - mtx::responses::Notifications notif; - std::string_view event_id, msg; + mtx::responses::Notifications notif; + std::string_view event_id, msg; - auto cursor = lmdb::cursor::open(txn, db); + auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(event_id, msg, MDB_NEXT)) { - auto obj = json::parse(msg); + while (cursor.get(event_id, msg, MDB_NEXT)) { + auto obj = json::parse(msg); - if (obj.count("event") == 0) - continue; + if (obj.count("event") == 0) + continue; - mtx::responses::Notification notification; - mtx::responses::from_json(obj, notification); + mtx::responses::Notification notification; + mtx::responses::from_json(obj, notification); - notif.notifications.push_back(notification); - } - cursor.close(); + notif.notifications.push_back(notification); + } + cursor.close(); - std::reverse(notif.notifications.begin(), notif.notifications.end()); + std::reverse(notif.notifications.begin(), notif.notifications.end()); - return notif; + return notif; } //! Add all notifications containing a user mention to the db. void Cache::saveTimelineMentions(const mtx::responses::Notifications &res) { - QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom; + QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom; - // Sort into room-specific 'buckets' - for (const auto ¬if : res.notifications) { - json val = notif; - notifsByRoom[notif.room_id].push_back(notif); - } + // Sort into room-specific 'buckets' + for (const auto ¬if : res.notifications) { + json val = notif; + notifsByRoom[notif.room_id].push_back(notif); + } - auto txn = lmdb::txn::begin(env_); - // Insert the entire set of mentions for each room at a time. - QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it = - notifsByRoom.constBegin(); - auto end = notifsByRoom.constEnd(); - while (it != end) { - nhlog::db()->debug("Storing notifications for " + it.key()); - saveTimelineMentions(txn, it.key(), std::move(it.value())); - ++it; - } + auto txn = lmdb::txn::begin(env_); + // Insert the entire set of mentions for each room at a time. + QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it = + notifsByRoom.constBegin(); + auto end = notifsByRoom.constEnd(); + while (it != end) { + nhlog::db()->debug("Storing notifications for " + it.key()); + saveTimelineMentions(txn, it.key(), std::move(it.value())); + ++it; + } - txn.commit(); + txn.commit(); } void @@ -3321,138 +3241,138 @@ Cache::saveTimelineMentions(lmdb::txn &txn, const std::string &room_id, const QList<mtx::responses::Notification> &res) { - auto db = getMentionsDb(txn, room_id); + auto db = getMentionsDb(txn, room_id); - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - for (const auto ¬if : res) { - const auto event_id = mtx::accessors::event_id(notif.event); + for (const auto ¬if : res) { + const auto event_id = mtx::accessors::event_id(notif.event); - // double check that we have the correct room_id... - if (room_id.compare(notif.room_id) != 0) { - return; - } + // double check that we have the correct room_id... + if (room_id.compare(notif.room_id) != 0) { + return; + } - json obj = notif; + json obj = notif; - db.put(txn, event_id, obj.dump()); - } + db.put(txn, event_id, obj.dump()); + } } void Cache::markSentNotification(const std::string &event_id) { - auto txn = lmdb::txn::begin(env_); - notificationsDb_.put(txn, event_id, ""); - txn.commit(); + auto txn = lmdb::txn::begin(env_); + notificationsDb_.put(txn, event_id, ""); + txn.commit(); } void Cache::removeReadNotification(const std::string &event_id) { - auto txn = lmdb::txn::begin(env_); + auto txn = lmdb::txn::begin(env_); - notificationsDb_.del(txn, event_id); + notificationsDb_.del(txn, event_id); - txn.commit(); + txn.commit(); } bool Cache::isNotificationSent(const std::string &event_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::string_view value; - bool res = notificationsDb_.get(txn, event_id, value); + std::string_view value; + bool res = notificationsDb_.get(txn, event_id, value); - return res; + return res; } std::vector<std::string> Cache::getRoomIds(lmdb::txn &txn) { - auto cursor = lmdb::cursor::open(txn, roomsDb_); + auto cursor = lmdb::cursor::open(txn, roomsDb_); - std::vector<std::string> rooms; + std::vector<std::string> rooms; - std::string_view room_id, _unused; - while (cursor.get(room_id, _unused, MDB_NEXT)) - rooms.emplace_back(room_id); + std::string_view room_id, _unused; + while (cursor.get(room_id, _unused, MDB_NEXT)) + rooms.emplace_back(room_id); - cursor.close(); + cursor.close(); - return rooms; + return rooms; } void Cache::deleteOldMessages() { - std::string_view indexVal, val; + std::string_view indexVal, val; - auto txn = lmdb::txn::begin(env_); - auto room_ids = getRoomIds(txn); + auto txn = lmdb::txn::begin(env_); + auto room_ids = getRoomIds(txn); - for (const auto &room_id : room_ids) { - auto orderDb = getEventOrderDb(txn, room_id); - auto evToOrderDb = getEventToOrderDb(txn, room_id); - auto o2m = getOrderToMessageDb(txn, room_id); - auto m2o = getMessageToOrderDb(txn, room_id); - auto eventsDb = getEventsDb(txn, room_id); - auto relationsDb = getRelationsDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, orderDb); + for (const auto &room_id : room_ids) { + auto orderDb = getEventOrderDb(txn, room_id); + auto evToOrderDb = getEventToOrderDb(txn, room_id); + auto o2m = getOrderToMessageDb(txn, room_id); + auto m2o = getMessageToOrderDb(txn, room_id); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, orderDb); - uint64_t first, last; - if (cursor.get(indexVal, val, MDB_LAST)) { - last = lmdb::from_sv<uint64_t>(indexVal); - } else { - continue; - } - if (cursor.get(indexVal, val, MDB_FIRST)) { - first = lmdb::from_sv<uint64_t>(indexVal); - } else { - continue; - } + uint64_t first, last; + if (cursor.get(indexVal, val, MDB_LAST)) { + last = lmdb::from_sv<uint64_t>(indexVal); + } else { + continue; + } + if (cursor.get(indexVal, val, MDB_FIRST)) { + first = lmdb::from_sv<uint64_t>(indexVal); + } else { + continue; + } - size_t message_count = static_cast<size_t>(last - first); - if (message_count < MAX_RESTORED_MESSAGES) - continue; - - bool start = true; - while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) && - message_count-- > MAX_RESTORED_MESSAGES) { - start = false; - auto obj = json::parse(std::string_view(val.data(), val.size())); - - if (obj.count("event_id") != 0) { - std::string event_id = obj["event_id"].get<std::string>(); - evToOrderDb.del(txn, event_id); - eventsDb.del(txn, event_id); - - relationsDb.del(txn, event_id); - - std::string_view order{}; - bool exists = m2o.get(txn, event_id, order); - if (exists) { - o2m.del(txn, order); - m2o.del(txn, event_id); - } - } - cursor.del(); + size_t message_count = static_cast<size_t>(last - first); + if (message_count < MAX_RESTORED_MESSAGES) + continue; + + bool start = true; + while (cursor.get(indexVal, val, start ? MDB_FIRST : MDB_NEXT) && + message_count-- > MAX_RESTORED_MESSAGES) { + start = false; + auto obj = json::parse(std::string_view(val.data(), val.size())); + + if (obj.count("event_id") != 0) { + std::string event_id = obj["event_id"].get<std::string>(); + evToOrderDb.del(txn, event_id); + eventsDb.del(txn, event_id); + + relationsDb.del(txn, event_id); + + std::string_view order{}; + bool exists = m2o.get(txn, event_id, order); + if (exists) { + o2m.del(txn, order); + m2o.del(txn, event_id); } - cursor.close(); + } + cursor.del(); } - txn.commit(); + cursor.close(); + } + txn.commit(); } void Cache::deleteOldData() noexcept { - try { - deleteOldMessages(); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to delete old messages: {}", e.what()); - } + try { + deleteOldMessages(); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to delete old messages: {}", e.what()); + } } void @@ -3460,241 +3380,232 @@ Cache::updateSpaces(lmdb::txn &txn, const std::set<std::string> &spaces_with_updates, std::set<std::string> rooms_with_updates) { - if (spaces_with_updates.empty() && rooms_with_updates.empty()) - return; + if (spaces_with_updates.empty() && rooms_with_updates.empty()) + return; - for (const auto &space : spaces_with_updates) { - // delete old entries - { - auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); - bool first = true; - std::string_view sp = space, space_child = ""; - - if (cursor.get(sp, space_child, MDB_SET)) { - while (cursor.get( - sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { - first = false; - spacesParentsDb_.del(txn, space_child, space); - } - } - cursor.close(); - spacesChildrenDb_.del(txn, space); + for (const auto &space : spaces_with_updates) { + // delete old entries + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view sp = space, space_child = ""; + + if (cursor.get(sp, space_child, MDB_SET)) { + while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + spacesParentsDb_.del(txn, space_child, space); } + } + cursor.close(); + spacesChildrenDb_.del(txn, space); + } - for (const auto &event : - getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) { - if (event.content.via.has_value() && event.state_key.size() > 3 && - event.state_key.at(0) == '!') { - spacesChildrenDb_.put(txn, space, event.state_key); - spacesParentsDb_.put(txn, event.state_key, space); - } - } + for (const auto &event : + getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) { + if (event.content.via.has_value() && event.state_key.size() > 3 && + event.state_key.at(0) == '!') { + spacesChildrenDb_.put(txn, space, event.state_key); + spacesParentsDb_.put(txn, event.state_key, space); + } } + } - const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels); + const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels); - for (const auto &room : rooms_with_updates) { - for (const auto &event : - getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) { - if (event.content.via.has_value() && event.state_key.size() > 3 && - event.state_key.at(0) == '!') { - const std::string &space = event.state_key; + for (const auto &room : rooms_with_updates) { + for (const auto &event : + getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) { + if (event.content.via.has_value() && event.state_key.size() > 3 && + event.state_key.at(0) == '!') { + const std::string &space = event.state_key; - auto pls = - getStateEvent<mtx::events::state::PowerLevels>(txn, space); + auto pls = getStateEvent<mtx::events::state::PowerLevels>(txn, space); - if (!pls) - continue; + if (!pls) + continue; - if (pls->content.user_level(event.sender) >= - pls->content.state_level(space_event_type)) { - spacesChildrenDb_.put(txn, space, room); - spacesParentsDb_.put(txn, room, space); - } - } + if (pls->content.user_level(event.sender) >= + pls->content.state_level(space_event_type)) { + spacesChildrenDb_.put(txn, space, room); + spacesParentsDb_.put(txn, room, space); } + } } + } } QMap<QString, std::optional<RoomInfo>> Cache::spaces() { - auto txn = ro_txn(env_); - - QMap<QString, std::optional<RoomInfo>> ret; - { - auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); - bool first = true; - std::string_view space_id, space_child; - while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) { - first = false; - - if (!space_child.empty()) { - std::string_view room_data; - if (roomsDb_.get(txn, space_id, room_data)) { - RoomInfo tmp = json::parse(std::move(room_data)); - ret.insert( - QString::fromUtf8(space_id.data(), space_id.size()), tmp); - } else { - ret.insert( - QString::fromUtf8(space_id.data(), space_id.size()), - std::nullopt); - } - } + auto txn = ro_txn(env_); + + QMap<QString, std::optional<RoomInfo>> ret; + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view space_id, space_child; + while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) { + first = false; + + if (!space_child.empty()) { + std::string_view room_data; + if (roomsDb_.get(txn, space_id, room_data)) { + RoomInfo tmp = json::parse(std::move(room_data)); + ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), tmp); + } else { + ret.insert(QString::fromUtf8(space_id.data(), space_id.size()), std::nullopt); } - cursor.close(); + } } + cursor.close(); + } - return ret; + return ret; } std::vector<std::string> Cache::getParentRoomIds(const std::string &room_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::vector<std::string> roomids; - { - auto cursor = lmdb::cursor::open(txn, spacesParentsDb_); - bool first = true; - std::string_view sp = room_id, space_parent; - if (cursor.get(sp, space_parent, MDB_SET)) { - while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { - first = false; - - if (!space_parent.empty()) - roomids.emplace_back(space_parent); - } - } - cursor.close(); + std::vector<std::string> roomids; + { + auto cursor = lmdb::cursor::open(txn, spacesParentsDb_); + bool first = true; + std::string_view sp = room_id, space_parent; + if (cursor.get(sp, space_parent, MDB_SET)) { + while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (!space_parent.empty()) + roomids.emplace_back(space_parent); + } } + cursor.close(); + } - return roomids; + return roomids; } std::vector<std::string> Cache::getChildRoomIds(const std::string &room_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::vector<std::string> roomids; - { - auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); - bool first = true; - std::string_view sp = room_id, space_child; - if (cursor.get(sp, space_child, MDB_SET)) { - while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { - first = false; - - if (!space_child.empty()) - roomids.emplace_back(space_child); - } - } - cursor.close(); + std::vector<std::string> roomids; + { + auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_); + bool first = true; + std::string_view sp = room_id, space_child; + if (cursor.get(sp, space_child, MDB_SET)) { + while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (!space_child.empty()) + roomids.emplace_back(space_child); + } } + cursor.close(); + } - return roomids; + return roomids; } std::vector<ImagePackInfo> Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers) { - auto txn = ro_txn(env_); - std::vector<ImagePackInfo> infos; - - auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack, - const std::string &source_room, - const std::string &state_key) { - if (!pack.pack || !stickers.has_value() || - (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) { - ImagePackInfo info; - info.source_room = source_room; - info.state_key = state_key; - info.pack.pack = pack.pack; - - for (const auto &img : pack.images) { - if (stickers.has_value() && img.second.overrides_usage() && - (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) - continue; - - info.pack.images.insert(img); - } - - if (!info.pack.images.empty()) - infos.push_back(std::move(info)); - } - }; - - // packs from account data - if (auto accountpack = - getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) { - auto tmp = - std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>( - &*accountpack); - if (tmp) - addPack(tmp->content, "", ""); - } - - // packs from rooms, that were enabled globally - if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) { - auto tmp = - std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( - &*roomPacks); - if (tmp) { - for (const auto &[room_id2, state_to_d] : tmp->content.rooms) { - // don't add stickers from this room twice - if (room_id2 == room_id) - continue; - - for (const auto &[state_id, d] : state_to_d) { - (void)d; - if (auto pack = - getStateEvent<mtx::events::msc2545::ImagePack>( - txn, room_id2, state_id)) - addPack(pack->content, room_id2, state_id); - } - } + auto txn = ro_txn(env_); + std::vector<ImagePackInfo> infos; + + auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack, + const std::string &source_room, + const std::string &state_key) { + if (!pack.pack || !stickers.has_value() || + (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) { + ImagePackInfo info; + info.source_room = source_room; + info.state_key = state_key; + info.pack.pack = pack.pack; + + for (const auto &img : pack.images) { + if (stickers.has_value() && img.second.overrides_usage() && + (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) + continue; + + info.pack.images.insert(img); + } + + if (!info.pack.images.empty()) + infos.push_back(std::move(info)); + } + }; + + // packs from account data + if (auto accountpack = + getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) { + auto tmp = + std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(&*accountpack); + if (tmp) + addPack(tmp->content, "", ""); + } + + // packs from rooms, that were enabled globally + if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) { + auto tmp = std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( + &*roomPacks); + if (tmp) { + for (const auto &[room_id2, state_to_d] : tmp->content.rooms) { + // don't add stickers from this room twice + if (room_id2 == room_id) + continue; + + for (const auto &[state_id, d] : state_to_d) { + (void)d; + if (auto pack = + getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id2, state_id)) + addPack(pack->content, room_id2, state_id); } + } } + } - // packs from current room - if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) { - addPack(pack->content, room_id, ""); - } - for (const auto &pack : - getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) { - addPack(pack.content, room_id, pack.state_key); - } + // packs from current room + if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) { + addPack(pack->content, room_id, ""); + } + for (const auto &pack : getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) { + addPack(pack.content, room_id, pack.state_key); + } - return infos; + return infos; } std::optional<mtx::events::collections::RoomAccountDataEvents> Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) { - auto txn = ro_txn(env_); - return getAccountData(txn, type, room_id); + auto txn = ro_txn(env_); + return getAccountData(txn, type, room_id); } std::optional<mtx::events::collections::RoomAccountDataEvents> Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { - try { - auto db = getAccountDataDb(txn, room_id); - - std::string_view data; - if (db.get(txn, to_string(type), data)) { - mtx::responses::utils::RoomAccountDataEvents events; - json j = json::array({ - json::parse(data), - }); - mtx::responses::utils::parse_room_account_data_events(j, events); - if (events.size() == 1) - return events.front(); - } - } catch (...) { + try { + auto db = getAccountDataDb(txn, room_id); + + std::string_view data; + if (db.get(txn, to_string(type), data)) { + mtx::responses::utils::RoomAccountDataEvents events; + json j = json::array({ + json::parse(data), + }); + mtx::responses::utils::parse_room_account_data_events(j, events); + if (events.size() == 1) + return events.front(); } - return std::nullopt; + } catch (...) { + } + return std::nullopt; } bool @@ -3702,465 +3613,436 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes const std::string &room_id, const std::string &user_id) { - using namespace mtx::events; - using namespace mtx::events::state; + using namespace mtx::events; + using namespace mtx::events::state; - auto txn = lmdb::txn::begin(env_); - auto db = getStatesDb(txn, room_id); + auto txn = lmdb::txn::begin(env_); + auto db = getStatesDb(txn, room_id); - int64_t min_event_level = std::numeric_limits<int64_t>::max(); - int64_t user_level = std::numeric_limits<int64_t>::min(); + int64_t min_event_level = std::numeric_limits<int64_t>::max(); + int64_t user_level = std::numeric_limits<int64_t>::min(); - std::string_view event; - bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event); + std::string_view event; + bool res = db.get(txn, to_string(EventType::RoomPowerLevels), event); - if (res) { - try { - StateEvent<PowerLevels> msg = - json::parse(std::string_view(event.data(), event.size())); + if (res) { + try { + StateEvent<PowerLevels> msg = json::parse(std::string_view(event.data(), event.size())); - user_level = msg.content.user_level(user_id); + user_level = msg.content.user_level(user_id); - for (const auto &ty : eventTypes) - min_event_level = - std::min(min_event_level, msg.content.state_level(to_string(ty))); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.power_levels event: {}", - e.what()); - } + for (const auto &ty : eventTypes) + min_event_level = std::min(min_event_level, msg.content.state_level(to_string(ty))); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.power_levels event: {}", e.what()); } + } - txn.commit(); + txn.commit(); - return user_level >= min_event_level; + return user_level >= min_event_level; } std::vector<std::string> Cache::roomMembers(const std::string &room_id) { - auto txn = ro_txn(env_); + auto txn = ro_txn(env_); - std::vector<std::string> members; - std::string_view user_id, unused; + std::vector<std::string> members; + std::string_view user_id, unused; - auto db = getMembersDb(txn, room_id); + auto db = getMembersDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(user_id, unused, MDB_NEXT)) - members.emplace_back(user_id); - cursor.close(); + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) + members.emplace_back(user_id); + cursor.close(); - return members; + return members; } crypto::Trust Cache::roomVerificationStatus(const std::string &room_id) { - crypto::Trust trust = crypto::Verified; + crypto::Trust trust = crypto::Verified; - try { - auto txn = lmdb::txn::begin(env_); - - auto db = getMembersDb(txn, room_id); - auto keysDb = getUserKeysDb(txn); - std::vector<std::string> keysToRequest; - - std::string_view user_id, unused; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(user_id, unused, MDB_NEXT)) { - auto verif = verificationStatus_(std::string(user_id), txn); - if (verif.unverified_device_count) { - trust = crypto::Unverified; - if (verif.verified_devices.empty() && verif.no_keys) { - // we probably don't have the keys yet, so query them - keysToRequest.push_back(std::string(user_id)); - } - } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified) - trust = crypto::TOFU; - } + try { + auto txn = lmdb::txn::begin(env_); - if (!keysToRequest.empty()) - markUserKeysOutOfDate(txn, keysDb, keysToRequest, ""); + auto db = getMembersDb(txn, room_id); + auto keysDb = getUserKeysDb(txn); + std::vector<std::string> keysToRequest; - } catch (std::exception &e) { - nhlog::db()->error( - "Failed to calculate verification status for {}: {}", room_id, e.what()); + std::string_view user_id, unused; + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) { + auto verif = verificationStatus_(std::string(user_id), txn); + if (verif.unverified_device_count) { trust = crypto::Unverified; + if (verif.verified_devices.empty() && verif.no_keys) { + // we probably don't have the keys yet, so query them + keysToRequest.push_back(std::string(user_id)); + } + } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified) + trust = crypto::TOFU; } - return trust; + if (!keysToRequest.empty()) + markUserKeysOutOfDate(txn, keysDb, keysToRequest, ""); + + } catch (std::exception &e) { + nhlog::db()->error("Failed to calculate verification status for {}: {}", room_id, e.what()); + trust = crypto::Unverified; + } + + return trust; } std::map<std::string, std::optional<UserKeyCache>> Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { - std::string_view keys; + std::string_view keys; - try { - auto txn = ro_txn(env_); - std::map<std::string, std::optional<UserKeyCache>> members; - - auto db = getMembersDb(txn, room_id); - auto keysDb = getUserKeysDb(txn); - - std::string_view user_id, unused; - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(user_id, unused, MDB_NEXT)) { - auto res = keysDb.get(txn, user_id, keys); - - if (res) { - auto k = json::parse(keys).get<UserKeyCache>(); - if (verified_only) { - auto verif = verificationStatus_(std::string(user_id), txn); - - if (verif.user_verified == crypto::Trust::Verified || - !verif.verified_devices.empty()) { - auto keyCopy = k; - keyCopy.device_keys.clear(); - - std::copy_if( - k.device_keys.begin(), - k.device_keys.end(), - std::inserter(keyCopy.device_keys, - keyCopy.device_keys.end()), - [&verif](const auto &key) { - auto curve25519 = key.second.keys.find( - "curve25519:" + key.first); - if (curve25519 == key.second.keys.end()) - return false; - if (auto t = - verif.verified_device_keys.find( - curve25519->second); - t == - verif.verified_device_keys.end() || - t->second != crypto::Trust::Verified) - return false; - - return key.first == - key.second.device_id && - std::find( - verif.verified_devices.begin(), - verif.verified_devices.end(), - key.first) != - verif.verified_devices.end(); - }); - - if (!keyCopy.device_keys.empty()) - members[std::string(user_id)] = - std::move(keyCopy); - } - } else { - members[std::string(user_id)] = std::move(k); - } - } else { - if (!verified_only) - members[std::string(user_id)] = {}; - } - } - cursor.close(); + try { + auto txn = ro_txn(env_); + std::map<std::string, std::optional<UserKeyCache>> members; - return members; - } catch (std::exception &e) { - nhlog::db()->debug("Error retrieving members: {}", e.what()); - return {}; + auto db = getMembersDb(txn, room_id); + auto keysDb = getUserKeysDb(txn); + + std::string_view user_id, unused; + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) { + auto res = keysDb.get(txn, user_id, keys); + + if (res) { + auto k = json::parse(keys).get<UserKeyCache>(); + if (verified_only) { + auto verif = verificationStatus_(std::string(user_id), txn); + + if (verif.user_verified == crypto::Trust::Verified || + !verif.verified_devices.empty()) { + auto keyCopy = k; + keyCopy.device_keys.clear(); + + std::copy_if( + k.device_keys.begin(), + k.device_keys.end(), + std::inserter(keyCopy.device_keys, keyCopy.device_keys.end()), + [&verif](const auto &key) { + auto curve25519 = key.second.keys.find("curve25519:" + key.first); + if (curve25519 == key.second.keys.end()) + return false; + if (auto t = verif.verified_device_keys.find(curve25519->second); + t == verif.verified_device_keys.end() || + t->second != crypto::Trust::Verified) + return false; + + return key.first == key.second.device_id && + std::find(verif.verified_devices.begin(), + verif.verified_devices.end(), + key.first) != verif.verified_devices.end(); + }); + + if (!keyCopy.device_keys.empty()) + members[std::string(user_id)] = std::move(keyCopy); + } + } else { + members[std::string(user_id)] = std::move(k); + } + } else { + if (!verified_only) + members[std::string(user_id)] = {}; + } } + cursor.close(); + + return members; + } catch (std::exception &e) { + nhlog::db()->debug("Error retrieving members: {}", e.what()); + return {}; + } } QString Cache::displayName(const QString &room_id, const QString &user_id) { - if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); - info && !info->name.empty()) - return QString::fromStdString(info->name); + if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); + info && !info->name.empty()) + return QString::fromStdString(info->name); - return user_id; + return user_id; } std::string Cache::displayName(const std::string &room_id, const std::string &user_id) { - if (auto info = getMember(room_id, user_id); info && !info->name.empty()) - return info->name; + if (auto info = getMember(room_id, user_id); info && !info->name.empty()) + return info->name; - return user_id; + return user_id; } QString Cache::avatarUrl(const QString &room_id, const QString &user_id) { - if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); - info && !info->avatar_url.empty()) - return QString::fromStdString(info->avatar_url); + if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); + info && !info->avatar_url.empty()) + return QString::fromStdString(info->avatar_url); - return ""; + return ""; } mtx::presence::PresenceState Cache::presenceState(const std::string &user_id) { - if (user_id.empty()) - return {}; + if (user_id.empty()) + return {}; - std::string_view presenceVal; + std::string_view presenceVal; - auto txn = lmdb::txn::begin(env_); - auto db = getPresenceDb(txn); - auto res = db.get(txn, user_id, presenceVal); + auto txn = lmdb::txn::begin(env_); + auto db = getPresenceDb(txn); + auto res = db.get(txn, user_id, presenceVal); - mtx::presence::PresenceState state = mtx::presence::offline; + mtx::presence::PresenceState state = mtx::presence::offline; - if (res) { - mtx::events::presence::Presence presence = - json::parse(std::string_view(presenceVal.data(), presenceVal.size())); - state = presence.presence; - } + if (res) { + mtx::events::presence::Presence presence = + json::parse(std::string_view(presenceVal.data(), presenceVal.size())); + state = presence.presence; + } - txn.commit(); + txn.commit(); - return state; + return state; } std::string Cache::statusMessage(const std::string &user_id) { - if (user_id.empty()) - return {}; + if (user_id.empty()) + return {}; - std::string_view presenceVal; + std::string_view presenceVal; - auto txn = lmdb::txn::begin(env_); - auto db = getPresenceDb(txn); - auto res = db.get(txn, user_id, presenceVal); + auto txn = lmdb::txn::begin(env_); + auto db = getPresenceDb(txn); + auto res = db.get(txn, user_id, presenceVal); - std::string status_msg; + std::string status_msg; - if (res) { - mtx::events::presence::Presence presence = json::parse(presenceVal); - status_msg = presence.status_msg; - } + if (res) { + mtx::events::presence::Presence presence = json::parse(presenceVal); + status_msg = presence.status_msg; + } - txn.commit(); + txn.commit(); - return status_msg; + return status_msg; } void to_json(json &j, const UserKeyCache &info) { - j["device_keys"] = info.device_keys; - j["seen_device_keys"] = info.seen_device_keys; - j["seen_device_ids"] = info.seen_device_ids; - j["master_keys"] = info.master_keys; - j["master_key_changed"] = info.master_key_changed; - j["user_signing_keys"] = info.user_signing_keys; - j["self_signing_keys"] = info.self_signing_keys; - j["updated_at"] = info.updated_at; - j["last_changed"] = info.last_changed; + j["device_keys"] = info.device_keys; + j["seen_device_keys"] = info.seen_device_keys; + j["seen_device_ids"] = info.seen_device_ids; + j["master_keys"] = info.master_keys; + j["master_key_changed"] = info.master_key_changed; + j["user_signing_keys"] = info.user_signing_keys; + j["self_signing_keys"] = info.self_signing_keys; + j["updated_at"] = info.updated_at; + j["last_changed"] = info.last_changed; } void from_json(const json &j, UserKeyCache &info) { - info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{}); - info.seen_device_keys = j.value("seen_device_keys", std::set<std::string>{}); - info.seen_device_ids = j.value("seen_device_ids", std::set<std::string>{}); - info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); - info.master_key_changed = j.value("master_key_changed", false); - info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); - info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{}); - info.updated_at = j.value("updated_at", ""); - info.last_changed = j.value("last_changed", ""); + info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{}); + info.seen_device_keys = j.value("seen_device_keys", std::set<std::string>{}); + info.seen_device_ids = j.value("seen_device_ids", std::set<std::string>{}); + info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); + info.master_key_changed = j.value("master_key_changed", false); + info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); + info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{}); + info.updated_at = j.value("updated_at", ""); + info.last_changed = j.value("last_changed", ""); } std::optional<UserKeyCache> Cache::userKeys(const std::string &user_id) { - auto txn = ro_txn(env_); - return userKeys_(user_id, txn); + auto txn = ro_txn(env_); + return userKeys_(user_id, txn); } std::optional<UserKeyCache> Cache::userKeys_(const std::string &user_id, lmdb::txn &txn) { - std::string_view keys; + std::string_view keys; - try { - auto db = getUserKeysDb(txn); - auto res = db.get(txn, user_id, keys); + try { + auto db = getUserKeysDb(txn); + auto res = db.get(txn, user_id, keys); - if (res) { - return json::parse(keys).get<UserKeyCache>(); - } else { - return {}; - } - } catch (std::exception &e) { - nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what()); - return {}; + if (res) { + return json::parse(keys).get<UserKeyCache>(); + } else { + return {}; } + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what()); + return {}; + } } void Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) { - auto txn = lmdb::txn::begin(env_); - auto db = getUserKeysDb(txn); + auto txn = lmdb::txn::begin(env_); + auto db = getUserKeysDb(txn); - std::map<std::string, UserKeyCache> updates; - - for (const auto &[user, keys] : keyQuery.device_keys) - updates[user].device_keys = keys; - for (const auto &[user, keys] : keyQuery.master_keys) - updates[user].master_keys = keys; - for (const auto &[user, keys] : keyQuery.user_signing_keys) - updates[user].user_signing_keys = keys; - for (const auto &[user, keys] : keyQuery.self_signing_keys) - updates[user].self_signing_keys = keys; - - for (auto &[user, update] : updates) { - nhlog::db()->debug("Updated user keys: {}", user); - - auto updateToWrite = update; - - std::string_view oldKeys; - auto res = db.get(txn, user, oldKeys); - - if (res) { - updateToWrite = json::parse(oldKeys).get<UserKeyCache>(); - auto last_changed = updateToWrite.last_changed; - // skip if we are tracking this and expect it to be up to date with the last - // sync token - if (!last_changed.empty() && last_changed != sync_token) { - nhlog::db()->debug("Not storing update for user {}, because " - "last_changed {}, but we fetched update for {}", - user, - last_changed, - sync_token); - continue; - } + std::map<std::string, UserKeyCache> updates; + + for (const auto &[user, keys] : keyQuery.device_keys) + updates[user].device_keys = keys; + for (const auto &[user, keys] : keyQuery.master_keys) + updates[user].master_keys = keys; + for (const auto &[user, keys] : keyQuery.user_signing_keys) + updates[user].user_signing_keys = keys; + for (const auto &[user, keys] : keyQuery.self_signing_keys) + updates[user].self_signing_keys = keys; + + for (auto &[user, update] : updates) { + nhlog::db()->debug("Updated user keys: {}", user); + + auto updateToWrite = update; - if (!updateToWrite.master_keys.keys.empty() && - update.master_keys.keys != updateToWrite.master_keys.keys) { - nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", - user, - updateToWrite.master_keys.keys.size(), - update.master_keys.keys.size()); - updateToWrite.master_key_changed = true; + std::string_view oldKeys; + auto res = db.get(txn, user, oldKeys); + + if (res) { + updateToWrite = json::parse(oldKeys).get<UserKeyCache>(); + auto last_changed = updateToWrite.last_changed; + // skip if we are tracking this and expect it to be up to date with the last + // sync token + if (!last_changed.empty() && last_changed != sync_token) { + nhlog::db()->debug("Not storing update for user {}, because " + "last_changed {}, but we fetched update for {}", + user, + last_changed, + sync_token); + continue; + } + + if (!updateToWrite.master_keys.keys.empty() && + update.master_keys.keys != updateToWrite.master_keys.keys) { + nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", + user, + updateToWrite.master_keys.keys.size(), + update.master_keys.keys.size()); + updateToWrite.master_key_changed = true; + } + + updateToWrite.master_keys = update.master_keys; + updateToWrite.self_signing_keys = update.self_signing_keys; + updateToWrite.user_signing_keys = update.user_signing_keys; + + auto oldDeviceKeys = std::move(updateToWrite.device_keys); + updateToWrite.device_keys.clear(); + + // Don't insert keys, which we have seen once already + for (const auto &[device_id, device_keys] : update.device_keys) { + if (oldDeviceKeys.count(device_id) && + oldDeviceKeys.at(device_id).keys == device_keys.keys) { + // this is safe, since the keys are the same + updateToWrite.device_keys[device_id] = device_keys; + } else { + bool keyReused = false; + for (const auto &[key_id, key] : device_keys.keys) { + (void)key_id; + if (updateToWrite.seen_device_keys.count(key)) { + nhlog::crypto()->warn( + "Key '{}' reused by ({}: {})", key, user, device_id); + keyReused = true; + break; + } + if (updateToWrite.seen_device_ids.count(device_id)) { + nhlog::crypto()->warn("device_id '{}' reused by ({})", device_id, user); + keyReused = true; + break; + } + } + + if (!keyReused && !oldDeviceKeys.count(device_id)) { + // ensure the key has a valid signature from itself + std::string device_signing_key = "ed25519:" + device_keys.device_id; + if (device_id != device_keys.device_id) { + nhlog::crypto()->warn("device {}:{} has a different device id " + "in the body: {}", + user, + device_id, + device_keys.device_id); + continue; + } + if (!device_keys.signatures.count(user) || + !device_keys.signatures.at(user).count(device_signing_key)) { + nhlog::crypto()->warn("device {}:{} has no signature", user, device_id); + continue; } - updateToWrite.master_keys = update.master_keys; - updateToWrite.self_signing_keys = update.self_signing_keys; - updateToWrite.user_signing_keys = update.user_signing_keys; - - auto oldDeviceKeys = std::move(updateToWrite.device_keys); - updateToWrite.device_keys.clear(); - - // Don't insert keys, which we have seen once already - for (const auto &[device_id, device_keys] : update.device_keys) { - if (oldDeviceKeys.count(device_id) && - oldDeviceKeys.at(device_id).keys == device_keys.keys) { - // this is safe, since the keys are the same - updateToWrite.device_keys[device_id] = device_keys; - } else { - bool keyReused = false; - for (const auto &[key_id, key] : device_keys.keys) { - (void)key_id; - if (updateToWrite.seen_device_keys.count(key)) { - nhlog::crypto()->warn( - "Key '{}' reused by ({}: {})", - key, - user, - device_id); - keyReused = true; - break; - } - if (updateToWrite.seen_device_ids.count( - device_id)) { - nhlog::crypto()->warn( - "device_id '{}' reused by ({})", - device_id, - user); - keyReused = true; - break; - } - } - - if (!keyReused && !oldDeviceKeys.count(device_id)) { - // ensure the key has a valid signature from itself - std::string device_signing_key = - "ed25519:" + device_keys.device_id; - if (device_id != device_keys.device_id) { - nhlog::crypto()->warn( - "device {}:{} has a different device id " - "in the body: {}", - user, - device_id, - device_keys.device_id); - continue; - } - if (!device_keys.signatures.count(user) || - !device_keys.signatures.at(user).count( - device_signing_key)) { - nhlog::crypto()->warn( - "device {}:{} has no signature", - user, - device_id); - continue; - } - - if (!mtx::crypto::ed25519_verify_signature( - device_keys.keys.at(device_signing_key), - json(device_keys), - device_keys.signatures.at(user).at( - device_signing_key))) { - nhlog::crypto()->warn( - "device {}:{} has an invalid signature", - user, - device_id); - continue; - } - - updateToWrite.device_keys[device_id] = device_keys; - } - } - - for (const auto &[key_id, key] : device_keys.keys) { - (void)key_id; - updateToWrite.seen_device_keys.insert(key); - } - updateToWrite.seen_device_ids.insert(device_id); + if (!mtx::crypto::ed25519_verify_signature( + device_keys.keys.at(device_signing_key), + json(device_keys), + device_keys.signatures.at(user).at(device_signing_key))) { + nhlog::crypto()->warn( + "device {}:{} has an invalid signature", user, device_id); + continue; } + + updateToWrite.device_keys[device_id] = device_keys; + } } - updateToWrite.updated_at = sync_token; - db.put(txn, user, json(updateToWrite).dump()); + + for (const auto &[key_id, key] : device_keys.keys) { + (void)key_id; + updateToWrite.seen_device_keys.insert(key); + } + updateToWrite.seen_device_ids.insert(device_id); + } } + updateToWrite.updated_at = sync_token; + db.put(txn, user, json(updateToWrite).dump()); + } - txn.commit(); + txn.commit(); - std::map<std::string, VerificationStatus> tmp; - const auto local_user = utils::localUser().toStdString(); + std::map<std::string, VerificationStatus> tmp; + const auto local_user = utils::localUser().toStdString(); - { - std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); - for (auto &[user_id, update] : updates) { - (void)update; - if (user_id == local_user) { - std::swap(tmp, verification_storage.status); - } else { - verification_storage.status.erase(user_id); - } - } + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); + for (auto &[user_id, update] : updates) { + (void)update; + if (user_id == local_user) { + std::swap(tmp, verification_storage.status); + } else { + verification_storage.status.erase(user_id); + } } + } - for (auto &[user_id, update] : updates) { - (void)update; - if (user_id == local_user) { - for (const auto &[user, status] : tmp) { - (void)status; - emit verificationStatusChanged(user); - } - } - emit verificationStatusChanged(user_id); + for (auto &[user_id, update] : updates) { + (void)update; + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); + } } + emit verificationStatusChanged(user_id); + } } void @@ -4169,780 +4051,772 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn, const std::vector<std::string> &user_ids, const std::string &sync_token) { - mtx::requests::QueryKeys query; - query.token = sync_token; - - for (const auto &user : user_ids) { - nhlog::db()->debug("Marking user keys out of date: {}", user); - - std::string_view oldKeys; + mtx::requests::QueryKeys query; + query.token = sync_token; - UserKeyCache cacheEntry; - auto res = db.get(txn, user, oldKeys); - if (res) { - cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size())) - .get<UserKeyCache>(); - } - cacheEntry.last_changed = sync_token; + for (const auto &user : user_ids) { + nhlog::db()->debug("Marking user keys out of date: {}", user); - db.put(txn, user, json(cacheEntry).dump()); + std::string_view oldKeys; - query.device_keys[user] = {}; + UserKeyCache cacheEntry; + auto res = db.get(txn, user, oldKeys); + if (res) { + cacheEntry = + json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>(); } + cacheEntry.last_changed = sync_token; - if (!query.device_keys.empty()) - http::client()->query_keys(query, - [this, sync_token](const mtx::responses::QueryKeys &keys, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to query device keys: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } + db.put(txn, user, json(cacheEntry).dump()); - emit userKeysUpdate(sync_token, keys); - }); + query.device_keys[user] = {}; + } + + if (!query.device_keys.empty()) + http::client()->query_keys( + query, + [this, sync_token](const mtx::responses::QueryKeys &keys, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit userKeysUpdate(sync_token, keys); + }); } void Cache::query_keys(const std::string &user_id, std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb) { - mtx::requests::QueryKeys req; - std::string last_changed; - { - auto txn = ro_txn(env_); - auto cache_ = userKeys_(user_id, txn); - - if (cache_.has_value()) { - if (cache_->updated_at == cache_->last_changed) { - cb(cache_.value(), {}); - return; - } else - nhlog::db()->info("Keys outdated for {}: {} vs {}", - user_id, - cache_->updated_at, - cache_->last_changed); - } else - nhlog::db()->info("No keys found for {}", user_id); - - req.device_keys[user_id] = {}; - - if (cache_) - last_changed = cache_->last_changed; - req.token = last_changed; - } - - // use context object so that we can disconnect again - QObject *context{new QObject(this)}; - QObject::connect( - this, - &Cache::verificationStatusChanged, - context, - [cb, user_id, context_ = context, this](std::string updated_user) mutable { - if (user_id == updated_user) { - context_->deleteLater(); - auto txn = ro_txn(env_); - auto keys = this->userKeys_(user_id, txn); - cb(keys.value_or(UserKeyCache{}), {}); - } - }, - Qt::QueuedConnection); + mtx::requests::QueryKeys req; + std::string last_changed; + { + auto txn = ro_txn(env_); + auto cache_ = userKeys_(user_id, txn); - http::client()->query_keys( - req, - [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {},{}", - mtx::errors::to_string(err->matrix_error.errcode), - static_cast<int>(err->status_code)); - cb({}, err); - return; - } - - emit userKeysUpdate(last_changed, res); - }); + if (cache_.has_value()) { + if (cache_->updated_at == cache_->last_changed) { + cb(cache_.value(), {}); + return; + } else + nhlog::db()->info("Keys outdated for {}: {} vs {}", + user_id, + cache_->updated_at, + cache_->last_changed); + } else + nhlog::db()->info("No keys found for {}", user_id); + + req.device_keys[user_id] = {}; + + if (cache_) + last_changed = cache_->last_changed; + req.token = last_changed; + } + + // use context object so that we can disconnect again + QObject *context{new QObject(this)}; + QObject::connect( + this, + &Cache::verificationStatusChanged, + context, + [cb, user_id, context_ = context, this](std::string updated_user) mutable { + if (user_id == updated_user) { + context_->deleteLater(); + auto txn = ro_txn(env_); + auto keys = this->userKeys_(user_id, txn); + cb(keys.value_or(UserKeyCache{}), {}); + } + }, + Qt::QueuedConnection); + + http::client()->query_keys( + req, + [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + cb({}, err); + return; + } + + emit userKeysUpdate(last_changed, res); + }); } void to_json(json &j, const VerificationCache &info) { - j["device_verified"] = info.device_verified; - j["device_blocked"] = info.device_blocked; + j["device_verified"] = info.device_verified; + j["device_blocked"] = info.device_blocked; } void from_json(const json &j, VerificationCache &info) { - info.device_verified = j.at("device_verified").get<std::set<std::string>>(); - info.device_blocked = j.at("device_blocked").get<std::set<std::string>>(); + info.device_verified = j.at("device_verified").get<std::set<std::string>>(); + info.device_blocked = j.at("device_blocked").get<std::set<std::string>>(); } void to_json(json &j, const OnlineBackupVersion &info) { - j["v"] = info.version; - j["a"] = info.algorithm; + j["v"] = info.version; + j["a"] = info.algorithm; } void from_json(const json &j, OnlineBackupVersion &info) { - info.version = j.at("v").get<std::string>(); - info.algorithm = j.at("a").get<std::string>(); + info.version = j.at("v").get<std::string>(); + info.algorithm = j.at("a").get<std::string>(); } std::optional<VerificationCache> Cache::verificationCache(const std::string &user_id, lmdb::txn &txn) { - std::string_view verifiedVal; + std::string_view verifiedVal; - auto db = getVerificationDb(txn); + auto db = getVerificationDb(txn); - try { - VerificationCache verified_state; - auto res = db.get(txn, user_id, verifiedVal); - if (res) { - verified_state = json::parse(verifiedVal); - return verified_state; - } else { - return {}; - } - } catch (std::exception &) { - return {}; + try { + VerificationCache verified_state; + auto res = db.get(txn, user_id, verifiedVal); + if (res) { + verified_state = json::parse(verifiedVal); + return verified_state; + } else { + return {}; } + } catch (std::exception &) { + return {}; + } } void Cache::markDeviceVerified(const std::string &user_id, const std::string &key) { - { - std::string_view val; - - auto txn = lmdb::txn::begin(env_); - auto db = getVerificationDb(txn); - - try { - VerificationCache verified_state; - auto res = db.get(txn, user_id, val); - if (res) { - verified_state = json::parse(val); - } + { + std::string_view val; - for (const auto &device : verified_state.device_verified) - if (device == key) - return; + auto txn = lmdb::txn::begin(env_); + auto db = getVerificationDb(txn); - verified_state.device_verified.insert(key); - db.put(txn, user_id, json(verified_state).dump()); - txn.commit(); - } catch (std::exception &) { - } + try { + VerificationCache verified_state; + auto res = db.get(txn, user_id, val); + if (res) { + verified_state = json::parse(val); + } + + for (const auto &device : verified_state.device_verified) + if (device == key) + return; + + verified_state.device_verified.insert(key); + db.put(txn, user_id, json(verified_state).dump()); + txn.commit(); + } catch (std::exception &) { } + } - const auto local_user = utils::localUser().toStdString(); - std::map<std::string, VerificationStatus> tmp; - { - std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); - if (user_id == local_user) { - std::swap(tmp, verification_storage.status); - } else { - verification_storage.status.erase(user_id); - } - } + const auto local_user = utils::localUser().toStdString(); + std::map<std::string, VerificationStatus> tmp; + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); if (user_id == local_user) { - for (const auto &[user, status] : tmp) { - (void)status; - emit verificationStatusChanged(user); - } + std::swap(tmp, verification_storage.status); } else { - emit verificationStatusChanged(user_id); + verification_storage.status.erase(user_id); + } + } + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); } + } else { + emit verificationStatusChanged(user_id); + } } void Cache::markDeviceUnverified(const std::string &user_id, const std::string &key) { - std::string_view val; + std::string_view val; - auto txn = lmdb::txn::begin(env_); - auto db = getVerificationDb(txn); + auto txn = lmdb::txn::begin(env_); + auto db = getVerificationDb(txn); - try { - VerificationCache verified_state; - auto res = db.get(txn, user_id, val); - if (res) { - verified_state = json::parse(val); - } + try { + VerificationCache verified_state; + auto res = db.get(txn, user_id, val); + if (res) { + verified_state = json::parse(val); + } - verified_state.device_verified.erase(key); + verified_state.device_verified.erase(key); - db.put(txn, user_id, json(verified_state).dump()); - txn.commit(); - } catch (std::exception &) { - } + db.put(txn, user_id, json(verified_state).dump()); + txn.commit(); + } catch (std::exception &) { + } - const auto local_user = utils::localUser().toStdString(); - std::map<std::string, VerificationStatus> tmp; - { - std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); - if (user_id == local_user) { - std::swap(tmp, verification_storage.status); - } else { - verification_storage.status.erase(user_id); - } - } + const auto local_user = utils::localUser().toStdString(); + std::map<std::string, VerificationStatus> tmp; + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); if (user_id == local_user) { - for (const auto &[user, status] : tmp) { - (void)status; - emit verificationStatusChanged(user); - } + std::swap(tmp, verification_storage.status); } else { - emit verificationStatusChanged(user_id); + verification_storage.status.erase(user_id); + } + } + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); } + } else { + emit verificationStatusChanged(user_id); + } } VerificationStatus Cache::verificationStatus(const std::string &user_id) { - auto txn = ro_txn(env_); - return verificationStatus_(user_id, txn); + auto txn = ro_txn(env_); + return verificationStatus_(user_id, txn); } VerificationStatus Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn) { - std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); - if (verification_storage.status.count(user_id)) - return verification_storage.status.at(user_id); + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); + if (verification_storage.status.count(user_id)) + return verification_storage.status.at(user_id); - VerificationStatus status; + VerificationStatus status; - // assume there is at least one unverified device until we have checked we have the device - // list for that user. - status.unverified_device_count = 1; - status.no_keys = true; + // assume there is at least one unverified device until we have checked we have the device + // list for that user. + status.unverified_device_count = 1; + status.no_keys = true; - if (auto verifCache = verificationCache(user_id, txn)) { - status.verified_devices = verifCache->device_verified; - } + if (auto verifCache = verificationCache(user_id, txn)) { + status.verified_devices = verifCache->device_verified; + } - const auto local_user = utils::localUser().toStdString(); + const auto local_user = utils::localUser().toStdString(); - crypto::Trust trustlevel = crypto::Trust::Unverified; - if (user_id == local_user) { - status.verified_devices.insert(http::client()->device_id()); - trustlevel = crypto::Trust::Verified; - } + crypto::Trust trustlevel = crypto::Trust::Unverified; + if (user_id == local_user) { + status.verified_devices.insert(http::client()->device_id()); + trustlevel = crypto::Trust::Verified; + } - auto verifyAtLeastOneSig = [](const auto &toVerif, - const std::map<std::string, std::string> &keys, - const std::string &keyOwner) { - if (!toVerif.signatures.count(keyOwner)) - return false; + auto verifyAtLeastOneSig = [](const auto &toVerif, + const std::map<std::string, std::string> &keys, + const std::string &keyOwner) { + if (!toVerif.signatures.count(keyOwner)) + return false; - for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) { - if (!keys.count(key_id)) - continue; + for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) { + if (!keys.count(key_id)) + continue; - if (mtx::crypto::ed25519_verify_signature( - keys.at(key_id), json(toVerif), signature)) - return true; - } - return false; - }; + if (mtx::crypto::ed25519_verify_signature(keys.at(key_id), json(toVerif), signature)) + return true; + } + return false; + }; + + auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) { + int currentVerifiedDevices = 0; + for (auto device_id : status.verified_devices) { + if (theirDeviceKeys.count(device_id)) + currentVerifiedDevices++; + } + status.unverified_device_count = + static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices; + }; + + try { + // for local user verify this device_key -> our master_key -> our self_signing_key + // -> our device_keys + // + // for other user verify this device_key -> our master_key -> our user_signing_key + // -> their master_key -> their self_signing_key -> their device_keys + // + // This means verifying the other user adds 2 extra steps,verifying our user_signing + // key and their master key + auto ourKeys = userKeys_(local_user, txn); + auto theirKeys = userKeys_(user_id, txn); + if (theirKeys) + status.no_keys = false; + + if (!ourKeys || !theirKeys) { + verification_storage.status[user_id] = status; + return status; + } + + // Update verified devices count to count without cross-signing + updateUnverifiedDevices(theirKeys->device_keys); - auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) { - int currentVerifiedDevices = 0; - for (auto device_id : status.verified_devices) { - if (theirDeviceKeys.count(device_id)) - currentVerifiedDevices++; - } - status.unverified_device_count = - static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices; - }; + { + auto &mk = ourKeys->master_keys; + std::string dev_id = "ed25519:" + http::client()->device_id(); + if (!mk.signatures.count(local_user) || !mk.signatures.at(local_user).count(dev_id) || + !mtx::crypto::ed25519_verify_signature(olm::client()->identity_keys().ed25519, + json(mk), + mk.signatures.at(local_user).at(dev_id))) { + nhlog::crypto()->debug("We have not verified our own master key"); + verification_storage.status[user_id] = status; + return status; + } + } - try { - // for local user verify this device_key -> our master_key -> our self_signing_key - // -> our device_keys - // - // for other user verify this device_key -> our master_key -> our user_signing_key - // -> their master_key -> their self_signing_key -> their device_keys - // - // This means verifying the other user adds 2 extra steps,verifying our user_signing - // key and their master key - auto ourKeys = userKeys_(local_user, txn); - auto theirKeys = userKeys_(user_id, txn); - if (theirKeys) - status.no_keys = false; - - if (!ourKeys || !theirKeys) { - verification_storage.status[user_id] = status; - return status; - } + auto master_keys = ourKeys->master_keys.keys; - // Update verified devices count to count without cross-signing - updateUnverifiedDevices(theirKeys->device_keys); - - { - auto &mk = ourKeys->master_keys; - std::string dev_id = "ed25519:" + http::client()->device_id(); - if (!mk.signatures.count(local_user) || - !mk.signatures.at(local_user).count(dev_id) || - !mtx::crypto::ed25519_verify_signature( - olm::client()->identity_keys().ed25519, - json(mk), - mk.signatures.at(local_user).at(dev_id))) { - nhlog::crypto()->debug("We have not verified our own master key"); - verification_storage.status[user_id] = status; - return status; - } - } + if (user_id != local_user) { + bool theirMasterKeyVerified = + verifyAtLeastOneSig(ourKeys->user_signing_keys, master_keys, local_user) && + verifyAtLeastOneSig( + theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user); - auto master_keys = ourKeys->master_keys.keys; - - if (user_id != local_user) { - bool theirMasterKeyVerified = - verifyAtLeastOneSig( - ourKeys->user_signing_keys, master_keys, local_user) && - verifyAtLeastOneSig( - theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user); - - if (theirMasterKeyVerified) - trustlevel = crypto::Trust::Verified; - else if (!theirKeys->master_key_changed) - trustlevel = crypto::Trust::TOFU; - else { - verification_storage.status[user_id] = status; - return status; - } + if (theirMasterKeyVerified) + trustlevel = crypto::Trust::Verified; + else if (!theirKeys->master_key_changed) + trustlevel = crypto::Trust::TOFU; + else { + verification_storage.status[user_id] = status; + return status; + } - master_keys = theirKeys->master_keys.keys; - } + master_keys = theirKeys->master_keys.keys; + } - status.user_verified = trustlevel; + status.user_verified = trustlevel; - verification_storage.status[user_id] = status; - if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id)) - return status; - - for (const auto &[device, device_key] : theirKeys->device_keys) { - (void)device; - try { - auto identkey = - device_key.keys.at("curve25519:" + device_key.device_id); - if (verifyAtLeastOneSig( - device_key, theirKeys->self_signing_keys.keys, user_id)) { - status.verified_devices.insert(device_key.device_id); - status.verified_device_keys[identkey] = trustlevel; - } - } catch (...) { - } - } + verification_storage.status[user_id] = status; + if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id)) + return status; - updateUnverifiedDevices(theirKeys->device_keys); - verification_storage.status[user_id] = status; - return status; - } catch (std::exception &e) { - nhlog::db()->error( - "Failed to calculate verification status of {}: {}", user_id, e.what()); - return status; + for (const auto &[device, device_key] : theirKeys->device_keys) { + (void)device; + try { + auto identkey = device_key.keys.at("curve25519:" + device_key.device_id); + if (verifyAtLeastOneSig(device_key, theirKeys->self_signing_keys.keys, user_id)) { + status.verified_devices.insert(device_key.device_id); + status.verified_device_keys[identkey] = trustlevel; + } + } catch (...) { + } } + + updateUnverifiedDevices(theirKeys->device_keys); + verification_storage.status[user_id] = status; + return status; + } catch (std::exception &e) { + nhlog::db()->error("Failed to calculate verification status of {}: {}", user_id, e.what()); + return status; + } } void to_json(json &j, const RoomInfo &info) { - j["name"] = info.name; - j["topic"] = info.topic; - j["avatar_url"] = info.avatar_url; - j["version"] = info.version; - j["is_invite"] = info.is_invite; - j["is_space"] = info.is_space; - j["join_rule"] = info.join_rule; - j["guest_access"] = info.guest_access; + j["name"] = info.name; + j["topic"] = info.topic; + j["avatar_url"] = info.avatar_url; + j["version"] = info.version; + j["is_invite"] = info.is_invite; + j["is_space"] = info.is_space; + j["join_rule"] = info.join_rule; + j["guest_access"] = info.guest_access; - if (info.member_count != 0) - j["member_count"] = info.member_count; + if (info.member_count != 0) + j["member_count"] = info.member_count; - if (info.tags.size() != 0) - j["tags"] = info.tags; + if (info.tags.size() != 0) + j["tags"] = info.tags; } void from_json(const json &j, RoomInfo &info) { - info.name = j.at("name"); - info.topic = j.at("topic"); - info.avatar_url = j.at("avatar_url"); - info.version = j.value( - "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); - info.is_invite = j.at("is_invite"); - info.is_space = j.value("is_space", false); - info.join_rule = j.at("join_rule"); - info.guest_access = j.at("guest_access"); + info.name = j.at("name"); + info.topic = j.at("topic"); + info.avatar_url = j.at("avatar_url"); + info.version = j.value( + "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); + info.is_invite = j.at("is_invite"); + info.is_space = j.value("is_space", false); + info.join_rule = j.at("join_rule"); + info.guest_access = j.at("guest_access"); - if (j.count("member_count")) - info.member_count = j.at("member_count"); + if (j.count("member_count")) + info.member_count = j.at("member_count"); - if (j.count("tags")) - info.tags = j.at("tags").get<std::vector<std::string>>(); + if (j.count("tags")) + info.tags = j.at("tags").get<std::vector<std::string>>(); } void to_json(json &j, const ReadReceiptKey &key) { - j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; + j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; } void from_json(const json &j, ReadReceiptKey &key) { - key.event_id = j.at("event_id").get<std::string>(); - key.room_id = j.at("room_id").get<std::string>(); + key.event_id = j.at("event_id").get<std::string>(); + key.room_id = j.at("room_id").get<std::string>(); } void to_json(json &j, const MemberInfo &info) { - j["name"] = info.name; - j["avatar_url"] = info.avatar_url; + j["name"] = info.name; + j["avatar_url"] = info.avatar_url; } void from_json(const json &j, MemberInfo &info) { - info.name = j.at("name"); - info.avatar_url = j.at("avatar_url"); + info.name = j.at("name"); + info.avatar_url = j.at("avatar_url"); } void to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg) { - obj["deviceids"] = msg.deviceids; + obj["deviceids"] = msg.deviceids; } void from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg) { - msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>(); + msg.deviceids = obj.at("deviceids").get<decltype(msg.deviceids)>(); } void to_json(nlohmann::json &obj, const SharedWithUsers &msg) { - obj["keys"] = msg.keys; + obj["keys"] = msg.keys; } void from_json(const nlohmann::json &obj, SharedWithUsers &msg) { - msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>(); + msg.keys = obj.at("keys").get<std::map<std::string, DeviceKeysToMsgIndex>>(); } void to_json(nlohmann::json &obj, const GroupSessionData &msg) { - obj["message_index"] = msg.message_index; - obj["ts"] = msg.timestamp; - obj["trust"] = msg.trusted; + obj["message_index"] = msg.message_index; + obj["ts"] = msg.timestamp; + obj["trust"] = msg.trusted; - obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key; - obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; + obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key; + obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; - obj["currently"] = msg.currently; + obj["currently"] = msg.currently; - obj["indices"] = msg.indices; + obj["indices"] = msg.indices; } void from_json(const nlohmann::json &obj, GroupSessionData &msg) { - msg.message_index = obj.at("message_index"); - msg.timestamp = obj.value("ts", 0ULL); - msg.trusted = obj.value("trust", true); + msg.message_index = obj.at("message_index"); + msg.timestamp = obj.value("ts", 0ULL); + msg.trusted = obj.value("trust", true); - msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", ""); - msg.forwarding_curve25519_key_chain = - obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{}); + msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", ""); + msg.forwarding_curve25519_key_chain = + obj.value("forwarding_curve25519_key_chain", std::vector<std::string>{}); - msg.currently = obj.value("currently", SharedWithUsers{}); + msg.currently = obj.value("currently", SharedWithUsers{}); - msg.indices = obj.value("indices", std::map<uint32_t, std::string>()); + msg.indices = obj.value("indices", std::map<uint32_t, std::string>()); } void to_json(nlohmann::json &obj, const DevicePublicKeys &msg) { - obj["ed25519"] = msg.ed25519; - obj["curve25519"] = msg.curve25519; + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; } void from_json(const nlohmann::json &obj, DevicePublicKeys &msg) { - msg.ed25519 = obj.at("ed25519"); - msg.curve25519 = obj.at("curve25519"); + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); } void to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) { - obj["room_id"] = msg.room_id; - obj["session_id"] = msg.session_id; - obj["sender_key"] = msg.sender_key; + obj["room_id"] = msg.room_id; + obj["session_id"] = msg.session_id; + obj["sender_key"] = msg.sender_key; } void from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) { - msg.room_id = obj.at("room_id"); - msg.session_id = obj.at("session_id"); - msg.sender_key = obj.at("sender_key"); + msg.room_id = obj.at("room_id"); + msg.session_id = obj.at("session_id"); + msg.sender_key = obj.at("sender_key"); } void to_json(nlohmann::json &obj, const StoredOlmSession &msg) { - obj["ts"] = msg.last_message_ts; - obj["s"] = msg.pickled_session; + obj["ts"] = msg.last_message_ts; + obj["s"] = msg.pickled_session; } void from_json(const nlohmann::json &obj, StoredOlmSession &msg) { - msg.last_message_ts = obj.at("ts").get<uint64_t>(); - msg.pickled_session = obj.at("s").get<std::string>(); + msg.last_message_ts = obj.at("ts").get<uint64_t>(); + msg.pickled_session = obj.at("s").get<std::string>(); } namespace cache { void init(const QString &user_id) { - qRegisterMetaType<RoomMember>(); - qRegisterMetaType<RoomSearchResult>(); - qRegisterMetaType<RoomInfo>(); - qRegisterMetaType<QMap<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); - qRegisterMetaType<mtx::responses::QueryKeys>(); + qRegisterMetaType<RoomMember>(); + qRegisterMetaType<RoomSearchResult>(); + qRegisterMetaType<RoomInfo>(); + qRegisterMetaType<QMap<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + qRegisterMetaType<mtx::responses::QueryKeys>(); - instance_ = std::make_unique<Cache>(user_id); + instance_ = std::make_unique<Cache>(user_id); } Cache * client() { - return instance_.get(); + return instance_.get(); } std::string displayName(const std::string &room_id, const std::string &user_id) { - return instance_->displayName(room_id, user_id); + return instance_->displayName(room_id, user_id); } QString displayName(const QString &room_id, const QString &user_id) { - return instance_->displayName(room_id, user_id); + return instance_->displayName(room_id, user_id); } QString avatarUrl(const QString &room_id, const QString &user_id) { - return instance_->avatarUrl(room_id, user_id); + return instance_->avatarUrl(room_id, user_id); } mtx::presence::PresenceState presenceState(const std::string &user_id) { - if (!instance_) - return {}; - return instance_->presenceState(user_id); + if (!instance_) + return {}; + return instance_->presenceState(user_id); } std::string statusMessage(const std::string &user_id) { - return instance_->statusMessage(user_id); + return instance_->statusMessage(user_id); } // user cache stores user keys std::optional<UserKeyCache> userKeys(const std::string &user_id) { - return instance_->userKeys(user_id); + return instance_->userKeys(user_id); } void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) { - instance_->updateUserKeys(sync_token, keyQuery); + instance_->updateUserKeys(sync_token, keyQuery); } // device & user verification cache std::optional<VerificationStatus> verificationStatus(const std::string &user_id) { - return instance_->verificationStatus(user_id); + return instance_->verificationStatus(user_id); } void markDeviceVerified(const std::string &user_id, const std::string &device) { - instance_->markDeviceVerified(user_id, device); + instance_->markDeviceVerified(user_id, device); } void markDeviceUnverified(const std::string &user_id, const std::string &device) { - instance_->markDeviceUnverified(user_id, device); + instance_->markDeviceUnverified(user_id, device); } std::vector<std::string> joinedRooms() { - return instance_->joinedRooms(); + return instance_->joinedRooms(); } QMap<QString, RoomInfo> roomInfo(bool withInvites) { - return instance_->roomInfo(withInvites); + return instance_->roomInfo(withInvites); } QHash<QString, RoomInfo> invites() { - return instance_->invites(); + return instance_->invites(); } QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - return instance_->getRoomName(txn, statesdb, membersdb); + return instance_->getRoomName(txn, statesdb, membersdb); } mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { - return instance_->getRoomJoinRule(txn, statesdb); + return instance_->getRoomJoinRule(txn, statesdb); } bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) { - return instance_->getRoomGuestAccess(txn, statesdb); + return instance_->getRoomGuestAccess(txn, statesdb); } QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) { - return instance_->getRoomTopic(txn, statesdb); + return instance_->getRoomTopic(txn, statesdb); } QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { - return instance_->getRoomAvatarUrl(txn, statesdb, membersdb); + return instance_->getRoomAvatarUrl(txn, statesdb, membersdb); } std::vector<RoomMember> getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) { - return instance_->getMembers(room_id, startIndex, len); + return instance_->getMembers(room_id, startIndex, len); } std::vector<RoomMember> getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) { - return instance_->getMembersFromInvite(room_id, startIndex, len); + return instance_->getMembersFromInvite(room_id, startIndex, len); } void saveState(const mtx::responses::Sync &res) { - instance_->saveState(res); + instance_->saveState(res); } bool isInitialized() { - return instance_->isInitialized(); + return instance_->isInitialized(); } std::string nextBatchToken() { - return instance_->nextBatchToken(); + return instance_->nextBatchToken(); } void deleteData() { - instance_->deleteData(); + instance_->deleteData(); } void removeInvite(lmdb::txn &txn, const std::string &room_id) { - instance_->removeInvite(txn, room_id); + instance_->removeInvite(txn, room_id); } void removeInvite(const std::string &room_id) { - instance_->removeInvite(room_id); + instance_->removeInvite(room_id); } void removeRoom(lmdb::txn &txn, const std::string &roomid) { - instance_->removeRoom(txn, roomid); + instance_->removeRoom(txn, roomid); } void removeRoom(const std::string &roomid) { - instance_->removeRoom(roomid); + instance_->removeRoom(roomid); } void removeRoom(const QString &roomid) { - instance_->removeRoom(roomid.toStdString()); + instance_->removeRoom(roomid.toStdString()); } void setup() { - instance_->setup(); + instance_->setup(); } bool runMigrations() { - return instance_->runMigrations(); + return instance_->runMigrations(); } cache::CacheVersion formatVersion() { - return instance_->formatVersion(); + return instance_->formatVersion(); } void setCurrentFormat() { - instance_->setCurrentFormat(); + instance_->setCurrentFormat(); } std::vector<QString> roomIds() { - return instance_->roomIds(); + return instance_->roomIds(); } QMap<QString, mtx::responses::Notifications> getTimelineMentions() { - return instance_->getTimelineMentions(); + return instance_->getTimelineMentions(); } //! Retrieve all the user ids from a room. std::vector<std::string> roomMembers(const std::string &room_id) { - return instance_->roomMembers(room_id); + return instance_->roomMembers(room_id); } //! Check if the given user has power leve greater than than @@ -4952,48 +4826,48 @@ hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, const std::string &room_id, const std::string &user_id) { - return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); + return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); } void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) { - instance_->updateReadReceipt(txn, room_id, receipts); + instance_->updateReadReceipt(txn, room_id, receipts); } UserReceipts readReceipts(const QString &event_id, const QString &room_id) { - return instance_->readReceipts(event_id, room_id); + return instance_->readReceipts(event_id, room_id); } std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id) { - return instance_->getEventIndex(room_id, event_id); + return instance_->getEventIndex(room_id, event_id); } std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) { - return instance_->lastInvisibleEventAfter(room_id, event_id); + return instance_->lastInvisibleEventAfter(room_id, event_id); } RoomInfo singleRoomInfo(const std::string &room_id) { - return instance_->singleRoomInfo(room_id); + return instance_->singleRoomInfo(room_id); } std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res) { - return instance_->roomsWithStateUpdates(res); + return instance_->roomsWithStateUpdates(res); } std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms) { - return instance_->getRoomInfo(rooms); + return instance_->getRoomInfo(rooms); } //! Calculates which the read status of a room. @@ -5001,74 +4875,74 @@ getRoomInfo(const std::vector<std::string> &rooms) bool calculateRoomReadStatus(const std::string &room_id) { - return instance_->calculateRoomReadStatus(room_id); + return instance_->calculateRoomReadStatus(room_id); } void calculateRoomReadStatus() { - instance_->calculateRoomReadStatus(); + instance_->calculateRoomReadStatus(); } void markSentNotification(const std::string &event_id) { - instance_->markSentNotification(event_id); + instance_->markSentNotification(event_id); } //! Removes an event from the sent notifications. void removeReadNotification(const std::string &event_id) { - instance_->removeReadNotification(event_id); + instance_->removeReadNotification(event_id); } //! Check if we have sent a desktop notification for the given event id. bool isNotificationSent(const std::string &event_id) { - return instance_->isNotificationSent(event_id); + return instance_->isNotificationSent(event_id); } //! Add all notifications containing a user mention to the db. void saveTimelineMentions(const mtx::responses::Notifications &res) { - instance_->saveTimelineMentions(res); + instance_->saveTimelineMentions(res); } //! Remove old unused data. void deleteOldMessages() { - instance_->deleteOldMessages(); + instance_->deleteOldMessages(); } void deleteOldData() noexcept { - instance_->deleteOldData(); + instance_->deleteOldData(); } //! Retrieve all saved room ids. std::vector<std::string> getRoomIds(lmdb::txn &txn) { - return instance_->getRoomIds(txn); + return instance_->getRoomIds(txn); } //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) { - instance_->setEncryptedRoom(txn, room_id); + instance_->setEncryptedRoom(txn, room_id); } bool isRoomEncrypted(const std::string &room_id) { - return instance_->isRoomEncrypted(room_id); + return instance_->isRoomEncrypted(room_id); } //! Check if a user is a member of the room. bool isRoomMember(const std::string &user_id, const std::string &room_id) { - return instance_->isRoomMember(user_id, room_id); + return instance_->isRoomMember(user_id, room_id); } // @@ -5079,40 +4953,40 @@ saveOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->saveOutboundMegolmSession(room_id, data, session); + instance_->saveOutboundMegolmSession(room_id, data, session); } OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id) { - return instance_->getOutboundMegolmSession(room_id); + return instance_->getOutboundMegolmSession(room_id); } bool outboundMegolmSessionExists(const std::string &room_id) noexcept { - return instance_->outboundMegolmSessionExists(room_id); + return instance_->outboundMegolmSessionExists(room_id); } void updateOutboundMegolmSession(const std::string &room_id, const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->updateOutboundMegolmSession(room_id, data, session); + instance_->updateOutboundMegolmSession(room_id, data, session); } void dropOutboundMegolmSession(const std::string &room_id) { - instance_->dropOutboundMegolmSession(room_id); + instance_->dropOutboundMegolmSession(room_id); } void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) { - instance_->importSessionKeys(keys); + instance_->importSessionKeys(keys); } mtx::crypto::ExportedSessionKeys exportSessionKeys() { - return instance_->exportSessionKeys(); + return instance_->exportSessionKeys(); } // @@ -5123,22 +4997,22 @@ saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session, const GroupSessionData &data) { - instance_->saveInboundMegolmSession(index, std::move(session), data); + instance_->saveInboundMegolmSession(index, std::move(session), data); } mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index) { - return instance_->getInboundMegolmSession(index); + return instance_->getInboundMegolmSession(index); } bool inboundMegolmSessionExists(const MegolmSessionIndex &index) { - return instance_->inboundMegolmSessionExists(index); + return instance_->inboundMegolmSessionExists(index); } std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index) { - return instance_->getMegolmSessionData(index); + return instance_->getMegolmSessionData(index); } // @@ -5149,43 +5023,43 @@ saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session, uint64_t timestamp) { - instance_->saveOlmSession(curve25519, std::move(session), timestamp); + instance_->saveOlmSession(curve25519, std::move(session), timestamp); } std::vector<std::string> getOlmSessions(const std::string &curve25519) { - return instance_->getOlmSessions(curve25519); + return instance_->getOlmSessions(curve25519); } std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, const std::string &session_id) { - return instance_->getOlmSession(curve25519, session_id); + return instance_->getOlmSession(curve25519, session_id); } std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519) { - return instance_->getLatestOlmSession(curve25519); + return instance_->getLatestOlmSession(curve25519); } void saveOlmAccount(const std::string &pickled) { - instance_->saveOlmAccount(pickled); + instance_->saveOlmAccount(pickled); } std::string restoreOlmAccount() { - return instance_->restoreOlmAccount(); + return instance_->restoreOlmAccount(); } void storeSecret(const std::string &name, const std::string &secret) { - instance_->storeSecret(name, secret); + instance_->storeSecret(name, secret); } std::optional<std::string> secret(const std::string &name) { - return instance_->secret(name); + return instance_->secret(name); } } // namespace cache diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 80dd1046..b7461848 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -19,48 +19,48 @@ Q_NAMESPACE //! How much a participant is trusted. enum Trust { - Unverified, //! Device unverified or master key changed. - TOFU, //! Device is signed by the sender, but the user is not verified, but they never - //! changed the master key. - Verified, //! User was verified and has crosssigned this device or device is verified. + Unverified, //! Device unverified or master key changed. + TOFU, //! Device is signed by the sender, but the user is not verified, but they never + //! changed the master key. + Verified, //! User was verified and has crosssigned this device or device is verified. }; Q_ENUM_NS(Trust) } struct DeviceKeysToMsgIndex { - // map from device key to message_index - // Using the device id is safe because we check for reuse on device list updates - // Using the device id makes our logic much easier to read. - std::map<std::string, uint64_t> deviceids; + // map from device key to message_index + // Using the device id is safe because we check for reuse on device list updates + // Using the device id makes our logic much easier to read. + std::map<std::string, uint64_t> deviceids; }; struct SharedWithUsers { - // userid to keys - std::map<std::string, DeviceKeysToMsgIndex> keys; + // userid to keys + std::map<std::string, DeviceKeysToMsgIndex> keys; }; // Extra information associated with an outbound megolm session. struct GroupSessionData { - uint64_t message_index = 0; - uint64_t timestamp = 0; + uint64_t message_index = 0; + uint64_t timestamp = 0; - // If we got the session via key sharing or forwarding, we can usually trust it. - // If it came from asymmetric key backup, it is not trusted. - // TODO(Nico): What about forwards? They might come from key backup? - bool trusted = true; + // If we got the session via key sharing or forwarding, we can usually trust it. + // If it came from asymmetric key backup, it is not trusted. + // TODO(Nico): What about forwards? They might come from key backup? + bool trusted = true; - std::string sender_claimed_ed25519_key; - std::vector<std::string> forwarding_curve25519_key_chain; + std::string sender_claimed_ed25519_key; + std::vector<std::string> forwarding_curve25519_key_chain; - //! map from index to event_id to check for replay attacks - std::map<uint32_t, std::string> indices; + //! map from index to event_id to check for replay attacks + std::map<uint32_t, std::string> indices; - // who has access to this session. - // Rotate, when a user leaves the room and share, when a user gets added. - SharedWithUsers currently; + // who has access to this session. + // Rotate, when a user leaves the room and share, when a user gets added. + SharedWithUsers currently; }; void @@ -70,14 +70,14 @@ from_json(const nlohmann::json &obj, GroupSessionData &msg); struct OutboundGroupSessionDataRef { - mtx::crypto::OutboundGroupSessionPtr session; - GroupSessionData data; + mtx::crypto::OutboundGroupSessionPtr session; + GroupSessionData data; }; struct DevicePublicKeys { - std::string ed25519; - std::string curve25519; + std::string ed25519; + std::string curve25519; }; void @@ -88,19 +88,19 @@ from_json(const nlohmann::json &obj, DevicePublicKeys &msg); //! Represents a unique megolm session identifier. struct MegolmSessionIndex { - MegolmSessionIndex() = default; - MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e) - : room_id(std::move(room_id_)) - , session_id(e.session_id) - , sender_key(e.sender_key) - {} - - //! The room in which this session exists. - std::string room_id; - //! The session_id of the megolm session. - std::string session_id; - //! The curve25519 public key of the sender. - std::string sender_key; + MegolmSessionIndex() = default; + MegolmSessionIndex(std::string room_id_, const mtx::events::msg::Encrypted &e) + : room_id(std::move(room_id_)) + , session_id(e.session_id) + , sender_key(e.sender_key) + {} + + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; }; void @@ -110,8 +110,8 @@ from_json(const nlohmann::json &obj, MegolmSessionIndex &msg); struct StoredOlmSession { - std::uint64_t last_message_ts = 0; - std::string pickled_session; + std::uint64_t last_message_ts = 0; + std::string pickled_session; }; void to_json(nlohmann::json &obj, const StoredOlmSession &msg); @@ -121,43 +121,43 @@ from_json(const nlohmann::json &obj, StoredOlmSession &msg); //! Verification status of a single user struct VerificationStatus { - //! True, if the users master key is verified - crypto::Trust user_verified = crypto::Trust::Unverified; - //! List of all devices marked as verified - std::set<std::string> verified_devices; - //! Map from sender key/curve25519 to trust status - std::map<std::string, crypto::Trust> verified_device_keys; - //! Count of unverified devices - int unverified_device_count = 0; - // if the keys are not in cache - bool no_keys = false; + //! True, if the users master key is verified + crypto::Trust user_verified = crypto::Trust::Unverified; + //! List of all devices marked as verified + std::set<std::string> verified_devices; + //! Map from sender key/curve25519 to trust status + std::map<std::string, crypto::Trust> verified_device_keys; + //! Count of unverified devices + int unverified_device_count = 0; + // if the keys are not in cache + bool no_keys = false; }; //! In memory cache of verification status struct VerificationStorage { - //! mapping of user to verification status - std::map<std::string, VerificationStatus> status; - std::mutex verification_storage_mtx; + //! mapping of user to verification status + std::map<std::string, VerificationStatus> status; + std::mutex verification_storage_mtx; }; // this will store the keys of the user with whom a encrypted room is shared with struct UserKeyCache { - //! Device id to device keys - std::map<std::string, mtx::crypto::DeviceKeys> device_keys; - //! cross signing keys - mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys; - //! Sync token when nheko last fetched the keys - std::string updated_at; - //! Sync token when the keys last changed. updated != last_changed means they are outdated. - std::string last_changed; - //! if the master key has ever changed - bool master_key_changed = false; - //! Device keys that were already used at least once - std::set<std::string> seen_device_keys; - //! Device ids that were already used at least once - std::set<std::string> seen_device_ids; + //! Device id to device keys + std::map<std::string, mtx::crypto::DeviceKeys> device_keys; + //! cross signing keys + mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys; + //! Sync token when nheko last fetched the keys + std::string updated_at; + //! Sync token when the keys last changed. updated != last_changed means they are outdated. + std::string last_changed; + //! if the master key has ever changed + bool master_key_changed = false; + //! Device keys that were already used at least once + std::set<std::string> seen_device_keys; + //! Device ids that were already used at least once + std::set<std::string> seen_device_ids; }; void @@ -169,10 +169,10 @@ from_json(const nlohmann::json &j, UserKeyCache &info); // UserKeyCache stores only keys of users with which encrypted room is shared struct VerificationCache { - //! list of verified device_ids with device-verification - std::set<std::string> device_verified; - //! list of devices the user blocks - std::set<std::string> device_blocked; + //! list of verified device_ids with device-verification + std::set<std::string> device_verified; + //! list of devices the user blocks + std::set<std::string> device_blocked; }; void @@ -182,10 +182,10 @@ from_json(const nlohmann::json &j, VerificationCache &info); struct OnlineBackupVersion { - //! the version of the online backup currently enabled - std::string version; - //! the algorithm used by the backup - std::string algorithm; + //! the version of the online backup currently enabled + std::string version; + //! the algorithm used by the backup + std::string algorithm; }; void diff --git a/src/CacheStructs.h b/src/CacheStructs.h index 5f4d392a..e28f5b2d 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -16,23 +16,23 @@ namespace cache { enum class CacheVersion : int { - Older = -1, - Current = 0, - Newer = 1, + Older = -1, + Current = 0, + Newer = 1, }; } struct RoomMember { - QString user_id; - QString display_name; + QString user_id; + QString display_name; }; //! Used to uniquely identify a list of read receipts. struct ReadReceiptKey { - std::string event_id; - std::string room_id; + std::string event_id; + std::string room_id; }; void @@ -43,49 +43,49 @@ from_json(const nlohmann::json &j, ReadReceiptKey &key); struct DescInfo { - QString event_id; - QString userid; - QString body; - QString descriptiveTime; - uint64_t timestamp; - QDateTime datetime; + QString event_id; + QString userid; + QString body; + QString descriptiveTime; + uint64_t timestamp; + QDateTime datetime; }; inline bool operator==(const DescInfo &a, const DescInfo &b) { - return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == - std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); } inline bool operator!=(const DescInfo &a, const DescInfo &b) { - return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != - std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); } //! UI info associated with a room. struct RoomInfo { - //! The calculated name of the room. - std::string name; - //! The topic of the room. - std::string topic; - //! The calculated avatar url of the room. - std::string avatar_url; - //! The calculated version of this room set at creation time. - std::string version; - //! Whether or not the room is an invite. - bool is_invite = false; - //! Wheter or not the room is a space - bool is_space = false; - //! Total number of members in the room. - size_t member_count = 0; - //! Who can access to the room. - mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; - bool guest_access = false; - //! The list of tags associated with this room - std::vector<std::string> tags; + //! The calculated name of the room. + std::string name; + //! The topic of the room. + std::string topic; + //! The calculated avatar url of the room. + std::string avatar_url; + //! The calculated version of this room set at creation time. + std::string version; + //! Whether or not the room is an invite. + bool is_invite = false; + //! Wheter or not the room is a space + bool is_space = false; + //! Total number of members in the room. + size_t member_count = 0; + //! Who can access to the room. + mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; + bool guest_access = false; + //! The list of tags associated with this room + std::vector<std::string> tags; }; void @@ -96,8 +96,8 @@ from_json(const nlohmann::json &j, RoomInfo &info); //! Basic information per member. struct MemberInfo { - std::string name; - std::string avatar_url; + std::string name; + std::string avatar_url; }; void @@ -107,13 +107,13 @@ from_json(const nlohmann::json &j, MemberInfo &info); struct RoomSearchResult { - std::string room_id; - RoomInfo info; + std::string room_id; + RoomInfo info; }; struct ImagePackInfo { - mtx::events::msc2545::ImagePack pack; - std::string source_room; - std::string state_key; + mtx::events::msc2545::ImagePack pack; + std::string source_room; + std::string state_key; }; diff --git a/src/Cache_p.h b/src/Cache_p.h index ff2f31e5..a15010e6 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -32,694 +32,658 @@ class Cache : public QObject { - Q_OBJECT + Q_OBJECT public: - Cache(const QString &userId, QObject *parent = nullptr); - - std::string displayName(const std::string &room_id, const std::string &user_id); - QString displayName(const QString &room_id, const QString &user_id); - QString avatarUrl(const QString &room_id, const QString &user_id); - - // presence - mtx::presence::PresenceState presenceState(const std::string &user_id); - std::string statusMessage(const std::string &user_id); - - // user cache stores user keys - std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys( - const std::string &room_id, - bool verified_only); - void updateUserKeys(const std::string &sync_token, - const mtx::responses::QueryKeys &keyQuery); - void markUserKeysOutOfDate(lmdb::txn &txn, - lmdb::dbi &db, - const std::vector<std::string> &user_ids, - const std::string &sync_token); - void query_keys(const std::string &user_id, - std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb); - - // device & user verification cache - std::optional<UserKeyCache> userKeys(const std::string &user_id); - VerificationStatus verificationStatus(const std::string &user_id); - void markDeviceVerified(const std::string &user_id, const std::string &device); - void markDeviceUnverified(const std::string &user_id, const std::string &device); - crypto::Trust roomVerificationStatus(const std::string &room_id); - - std::vector<std::string> joinedRooms(); - - QMap<QString, RoomInfo> roomInfo(bool withInvites = true); - std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); - QHash<QString, RoomInfo> invites(); - std::optional<RoomInfo> invite(std::string_view roomid); - QMap<QString, std::optional<RoomInfo>> spaces(); - - //! Calculate & return the name of the room. - QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - //! Get room join rules - mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); - bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the topic of the room if any. - QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the room avatar's url if any. - QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - //! Retrieve the version of the room if any. - QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve if the room is a space - bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb); - - //! Get a specific state event - template<typename T> - std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id, - std::string_view state_key = "") - { - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - return getStateEvent<T>(txn, room_id, state_key); - } - - //! retrieve a specific event from account data - //! pass empty room_id for global account data - std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData( - mtx::events::EventType type, - const std::string &room_id = ""); - - //! Retrieve member info from a room. - std::vector<RoomMember> getMembers(const std::string &room_id, - std::size_t startIndex = 0, - std::size_t len = 30); - - std::vector<RoomMember> getMembersFromInvite(const std::string &room_id, - std::size_t startIndex = 0, - std::size_t len = 30); - size_t memberCount(const std::string &room_id); - - void saveState(const mtx::responses::Sync &res); - bool isInitialized(); - bool isDatabaseReady() { return databaseReady_ && isInitialized(); } - - std::string nextBatchToken(); - - void deleteData(); - - void removeInvite(lmdb::txn &txn, const std::string &room_id); - void removeInvite(const std::string &room_id); - void removeRoom(lmdb::txn &txn, const std::string &roomid); - void removeRoom(const std::string &roomid); - void setup(); - - cache::CacheVersion formatVersion(); - void setCurrentFormat(); - bool runMigrations(); - - std::vector<QString> roomIds(); - QMap<QString, mtx::responses::Notifications> getTimelineMentions(); - - //! Retrieve all the user ids from a room. - std::vector<std::string> roomMembers(const std::string &room_id); - - //! Check if the given user has power leve greater than than - //! lowest power level of the given events. - bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + Cache(const QString &userId, QObject *parent = nullptr); + + std::string displayName(const std::string &room_id, const std::string &user_id); + QString displayName(const QString &room_id, const QString &user_id); + QString avatarUrl(const QString &room_id, const QString &user_id); + + // presence + mtx::presence::PresenceState presenceState(const std::string &user_id); + std::string statusMessage(const std::string &user_id); + + // user cache stores user keys + std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys( + const std::string &room_id, + bool verified_only); + void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); + void markUserKeysOutOfDate(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector<std::string> &user_ids, + const std::string &sync_token); + void query_keys(const std::string &user_id, + std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb); + + // device & user verification cache + std::optional<UserKeyCache> userKeys(const std::string &user_id); + VerificationStatus verificationStatus(const std::string &user_id); + void markDeviceVerified(const std::string &user_id, const std::string &device); + void markDeviceUnverified(const std::string &user_id, const std::string &device); + crypto::Trust roomVerificationStatus(const std::string &room_id); + + std::vector<std::string> joinedRooms(); + + QMap<QString, RoomInfo> roomInfo(bool withInvites = true); + std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); + QHash<QString, RoomInfo> invites(); + std::optional<RoomInfo> invite(std::string_view roomid); + QMap<QString, std::optional<RoomInfo>> spaces(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Get room join rules + mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); + bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Retrieve the version of the room if any. + QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve if the room is a space + bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb); + + //! Get a specific state event + template<typename T> + std::optional<mtx::events::StateEvent<T>> getStateEvent(const std::string &room_id, + std::string_view state_key = "") + { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + return getStateEvent<T>(txn, room_id, state_key); + } + + //! retrieve a specific event from account data + //! pass empty room_id for global account data + std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData( + mtx::events::EventType type, + const std::string &room_id = ""); + + //! Retrieve member info from a room. + std::vector<RoomMember> getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + + std::vector<RoomMember> getMembersFromInvite(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + size_t memberCount(const std::string &room_id); + + void saveState(const mtx::responses::Sync &res); + bool isInitialized(); + bool isDatabaseReady() { return databaseReady_ && isInitialized(); } + + std::string nextBatchToken(); + + void deleteData(); + + void removeInvite(lmdb::txn &txn, const std::string &room_id); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void setup(); + + cache::CacheVersion formatVersion(); + void setCurrentFormat(); + bool runMigrations(); + + std::vector<QString> roomIds(); + QMap<QString, mtx::responses::Notifications> getTimelineMentions(); + + //! Retrieve all the user ids from a room. + std::vector<std::string> roomMembers(const std::string &room_id); + + //! Check if the given user has power leve greater than than + //! lowest power level of the given events. + bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id); + + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; + void updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + + RoomInfo singleRoomInfo(const std::string &room_id); + std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); + std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); + + //! Calculates which the read status of a room. + //! Whether all the events in the timeline have been read. + bool calculateRoomReadStatus(const std::string &room_id); + void calculateRoomReadStatus(); + + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + + //! Add all notifications containing a user mention to the db. + void saveTimelineMentions(const mtx::responses::Notifications &res); + + //! retrieve events in timeline and related functions + struct Messages + { + mtx::responses::Timeline timeline; + uint64_t next_index; + bool end_of_cache = false; + }; + Messages getTimelineMessages(lmdb::txn &txn, const std::string &room_id, - const std::string &user_id); - - //! Adds a user to the read list for the given event. - //! - //! There should be only one user id present in a receipt list per room. - //! The user id should be removed from any other lists. - using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; - void updateReadReceipt(lmdb::txn &txn, - const std::string &room_id, - const Receipts &receipts); - - //! Retrieve all the read receipts for the given event id and room. - //! - //! Returns a map of user ids and the time of the read receipt in milliseconds. - using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; - UserReceipts readReceipts(const QString &event_id, const QString &room_id); - - RoomInfo singleRoomInfo(const std::string &room_id); - std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); - std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); - - //! Calculates which the read status of a room. - //! Whether all the events in the timeline have been read. - bool calculateRoomReadStatus(const std::string &room_id); - void calculateRoomReadStatus(); - - void markSentNotification(const std::string &event_id); - //! Removes an event from the sent notifications. - void removeReadNotification(const std::string &event_id); - //! Check if we have sent a desktop notification for the given event id. - bool isNotificationSent(const std::string &event_id); - - //! Add all notifications containing a user mention to the db. - void saveTimelineMentions(const mtx::responses::Notifications &res); - - //! retrieve events in timeline and related functions - struct Messages - { - mtx::responses::Timeline timeline; - uint64_t next_index; - bool end_of_cache = false; - }; - Messages getTimelineMessages(lmdb::txn &txn, - const std::string &room_id, - uint64_t index = std::numeric_limits<uint64_t>::max(), - bool forward = false); - - std::optional<mtx::events::collections::TimelineEvent> getEvent( - const std::string &room_id, - const std::string &event_id); - void storeEvent(const std::string &room_id, - const std::string &event_id, - const mtx::events::collections::TimelineEvent &event); - void replaceEvent(const std::string &room_id, - const std::string &event_id, - const mtx::events::collections::TimelineEvent &event); - std::vector<std::string> relatedEvents(const std::string &room_id, - const std::string &event_id); - - struct TimelineRange - { - uint64_t first, last; + uint64_t index = std::numeric_limits<uint64_t>::max(), + bool forward = false); + + std::optional<mtx::events::collections::TimelineEvent> getEvent(const std::string &room_id, + const std::string &event_id); + void storeEvent(const std::string &room_id, + const std::string &event_id, + const mtx::events::collections::TimelineEvent &event); + void replaceEvent(const std::string &room_id, + const std::string &event_id, + const mtx::events::collections::TimelineEvent &event); + std::vector<std::string> relatedEvents(const std::string &room_id, const std::string &event_id); + + struct TimelineRange + { + uint64_t first, last; + }; + std::optional<TimelineRange> getTimelineRange(const std::string &room_id); + std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::string_view event_id); + std::optional<uint64_t> getEventIndex(const std::string &room_id, std::string_view event_id); + std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter( + const std::string &room_id, + std::string_view event_id); + std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index); + std::optional<uint64_t> getArrivalIndex(const std::string &room_id, std::string_view event_id); + + std::string previousBatchToken(const std::string &room_id); + uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); + void savePendingMessage(const std::string &room_id, + const mtx::events::collections::TimelineEvent &message); + std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage( + const std::string &room_id); + void removePendingStatus(const std::string &room_id, const std::string &txn_id); + + //! clear timeline keeping only the latest batch + void clearTimeline(const std::string &room_id); + + //! Remove old unused data. + void deleteOldMessages(); + void deleteOldData() noexcept; + //! Retrieve all saved room ids. + std::vector<std::string> getRoomIds(lmdb::txn &txn); + std::vector<std::string> getParentRoomIds(const std::string &room_id); + std::vector<std::string> getChildRoomIds(const std::string &room_id); + + std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, + std::optional<bool> stickers); + + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + std::optional<mtx::events::state::Encryption> roomEncryptionSettings( + const std::string &room_id); + + //! Check if a user is a member of the room. + bool isRoomMember(const std::string &user_id, const std::string &room_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const GroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr &session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, + const GroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr &session); + void dropOutboundMegolmSession(const std::string &room_id); + + void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); + mtx::crypto::ExportedSessionKeys exportSessionKeys(); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data); + mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index); + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, + mtx::crypto::OlmSessionPtr session, + uint64_t timestamp); + std::vector<std::string> getOlmSessions(const std::string &curve25519); + std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, + const std::string &session_id); + std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession(const std::string &curve25519); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void saveBackupVersion(const OnlineBackupVersion &data); + void deleteBackupVersion(); + std::optional<OnlineBackupVersion> backupVersion(); + + void storeSecret(const std::string name, const std::string secret, bool internal = false); + void deleteSecret(const std::string name, bool internal = false); + std::optional<std::string> secret(const std::string name, bool internal = false); + + std::string pickleSecret(); + + template<class T> + constexpr static bool isStateEvent_ = + std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>, + mtx::events::StateEvent<decltype(std::declval<T>().content)>>; + + static int compare_state_key(const MDB_val *a, const MDB_val *b) + { + auto get_skey = [](const MDB_val *v) { + return nlohmann::json::parse( + std::string_view(static_cast<const char *>(v->mv_data), v->mv_size)) + .value("key", ""); }; - std::optional<TimelineRange> getTimelineRange(const std::string &room_id); - std::optional<uint64_t> getTimelineIndex(const std::string &room_id, - std::string_view event_id); - std::optional<uint64_t> getEventIndex(const std::string &room_id, - std::string_view event_id); - std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter( - const std::string &room_id, - std::string_view event_id); - std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index); - std::optional<uint64_t> getArrivalIndex(const std::string &room_id, - std::string_view event_id); - - std::string previousBatchToken(const std::string &room_id); - uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); - void savePendingMessage(const std::string &room_id, - const mtx::events::collections::TimelineEvent &message); - std::optional<mtx::events::collections::TimelineEvent> firstPendingMessage( - const std::string &room_id); - void removePendingStatus(const std::string &room_id, const std::string &txn_id); - - //! clear timeline keeping only the latest batch - void clearTimeline(const std::string &room_id); - - //! Remove old unused data. - void deleteOldMessages(); - void deleteOldData() noexcept; - //! Retrieve all saved room ids. - std::vector<std::string> getRoomIds(lmdb::txn &txn); - std::vector<std::string> getParentRoomIds(const std::string &room_id); - std::vector<std::string> getChildRoomIds(const std::string &room_id); - - std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, - std::optional<bool> stickers); - - //! Mark a room that uses e2e encryption. - void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); - bool isRoomEncrypted(const std::string &room_id); - std::optional<mtx::events::state::Encryption> roomEncryptionSettings( - const std::string &room_id); - - //! Check if a user is a member of the room. - bool isRoomMember(const std::string &user_id, const std::string &room_id); - - // - // Outbound Megolm Sessions - // - void saveOutboundMegolmSession(const std::string &room_id, - const GroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr &session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); - bool outboundMegolmSessionExists(const std::string &room_id) noexcept; - void updateOutboundMegolmSession(const std::string &room_id, - const GroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr &session); - void dropOutboundMegolmSession(const std::string &room_id); - - void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); - mtx::crypto::ExportedSessionKeys exportSessionKeys(); - - // - // Inbound Megolm Sessions - // - void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session, - const GroupSessionData &data); - mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( - const MegolmSessionIndex &index); - bool inboundMegolmSessionExists(const MegolmSessionIndex &index); - std::optional<GroupSessionData> getMegolmSessionData(const MegolmSessionIndex &index); - - // - // Olm Sessions - // - void saveOlmSession(const std::string &curve25519, - mtx::crypto::OlmSessionPtr session, - uint64_t timestamp); - std::vector<std::string> getOlmSessions(const std::string &curve25519); - std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, - const std::string &session_id); - std::optional<mtx::crypto::OlmSessionPtr> getLatestOlmSession( - const std::string &curve25519); - - void saveOlmAccount(const std::string &pickled); - std::string restoreOlmAccount(); - - void saveBackupVersion(const OnlineBackupVersion &data); - void deleteBackupVersion(); - std::optional<OnlineBackupVersion> backupVersion(); - - void storeSecret(const std::string name, const std::string secret, bool internal = false); - void deleteSecret(const std::string name, bool internal = false); - std::optional<std::string> secret(const std::string name, bool internal = false); - - std::string pickleSecret(); - - template<class T> - constexpr static bool isStateEvent_ = - std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>, - mtx::events::StateEvent<decltype(std::declval<T>().content)>>; - - static int compare_state_key(const MDB_val *a, const MDB_val *b) - { - auto get_skey = [](const MDB_val *v) { - return nlohmann::json::parse( - std::string_view(static_cast<const char *>(v->mv_data), - v->mv_size)) - .value("key", ""); - }; - - return get_skey(a).compare(get_skey(b)); - } + + 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); - void removeNotification(const QString &room_id, const QString &event_id); - void userKeysUpdate(const std::string &sync_token, - const mtx::responses::QueryKeys &keyQuery); - void verificationStatusChanged(const std::string &userid); - void secretChanged(const std::string name); + void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); + void roomReadStatus(const std::map<QString, bool> &status); + void removeNotification(const QString &room_id, const QString &event_id); + void userKeysUpdate(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); + void verificationStatusChanged(const std::string &userid); + void secretChanged(const std::string name); private: - //! Save an invited room. - void saveInvite(lmdb::txn &txn, + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + //! Add a notification containing a user mention to the db. + void saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList<mtx::responses::Notification> &res); + + //! Get timeline items that a user was mentions in for a given room + mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, + const std::string &room_id); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db); + + std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id); + + std::string getLastEventId(lmdb::txn &txn, const std::string &room_id); + void saveTimelineMessages(lmdb::txn &txn, + lmdb::dbi &eventsDb, + const std::string &room_id, + const mtx::responses::Timeline &res); + + //! retrieve a specific event from account data + //! pass empty room_id for global account data + std::optional<mtx::events::collections::RoomAccountDataEvents> + getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id); + bool isHiddenEvent(lmdb::txn &txn, + mtx::events::collections::TimelineEvents e, + const std::string &room_id); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template<class T> + void saveStateEvents(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &stateskeydb, + lmdb::dbi &membersdb, + lmdb::dbi &eventsDb, + const std::string &room_id, + const std::vector<T> &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e); + } + + template<class T> + void saveStateEvent(lmdb::txn &txn, lmdb::dbi &statesdb, + lmdb::dbi &stateskeydb, lmdb::dbi &membersdb, - const mtx::responses::InvitedRoom &room); - - //! Add a notification containing a user mention to the db. - void saveTimelineMentions(lmdb::txn &txn, - const std::string &room_id, - const QList<mtx::responses::Notification> &res); - - //! Get timeline items that a user was mentions in for a given room - mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, - const std::string &room_id); - - QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db); - - std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id); - - std::string getLastEventId(lmdb::txn &txn, const std::string &room_id); - void saveTimelineMessages(lmdb::txn &txn, - lmdb::dbi &eventsDb, - const std::string &room_id, - const mtx::responses::Timeline &res); - - //! retrieve a specific event from account data - //! pass empty room_id for global account data - std::optional<mtx::events::collections::RoomAccountDataEvents> - getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id); - bool isHiddenEvent(lmdb::txn &txn, - mtx::events::collections::TimelineEvents e, - const std::string &room_id); - - //! Remove a room from the cache. - // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); - template<class T> - void saveStateEvents(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &stateskeydb, - lmdb::dbi &membersdb, - lmdb::dbi &eventsDb, - const std::string &room_id, - const std::vector<T> &events) - { - for (const auto &e : events) - saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e); + lmdb::dbi &eventsDb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) { + switch (e->content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = + e->content.display_name.empty() ? e->state_key : e->content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e->content.avatar_url}; + + membersdb.put(txn, e->state_key, json(tmp).dump()); + break; + } + default: { + membersdb.del(txn, e->state_key, ""); + break; + } + } + + return; + } else if (std::holds_alternative<StateEvent<Encryption>>(event)) { + setEncryptedRoom(txn, room_id); + return; } - template<class T> - void saveStateEvent(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &stateskeydb, - lmdb::dbi &membersdb, - lmdb::dbi &eventsDb, - const std::string &room_id, - const T &event) - { - using namespace mtx::events; - using namespace mtx::events::state; - - if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) { - switch (e->content.membership) { - // - // We only keep users with invite or join membership. - // - case Membership::Invite: - case Membership::Join: { - auto display_name = e->content.display_name.empty() - ? e->state_key - : e->content.display_name; - - // Lightweight representation of a member. - MemberInfo tmp{display_name, e->content.avatar_url}; - - membersdb.put(txn, e->state_key, json(tmp).dump()); - break; - } - default: { - membersdb.del(txn, e->state_key, ""); - break; - } - } - - return; - } else if (std::holds_alternative<StateEvent<Encryption>>(event)) { - setEncryptedRoom(txn, room_id); - return; - } - - std::visit( - [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { - if constexpr (isStateEvent_<decltype(e)>) { - eventsDb.put(txn, e.event_id, json(e).dump()); - - if (e.type != EventType::Unsupported) { - if (std::is_same_v< - std::remove_cv_t< - std::remove_reference_t<decltype(e)>>, - StateEvent<mtx::events::msg::Redacted>>) { - if (e.type == EventType::RoomMember) - membersdb.del(txn, e.state_key, ""); - else if (e.state_key.empty()) - statesdb.del(txn, to_string(e.type)); - else - stateskeydb.del( - txn, - to_string(e.type), - json::object({ - {"key", e.state_key}, - {"id", e.event_id}, - }) - .dump()); - } else if (e.state_key.empty()) - statesdb.put( - txn, to_string(e.type), json(e).dump()); - else - stateskeydb.put( - txn, - to_string(e.type), - json::object({ - {"key", e.state_key}, - {"id", e.event_id}, - }) - .dump()); - } - } - }, - event); + std::visit( + [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { + if constexpr (isStateEvent_<decltype(e)>) { + eventsDb.put(txn, e.event_id, json(e).dump()); + + if (e.type != EventType::Unsupported) { + if (std::is_same_v<std::remove_cv_t<std::remove_reference_t<decltype(e)>>, + StateEvent<mtx::events::msg::Redacted>>) { + if (e.type == EventType::RoomMember) + membersdb.del(txn, e.state_key, ""); + else if (e.state_key.empty()) + statesdb.del(txn, to_string(e.type)); + else + stateskeydb.del(txn, + to_string(e.type), + json::object({ + {"key", e.state_key}, + {"id", e.event_id}, + }) + .dump()); + } else if (e.state_key.empty()) + statesdb.put(txn, to_string(e.type), json(e).dump()); + else + stateskeydb.put(txn, + to_string(e.type), + json::object({ + {"key", e.state_key}, + {"id", e.event_id}, + }) + .dump()); + } + } + }, + event); + } + + template<typename T> + std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn &txn, + const std::string &room_id, + std::string_view state_key = "") + { + constexpr auto type = mtx::events::state_content_to_type<T>; + static_assert(type != mtx::events::EventType::Unsupported, + "Not a supported type in state events."); + + if (room_id.empty()) + return std::nullopt; + const auto typeStr = to_string(type); + + std::string_view value; + if (state_key.empty()) { + auto db = getStatesDb(txn, room_id); + if (!db.get(txn, typeStr, value)) { + return std::nullopt; + } + } else { + auto db = getStatesKeyDb(txn, room_id); + std::string d = json::object({{"key", state_key}}).dump(); + std::string_view data = d; + std::string_view typeStrV = typeStr; + + auto cursor = lmdb::cursor::open(txn, db); + if (!cursor.get(typeStrV, data, MDB_GET_BOTH)) + return std::nullopt; + + try { + auto eventsDb = getEventsDb(txn, room_id); + if (!eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value)) + return std::nullopt; + } catch (std::exception &e) { + return std::nullopt; + } } - template<typename T> - std::optional<mtx::events::StateEvent<T>> getStateEvent(lmdb::txn &txn, - const std::string &room_id, - std::string_view state_key = "") - { - constexpr auto type = mtx::events::state_content_to_type<T>; - static_assert(type != mtx::events::EventType::Unsupported, - "Not a supported type in state events."); - - if (room_id.empty()) - return std::nullopt; - const auto typeStr = to_string(type); - - std::string_view value; - if (state_key.empty()) { - auto db = getStatesDb(txn, room_id); - if (!db.get(txn, typeStr, value)) { - return std::nullopt; - } - } else { - auto db = getStatesKeyDb(txn, room_id); - std::string d = json::object({{"key", state_key}}).dump(); - std::string_view data = d; - std::string_view typeStrV = typeStr; - - auto cursor = lmdb::cursor::open(txn, db); - if (!cursor.get(typeStrV, data, MDB_GET_BOTH)) - return std::nullopt; - - try { - auto eventsDb = getEventsDb(txn, room_id); - if (!eventsDb.get( - txn, json::parse(data)["id"].get<std::string>(), value)) - return std::nullopt; - } catch (std::exception &e) { - return std::nullopt; - } - } - - try { - return json::parse(value).get<mtx::events::StateEvent<T>>(); - } catch (std::exception &e) { - return std::nullopt; - } + try { + return json::parse(value).get<mtx::events::StateEvent<T>>(); + } catch (std::exception &e) { + return std::nullopt; } + } - template<typename T> - std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn, - const std::string &room_id) + template<typename T> + std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn, + const std::string &room_id) - { - constexpr auto type = mtx::events::state_content_to_type<T>; - static_assert(type != mtx::events::EventType::Unsupported, - "Not a supported type in state events."); - - if (room_id.empty()) - return {}; - - std::vector<mtx::events::StateEvent<T>> events; - - { - auto db = getStatesKeyDb(txn, room_id); - auto eventsDb = getEventsDb(txn, room_id); - const auto typeStr = to_string(type); - std::string_view typeStrV = typeStr; - std::string_view data; - std::string_view value; - - auto cursor = lmdb::cursor::open(txn, db); - bool first = true; - if (cursor.get(typeStrV, data, MDB_SET)) { - while (cursor.get( - typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { - first = false; - - if (eventsDb.get(txn, - json::parse(data)["id"].get<std::string>(), - value)) - events.push_back( - json::parse(value) - .get<mtx::events::StateEvent<T>>()); - } - } - } + { + constexpr auto type = mtx::events::state_content_to_type<T>; + static_assert(type != mtx::events::EventType::Unsupported, + "Not a supported type in state events."); - return events; - } - void saveInvites(lmdb::txn &txn, - const std::map<std::string, mtx::responses::InvitedRoom> &rooms); + if (room_id.empty()) + return {}; - void savePresence( - lmdb::txn &txn, - const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates); + std::vector<mtx::events::StateEvent<T>> events; - //! Sends signals for the rooms that are removed. - void removeLeftRooms(lmdb::txn &txn, - const std::map<std::string, mtx::responses::LeftRoom> &rooms) { - for (const auto &room : rooms) { - removeRoom(txn, room.first); - - // Clean up leftover invites. - removeInvite(txn, room.first); + auto db = getStatesKeyDb(txn, room_id); + auto eventsDb = getEventsDb(txn, room_id); + const auto typeStr = to_string(type); + std::string_view typeStrV = typeStr; + std::string_view data; + std::string_view value; + + auto cursor = lmdb::cursor::open(txn, db); + bool first = true; + if (cursor.get(typeStrV, data, MDB_SET)) { + while (cursor.get(typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) { + first = false; + + if (eventsDb.get(txn, json::parse(data)["id"].get<std::string>(), value)) + events.push_back(json::parse(value).get<mtx::events::StateEvent<T>>()); } + } } - void updateSpaces(lmdb::txn &txn, - const std::set<std::string> &spaces_with_updates, - std::set<std::string> rooms_with_updates); - - lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); - } - - lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE); - } - - lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY); - } - - // inverse of EventOrderDb - lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE); - } - - lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE); - } - - lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY); - } - - lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY); - } - - lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT); - } - - lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); - } - - lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); - } - - lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); - } - - lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id) - { - auto db = lmdb::dbi::open( - txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT); - lmdb::dbi_set_dupsort(txn, db, compare_state_key); - return db; - } - - lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE); - } - - lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); - } - - lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); - } + return events; + } + void saveInvites(lmdb::txn &txn, + const std::map<std::string, mtx::responses::InvitedRoom> &rooms); - lmdb::dbi getPresenceDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "presence", MDB_CREATE); - } + void savePresence( + lmdb::txn &txn, + const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates); - lmdb::dbi getUserKeysDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "user_key", MDB_CREATE); - } + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map<std::string, mtx::responses::LeftRoom> &rooms) + { + for (const auto &room : rooms) { + removeRoom(txn, room.first); - lmdb::dbi getVerificationDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "verified", MDB_CREATE); - } - - //! Retrieves or creates the database that stores the open OLM sessions between our device - //! and the given curve25519 key which represents another device. - //! - //! Each entry is a map from the session_id to the pickled representation of the session. - lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) - { - return lmdb::dbi::open( - txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE); + // Clean up leftover invites. + removeInvite(txn, room.first); } - - QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) - { - if (!event.content.display_name.empty()) - return QString::fromStdString(event.content.display_name); - - return QString::fromStdString(event.state_key); - } - - std::optional<VerificationCache> verificationCache(const std::string &user_id, - lmdb::txn &txn); - VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn); - std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn); - - void setNextBatchToken(lmdb::txn &txn, const std::string &token); - - lmdb::env env_; - lmdb::dbi syncStateDb_; - lmdb::dbi roomsDb_; - lmdb::dbi spacesChildrenDb_, spacesParentsDb_; - lmdb::dbi invitesDb_; - lmdb::dbi readReceiptsDb_; - lmdb::dbi notificationsDb_; - - lmdb::dbi devicesDb_; - lmdb::dbi deviceKeysDb_; - - lmdb::dbi inboundMegolmSessionDb_; - lmdb::dbi outboundMegolmSessionDb_; - lmdb::dbi megolmSessionDataDb_; - - lmdb::dbi encryptedRooms_; - - QString localUserId_; - QString cacheDirectory_; - - std::string pickle_secret_; - - VerificationStorage verification_storage; - - bool databaseReady_ = false; + } + + void updateSpaces(lmdb::txn &txn, + const std::set<std::string> &spaces_with_updates, + std::set<std::string> rooms_with_updates); + + lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); + } + + lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE); + } + + lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY); + } + + // inverse of EventOrderDb + lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE); + } + + lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE); + } + + lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY); + } + + lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY); + } + + lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT); + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id) + { + auto db = lmdb::dbi::open( + txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT); + lmdb::dbi_set_dupsort(txn, db, compare_state_key); + return db; + } + + lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); + } + + lmdb::dbi getPresenceDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "presence", MDB_CREATE); } + + lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); } + + lmdb::dbi getVerificationDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "verified", MDB_CREATE); + } + + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + std::optional<VerificationCache> verificationCache(const std::string &user_id, lmdb::txn &txn); + VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn); + std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn); + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); + + lmdb::env env_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; + lmdb::dbi spacesChildrenDb_, spacesParentsDb_; + lmdb::dbi invitesDb_; + lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; + + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + lmdb::dbi megolmSessionDataDb_; + + lmdb::dbi encryptedRooms_; + + QString localUserId_; + QString cacheDirectory_; + + std::string pickle_secret_; + + VerificationStorage verification_storage; + + bool databaseReady_ = false; }; namespace cache { diff --git a/src/CallDevices.cpp b/src/CallDevices.cpp index 825d2f72..be185470 100644 --- a/src/CallDevices.cpp +++ b/src/CallDevices.cpp @@ -27,20 +27,20 @@ namespace { struct AudioSource { - std::string name; - GstDevice *device; + std::string name; + GstDevice *device; }; struct VideoSource { - struct Caps - { - std::string resolution; - std::vector<std::string> frameRates; - }; - std::string name; - GstDevice *device; - std::vector<Caps> caps; + struct Caps + { + std::string resolution; + std::vector<std::string> frameRates; + }; + std::string name; + GstDevice *device; + std::vector<Caps> caps; }; std::vector<AudioSource> audioSources_; @@ -50,315 +50,304 @@ using FrameRate = std::pair<int, int>; std::optional<FrameRate> getFrameRate(const GValue *value) { - if (GST_VALUE_HOLDS_FRACTION(value)) { - gint num = gst_value_get_fraction_numerator(value); - gint den = gst_value_get_fraction_denominator(value); - return FrameRate{num, den}; - } - return std::nullopt; + if (GST_VALUE_HOLDS_FRACTION(value)) { + gint num = gst_value_get_fraction_numerator(value); + gint den = gst_value_get_fraction_denominator(value); + return FrameRate{num, den}; + } + return std::nullopt; } void addFrameRate(std::vector<std::string> &rates, const FrameRate &rate) { - constexpr double minimumFrameRate = 15.0; - if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate) - rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); + constexpr double minimumFrameRate = 15.0; + if (static_cast<double>(rate.first) / rate.second >= minimumFrameRate) + rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); } void setDefaultDevice(bool isVideo) { - auto settings = ChatPage::instance()->userSettings(); - if (isVideo && settings->camera().isEmpty()) { - const VideoSource &camera = videoSources_.front(); - settings->setCamera(QString::fromStdString(camera.name)); - settings->setCameraResolution( - QString::fromStdString(camera.caps.front().resolution)); - settings->setCameraFrameRate( - QString::fromStdString(camera.caps.front().frameRates.front())); - } else if (!isVideo && settings->microphone().isEmpty()) { - settings->setMicrophone(QString::fromStdString(audioSources_.front().name)); - } + auto settings = ChatPage::instance()->userSettings(); + if (isVideo && settings->camera().isEmpty()) { + const VideoSource &camera = videoSources_.front(); + settings->setCamera(QString::fromStdString(camera.name)); + settings->setCameraResolution(QString::fromStdString(camera.caps.front().resolution)); + settings->setCameraFrameRate( + QString::fromStdString(camera.caps.front().frameRates.front())); + } else if (!isVideo && settings->microphone().isEmpty()) { + settings->setMicrophone(QString::fromStdString(audioSources_.front().name)); + } } void addDevice(GstDevice *device) { - if (!device) - return; - - gchar *name = gst_device_get_display_name(device); - gchar *type = gst_device_get_device_class(device); - bool isVideo = !std::strncmp(type, "Video", 5); - g_free(type); - nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name); - if (!isVideo) { - audioSources_.push_back({name, device}); - g_free(name); - setDefaultDevice(false); - return; - } - - GstCaps *gstcaps = gst_device_get_caps(device); - if (!gstcaps) { - nhlog::ui()->debug("WebRTC: unable to get caps for {}", name); - g_free(name); - return; - } + if (!device) + return; + + gchar *name = gst_device_get_display_name(device); + gchar *type = gst_device_get_device_class(device); + bool isVideo = !std::strncmp(type, "Video", 5); + g_free(type); + nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name); + if (!isVideo) { + audioSources_.push_back({name, device}); + g_free(name); + setDefaultDevice(false); + return; + } - VideoSource source{name, device, {}}; + GstCaps *gstcaps = gst_device_get_caps(device); + if (!gstcaps) { + nhlog::ui()->debug("WebRTC: unable to get caps for {}", name); g_free(name); - guint nCaps = gst_caps_get_size(gstcaps); - for (guint i = 0; i < nCaps; ++i) { - GstStructure *structure = gst_caps_get_structure(gstcaps, i); - const gchar *struct_name = gst_structure_get_name(structure); - if (!std::strcmp(struct_name, "video/x-raw")) { - gint widthpx, heightpx; - if (gst_structure_get(structure, - "width", - G_TYPE_INT, - &widthpx, - "height", - G_TYPE_INT, - &heightpx, - nullptr)) { - VideoSource::Caps caps; - caps.resolution = - std::to_string(widthpx) + "x" + std::to_string(heightpx); - const GValue *value = - gst_structure_get_value(structure, "framerate"); - if (auto fr = getFrameRate(value); fr) - addFrameRate(caps.frameRates, *fr); - else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) { - addFrameRate( - caps.frameRates, - *getFrameRate(gst_value_get_fraction_range_min(value))); - addFrameRate( - caps.frameRates, - *getFrameRate(gst_value_get_fraction_range_max(value))); - } else if (GST_VALUE_HOLDS_LIST(value)) { - guint nRates = gst_value_list_get_size(value); - for (guint j = 0; j < nRates; ++j) { - const GValue *rate = - gst_value_list_get_value(value, j); - if (auto frate = getFrameRate(rate); frate) - addFrameRate(caps.frameRates, *frate); - } - } - if (!caps.frameRates.empty()) - source.caps.push_back(std::move(caps)); - } + return; + } + + VideoSource source{name, device, {}}; + g_free(name); + guint nCaps = gst_caps_get_size(gstcaps); + for (guint i = 0; i < nCaps; ++i) { + GstStructure *structure = gst_caps_get_structure(gstcaps, i); + const gchar *struct_name = gst_structure_get_name(structure); + if (!std::strcmp(struct_name, "video/x-raw")) { + gint widthpx, heightpx; + if (gst_structure_get(structure, + "width", + G_TYPE_INT, + &widthpx, + "height", + G_TYPE_INT, + &heightpx, + nullptr)) { + VideoSource::Caps caps; + caps.resolution = std::to_string(widthpx) + "x" + std::to_string(heightpx); + const GValue *value = gst_structure_get_value(structure, "framerate"); + if (auto fr = getFrameRate(value); fr) + addFrameRate(caps.frameRates, *fr); + else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) { + addFrameRate(caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_min(value))); + addFrameRate(caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_max(value))); + } else if (GST_VALUE_HOLDS_LIST(value)) { + guint nRates = gst_value_list_get_size(value); + for (guint j = 0; j < nRates; ++j) { + const GValue *rate = gst_value_list_get_value(value, j); + if (auto frate = getFrameRate(rate); frate) + addFrameRate(caps.frameRates, *frate); + } } + if (!caps.frameRates.empty()) + source.caps.push_back(std::move(caps)); + } } - gst_caps_unref(gstcaps); - videoSources_.push_back(std::move(source)); - setDefaultDevice(true); + } + gst_caps_unref(gstcaps); + videoSources_.push_back(std::move(source)); + setDefaultDevice(true); } template<typename T> bool removeDevice(T &sources, GstDevice *device, bool changed) { - if (auto it = std::find_if(sources.begin(), - sources.end(), - [device](const auto &s) { return s.device == device; }); - it != sources.end()) { - nhlog::ui()->debug(std::string("WebRTC: device ") + - (changed ? "changed: " : "removed: ") + "{}", - it->name); - gst_object_unref(device); - sources.erase(it); - return true; - } - return false; + if (auto it = std::find_if( + sources.begin(), sources.end(), [device](const auto &s) { return s.device == device; }); + it != sources.end()) { + nhlog::ui()->debug( + std::string("WebRTC: device ") + (changed ? "changed: " : "removed: ") + "{}", it->name); + gst_object_unref(device); + sources.erase(it); + return true; + } + return false; } void removeDevice(GstDevice *device, bool changed) { - if (device) { - if (removeDevice(audioSources_, device, changed) || - removeDevice(videoSources_, device, changed)) - return; - } + if (device) { + if (removeDevice(audioSources_, device, changed) || + removeDevice(videoSources_, device, changed)) + return; + } } gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) { - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_DEVICE_ADDED: { - GstDevice *device; - gst_message_parse_device_added(msg, &device); - addDevice(device); - emit CallDevices::instance().devicesChanged(); - break; - } - case GST_MESSAGE_DEVICE_REMOVED: { - GstDevice *device; - gst_message_parse_device_removed(msg, &device); - removeDevice(device, false); - emit CallDevices::instance().devicesChanged(); - break; - } - case GST_MESSAGE_DEVICE_CHANGED: { - GstDevice *device; - GstDevice *oldDevice; - gst_message_parse_device_changed(msg, &device, &oldDevice); - removeDevice(oldDevice, true); - addDevice(device); - break; - } - default: - break; - } - return TRUE; + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_DEVICE_ADDED: { + GstDevice *device; + gst_message_parse_device_added(msg, &device); + addDevice(device); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_REMOVED: { + GstDevice *device; + gst_message_parse_device_removed(msg, &device); + removeDevice(device, false); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_CHANGED: { + GstDevice *device; + GstDevice *oldDevice; + gst_message_parse_device_changed(msg, &device, &oldDevice); + removeDevice(oldDevice, true); + addDevice(device); + break; + } + default: + break; + } + return TRUE; } template<typename T> std::vector<std::string> deviceNames(T &sources, const std::string &defaultDevice) { - std::vector<std::string> ret; - ret.reserve(sources.size()); - for (const auto &s : sources) - ret.push_back(s.name); + std::vector<std::string> ret; + ret.reserve(sources.size()); + for (const auto &s : sources) + ret.push_back(s.name); - // move default device to top of the list - if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end()) - std::swap(ret.front(), *it); + // move default device to top of the list + if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end()) + std::swap(ret.front(), *it); - return ret; + return ret; } std::optional<VideoSource> getVideoSource(const std::string &cameraName) { - if (auto it = std::find_if(videoSources_.cbegin(), - videoSources_.cend(), - [&cameraName](const auto &s) { return s.name == cameraName; }); - it != videoSources_.cend()) { - return *it; - } - return std::nullopt; + if (auto it = std::find_if(videoSources_.cbegin(), + videoSources_.cend(), + [&cameraName](const auto &s) { return s.name == cameraName; }); + it != videoSources_.cend()) { + return *it; + } + return std::nullopt; } std::pair<int, int> tokenise(std::string_view str, char delim) { - std::pair<int, int> ret; - ret.first = std::atoi(str.data()); - auto pos = str.find_first_of(delim); - ret.second = std::atoi(str.data() + pos + 1); - return ret; + std::pair<int, int> ret; + ret.first = std::atoi(str.data()); + auto pos = str.find_first_of(delim); + ret.second = std::atoi(str.data() + pos + 1); + return ret; } } void CallDevices::init() { - static GstDeviceMonitor *monitor = nullptr; - if (!monitor) { - monitor = gst_device_monitor_new(); - GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); - gst_device_monitor_add_filter(monitor, "Audio/Source", caps); - gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps); - gst_caps_unref(caps); - caps = gst_caps_new_empty_simple("video/x-raw"); - gst_device_monitor_add_filter(monitor, "Video/Source", caps); - gst_device_monitor_add_filter(monitor, "Video/Duplex", caps); - gst_caps_unref(caps); - - GstBus *bus = gst_device_monitor_get_bus(monitor); - gst_bus_add_watch(bus, newBusMessage, nullptr); - gst_object_unref(bus); - if (!gst_device_monitor_start(monitor)) { - nhlog::ui()->error("WebRTC: failed to start device monitor"); - return; - } + static GstDeviceMonitor *monitor = nullptr; + if (!monitor) { + monitor = gst_device_monitor_new(); + GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); + gst_device_monitor_add_filter(monitor, "Audio/Source", caps); + gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps); + gst_caps_unref(caps); + caps = gst_caps_new_empty_simple("video/x-raw"); + gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_device_monitor_add_filter(monitor, "Video/Duplex", caps); + gst_caps_unref(caps); + + GstBus *bus = gst_device_monitor_get_bus(monitor); + gst_bus_add_watch(bus, newBusMessage, nullptr); + gst_object_unref(bus); + if (!gst_device_monitor_start(monitor)) { + nhlog::ui()->error("WebRTC: failed to start device monitor"); + return; } + } } bool CallDevices::haveMic() const { - return !audioSources_.empty(); + return !audioSources_.empty(); } bool CallDevices::haveCamera() const { - return !videoSources_.empty(); + return !videoSources_.empty(); } std::vector<std::string> CallDevices::names(bool isVideo, const std::string &defaultDevice) const { - return isVideo ? deviceNames(videoSources_, defaultDevice) - : deviceNames(audioSources_, defaultDevice); + return isVideo ? deviceNames(videoSources_, defaultDevice) + : deviceNames(audioSources_, defaultDevice); } std::vector<std::string> CallDevices::resolutions(const std::string &cameraName) const { - std::vector<std::string> ret; - if (auto s = getVideoSource(cameraName); s) { - ret.reserve(s->caps.size()); - for (const auto &c : s->caps) - ret.push_back(c.resolution); - } - return ret; + std::vector<std::string> ret; + if (auto s = getVideoSource(cameraName); s) { + ret.reserve(s->caps.size()); + for (const auto &c : s->caps) + ret.push_back(c.resolution); + } + return ret; } std::vector<std::string> CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const { - if (auto s = getVideoSource(cameraName); s) { - if (auto it = - std::find_if(s->caps.cbegin(), + if (auto s = getVideoSource(cameraName); s) { + if (auto it = std::find_if(s->caps.cbegin(), s->caps.cend(), [&](const auto &c) { return c.resolution == resolution; }); - it != s->caps.cend()) - return it->frameRates; - } - return {}; + it != s->caps.cend()) + return it->frameRates; + } + return {}; } GstDevice * CallDevices::audioDevice() const { - std::string name = ChatPage::instance()->userSettings()->microphone().toStdString(); - if (auto it = std::find_if(audioSources_.cbegin(), - audioSources_.cend(), - [&name](const auto &s) { return s.name == name; }); - it != audioSources_.cend()) { - nhlog::ui()->debug("WebRTC: microphone: {}", name); - return it->device; - } else { - nhlog::ui()->error("WebRTC: unknown microphone: {}", name); - return nullptr; - } + std::string name = ChatPage::instance()->userSettings()->microphone().toStdString(); + if (auto it = std::find_if(audioSources_.cbegin(), + audioSources_.cend(), + [&name](const auto &s) { return s.name == name; }); + it != audioSources_.cend()) { + nhlog::ui()->debug("WebRTC: microphone: {}", name); + return it->device; + } else { + nhlog::ui()->error("WebRTC: unknown microphone: {}", name); + return nullptr; + } } GstDevice * CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const { - auto settings = ChatPage::instance()->userSettings(); - std::string name = settings->camera().toStdString(); - if (auto s = getVideoSource(name); s) { - nhlog::ui()->debug("WebRTC: camera: {}", name); - resolution = tokenise(settings->cameraResolution().toStdString(), 'x'); - frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/'); - nhlog::ui()->debug( - "WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); - nhlog::ui()->debug( - "WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); - return s->device; - } else { - nhlog::ui()->error("WebRTC: unknown camera: {}", name); - return nullptr; - } + auto settings = ChatPage::instance()->userSettings(); + std::string name = settings->camera().toStdString(); + if (auto s = getVideoSource(name); s) { + nhlog::ui()->debug("WebRTC: camera: {}", name); + resolution = tokenise(settings->cameraResolution().toStdString(), 'x'); + frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/'); + nhlog::ui()->debug("WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); + nhlog::ui()->debug("WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); + return s->device; + } else { + nhlog::ui()->error("WebRTC: unknown camera: {}", name); + return nullptr; + } } #else @@ -366,31 +355,31 @@ CallDevices::videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &f bool CallDevices::haveMic() const { - return false; + return false; } bool CallDevices::haveCamera() const { - return false; + return false; } std::vector<std::string> CallDevices::names(bool, const std::string &) const { - return {}; + return {}; } std::vector<std::string> CallDevices::resolutions(const std::string &) const { - return {}; + return {}; } std::vector<std::string> CallDevices::frameRates(const std::string &, const std::string &) const { - return {}; + return {}; } #endif diff --git a/src/CallDevices.h b/src/CallDevices.h index 69325f97..d30ce644 100644 --- a/src/CallDevices.h +++ b/src/CallDevices.h @@ -14,35 +14,34 @@ typedef struct _GstDevice GstDevice; class CallDevices : public QObject { - Q_OBJECT + Q_OBJECT public: - static CallDevices &instance() - { - static CallDevices instance; - return instance; - } - - bool haveMic() const; - bool haveCamera() const; - std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const; - std::vector<std::string> resolutions(const std::string &cameraName) const; - std::vector<std::string> frameRates(const std::string &cameraName, - const std::string &resolution) const; + static CallDevices &instance() + { + static CallDevices instance; + return instance; + } + + bool haveMic() const; + bool haveCamera() const; + std::vector<std::string> names(bool isVideo, const std::string &defaultDevice) const; + std::vector<std::string> resolutions(const std::string &cameraName) const; + std::vector<std::string> frameRates(const std::string &cameraName, + const std::string &resolution) const; signals: - void devicesChanged(); + void devicesChanged(); private: - CallDevices(); + CallDevices(); - friend class WebRTCSession; - void init(); - GstDevice *audioDevice() const; - GstDevice *videoDevice(std::pair<int, int> &resolution, - std::pair<int, int> &frameRate) const; + friend class WebRTCSession; + void init(); + GstDevice *audioDevice() const; + GstDevice *videoDevice(std::pair<int, int> &resolution, std::pair<int, int> &frameRate) const; public: - CallDevices(CallDevices const &) = delete; - void operator=(CallDevices const &) = delete; + CallDevices(CallDevices const &) = delete; + void operator=(CallDevices const &) = delete; }; diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 601c9d6b..0f701b0d 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -54,206 +54,197 @@ CallManager::CallManager(QObject *parent) , session_(WebRTCSession::instance()) , turnServerTimer_(this) { - qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>(); - qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>(); - qRegisterMetaType<mtx::responses::TurnServer>(); - - connect( - &session_, - &WebRTCSession::offerCreated, - this, - [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { - nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_); - emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_}); - emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"}); - std::string callid(callid_); - QTimer::singleShot(timeoutms_, this, [this, callid]() { - if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) { - hangUp(CallHangUp::Reason::InviteTimeOut); - emit ChatPage::instance()->showNotification( - "The remote side failed to pick up."); - } - }); + qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>(); + qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>(); + qRegisterMetaType<mtx::responses::TurnServer>(); + + connect( + &session_, + &WebRTCSession::offerCreated, + this, + [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { + nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_); + emit newMessage(roomid_, CallInvite{callid_, sdp, "0", timeoutms_}); + emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"}); + std::string callid(callid_); + QTimer::singleShot(timeoutms_, this, [this, callid]() { + if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) { + hangUp(CallHangUp::Reason::InviteTimeOut); + emit ChatPage::instance()->showNotification("The remote side failed to pick up."); + } }); + }); + + connect( + &session_, + &WebRTCSession::answerCreated, + this, + [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { + nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_); + emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"}); + emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"}); + }); + + connect(&session_, + &WebRTCSession::newICECandidate, + this, + [this](const CallCandidates::Candidate &candidate) { + nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_); + emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"}); + }); + + connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); + + connect( + this, &CallManager::turnServerRetrieved, this, [this](const mtx::responses::TurnServer &res) { + nhlog::net()->info("TURN server(s) retrieved from homeserver:"); + nhlog::net()->info("username: {}", res.username); + nhlog::net()->info("ttl: {} seconds", res.ttl); + for (const auto &u : res.uris) + nhlog::net()->info("uri: {}", u); + + // Request new credentials close to expiry + // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 + turnURIs_ = getTurnURIs(res); + uint32_t ttl = std::max(res.ttl, UINT32_C(3600)); + if (res.ttl < 3600) + nhlog::net()->warn("Setting ttl to 1 hour"); + turnServerTimer_.setInterval(ttl * 1000 * 0.9); + }); + + connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) { + switch (state) { + case webrtc::State::DISCONNECTED: + playRingtone(QUrl("qrc:/media/media/callend.ogg"), false); + clear(); + break; + case webrtc::State::ICEFAILED: { + QString error("Call connection failed."); + if (turnURIs_.empty()) + error += " Your homeserver has no configured TURN server."; + emit ChatPage::instance()->showNotification(error); + hangUp(CallHangUp::Reason::ICEFailed); + break; + } + default: + break; + } + emit newCallState(); + }); - connect( - &session_, - &WebRTCSession::answerCreated, - this, - [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { - nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_); - emit newMessage(roomid_, CallAnswer{callid_, sdp, "0"}); - emit newMessage(roomid_, CallCandidates{callid_, candidates, "0"}); - }); + connect( + &CallDevices::instance(), &CallDevices::devicesChanged, this, &CallManager::devicesChanged); - connect(&session_, - &WebRTCSession::newICECandidate, - this, - [this](const CallCandidates::Candidate &candidate) { - nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_); - emit newMessage(roomid_, CallCandidates{callid_, {candidate}, "0"}); - }); - - connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); - - connect(this, - &CallManager::turnServerRetrieved, - this, - [this](const mtx::responses::TurnServer &res) { - nhlog::net()->info("TURN server(s) retrieved from homeserver:"); - nhlog::net()->info("username: {}", res.username); - nhlog::net()->info("ttl: {} seconds", res.ttl); - for (const auto &u : res.uris) - nhlog::net()->info("uri: {}", u); - - // Request new credentials close to expiry - // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 - turnURIs_ = getTurnURIs(res); - uint32_t ttl = std::max(res.ttl, UINT32_C(3600)); - if (res.ttl < 3600) - nhlog::net()->warn("Setting ttl to 1 hour"); - turnServerTimer_.setInterval(ttl * 1000 * 0.9); - }); - - connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) { - switch (state) { - case webrtc::State::DISCONNECTED: - playRingtone(QUrl("qrc:/media/media/callend.ogg"), false); - clear(); - break; - case webrtc::State::ICEFAILED: { - QString error("Call connection failed."); - if (turnURIs_.empty()) - error += " Your homeserver has no configured TURN server."; - emit ChatPage::instance()->showNotification(error); - hangUp(CallHangUp::Reason::ICEFailed); - break; - } + connect( + &player_, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) { + if (status == QMediaPlayer::LoadedMedia) + player_.play(); + }); + + connect(&player_, + QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error), + [this](QMediaPlayer::Error error) { + stopRingtone(); + switch (error) { + case QMediaPlayer::FormatError: + case QMediaPlayer::ResourceError: + nhlog::ui()->error("WebRTC: valid ringtone file not found"); + break; + case QMediaPlayer::AccessDeniedError: + nhlog::ui()->error("WebRTC: access to ringtone file denied"); + break; default: - break; + nhlog::ui()->error("WebRTC: unable to play ringtone"); + break; } - emit newCallState(); - }); - - connect(&CallDevices::instance(), - &CallDevices::devicesChanged, - this, - &CallManager::devicesChanged); - - connect(&player_, - &QMediaPlayer::mediaStatusChanged, - this, - [this](QMediaPlayer::MediaStatus status) { - if (status == QMediaPlayer::LoadedMedia) - player_.play(); - }); - - connect(&player_, - QOverload<QMediaPlayer::Error>::of(&QMediaPlayer::error), - [this](QMediaPlayer::Error error) { - stopRingtone(); - switch (error) { - case QMediaPlayer::FormatError: - case QMediaPlayer::ResourceError: - nhlog::ui()->error("WebRTC: valid ringtone file not found"); - break; - case QMediaPlayer::AccessDeniedError: - nhlog::ui()->error("WebRTC: access to ringtone file denied"); - break; - default: - nhlog::ui()->error("WebRTC: unable to play ringtone"); - break; - } - }); + }); } void CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex) { - if (isOnCall()) - return; - if (callType == CallType::SCREEN) { - if (!screenShareSupported()) - return; - if (windows_.empty() || windowIndex >= windows_.size()) { - nhlog::ui()->error("WebRTC: window index out of range"); - return; - } - } - - auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); - if (roomInfo.member_count != 2) { - emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms."); - return; - } - - std::string errorMessage; - if (!session_.havePlugins(false, &errorMessage) || - ((callType == CallType::VIDEO || callType == CallType::SCREEN) && - !session_.havePlugins(true, &errorMessage))) { - emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); - return; - } - - callType_ = callType; - roomid_ = roomid; - session_.setTurnServers(turnURIs_); - generateCallID(); - std::string strCallType = callType_ == CallType::VOICE - ? "voice" - : (callType_ == CallType::VIDEO ? "video" : "screen"); - nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType); - std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); - const RoomMember &callee = - members.front().user_id == utils::localUser() ? members.back() : members.front(); - callParty_ = callee.user_id; - callPartyDisplayName_ = - callee.display_name.isEmpty() ? callee.user_id : callee.display_name; - callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); - emit newInviteState(); - playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true); - if (!session_.createOffer( - callType, callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) { - emit ChatPage::instance()->showNotification("Problem setting up call."); - endCall(); + if (isOnCall()) + return; + if (callType == CallType::SCREEN) { + if (!screenShareSupported()) + return; + if (windows_.empty() || windowIndex >= windows_.size()) { + nhlog::ui()->error("WebRTC: window index out of range"); + return; } + } + + auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); + if (roomInfo.member_count != 2) { + emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms."); + return; + } + + std::string errorMessage; + if (!session_.havePlugins(false, &errorMessage) || + ((callType == CallType::VIDEO || callType == CallType::SCREEN) && + !session_.havePlugins(true, &errorMessage))) { + emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); + return; + } + + callType_ = callType; + roomid_ = roomid; + session_.setTurnServers(turnURIs_); + generateCallID(); + std::string strCallType = + callType_ == CallType::VOICE ? "voice" : (callType_ == CallType::VIDEO ? "video" : "screen"); + nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType); + std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); + const RoomMember &callee = + members.front().user_id == utils::localUser() ? members.back() : members.front(); + callParty_ = callee.user_id; + callPartyDisplayName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name; + callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + emit newInviteState(); + playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true); + if (!session_.createOffer(callType, + callType == CallType::SCREEN ? windows_[windowIndex].second : 0)) { + emit ChatPage::instance()->showNotification("Problem setting up call."); + endCall(); + } } namespace { std::string callHangUpReasonString(CallHangUp::Reason reason) { - switch (reason) { - case CallHangUp::Reason::ICEFailed: - return "ICE failed"; - case CallHangUp::Reason::InviteTimeOut: - return "Invite time out"; - default: - return "User"; - } + switch (reason) { + case CallHangUp::Reason::ICEFailed: + return "ICE failed"; + case CallHangUp::Reason::InviteTimeOut: + return "Invite time out"; + default: + return "User"; + } } } void CallManager::hangUp(CallHangUp::Reason reason) { - if (!callid_.empty()) { - nhlog::ui()->debug( - "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason)); - emit newMessage(roomid_, CallHangUp{callid_, "0", reason}); - endCall(); - } + if (!callid_.empty()) { + nhlog::ui()->debug( + "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason)); + emit newMessage(roomid_, CallHangUp{callid_, "0", reason}); + endCall(); + } } void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) { #ifdef GSTREAMER_AVAILABLE - if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) || - handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event)) - return; + if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) || + handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event)) + return; #else - (void)event; + (void)event; #endif } @@ -261,325 +252,321 @@ template<typename T> bool CallManager::handleEvent(const mtx::events::collections::TimelineEvents &event) { - if (std::holds_alternative<RoomEvent<T>>(event)) { - handleEvent(std::get<RoomEvent<T>>(event)); - return true; - } - return false; + if (std::holds_alternative<RoomEvent<T>>(event)) { + handleEvent(std::get<RoomEvent<T>>(event)); + return true; + } + return false; } void CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) { - const char video[] = "m=video"; - const std::string &sdp = callInviteEvent.content.sdp; - bool isVideo = std::search(sdp.cbegin(), - sdp.cend(), - std::cbegin(video), - std::cend(video) - 1, - [](unsigned char c1, unsigned char c2) { - return std::tolower(c1) == std::tolower(c2); - }) != sdp.cend(); - - nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}", - callInviteEvent.content.call_id, - (isVideo ? "video" : "voice"), - callInviteEvent.sender); - - if (callInviteEvent.content.call_id.empty()) - return; - - auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); - if (isOnCall() || roomInfo.member_count != 2) { - emit newMessage(QString::fromStdString(callInviteEvent.room_id), - CallHangUp{callInviteEvent.content.call_id, - "0", - CallHangUp::Reason::InviteTimeOut}); - return; - } - - const QString &ringtone = ChatPage::instance()->userSettings()->ringtone(); - if (ringtone != "Mute") - playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg") - : QUrl::fromLocalFile(ringtone), - true); - roomid_ = QString::fromStdString(callInviteEvent.room_id); - callid_ = callInviteEvent.content.call_id; - remoteICECandidates_.clear(); - - std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); - const RoomMember &caller = - members.front().user_id == utils::localUser() ? members.back() : members.front(); - callParty_ = caller.user_id; - callPartyDisplayName_ = - caller.display_name.isEmpty() ? caller.user_id : caller.display_name; - callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); - - haveCallInvite_ = true; - callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; - inviteSDP_ = callInviteEvent.content.sdp; - emit newInviteState(); + const char video[] = "m=video"; + const std::string &sdp = callInviteEvent.content.sdp; + bool isVideo = std::search(sdp.cbegin(), + sdp.cend(), + std::cbegin(video), + std::cend(video) - 1, + [](unsigned char c1, unsigned char c2) { + return std::tolower(c1) == std::tolower(c2); + }) != sdp.cend(); + + nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}", + callInviteEvent.content.call_id, + (isVideo ? "video" : "voice"), + callInviteEvent.sender); + + if (callInviteEvent.content.call_id.empty()) + return; + + auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); + if (isOnCall() || roomInfo.member_count != 2) { + emit newMessage( + QString::fromStdString(callInviteEvent.room_id), + CallHangUp{callInviteEvent.content.call_id, "0", CallHangUp::Reason::InviteTimeOut}); + return; + } + + const QString &ringtone = ChatPage::instance()->userSettings()->ringtone(); + if (ringtone != "Mute") + playRingtone(ringtone == "Default" ? QUrl("qrc:/media/media/ring.ogg") + : QUrl::fromLocalFile(ringtone), + true); + roomid_ = QString::fromStdString(callInviteEvent.room_id); + callid_ = callInviteEvent.content.call_id; + remoteICECandidates_.clear(); + + std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); + const RoomMember &caller = + members.front().user_id == utils::localUser() ? members.back() : members.front(); + callParty_ = caller.user_id; + callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name; + callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + + haveCallInvite_ = true; + callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; + inviteSDP_ = callInviteEvent.content.sdp; + emit newInviteState(); } void CallManager::acceptInvite() { - if (!haveCallInvite_) - return; - - stopRingtone(); - std::string errorMessage; - if (!session_.havePlugins(false, &errorMessage) || - (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) { - emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); - hangUp(); - return; - } - - session_.setTurnServers(turnURIs_); - if (!session_.acceptOffer(inviteSDP_)) { - emit ChatPage::instance()->showNotification("Problem setting up call."); - hangUp(); - return; - } - session_.acceptICECandidates(remoteICECandidates_); - remoteICECandidates_.clear(); - haveCallInvite_ = false; - emit newInviteState(); + if (!haveCallInvite_) + return; + + stopRingtone(); + std::string errorMessage; + if (!session_.havePlugins(false, &errorMessage) || + (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) { + emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); + hangUp(); + return; + } + + session_.setTurnServers(turnURIs_); + if (!session_.acceptOffer(inviteSDP_)) { + emit ChatPage::instance()->showNotification("Problem setting up call."); + hangUp(); + return; + } + session_.acceptICECandidates(remoteICECandidates_); + remoteICECandidates_.clear(); + haveCallInvite_ = false; + emit newInviteState(); } void CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) { - if (callCandidatesEvent.sender == utils::localUser().toStdString()) - return; - - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}", - callCandidatesEvent.content.call_id, - callCandidatesEvent.sender); - - if (callid_ == callCandidatesEvent.content.call_id) { - if (isOnCall()) - session_.acceptICECandidates(callCandidatesEvent.content.candidates); - else { - // CallInvite has been received and we're awaiting localUser to accept or - // reject the call - for (const auto &c : callCandidatesEvent.content.candidates) - remoteICECandidates_.push_back(c); - } + if (callCandidatesEvent.sender == utils::localUser().toStdString()) + return; + + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}", + callCandidatesEvent.content.call_id, + callCandidatesEvent.sender); + + if (callid_ == callCandidatesEvent.content.call_id) { + if (isOnCall()) + session_.acceptICECandidates(callCandidatesEvent.content.candidates); + else { + // CallInvite has been received and we're awaiting localUser to accept or + // reject the call + for (const auto &c : callCandidatesEvent.content.candidates) + remoteICECandidates_.push_back(c); } + } } void CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) { - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}", - callAnswerEvent.content.call_id, - callAnswerEvent.sender); - - if (callAnswerEvent.sender == utils::localUser().toStdString() && - callid_ == callAnswerEvent.content.call_id) { - if (!isOnCall()) { - emit ChatPage::instance()->showNotification( - "Call answered on another device."); - stopRingtone(); - haveCallInvite_ = false; - emit newInviteState(); - } - return; + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}", + callAnswerEvent.content.call_id, + callAnswerEvent.sender); + + if (callAnswerEvent.sender == utils::localUser().toStdString() && + callid_ == callAnswerEvent.content.call_id) { + if (!isOnCall()) { + emit ChatPage::instance()->showNotification("Call answered on another device."); + stopRingtone(); + haveCallInvite_ = false; + emit newInviteState(); } + return; + } - if (isOnCall() && callid_ == callAnswerEvent.content.call_id) { - stopRingtone(); - if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { - emit ChatPage::instance()->showNotification("Problem setting up call."); - hangUp(); - } + if (isOnCall() && callid_ == callAnswerEvent.content.call_id) { + stopRingtone(); + if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { + emit ChatPage::instance()->showNotification("Problem setting up call."); + hangUp(); } + } } void CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent) { - nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}", - callHangUpEvent.content.call_id, - callHangUpReasonString(callHangUpEvent.content.reason), - callHangUpEvent.sender); + nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}", + callHangUpEvent.content.call_id, + callHangUpReasonString(callHangUpEvent.content.reason), + callHangUpEvent.sender); - if (callid_ == callHangUpEvent.content.call_id) - endCall(); + if (callid_ == callHangUpEvent.content.call_id) + endCall(); } void CallManager::toggleMicMute() { - session_.toggleMicMute(); - emit micMuteChanged(); + session_.toggleMicMute(); + emit micMuteChanged(); } bool CallManager::callsSupported() { #ifdef GSTREAMER_AVAILABLE - return true; + return true; #else - return false; + return false; #endif } bool CallManager::screenShareSupported() { - return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"); + return std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"); } QStringList CallManager::devices(bool isVideo) const { - QStringList ret; - const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera() - : ChatPage::instance()->userSettings()->microphone(); - std::vector<std::string> devices = - CallDevices::instance().names(isVideo, defaultDevice.toStdString()); - ret.reserve(devices.size()); - std::transform(devices.cbegin(), - devices.cend(), - std::back_inserter(ret), - [](const auto &d) { return QString::fromStdString(d); }); - - return ret; + QStringList ret; + const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera() + : ChatPage::instance()->userSettings()->microphone(); + std::vector<std::string> devices = + CallDevices::instance().names(isVideo, defaultDevice.toStdString()); + ret.reserve(devices.size()); + std::transform(devices.cbegin(), devices.cend(), std::back_inserter(ret), [](const auto &d) { + return QString::fromStdString(d); + }); + + return ret; } void CallManager::generateCallID() { - using namespace std::chrono; - uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); - callid_ = "c" + std::to_string(ms); + using namespace std::chrono; + uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); + callid_ = "c" + std::to_string(ms); } void CallManager::clear() { - roomid_.clear(); - callParty_.clear(); - callPartyDisplayName_.clear(); - callPartyAvatarUrl_.clear(); - callid_.clear(); - callType_ = CallType::VOICE; - haveCallInvite_ = false; - emit newInviteState(); - inviteSDP_.clear(); - remoteICECandidates_.clear(); + roomid_.clear(); + callParty_.clear(); + callPartyDisplayName_.clear(); + callPartyAvatarUrl_.clear(); + callid_.clear(); + callType_ = CallType::VOICE; + haveCallInvite_ = false; + emit newInviteState(); + inviteSDP_.clear(); + remoteICECandidates_.clear(); } void CallManager::endCall() { - stopRingtone(); - session_.end(); - clear(); + stopRingtone(); + session_.end(); + clear(); } void CallManager::refreshTurnServer() { - turnURIs_.clear(); - turnServerTimer_.start(2000); + turnURIs_.clear(); + turnServerTimer_.start(2000); } void CallManager::retrieveTurnServer() { - http::client()->get_turn_server( - [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) { - if (err) { - turnServerTimer_.setInterval(5000); - return; - } - emit turnServerRetrieved(res); - }); + http::client()->get_turn_server( + [this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) { + if (err) { + turnServerTimer_.setInterval(5000); + return; + } + emit turnServerRetrieved(res); + }); } void CallManager::playRingtone(const QUrl &ringtone, bool repeat) { - static QMediaPlaylist playlist; - playlist.clear(); - playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop - : QMediaPlaylist::CurrentItemOnce); - playlist.addMedia(ringtone); - player_.setVolume(100); - player_.setPlaylist(&playlist); + static QMediaPlaylist playlist; + playlist.clear(); + playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop + : QMediaPlaylist::CurrentItemOnce); + playlist.addMedia(ringtone); + player_.setVolume(100); + player_.setPlaylist(&playlist); } void CallManager::stopRingtone() { - player_.setPlaylist(nullptr); + player_.setPlaylist(nullptr); } QStringList CallManager::windowList() { - windows_.clear(); - windows_.push_back({tr("Entire screen"), 0}); + windows_.clear(); + windows_.push_back({tr("Entire screen"), 0}); #ifdef XCB_AVAILABLE - std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection( - xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); }); - if (xcb_connection_has_error(connection.get())) { - nhlog::ui()->error("Failed to connect to X server"); - return {}; - } - - xcb_ewmh_connection_t ewmh; - if (!xcb_ewmh_init_atoms_replies( - &ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) { - nhlog::ui()->error("Failed to connect to EWMH server"); - return {}; + std::unique_ptr<xcb_connection_t, std::function<void(xcb_connection_t *)>> connection( + xcb_connect(nullptr, nullptr), [](xcb_connection_t *c) { xcb_disconnect(c); }); + if (xcb_connection_has_error(connection.get())) { + nhlog::ui()->error("Failed to connect to X server"); + return {}; + } + + xcb_ewmh_connection_t ewmh; + if (!xcb_ewmh_init_atoms_replies( + &ewmh, xcb_ewmh_init_atoms(connection.get(), &ewmh), nullptr)) { + nhlog::ui()->error("Failed to connect to EWMH server"); + return {}; + } + std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>> + ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); }); + + for (int i = 0; i < ewmh.nb_screens; i++) { + xcb_ewmh_get_windows_reply_t clients; + if (!xcb_ewmh_get_client_list_reply( + &ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) { + nhlog::ui()->error("Failed to request window list"); + return {}; } - std::unique_ptr<xcb_ewmh_connection_t, std::function<void(xcb_ewmh_connection_t *)>> - ewmhconnection(&ewmh, [](xcb_ewmh_connection_t *c) { xcb_ewmh_connection_wipe(c); }); - - for (int i = 0; i < ewmh.nb_screens; i++) { - xcb_ewmh_get_windows_reply_t clients; - if (!xcb_ewmh_get_client_list_reply( - &ewmh, xcb_ewmh_get_client_list(&ewmh, i), &clients, nullptr)) { - nhlog::ui()->error("Failed to request window list"); - return {}; - } - for (uint32_t w = 0; w < clients.windows_len; w++) { - xcb_window_t window = clients.windows[w]; + for (uint32_t w = 0; w < clients.windows_len; w++) { + xcb_window_t window = clients.windows[w]; - std::string name; - xcb_ewmh_get_utf8_strings_reply_t data; - auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) { - std::string name(r->strings, r->strings_len); - xcb_ewmh_get_utf8_strings_reply_wipe(r); - return name; - }; + std::string name; + xcb_ewmh_get_utf8_strings_reply_t data; + auto getName = [](xcb_ewmh_get_utf8_strings_reply_t *r) { + std::string name(r->strings, r->strings_len); + xcb_ewmh_get_utf8_strings_reply_wipe(r); + return name; + }; - xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window); - if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr)) - name = getName(&data); + xcb_get_property_cookie_t cookie = xcb_ewmh_get_wm_name(&ewmh, window); + if (xcb_ewmh_get_wm_name_reply(&ewmh, cookie, &data, nullptr)) + name = getName(&data); - cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window); - if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr)) - name = getName(&data); + cookie = xcb_ewmh_get_wm_visible_name(&ewmh, window); + if (xcb_ewmh_get_wm_visible_name_reply(&ewmh, cookie, &data, nullptr)) + name = getName(&data); - windows_.push_back({QString::fromStdString(name), window}); - } - xcb_ewmh_get_windows_reply_wipe(&clients); + windows_.push_back({QString::fromStdString(name), window}); } + xcb_ewmh_get_windows_reply_wipe(&clients); + } #endif - QStringList ret; - ret.reserve(windows_.size()); - for (const auto &w : windows_) - ret.append(w.first); + QStringList ret; + ret.reserve(windows_.size()); + for (const auto &w : windows_) + ret.append(w.first); - return ret; + return ret; } #ifdef GSTREAMER_AVAILABLE @@ -591,22 +578,22 @@ unsigned int busWatchId_ = 0; gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer G_GNUC_UNUSED) { - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_EOS: - if (pipe_) { - gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL); - gst_object_unref(pipe_); - pipe_ = nullptr; - } - if (busWatchId_) { - g_source_remove(busWatchId_); - busWatchId_ = 0; - } - break; - default: - break; + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + if (pipe_) { + gst_element_set_state(GST_ELEMENT(pipe_), GST_STATE_NULL); + gst_object_unref(pipe_); + pipe_ = nullptr; + } + if (busWatchId_) { + g_source_remove(busWatchId_); + busWatchId_ = 0; } - return TRUE; + break; + default: + break; + } + return TRUE; } } #endif @@ -615,50 +602,50 @@ void CallManager::previewWindow(unsigned int index) const { #ifdef GSTREAMER_AVAILABLE - if (windows_.empty() || index >= windows_.size() || !gst_is_initialized()) - return; - - GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr); - if (!ximagesrc) { - nhlog::ui()->error("Failed to create ximagesrc"); - return; - } - GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); - GstElement *videoscale = gst_element_factory_make("videoscale", nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); - GstElement *ximagesink = gst_element_factory_make("ximagesink", nullptr); - - g_object_set(ximagesrc, "use-damage", FALSE, nullptr); - g_object_set(ximagesrc, "show-pointer", FALSE, nullptr); - g_object_set(ximagesrc, "xid", windows_[index].second, nullptr); - - GstCaps *caps = gst_caps_new_simple( - "video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr); - g_object_set(capsfilter, "caps", caps, nullptr); - gst_caps_unref(caps); - - pipe_ = gst_pipeline_new(nullptr); - gst_bin_add_many( - GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr); - if (!gst_element_link_many( - ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) { - nhlog::ui()->error("Failed to link preview window elements"); - gst_object_unref(pipe_); - pipe_ = nullptr; - return; - } - if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { - nhlog::ui()->error("Unable to start preview pipeline"); - gst_object_unref(pipe_); - pipe_ = nullptr; - return; - } - - GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); - busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr); - gst_object_unref(bus); + if (windows_.empty() || index >= windows_.size() || !gst_is_initialized()) + return; + + GstElement *ximagesrc = gst_element_factory_make("ximagesrc", nullptr); + if (!ximagesrc) { + nhlog::ui()->error("Failed to create ximagesrc"); + return; + } + GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); + GstElement *videoscale = gst_element_factory_make("videoscale", nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + GstElement *ximagesink = gst_element_factory_make("ximagesink", nullptr); + + g_object_set(ximagesrc, "use-damage", FALSE, nullptr); + g_object_set(ximagesrc, "show-pointer", FALSE, nullptr); + g_object_set(ximagesrc, "xid", windows_[index].second, nullptr); + + GstCaps *caps = gst_caps_new_simple( + "video/x-raw", "width", G_TYPE_INT, 480, "height", G_TYPE_INT, 360, nullptr); + g_object_set(capsfilter, "caps", caps, nullptr); + gst_caps_unref(caps); + + pipe_ = gst_pipeline_new(nullptr); + gst_bin_add_many( + GST_BIN(pipe_), ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr); + if (!gst_element_link_many( + ximagesrc, videoconvert, videoscale, capsfilter, ximagesink, nullptr)) { + nhlog::ui()->error("Failed to link preview window elements"); + gst_object_unref(pipe_); + pipe_ = nullptr; + return; + } + if (gst_element_set_state(pipe_, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { + nhlog::ui()->error("Unable to start preview pipeline"); + gst_object_unref(pipe_); + pipe_ = nullptr; + return; + } + + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); + busWatchId_ = gst_bus_add_watch(bus, newBusMessage, nullptr); + gst_object_unref(bus); #else - (void)index; + (void)index; #endif } @@ -666,29 +653,28 @@ namespace { std::vector<std::string> getTurnURIs(const mtx::responses::TurnServer &turnServer) { - // gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp) - // where username and password are percent-encoded - std::vector<std::string> ret; - for (const auto &uri : turnServer.uris) { - if (auto c = uri.find(':'); c == std::string::npos) { - nhlog::ui()->error("Invalid TURN server uri: {}", uri); - continue; - } else { - std::string scheme = std::string(uri, 0, c); - if (scheme != "turn" && scheme != "turns") { - nhlog::ui()->error("Invalid TURN server uri: {}", uri); - continue; - } - - QString encodedUri = - QString::fromStdString(scheme) + "://" + - QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + - ":" + - QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + - "@" + QString::fromStdString(std::string(uri, ++c)); - ret.push_back(encodedUri.toStdString()); - } + // gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp) + // where username and password are percent-encoded + std::vector<std::string> ret; + for (const auto &uri : turnServer.uris) { + if (auto c = uri.find(':'); c == std::string::npos) { + nhlog::ui()->error("Invalid TURN server uri: {}", uri); + continue; + } else { + std::string scheme = std::string(uri, 0, c); + if (scheme != "turn" && scheme != "turns") { + nhlog::ui()->error("Invalid TURN server uri: {}", uri); + continue; + } + + QString encodedUri = + QString::fromStdString(scheme) + "://" + + QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + ":" + + QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + "@" + + QString::fromStdString(std::string(uri, ++c)); + ret.push_back(encodedUri.toStdString()); } - return ret; + } + return ret; } } diff --git a/src/CallManager.h b/src/CallManager.h index 407b8366..22f31814 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -26,93 +26,92 @@ class QUrl; class CallManager : public QObject { - Q_OBJECT - Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState) - Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState) - Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState) - Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState) - Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState) - Q_PROPERTY(QString callPartyDisplayName READ callPartyDisplayName NOTIFY newInviteState) - Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState) - Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) - Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState) - Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged) - Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged) - Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT) - Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT) + Q_OBJECT + Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState) + Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState) + Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState) + Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState) + Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState) + Q_PROPERTY(QString callPartyDisplayName READ callPartyDisplayName NOTIFY newInviteState) + Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState) + Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) + Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState) + Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged) + Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged) + Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT) + Q_PROPERTY(bool screenShareSupported READ screenShareSupported CONSTANT) public: - CallManager(QObject *); + CallManager(QObject *); - bool haveCallInvite() const { return haveCallInvite_; } - bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; } - webrtc::CallType callType() const { return callType_; } - webrtc::State callState() const { return session_.state(); } - QString callParty() const { return callParty_; } - QString callPartyDisplayName() const { return callPartyDisplayName_; } - QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } - bool isMicMuted() const { return session_.isMicMuted(); } - bool haveLocalPiP() const { return session_.haveLocalPiP(); } - QStringList mics() const { return devices(false); } - QStringList cameras() const { return devices(true); } - void refreshTurnServer(); + bool haveCallInvite() const { return haveCallInvite_; } + bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; } + webrtc::CallType callType() const { return callType_; } + webrtc::State callState() const { return session_.state(); } + QString callParty() const { return callParty_; } + QString callPartyDisplayName() const { return callPartyDisplayName_; } + QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } + bool isMicMuted() const { return session_.isMicMuted(); } + bool haveLocalPiP() const { return session_.haveLocalPiP(); } + QStringList mics() const { return devices(false); } + QStringList cameras() const { return devices(true); } + void refreshTurnServer(); - static bool callsSupported(); - static bool screenShareSupported(); + static bool callsSupported(); + static bool screenShareSupported(); public slots: - void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0); - void syncEvent(const mtx::events::collections::TimelineEvents &event); - void toggleMicMute(); - void toggleLocalPiP() { session_.toggleLocalPiP(); } - void acceptInvite(); - void hangUp( - mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); - QStringList windowList(); - void previewWindow(unsigned int windowIndex) const; + void sendInvite(const QString &roomid, webrtc::CallType, unsigned int windowIndex = 0); + void syncEvent(const mtx::events::collections::TimelineEvents &event); + void toggleMicMute(); + void toggleLocalPiP() { session_.toggleLocalPiP(); } + void acceptInvite(); + void hangUp(mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); + QStringList windowList(); + void previewWindow(unsigned int windowIndex) const; signals: - void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &); - void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); - void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); - void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); - void newInviteState(); - void newCallState(); - void micMuteChanged(); - void devicesChanged(); - void turnServerRetrieved(const mtx::responses::TurnServer &); + void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &); + void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); + void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); + void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); + void newInviteState(); + void newCallState(); + void micMuteChanged(); + void devicesChanged(); + void turnServerRetrieved(const mtx::responses::TurnServer &); private slots: - void retrieveTurnServer(); + void retrieveTurnServer(); private: - WebRTCSession &session_; - QString roomid_; - QString callParty_; - QString callPartyDisplayName_; - QString callPartyAvatarUrl_; - std::string callid_; - const uint32_t timeoutms_ = 120000; - webrtc::CallType callType_ = webrtc::CallType::VOICE; - bool haveCallInvite_ = false; - std::string inviteSDP_; - std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_; - std::vector<std::string> turnURIs_; - QTimer turnServerTimer_; - QMediaPlayer player_; - std::vector<std::pair<QString, uint32_t>> windows_; + WebRTCSession &session_; + QString roomid_; + QString callParty_; + QString callPartyDisplayName_; + QString callPartyAvatarUrl_; + std::string callid_; + const uint32_t timeoutms_ = 120000; + webrtc::CallType callType_ = webrtc::CallType::VOICE; + bool haveCallInvite_ = false; + std::string inviteSDP_; + std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_; + std::vector<std::string> turnURIs_; + QTimer turnServerTimer_; + QMediaPlayer player_; + std::vector<std::pair<QString, uint32_t>> windows_; - template<typename T> - bool handleEvent(const mtx::events::collections::TimelineEvents &event); - void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &); - void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &); - void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &); - void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &); - void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo); - void generateCallID(); - QStringList devices(bool isVideo) const; - void clear(); - void endCall(); - void playRingtone(const QUrl &ringtone, bool repeat); - void stopRingtone(); + template<typename T> + bool handleEvent(const mtx::events::collections::TimelineEvents &event); + void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &); + void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &); + void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo); + void generateCallID(); + QStringList devices(bool isVideo) const; + void clear(); + void endCall(); + void playRingtone(const QUrl &ringtone, bool repeat); + void stopRingtone(); }; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 3887f8b8..673f39ee 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -50,660 +50,641 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) , notificationsManager(this) , callManager_(new CallManager(this)) { - setObjectName("chatPage"); - - instance_ = this; - - qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); - qRegisterMetaType<std::optional<RelatedInfo>>(); - qRegisterMetaType<mtx::presence::PresenceState>(); - qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>(); - qRegisterMetaType<SecretsToDecrypt>(); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - view_manager_ = new TimelineViewManager(callManager_, this); - - topLayout_->addWidget(view_manager_->getWidget()); - - connect(this, - &ChatPage::downloadedSecrets, - this, - &ChatPage::decryptDownloadedSecrets, - Qt::QueuedConnection); - - connect(this, &ChatPage::connectionLost, this, [this]() { - nhlog::net()->info("connectivity lost"); - isConnected_ = false; - http::client()->shutdown(); - }); - connect(this, &ChatPage::connectionRestored, this, [this]() { - nhlog::net()->info("trying to re-connect"); - isConnected_ = true; - - // Drop all pending connections. - http::client()->shutdown(); - trySync(); - }); - - connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); - connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { - if (http::client()->access_token().empty()) { - connectivityTimer_.stop(); - return; - } + setObjectName("chatPage"); - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - emit connectionLost(); - return; - } + instance_ = this; - if (!isConnected_) - emit connectionRestored(); - }); - }); + qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); + qRegisterMetaType<std::optional<RelatedInfo>>(); + qRegisterMetaType<mtx::presence::PresenceState>(); + qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>(); + qRegisterMetaType<SecretsToDecrypt>(); - connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); - connect( - view_manager_, - &TimelineViewManager::inviteUsers, - this, - [this](QString roomId, QStringList users) { - for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() { - const auto user = users.at(ii); - - http::client()->invite_user( - roomId.toStdString(), - user.toStdString(), - [this, user](const mtx::responses::RoomInvite &, - mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to invite user: %1").arg(user)); - return; - } - - emit showNotification(tr("Invited user: %1").arg(user)); - }); - }); - } - }); + view_manager_ = new TimelineViewManager(callManager_, this); + + topLayout_->addWidget(view_manager_->getWidget()); + + connect(this, + &ChatPage::downloadedSecrets, + this, + &ChatPage::decryptDownloadedSecrets, + Qt::QueuedConnection); + + connect(this, &ChatPage::connectionLost, this, [this]() { + nhlog::net()->info("connectivity lost"); + isConnected_ = false; + http::client()->shutdown(); + }); + connect(this, &ChatPage::connectionRestored, this, [this]() { + nhlog::net()->info("trying to re-connect"); + isConnected_ = true; - connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); - connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); - connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); - connect(this, - &ChatPage::highlightedNotifsRetrieved, - this, - [](const mtx::responses::Notifications ¬if) { - try { - cache::saveTimelineMentions(notif); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to save mentions: {}", e.what()); + // Drop all pending connections. + http::client()->shutdown(); + trySync(); + }); + + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); + connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { + if (http::client()->access_token().empty()) { + connectivityTimer_.stop(); + return; + } + + http::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + emit connectionLost(); + return; + } + + if (!isConnected_) + emit connectionRestored(); + }); + }); + + connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); + + connect( + view_manager_, + &TimelineViewManager::inviteUsers, + this, + [this](QString roomId, QStringList users) { + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() { + const auto user = users.at(ii); + + http::client()->invite_user( + roomId.toStdString(), + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to invite user: %1").arg(user)); + return; } - }); - - connect(¬ificationsManager, - &NotificationsManager::notificationClicked, - this, - [this](const QString &roomid, const QString &eventid) { - Q_UNUSED(eventid) - view_manager_->rooms()->setCurrentRoom(roomid); - activateWindow(); - }); - connect(¬ificationsManager, - &NotificationsManager::sendNotificationReply, - this, - [this](const QString &roomid, const QString &eventid, const QString &body) { - view_manager_->rooms()->setCurrentRoom(roomid); - view_manager_->queueReply(roomid, eventid, body); - activateWindow(); - }); - - connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { - // ensure the qml context is shutdown before we destroy all other singletons - // Otherwise Qml will try to access the room list or settings, after they have been - // destroyed - topLayout_->removeWidget(view_manager_->getWidget()); - delete view_manager_->getWidget(); - }); - - connect( - this, - &ChatPage::initializeViews, - view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }, - Qt::QueuedConnection); - connect(this, - &ChatPage::initializeEmptyViews, - view_manager_, - &TimelineViewManager::initializeRoomlist); - connect( - this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged); - connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { - view_manager_->sync(rooms); - - static unsigned int prevNotificationCount = 0; - unsigned int notificationCount = 0; - for (const auto &room : rooms.join) { - notificationCount += room.second.unread_notifications.notification_count; - } - // HACK: If we had less notifications last time we checked, send an alert if the - // user wanted one. Technically, this may cause an alert to be missed if new ones - // come in while you are reading old ones. Since the window is almost certainly open - // in this edge case, that's probably a non-issue. - // TODO: Replace this once we have proper pushrules support. This is a horrible hack - if (prevNotificationCount < notificationCount) { - if (userSettings_->hasAlertOnNotification()) - QApplication::alert(this); + emit showNotification(tr("Invited user: %1").arg(user)); + }); + }); + } + }); + + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); + connect(this, &ChatPage::changeToRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); + connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); + connect(this, + &ChatPage::highlightedNotifsRetrieved, + this, + [](const mtx::responses::Notifications ¬if) { + try { + cache::saveTimelineMentions(notif); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to save mentions: {}", e.what()); } - prevNotificationCount = notificationCount; - - // No need to check amounts for this section, as this function internally checks for - // duplicates. - if (notificationCount && userSettings_->hasNotifications()) - http::client()->notifications( - 5, - "", - "", - [this](const mtx::responses::Notifications &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve notifications: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit notificationsRetrieved(std::move(res)); - }); - }); - - connect( - this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection); - connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection); - connect( - this, - &ChatPage::tryDelayedSyncCb, - this, - [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); }, - Qt::QueuedConnection); - - connect(this, - &ChatPage::newSyncResponse, - this, - &ChatPage::handleSyncResponse, - Qt::QueuedConnection); + }); + + connect(¬ificationsManager, + &NotificationsManager::notificationClicked, + this, + [this](const QString &roomid, const QString &eventid) { + Q_UNUSED(eventid) + view_manager_->rooms()->setCurrentRoom(roomid); + activateWindow(); + }); + connect(¬ificationsManager, + &NotificationsManager::sendNotificationReply, + this, + [this](const QString &roomid, const QString &eventid, const QString &body) { + view_manager_->rooms()->setCurrentRoom(roomid); + view_manager_->queueReply(roomid, eventid, body); + activateWindow(); + }); + + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + // ensure the qml context is shutdown before we destroy all other singletons + // Otherwise Qml will try to access the room list or settings, after they have been + // destroyed + topLayout_->removeWidget(view_manager_->getWidget()); + delete view_manager_->getWidget(); + }); + + connect( + this, + &ChatPage::initializeViews, + view_manager_, + [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }, + Qt::QueuedConnection); + connect(this, + &ChatPage::initializeEmptyViews, + view_manager_, + &TimelineViewManager::initializeRoomlist); + connect( + this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged); + connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { + view_manager_->sync(rooms); + + static unsigned int prevNotificationCount = 0; + unsigned int notificationCount = 0; + for (const auto &room : rooms.join) { + notificationCount += room.second.unread_notifications.notification_count; + } - connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); + // HACK: If we had less notifications last time we checked, send an alert if the + // user wanted one. Technically, this may cause an alert to be missed if new ones + // come in while you are reading old ones. Since the window is almost certainly open + // in this edge case, that's probably a non-issue. + // TODO: Replace this once we have proper pushrules support. This is a horrible hack + if (prevNotificationCount < notificationCount) { + if (userSettings_->hasAlertOnNotification()) + QApplication::alert(this); + } + prevNotificationCount = notificationCount; + + // No need to check amounts for this section, as this function internally checks for + // duplicates. + if (notificationCount && userSettings_->hasNotifications()) + http::client()->notifications( + 5, + "", + "", + [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } - connectCallMessage<mtx::events::msg::CallInvite>(); - connectCallMessage<mtx::events::msg::CallCandidates>(); - connectCallMessage<mtx::events::msg::CallAnswer>(); - connectCallMessage<mtx::events::msg::CallHangUp>(); + emit notificationsRetrieved(std::move(res)); + }); + }); + + connect( + this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection); + connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync, Qt::QueuedConnection); + connect( + this, + &ChatPage::tryDelayedSyncCb, + this, + [this]() { QTimer::singleShot(RETRY_TIMEOUT, this, &ChatPage::trySync); }, + Qt::QueuedConnection); + + connect( + this, &ChatPage::newSyncResponse, this, &ChatPage::handleSyncResponse, Qt::QueuedConnection); + + connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); + + connectCallMessage<mtx::events::msg::CallInvite>(); + connectCallMessage<mtx::events::msg::CallCandidates>(); + connectCallMessage<mtx::events::msg::CallAnswer>(); + connectCallMessage<mtx::events::msg::CallHangUp>(); } void ChatPage::logout() { - resetUI(); - deleteConfigs(); + resetUI(); + deleteConfigs(); - emit closing(); - connectivityTimer_.stop(); + emit closing(); + connectivityTimer_.stop(); } void ChatPage::dropToLoginPage(const QString &msg) { - nhlog::ui()->info("dropping to the login page: {}", msg.toStdString()); + nhlog::ui()->info("dropping to the login page: {}", msg.toStdString()); - http::client()->shutdown(); - connectivityTimer_.stop(); + http::client()->shutdown(); + connectivityTimer_.stop(); - resetUI(); - deleteConfigs(); + resetUI(); + deleteConfigs(); - emit showLoginPage(msg); + emit showLoginPage(msg); } void ChatPage::resetUI() { - view_manager_->clearAll(); + view_manager_->clearAll(); - emit unreadMessages(0); + emit unreadMessages(0); } void ChatPage::deleteConfigs() { - auto settings = UserSettings::instance()->qsettings(); - - if (UserSettings::instance()->profile() != "") { - settings->beginGroup("profile"); - settings->beginGroup(UserSettings::instance()->profile()); - } - settings->beginGroup("auth"); - settings->remove(""); - settings->endGroup(); // auth - - http::client()->shutdown(); - cache::deleteData(); + auto settings = UserSettings::instance()->qsettings(); + + if (UserSettings::instance()->profile() != "") { + settings->beginGroup("profile"); + settings->beginGroup(UserSettings::instance()->profile()); + } + settings->beginGroup("auth"); + settings->remove(""); + settings->endGroup(); // auth + + http::client()->shutdown(); + cache::deleteData(); } void ChatPage::bootstrap(QString userid, QString homeserver, QString token) { - using namespace mtx::identifiers; + using namespace mtx::identifiers; - try { - http::client()->set_user(parse<User>(userid.toStdString())); - } catch (const std::invalid_argument &) { - nhlog::ui()->critical("bootstrapped with invalid user_id: {}", - userid.toStdString()); - } + try { + http::client()->set_user(parse<User>(userid.toStdString())); + } catch (const std::invalid_argument &) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", userid.toStdString()); + } - http::client()->set_server(homeserver.toStdString()); - http::client()->set_access_token(token.toStdString()); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); + http::client()->set_server(homeserver.toStdString()); + http::client()->set_access_token(token.toStdString()); + http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation()); - // The Olm client needs the user_id & device_id that will be included - // in the generated payloads & keys. - olm::client()->set_user_id(http::client()->user_id().to_string()); - olm::client()->set_device_id(http::client()->device_id()); + // The Olm client needs the user_id & device_id that will be included + // in the generated payloads & keys. + olm::client()->set_user_id(http::client()->user_id().to_string()); + olm::client()->set_device_id(http::client()->device_id()); - try { - cache::init(userid); - - connect(cache::client(), - &Cache::newReadReceipts, - view_manager_, - &TimelineViewManager::updateReadReceipts); - - connect(cache::client(), - &Cache::removeNotification, - ¬ificationsManager, - &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, + try { + cache::init(userid); + + connect(cache::client(), + &Cache::newReadReceipts, + view_manager_, + &TimelineViewManager::updateReadReceipts); + + connect(cache::client(), + &Cache::removeNotification, + ¬ificationsManager, + &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; - } + QCoreApplication::quit(); } - - } catch (const lmdb::error &e) { - nhlog::db()->critical("failure during boot: {}", e.what()); - cache::deleteData(); - nhlog::net()->info("falling back to initial sync"); - } - - 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())); + loadStateFromCache(); 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())); + } 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; + } } - getProfileInfo(); - getBackupVersion(); - tryInitialSync(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failure during boot: {}", e.what()); + cache::deleteData(); + nhlog::net()->info("falling back to initial sync"); + } + + 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 ChatPage::loadStateFromCache() { - nhlog::db()->info("restoring state from cache"); - - try { - olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret()); - - emit initializeEmptyViews(); - emit initializeMentions(cache::getTimelineMentions()); - - cache::calculateRoomReadStatus(); - - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); - emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again.")); - return; - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to restore cache: {}", e.what()); - emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); - return; - } catch (const json::exception &e) { - nhlog::db()->critical("failed to parse cache data: {}", e.what()); - emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); - return; - } catch (const std::exception &e) { - nhlog::db()->critical("failed to load cache data: {}", e.what()); - emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); - return; - } - - nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - - getProfileInfo(); - getBackupVersion(); - verifyOneTimeKeyCountAfterStartup(); - - emit contentLoaded(); - - // Start receiving events. - emit trySyncCb(); + nhlog::db()->info("restoring state from cache"); + + try { + olm::client()->load(cache::restoreOlmAccount(), cache::client()->pickleSecret()); + + emit initializeEmptyViews(); + emit initializeMentions(cache::getTimelineMentions()); + + cache::calculateRoomReadStatus(); + + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again.")); + return; + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to restore cache: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); + return; + } catch (const json::exception &e) { + nhlog::db()->critical("failed to parse cache data: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); + return; + } catch (const std::exception &e) { + nhlog::db()->critical("failed to load cache data: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); + return; + } + + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + + getProfileInfo(); + getBackupVersion(); + verifyOneTimeKeyCountAfterStartup(); + + emit contentLoaded(); + + // Start receiving events. + emit trySyncCb(); } void ChatPage::removeRoom(const QString &room_id) { - try { - cache::removeRoom(room_id); - cache::removeInvite(room_id.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failure while removing room: {}", e.what()); - // TODO: Notify the user. - } + try { + cache::removeRoom(room_id); + cache::removeInvite(room_id.toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failure while removing room: {}", e.what()); + // TODO: Notify the user. + } } void ChatPage::sendNotifications(const mtx::responses::Notifications &res) { - for (const auto &item : res.notifications) { - const auto event_id = mtx::accessors::event_id(item.event); - - try { - if (item.read) { - cache::removeReadNotification(event_id); - continue; - } - - if (!cache::isNotificationSent(event_id)) { - const auto room_id = QString::fromStdString(item.room_id); - - // We should only sent one notification per event. - cache::markSentNotification(event_id); - - // Don't send a notification when the current room is opened. - if (isRoomActive(room_id)) - continue; - - if (userSettings_->hasDesktopNotifications()) { - auto info = cache::singleRoomInfo(item.room_id); + for (const auto &item : res.notifications) { + const auto event_id = mtx::accessors::event_id(item.event); - AvatarProvider::resolve( - QString::fromStdString(info.avatar_url), - 96, - this, - [this, item](QPixmap image) { - notificationsManager.postNotification( - item, image.toImage()); - }); - } - } - } catch (const lmdb::error &e) { - nhlog::db()->warn("error while sending notification: {}", e.what()); + try { + if (item.read) { + cache::removeReadNotification(event_id); + continue; + } + + if (!cache::isNotificationSent(event_id)) { + const auto room_id = QString::fromStdString(item.room_id); + + // We should only sent one notification per event. + cache::markSentNotification(event_id); + + // Don't send a notification when the current room is opened. + if (isRoomActive(room_id)) + continue; + + if (userSettings_->hasDesktopNotifications()) { + auto info = cache::singleRoomInfo(item.room_id); + + AvatarProvider::resolve(QString::fromStdString(info.avatar_url), + 96, + this, + [this, item](QPixmap image) { + notificationsManager.postNotification( + item, image.toImage()); + }); } + } + } catch (const lmdb::error &e) { + nhlog::db()->warn("error while sending notification: {}", e.what()); } + } } void ChatPage::tryInitialSync() { - nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - // Upload one time keys for the device. - nhlog::crypto()->info("generating one time keys"); - olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); + // Upload one time keys for the device. + nhlog::crypto()->info("generating one time keys"); + olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast<int>(err->status_code); + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast<int>(err->status_code); - if (status_code == 404) { - nhlog::net()->warn( - "skipping key uploading. server doesn't provide /keys/upload"); - return startInitialSync(); - } + if (status_code == 404) { + nhlog::net()->warn("skipping key uploading. server doesn't provide /keys/upload"); + return startInitialSync(); + } - nhlog::crypto()->critical("failed to upload one time keys: {} {}", - err->matrix_error.error, - status_code); + nhlog::crypto()->critical( + "failed to upload one time keys: {} {}", err->matrix_error.error, status_code); - QString errorMsg(tr("Failed to setup encryption keys. Server response: " - "%1 %2. Please try again later.") - .arg(QString::fromStdString(err->matrix_error.error)) - .arg(status_code)); + QString errorMsg(tr("Failed to setup encryption keys. Server response: " + "%1 %2. Please try again later.") + .arg(QString::fromStdString(err->matrix_error.error)) + .arg(status_code)); - emit dropToLoginPageCb(errorMsg); - return; - } + emit dropToLoginPageCb(errorMsg); + return; + } - olm::mark_keys_as_published(); + olm::mark_keys_as_published(); - for (const auto &entry : res.one_time_key_counts) - nhlog::net()->info( - "uploaded {} {} one-time keys", entry.second, entry.first); + for (const auto &entry : res.one_time_key_counts) + nhlog::net()->info("uploaded {} {} one-time keys", entry.second, entry.first); - startInitialSync(); - }); + startInitialSync(); + }); } void ChatPage::startInitialSync() { - nhlog::net()->info("trying initial sync"); - - mtx::http::SyncOpts opts; - opts.timeout = 0; - opts.set_presence = currentPresence(); - - http::client()->sync( - opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { - // TODO: Initial Sync should include mentions as well... + nhlog::net()->info("trying initial sync"); + + mtx::http::SyncOpts opts; + opts.timeout = 0; + opts.set_presence = currentPresence(); + + http::client()->sync(opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + // TODO: Initial Sync should include mentions as well... + + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast<int>(err->status_code); + + nhlog::net()->error("initial sync error: {} {} {} {}", + err->parse_error, + status_code, + err->error_code, + err_code); + + // non http related errors + if (status_code <= 0 || status_code >= 600) { + startInitialSync(); + return; + } - if (err) { - const auto error = QString::fromStdString(err->matrix_error.error); - const auto msg = tr("Please try to login again: %1").arg(error); - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); - const int status_code = static_cast<int>(err->status_code); - - nhlog::net()->error("initial sync error: {} {} {} {}", - err->parse_error, - status_code, - err->error_code, - err_code); - - // non http related errors - if (status_code <= 0 || status_code >= 600) { - startInitialSync(); - return; - } - - switch (status_code) { - case 502: - case 504: - case 524: { - startInitialSync(); - return; - } - default: { - emit dropToLoginPageCb(msg); - return; - } - } - } + switch (status_code) { + case 502: + case 504: + case 524: { + startInitialSync(); + return; + } + default: { + emit dropToLoginPageCb(msg); + return; + } + } + } - nhlog::net()->info("initial sync completed"); + nhlog::net()->info("initial sync completed"); - try { - cache::client()->saveState(res); + try { + cache::client()->saveState(res); - olm::handle_to_device_messages(res.to_device.events); + olm::handle_to_device_messages(res.to_device.events); - emit initializeViews(std::move(res.rooms)); - emit initializeMentions(cache::getTimelineMentions()); + emit initializeViews(std::move(res.rooms)); + emit initializeMentions(cache::getTimelineMentions()); - cache::calculateRoomReadStatus(); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to save state after initial sync: {}", - e.what()); - startInitialSync(); - return; - } + cache::calculateRoomReadStatus(); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to save state after initial sync: {}", e.what()); + startInitialSync(); + return; + } - emit trySyncCb(); - emit contentLoaded(); - }); + emit trySyncCb(); + emit contentLoaded(); + }); } void ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token) { - try { - if (prev_batch_token != cache::nextBatchToken()) { - nhlog::net()->warn("Duplicate sync, dropping"); - return; - } - } catch (const lmdb::error &e) { - nhlog::db()->warn("Logged out in the mean time, dropping sync"); + try { + if (prev_batch_token != cache::nextBatchToken()) { + nhlog::net()->warn("Duplicate sync, dropping"); + return; } + } catch (const lmdb::error &e) { + nhlog::db()->warn("Logged out in the mean time, dropping sync"); + } - nhlog::net()->debug("sync completed: {}", res.next_batch); + nhlog::net()->debug("sync completed: {}", res.next_batch); - // Ensure that we have enough one-time keys available. - ensureOneTimeKeyCount(res.device_one_time_keys_count); + // Ensure that we have enough one-time keys available. + ensureOneTimeKeyCount(res.device_one_time_keys_count); - // TODO: fine grained error handling - try { - cache::client()->saveState(res); - olm::handle_to_device_messages(res.to_device.events); + // TODO: fine grained error handling + try { + cache::client()->saveState(res); + olm::handle_to_device_messages(res.to_device.events); - auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); + auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); - emit syncUI(res.rooms); + emit syncUI(res.rooms); - // if we process a lot of syncs (1 every 200ms), this means we clean the - // db every 100s - static int syncCounter = 0; - if (syncCounter++ >= 500) { - cache::deleteOldData(); - syncCounter = 0; - } - } catch (const lmdb::map_full_error &e) { - nhlog::db()->error("lmdb is full: {}", e.what()); - cache::deleteOldData(); - } catch (const lmdb::error &e) { - nhlog::db()->error("saving sync response: {}", e.what()); + // if we process a lot of syncs (1 every 200ms), this means we clean the + // db every 100s + static int syncCounter = 0; + if (syncCounter++ >= 500) { + cache::deleteOldData(); + syncCounter = 0; } - - emit trySyncCb(); + } catch (const lmdb::map_full_error &e) { + nhlog::db()->error("lmdb is full: {}", e.what()); + cache::deleteOldData(); + } catch (const lmdb::error &e) { + nhlog::db()->error("saving sync response: {}", e.what()); + } + + emit trySyncCb(); } void ChatPage::trySync() { - mtx::http::SyncOpts opts; - opts.set_presence = currentPresence(); - - if (!connectivityTimer_.isActive()) - connectivityTimer_.start(); - - try { - opts.since = cache::nextBatchToken(); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); - return; - } - - http::client()->sync( - opts, - [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) { - if (err) { - const auto error = QString::fromStdString(err->matrix_error.error); - const auto msg = tr("Please try to login again: %1").arg(error); - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); - const int status_code = static_cast<int>(err->status_code); - - if ((http::is_logged_in() && - (err->matrix_error.errcode == - mtx::errors::ErrorCode::M_UNKNOWN_TOKEN || - err->matrix_error.errcode == - mtx::errors::ErrorCode::M_MISSING_TOKEN)) || - !http::is_logged_in()) { - emit dropToLoginPageCb(msg); - return; - } - - nhlog::net()->error("sync error: {} {} {} {}", - err->parse_error, - status_code, - err->error_code, - err_code); - emit tryDelayedSyncCb(); - return; - } - - emit newSyncResponse(res, since); - }); + mtx::http::SyncOpts opts; + opts.set_presence = currentPresence(); + + if (!connectivityTimer_.isActive()) + connectivityTimer_.start(); + + try { + opts.since = cache::nextBatchToken(); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); + return; + } + + http::client()->sync( + opts, [this, since = opts.since](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast<int>(err->status_code); + + if ((http::is_logged_in() && + (err->matrix_error.errcode == mtx::errors::ErrorCode::M_UNKNOWN_TOKEN || + err->matrix_error.errcode == mtx::errors::ErrorCode::M_MISSING_TOKEN)) || + !http::is_logged_in()) { + emit dropToLoginPageCb(msg); + return; + } + + nhlog::net()->error("sync error: {} {} {} {}", + err->parse_error, + status_code, + err->error_code, + err_code); + emit tryDelayedSyncCb(); + return; + } + + emit newSyncResponse(res, since); + }); } void ChatPage::joinRoom(const QString &room) { - const auto room_id = room.toStdString(); - joinRoomVia(room_id, {}, false); + const auto room_id = room.toStdString(); + joinRoomVia(room_id, {}, false); } void @@ -711,692 +692,662 @@ ChatPage::joinRoomVia(const std::string &room_id, const std::vector<std::string> &via, bool promptForConfirmation) { - if (promptForConfirmation && - QMessageBox::Yes != - QMessageBox::question( - this, - tr("Confirm join"), - tr("Do you really want to join %1?").arg(QString::fromStdString(room_id)))) - return; - - http::client()->join_room( - room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to join room: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit tr("You joined the room"); - - // We remove any invites with the same room_id. - try { - cache::removeInvite(room_id); - } catch (const lmdb::error &e) { - emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); - } - - view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id)); - }); + if (promptForConfirmation && + QMessageBox::Yes != + QMessageBox::question( + this, + tr("Confirm join"), + tr("Do you really want to join %1?").arg(QString::fromStdString(room_id)))) + return; + + http::client()->join_room( + room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to join room: %1").arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit tr("You joined the room"); + + // We remove any invites with the same room_id. + try { + cache::removeInvite(room_id); + } catch (const lmdb::error &e) { + emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); + } + + view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id)); + }); } void ChatPage::createRoom(const mtx::requests::CreateRoom &req) { - http::client()->create_room( - req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { - if (err) { - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); - const auto error = err->matrix_error.error; - const int status_code = static_cast<int>(err->status_code); - - nhlog::net()->warn( - "failed to create room: {} {} ({})", error, err_code, status_code); - - emit showNotification( - tr("Room creation failed: %1").arg(QString::fromStdString(error))); - return; - } - - QString newRoomId = QString::fromStdString(res.room_id.to_string()); - emit showNotification(tr("Room %1 created.").arg(newRoomId)); - emit newRoom(newRoomId); - emit changeToRoom(newRoomId); - }); + http::client()->create_room( + req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { + if (err) { + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const auto error = err->matrix_error.error; + const int status_code = static_cast<int>(err->status_code); + + nhlog::net()->warn("failed to create room: {} {} ({})", error, err_code, status_code); + + emit showNotification( + tr("Room creation failed: %1").arg(QString::fromStdString(error))); + return; + } + + QString newRoomId = QString::fromStdString(res.room_id.to_string()); + emit showNotification(tr("Room %1 created.").arg(newRoomId)); + emit newRoom(newRoomId); + emit changeToRoom(newRoomId); + }); } void ChatPage::leaveRoom(const QString &room_id) { - http::client()->leave_room( - room_id.toStdString(), - [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to leave room: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit leftRoom(room_id); - }); + http::client()->leave_room( + room_id.toStdString(), + [this, room_id](const mtx::responses::Empty &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to leave room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit leftRoom(room_id); + }); } void ChatPage::changeRoom(const QString &room_id) { - view_manager_->rooms()->setCurrentRoom(room_id); + view_manager_->rooms()->setCurrentRoom(room_id); } void ChatPage::inviteUser(QString userid, QString reason) { - auto room = currentRoom(); - - if (QMessageBox::question(this, - tr("Confirm invite"), - tr("Do you really want to invite %1 (%2)?") - .arg(cache::displayName(room, userid)) - .arg(userid)) != QMessageBox::Yes) - return; - - http::client()->invite_user( - room.toStdString(), - userid.toStdString(), - [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to invite %1 to %2: %3") - .arg(userid) - .arg(room) - .arg(QString::fromStdString(err->matrix_error.error))); - } else - emit showNotification(tr("Invited user: %1").arg(userid)); - }, - reason.trimmed().toStdString()); + auto room = currentRoom(); + + if (QMessageBox::question(this, + tr("Confirm invite"), + tr("Do you really want to invite %1 (%2)?") + .arg(cache::displayName(room, userid)) + .arg(userid)) != QMessageBox::Yes) + return; + + http::client()->invite_user( + room.toStdString(), + userid.toStdString(), + [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to invite %1 to %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Invited user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); } void ChatPage::kickUser(QString userid, QString reason) { - auto room = currentRoom(); - - if (QMessageBox::question(this, - tr("Confirm kick"), - tr("Do you really want to kick %1 (%2)?") - .arg(cache::displayName(room, userid)) - .arg(userid)) != QMessageBox::Yes) - return; - - http::client()->kick_user( - room.toStdString(), - userid.toStdString(), - [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to kick %1 from %2: %3") - .arg(userid) - .arg(room) - .arg(QString::fromStdString(err->matrix_error.error))); - } else - emit showNotification(tr("Kicked user: %1").arg(userid)); - }, - reason.trimmed().toStdString()); + auto room = currentRoom(); + + if (QMessageBox::question(this, + tr("Confirm kick"), + tr("Do you really want to kick %1 (%2)?") + .arg(cache::displayName(room, userid)) + .arg(userid)) != QMessageBox::Yes) + return; + + http::client()->kick_user( + room.toStdString(), + userid.toStdString(), + [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to kick %1 from %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Kicked user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); } void ChatPage::banUser(QString userid, QString reason) { - auto room = currentRoom(); - - if (QMessageBox::question(this, - tr("Confirm ban"), - tr("Do you really want to ban %1 (%2)?") - .arg(cache::displayName(room, userid)) - .arg(userid)) != QMessageBox::Yes) - return; - - http::client()->ban_user( - room.toStdString(), - userid.toStdString(), - [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to ban %1 in %2: %3") - .arg(userid) - .arg(room) - .arg(QString::fromStdString(err->matrix_error.error))); - } else - emit showNotification(tr("Banned user: %1").arg(userid)); - }, - reason.trimmed().toStdString()); + auto room = currentRoom(); + + if (QMessageBox::question(this, + tr("Confirm ban"), + tr("Do you really want to ban %1 (%2)?") + .arg(cache::displayName(room, userid)) + .arg(userid)) != QMessageBox::Yes) + return; + + http::client()->ban_user( + room.toStdString(), + userid.toStdString(), + [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to ban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Banned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); } void ChatPage::unbanUser(QString userid, QString reason) { - auto room = currentRoom(); - - if (QMessageBox::question(this, - tr("Confirm unban"), - tr("Do you really want to unban %1 (%2)?") - .arg(cache::displayName(room, userid)) - .arg(userid)) != QMessageBox::Yes) - return; - - http::client()->unban_user( - room.toStdString(), - userid.toStdString(), - [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to unban %1 in %2: %3") - .arg(userid) - .arg(room) - .arg(QString::fromStdString(err->matrix_error.error))); - } else - emit showNotification(tr("Unbanned user: %1").arg(userid)); - }, - reason.trimmed().toStdString()); + auto room = currentRoom(); + + if (QMessageBox::question(this, + tr("Confirm unban"), + tr("Do you really want to unban %1 (%2)?") + .arg(cache::displayName(room, userid)) + .arg(userid)) != QMessageBox::Yes) + return; + + http::client()->unban_user( + room.toStdString(), + userid.toStdString(), + [this, userid, room](const mtx::responses::Empty &, mtx::http::RequestErr err) { + if (err) { + emit showNotification(tr("Failed to unban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Unbanned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); } void ChatPage::receivedSessionKey(const std::string &room_id, const std::string &session_id) { - view_manager_->receivedSessionKey(room_id, session_id); + view_manager_->receivedSessionKey(room_id, session_id); } QString ChatPage::status() const { - return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString())); + return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString())); } void ChatPage::setStatus(const QString &status) { - http::client()->put_presence_status( - currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to set presence status_msg: {}", - err->matrix_error.error); - } - }); + http::client()->put_presence_status( + currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to set presence status_msg: {}", err->matrix_error.error); + } + }); } mtx::presence::PresenceState ChatPage::currentPresence() const { - switch (userSettings_->presence()) { - case UserSettings::Presence::Online: - return mtx::presence::online; - case UserSettings::Presence::Unavailable: - return mtx::presence::unavailable; - case UserSettings::Presence::Offline: - return mtx::presence::offline; - default: - return mtx::presence::online; - } + switch (userSettings_->presence()) { + case UserSettings::Presence::Online: + return mtx::presence::online; + case UserSettings::Presence::Unavailable: + return mtx::presence::unavailable; + case UserSettings::Presence::Offline: + return mtx::presence::offline; + default: + return mtx::presence::online; + } } void ChatPage::verifyOneTimeKeyCountAfterStartup() { - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code), - static_cast<int>(err->error_code)); - - if (err->status_code < 400 || err->status_code >= 500) - return; - } - - std::map<std::string, uint16_t> key_counts; - auto count = 0; - if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519); - c == res.one_time_key_counts.end()) { - key_counts[mtx::crypto::SIGNED_CURVE25519] = 0; - } else { - key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second; - count = c->second; - } - - nhlog::crypto()->info( - "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519); - - ensureOneTimeKeyCount(key_counts); - }); + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code), + static_cast<int>(err->error_code)); + + if (err->status_code < 400 || err->status_code >= 500) + return; + } + + std::map<std::string, uint16_t> key_counts; + auto count = 0; + if (auto c = res.one_time_key_counts.find(mtx::crypto::SIGNED_CURVE25519); + c == res.one_time_key_counts.end()) { + key_counts[mtx::crypto::SIGNED_CURVE25519] = 0; + } else { + key_counts[mtx::crypto::SIGNED_CURVE25519] = c->second; + count = c->second; + } + + nhlog::crypto()->info( + "Fetched server key count {} {}", count, mtx::crypto::SIGNED_CURVE25519); + + ensureOneTimeKeyCount(key_counts); + }); } void ChatPage::ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts) { - if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) { - nhlog::crypto()->debug( - "Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519); - - if (count->second < MAX_ONETIME_KEYS) { - const int nkeys = MAX_ONETIME_KEYS - count->second; - - nhlog::crypto()->info( - "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); - olm::client()->generate_one_time_keys(nkeys); - - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn( - "failed to update one-time keys: {} {} {}", + if (auto count = counts.find(mtx::crypto::SIGNED_CURVE25519); count != counts.end()) { + nhlog::crypto()->debug( + "Updated server key count {} {}", count->second, mtx::crypto::SIGNED_CURVE25519); + + if (count->second < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - count->second; + + nhlog::crypto()->info("uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); + olm::client()->generate_one_time_keys(nkeys); + + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", err->matrix_error.error, static_cast<int>(err->status_code), static_cast<int>(err->error_code)); - if (err->status_code < 400 || err->status_code >= 500) - return; - } - - // mark as published anyway, otherwise we may end up in a loop. - olm::mark_keys_as_published(); - }); - } else if (count->second > 2 * MAX_ONETIME_KEYS) { - nhlog::crypto()->warn("too many one-time keys, deleting 1"); - mtx::requests::ClaimKeys req; - req.one_time_keys[http::client()->user_id().to_string()] - [http::client()->device_id()] = - std::string(mtx::crypto::SIGNED_CURVE25519); - http::client()->claim_keys( - req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) { - if (err) - nhlog::crypto()->warn( - "failed to clear 1 one-time key: {} {} {}", + if (err->status_code < 400 || err->status_code >= 500) + return; + } + + // mark as published anyway, otherwise we may end up in a loop. + olm::mark_keys_as_published(); + }); + } else if (count->second > 2 * MAX_ONETIME_KEYS) { + nhlog::crypto()->warn("too many one-time keys, deleting 1"); + mtx::requests::ClaimKeys req; + req.one_time_keys[http::client()->user_id().to_string()][http::client()->device_id()] = + std::string(mtx::crypto::SIGNED_CURVE25519); + http::client()->claim_keys( + req, [](const mtx::responses::ClaimKeys &, mtx::http::RequestErr err) { + if (err) + nhlog::crypto()->warn("failed to clear 1 one-time key: {} {} {}", err->matrix_error.error, static_cast<int>(err->status_code), static_cast<int>(err->error_code)); - else - nhlog::crypto()->info("cleared 1 one-time key"); - }); - } + else + nhlog::crypto()->info("cleared 1 one-time key"); + }); } + } } void ChatPage::getProfileInfo() { - const auto userid = utils::localUser().toStdString(); + const auto userid = utils::localUser().toStdString(); - http::client()->get_profile( - userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve own profile info"); - return; - } + http::client()->get_profile( + userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve own profile info"); + return; + } - emit setUserDisplayName(QString::fromStdString(res.display_name)); + emit setUserDisplayName(QString::fromStdString(res.display_name)); - emit setUserAvatar(QString::fromStdString(res.avatar_url)); - }); + emit setUserAvatar(QString::fromStdString(res.avatar_url)); + }); } void ChatPage::getBackupVersion() { - if (!UserSettings::instance()->useOnlineKeyBackup()) { - nhlog::crypto()->info("Online key backup disabled."); - return; - } - - http::client()->backup_version( - [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("Failed to retrieve backup version"); - if (err->status_code == 404) - cache::client()->deleteBackupVersion(); - return; + if (!UserSettings::instance()->useOnlineKeyBackup()) { + nhlog::crypto()->info("Online key backup disabled."); + return; + } + + http::client()->backup_version( + [this](const mtx::responses::backup::BackupVersion &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("Failed to retrieve backup version"); + if (err->status_code == 404) + cache::client()->deleteBackupVersion(); + return; + } + + // switch to UI thread for secrets stuff + QTimer::singleShot(0, this, [res] { + auto auth_data = nlohmann::json::parse(res.auth_data); + + if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") { + auto key = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1); + if (!key) { + nhlog::crypto()->info("No key for online key backup."); + cache::client()->deleteBackupVersion(); + return; } - // switch to UI thread for secrets stuff - QTimer::singleShot(0, this, [res] { - auto auth_data = nlohmann::json::parse(res.auth_data); - - if (res.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") { - auto key = - cache::secret(mtx::secret_storage::secrets::megolm_backup_v1); - if (!key) { - nhlog::crypto()->info("No key for online key backup."); - cache::client()->deleteBackupVersion(); - return; - } - - using namespace mtx::crypto; - auto pubkey = CURVE25519_public_key_from_private( - to_binary_buf(base642bin(*key))); - - if (auth_data["public_key"].get<std::string>() != pubkey) { - nhlog::crypto()->info( - "Our backup key {} does not match the one " + using namespace mtx::crypto; + auto pubkey = CURVE25519_public_key_from_private(to_binary_buf(base642bin(*key))); + + if (auth_data["public_key"].get<std::string>() != pubkey) { + nhlog::crypto()->info("Our backup key {} does not match the one " "used in the online backup {}", pubkey, auth_data["public_key"]); - cache::client()->deleteBackupVersion(); - return; - } - - nhlog::crypto()->info("Using online key backup."); - OnlineBackupVersion data{}; - data.algorithm = res.algorithm; - data.version = res.version; - cache::client()->saveBackupVersion(data); - } else { - nhlog::crypto()->info("Unsupported key backup algorithm: {}", - res.algorithm); - cache::client()->deleteBackupVersion(); - } - }); + cache::client()->deleteBackupVersion(); + return; + } + + nhlog::crypto()->info("Using online key backup."); + OnlineBackupVersion data{}; + data.algorithm = res.algorithm; + data.version = res.version; + cache::client()->saveBackupVersion(data); + } else { + nhlog::crypto()->info("Unsupported key backup algorithm: {}", res.algorithm); + cache::client()->deleteBackupVersion(); + } }); + }); } void ChatPage::initiateLogout() { - http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) { - if (err) { - // TODO: handle special errors - emit contentLoaded(); - nhlog::net()->warn("failed to logout: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } + http::client()->logout([this](const mtx::responses::Logout &, mtx::http::RequestErr err) { + if (err) { + // TODO: handle special errors + emit contentLoaded(); + nhlog::net()->warn("failed to logout: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } - emit loggedOut(); - }); + emit loggedOut(); + }); - emit showOverlayProgressBar(); + emit showOverlayProgressBar(); } template<typename T> void ChatPage::connectCallMessage() { - connect(callManager_, - qOverload<const QString &, const T &>(&CallManager::newMessage), - view_manager_, - qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage)); + connect(callManager_, + qOverload<const QString &, const T &>(&CallManager::newMessage), + view_manager_, + qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage)); } void ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, const SecretsToDecrypt &secrets) { - QString text = QInputDialog::getText( - ChatPage::instance(), - QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"), - keyDesc.name.empty() - ? QCoreApplication::translate( - "CrossSigningSecrets", - "Enter your recovery key or passphrase to decrypt your secrets:") - : QCoreApplication::translate( - "CrossSigningSecrets", - "Enter your recovery key or passphrase called %1 to decrypt your secrets:") - .arg(QString::fromStdString(keyDesc.name)), - QLineEdit::Password); - - if (text.isEmpty()) - return; - - auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc); - - if (!decryptionKey && keyDesc.passphrase) { - try { - decryptionKey = - mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", - e.what()); - } - } - - if (!decryptionKey) { - QMessageBox::information( - ChatPage::instance(), - QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"), - QCoreApplication::translate("CrossSigningSecrets", - "Failed to decrypt secrets with the " - "provided recovery key or passphrase")); - return; + QString text = QInputDialog::getText( + ChatPage::instance(), + QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"), + keyDesc.name.empty() + ? QCoreApplication::translate( + "CrossSigningSecrets", "Enter your recovery key or passphrase to decrypt your secrets:") + : QCoreApplication::translate( + "CrossSigningSecrets", + "Enter your recovery key or passphrase called %1 to decrypt your secrets:") + .arg(QString::fromStdString(keyDesc.name)), + QLineEdit::Password); + + if (text.isEmpty()) + return; + + auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc); + + if (!decryptionKey && keyDesc.passphrase) { + try { + decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to derive secret key from passphrase: {}", e.what()); } + } - for (const auto &[secretName, encryptedSecret] : secrets) { - auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName); - if (!decrypted.empty()) - cache::storeSecret(secretName, decrypted); - } + if (!decryptionKey) { + QMessageBox::information( + ChatPage::instance(), + QCoreApplication::translate("CrossSigningSecrets", "Decryption failed"), + QCoreApplication::translate("CrossSigningSecrets", + "Failed to decrypt secrets with the " + "provided recovery key or passphrase")); + return; + } + + for (const auto &[secretName, encryptedSecret] : secrets) { + auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName); + if (!decrypted.empty()) + cache::storeSecret(secretName, decrypted); + } } void ChatPage::startChat(QString userid) { - auto joined_rooms = cache::joinedRooms(); - auto room_infos = cache::getRoomInfo(joined_rooms); - - for (std::string room_id : joined_rooms) { - if (room_infos[QString::fromStdString(room_id)].member_count == 2) { - auto room_members = cache::roomMembers(room_id); - if (std::find(room_members.begin(), - room_members.end(), - (userid).toStdString()) != room_members.end()) { - view_manager_->rooms()->setCurrentRoom( - QString::fromStdString(room_id)); - return; - } - } - } - - if (QMessageBox::Yes != - QMessageBox::question( - this, - tr("Confirm invite"), - tr("Do you really want to start a private chat with %1?").arg(userid))) + auto joined_rooms = cache::joinedRooms(); + auto room_infos = cache::getRoomInfo(joined_rooms); + + for (std::string room_id : joined_rooms) { + if (room_infos[QString::fromStdString(room_id)].member_count == 2) { + auto room_members = cache::roomMembers(room_id); + if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != + room_members.end()) { + view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id)); return; - - mtx::requests::CreateRoom req; - req.preset = mtx::requests::Preset::PrivateChat; - req.visibility = mtx::common::RoomVisibility::Private; - if (utils::localUser() != userid) { - req.invite = {userid.toStdString()}; - req.is_direct = true; + } } - emit ChatPage::instance()->createRoom(req); + } + + if (QMessageBox::Yes != + QMessageBox::question( + this, + tr("Confirm invite"), + tr("Do you really want to start a private chat with %1?").arg(userid))) + return; + + mtx::requests::CreateRoom req; + req.preset = mtx::requests::Preset::PrivateChat; + req.visibility = mtx::common::RoomVisibility::Private; + if (utils::localUser() != userid) { + req.invite = {userid.toStdString()}; + req.is_direct = true; + } + emit ChatPage::instance()->createRoom(req); } static QString mxidFromSegments(QStringRef sigil, QStringRef mxid) { - if (mxid.isEmpty()) - return ""; - - auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8()); - - if (sigil == "u") { - return "@" + mxid_; - } else if (sigil == "roomid") { - return "!" + mxid_; - } else if (sigil == "r") { - return "#" + mxid_; - //} else if (sigil == "group") { - // return "+" + mxid_; - } else { - return ""; - } + if (mxid.isEmpty()) + return ""; + + auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8()); + + if (sigil == "u") { + return "@" + mxid_; + } else if (sigil == "roomid") { + return "!" + mxid_; + } else if (sigil == "r") { + return "#" + mxid_; + //} else if (sigil == "group") { + // return "+" + mxid_; + } else { + return ""; + } } bool ChatPage::handleMatrixUri(const QByteArray &uri) { - nhlog::ui()->info("Received uri! {}", uri.toStdString()); - QUrl uri_{QString::fromUtf8(uri)}; - - // Convert matrix.to URIs to proper format - if (uri_.scheme() == "https" && uri_.host() == "matrix.to") { - QString p = uri_.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray newUri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - newUri.append("?" + query.toUtf8()); - return handleMatrixUri(QUrl::fromEncoded(newUri)); - } else if (identifier.startsWith("#")) { - QByteArray newUri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - newUri.append( - "/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - newUri.append("?" + query.toUtf8()); - return handleMatrixUri(QUrl::fromEncoded(newUri)); - } else if (identifier.startsWith("!")) { - QByteArray newUri = "matrix:roomid/" + QUrl::toPercentEncoding( - identifier.remove(0, 1)); - if (!eventId.isEmpty()) - newUri.append( - "/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - newUri.append("?" + query.toUtf8()); - return handleMatrixUri(QUrl::fromEncoded(newUri)); - } - } + nhlog::ui()->info("Received uri! {}", uri.toStdString()); + QUrl uri_{QString::fromUtf8(uri)}; + + // Convert matrix.to URIs to proper format + if (uri_.scheme() == "https" && uri_.host() == "matrix.to") { + QString p = uri_.fragment(QUrl::FullyEncoded); + if (p.startsWith("/")) + p.remove(0, 1); + + auto temp = p.split("?"); + QString query; + if (temp.size() >= 2) + query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); + + temp = temp.first().split("/"); + auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); + QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); + if (!identifier.isEmpty()) { + if (identifier.startsWith("@")) { + QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!query.isEmpty()) + newUri.append("?" + query.toUtf8()); + return handleMatrixUri(QUrl::fromEncoded(newUri)); + } else if (identifier.startsWith("#")) { + QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + newUri.append("?" + query.toUtf8()); + return handleMatrixUri(QUrl::fromEncoded(newUri)); + } else if (identifier.startsWith("!")) { + QByteArray newUri = + "matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + newUri.append("?" + query.toUtf8()); + return handleMatrixUri(QUrl::fromEncoded(newUri)); + } } + } - // non-matrix URIs are not handled by us, return false - if (uri_.scheme() != "matrix") - return false; - - auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded); - if (tempPath.startsWith('/')) - tempPath.remove(0, 1); - auto segments = tempPath.splitRef('/'); - - if (segments.size() != 2 && segments.size() != 4) - return false; - - auto sigil1 = segments[0]; - auto mxid1 = mxidFromSegments(sigil1, segments[1]); - if (mxid1.isEmpty()) - return false; - - QString mxid2; - if (segments.size() == 4 && segments[2] == "e") { - if (segments[3].isEmpty()) - return false; - else - mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8()); - } + // non-matrix URIs are not handled by us, return false + if (uri_.scheme() != "matrix") + return false; - std::vector<std::string> vias; - QString action; + auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded); + if (tempPath.startsWith('/')) + tempPath.remove(0, 1); + auto segments = tempPath.splitRef('/'); - for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) { - nhlog::ui()->info("item: {}", item.toStdString()); + if (segments.size() != 2 && segments.size() != 4) + return false; - if (item.startsWith("action=")) { - action = item.remove("action="); - } else if (item.startsWith("via=")) { - vias.push_back( - QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString()); - } + auto sigil1 = segments[0]; + auto mxid1 = mxidFromSegments(sigil1, segments[1]); + if (mxid1.isEmpty()) + return false; + + QString mxid2; + if (segments.size() == 4 && segments[2] == "e") { + if (segments[3].isEmpty()) + return false; + else + mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8()); + } + + std::vector<std::string> vias; + QString action; + + for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) { + nhlog::ui()->info("item: {}", item.toStdString()); + + if (item.startsWith("action=")) { + action = item.remove("action="); + } else if (item.startsWith("via=")) { + vias.push_back(QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString()); } + } - if (sigil1 == "u") { - if (action.isEmpty()) { - auto t = view_manager_->rooms()->currentRoom(); - if (t && - cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) { - t->openUserProfile(mxid1); - return true; - } - emit view_manager_->openGlobalUserProfile(mxid1); - } else if (action == "chat") { - this->startChat(mxid1); - } + if (sigil1 == "u") { + if (action.isEmpty()) { + auto t = view_manager_->rooms()->currentRoom(); + if (t && cache::isRoomMember(mxid1.toStdString(), t->roomId().toStdString())) { + t->openUserProfile(mxid1); return true; - } else if (sigil1 == "roomid") { - auto joined_rooms = cache::joinedRooms(); - auto targetRoomId = mxid1.toStdString(); - - for (auto roomid : joined_rooms) { - if (roomid == targetRoomId) { - view_manager_->rooms()->setCurrentRoom(mxid1); - if (!mxid2.isEmpty()) - view_manager_->showEvent(mxid1, mxid2); - return true; - } - } + } + emit view_manager_->openGlobalUserProfile(mxid1); + } else if (action == "chat") { + this->startChat(mxid1); + } + return true; + } else if (sigil1 == "roomid") { + auto joined_rooms = cache::joinedRooms(); + auto targetRoomId = mxid1.toStdString(); - if (action == "join" || action.isEmpty()) { - joinRoomVia(targetRoomId, vias); - return true; - } - return false; - } else if (sigil1 == "r") { - auto joined_rooms = cache::joinedRooms(); - auto targetRoomAlias = mxid1.toStdString(); - - for (auto roomid : joined_rooms) { - auto aliases = cache::client()->getRoomAliases(roomid); - if (aliases) { - if (aliases->alias == targetRoomAlias) { - view_manager_->rooms()->setCurrentRoom( - QString::fromStdString(roomid)); - if (!mxid2.isEmpty()) - view_manager_->showEvent( - QString::fromStdString(roomid), mxid2); - return true; - } - } - } + for (auto roomid : joined_rooms) { + if (roomid == targetRoomId) { + view_manager_->rooms()->setCurrentRoom(mxid1); + if (!mxid2.isEmpty()) + view_manager_->showEvent(mxid1, mxid2); + return true; + } + } - if (action == "join" || action.isEmpty()) { - joinRoomVia(mxid1.toStdString(), vias); - return true; + if (action == "join" || action.isEmpty()) { + joinRoomVia(targetRoomId, vias); + return true; + } + return false; + } else if (sigil1 == "r") { + auto joined_rooms = cache::joinedRooms(); + auto targetRoomAlias = mxid1.toStdString(); + + for (auto roomid : joined_rooms) { + auto aliases = cache::client()->getRoomAliases(roomid); + if (aliases) { + if (aliases->alias == targetRoomAlias) { + view_manager_->rooms()->setCurrentRoom(QString::fromStdString(roomid)); + if (!mxid2.isEmpty()) + view_manager_->showEvent(QString::fromStdString(roomid), mxid2); + return true; } - return false; + } + } + + if (action == "join" || action.isEmpty()) { + joinRoomVia(mxid1.toStdString(), vias); + return true; } return false; + } + return false; } bool ChatPage::handleMatrixUri(const QUrl &uri) { - return handleMatrixUri( - uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); + return handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); } bool ChatPage::isRoomActive(const QString &room_id) { - return isActiveWindow() && currentRoom() == room_id; + return isActiveWindow() && currentRoom() == room_id; } QString ChatPage::currentRoom() const { - if (view_manager_->rooms()->currentRoom()) - return view_manager_->rooms()->currentRoom()->roomId(); - else - return ""; + if (view_manager_->rooms()->currentRoom()) + return view_manager_->rooms()->currentRoom()->roomId(); + else + return ""; } diff --git a/src/ChatPage.h b/src/ChatPage.h index 66e4c6ab..8f3dc53e 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -52,186 +52,181 @@ using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2E class ChatPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); + ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); - // Initialize all the components of the UI. - void bootstrap(QString userid, QString homeserver, QString token); + // Initialize all the components of the UI. + void bootstrap(QString userid, QString homeserver, QString token); - static ChatPage *instance() { return instance_; } + static ChatPage *instance() { return instance_; } - QSharedPointer<UserSettings> userSettings() { return userSettings_; } - CallManager *callManager() { return callManager_; } - TimelineViewManager *timelineManager() { return view_manager_; } - void deleteConfigs(); + QSharedPointer<UserSettings> userSettings() { return userSettings_; } + CallManager *callManager() { return callManager_; } + TimelineViewManager *timelineManager() { return view_manager_; } + void deleteConfigs(); - void initiateLogout(); + void initiateLogout(); - QString status() const; - void setStatus(const QString &status); + QString status() const; + void setStatus(const QString &status); - mtx::presence::PresenceState currentPresence() const; + mtx::presence::PresenceState currentPresence() const; - // TODO(Nico): Get rid of this! - QString currentRoom() const; + // TODO(Nico): Get rid of this! + QString currentRoom() const; public slots: - bool handleMatrixUri(const QByteArray &uri); - bool handleMatrixUri(const QUrl &uri); - - void startChat(QString userid); - void leaveRoom(const QString &room_id); - void createRoom(const mtx::requests::CreateRoom &req); - void joinRoom(const QString &room); - void joinRoomVia(const std::string &room_id, - const std::vector<std::string> &via, - bool promptForConfirmation = true); - - void inviteUser(QString userid, QString reason); - void kickUser(QString userid, QString reason); - void banUser(QString userid, QString reason); - void unbanUser(QString userid, QString reason); - - void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, - const SecretsToDecrypt &secrets); + bool handleMatrixUri(const QByteArray &uri); + bool handleMatrixUri(const QUrl &uri); + + void startChat(QString userid); + void leaveRoom(const QString &room_id); + void createRoom(const mtx::requests::CreateRoom &req); + void joinRoom(const QString &room); + void joinRoomVia(const std::string &room_id, + const std::vector<std::string> &via, + bool promptForConfirmation = true); + + void inviteUser(QString userid, QString reason); + void kickUser(QString userid, QString reason); + void banUser(QString userid, QString reason); + void unbanUser(QString userid, QString reason); + + void receivedSessionKey(const std::string &room_id, const std::string &session_id); + void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, + const SecretsToDecrypt &secrets); signals: - void connectionLost(); - void connectionRestored(); - - void notificationsRetrieved(const mtx::responses::Notifications &); - void highlightedNotifsRetrieved(const mtx::responses::Notifications &, - const QPoint widgetPos); - - void contentLoaded(); - void closing(); - void changeWindowTitle(const int); - void unreadMessages(int count); - void showNotification(const QString &msg); - void showLoginPage(const QString &msg); - void showUserSettingsPage(); - void showOverlayProgressBar(); - - void ownProfileOk(); - void setUserDisplayName(const QString &name); - void setUserAvatar(const QString &avatar); - void loggedOut(); - - void trySyncCb(); - void tryDelayedSyncCb(); - void tryInitialSyncCb(); - void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token); - void leftRoom(const QString &room_id); - void newRoom(const QString &room_id); - void changeToRoom(const QString &room_id); - - void initializeViews(const mtx::responses::Rooms &rooms); - void initializeEmptyViews(); - void initializeMentions(const QMap<QString, mtx::responses::Notifications> ¬ifs); - void syncUI(const mtx::responses::Rooms &rooms); - void dropToLoginPageCb(const QString &msg); - - void notifyMessage(const QString &roomid, - const QString &eventid, - const QString &roomname, - const QString &sender, - const QString &message, - const QImage &icon); - - void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); - void themeChanged(); - void decryptSidebarChanged(); - void chatFocusChanged(const bool focused); - - //! Signals for device verificaiton - void receivedDeviceVerificationAccept( - const mtx::events::msg::KeyVerificationAccept &message); - void receivedDeviceVerificationRequest( - const mtx::events::msg::KeyVerificationRequest &message, - std::string sender); - void receivedRoomDeviceVerificationRequest( - const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, - TimelineModel *model); - void receivedDeviceVerificationCancel( - const mtx::events::msg::KeyVerificationCancel &message); - void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message); - void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message); - void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message, - std::string sender); - void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message); - void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message); - - void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, - const SecretsToDecrypt &secrets); + void connectionLost(); + void connectionRestored(); + + void notificationsRetrieved(const mtx::responses::Notifications &); + void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos); + + void contentLoaded(); + void closing(); + void changeWindowTitle(const int); + void unreadMessages(int count); + void showNotification(const QString &msg); + void showLoginPage(const QString &msg); + void showUserSettingsPage(); + void showOverlayProgressBar(); + + void ownProfileOk(); + void setUserDisplayName(const QString &name); + void setUserAvatar(const QString &avatar); + void loggedOut(); + + void trySyncCb(); + void tryDelayedSyncCb(); + void tryInitialSyncCb(); + void newSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token); + void leftRoom(const QString &room_id); + void newRoom(const QString &room_id); + void changeToRoom(const QString &room_id); + + void initializeViews(const mtx::responses::Rooms &rooms); + void initializeEmptyViews(); + void initializeMentions(const QMap<QString, mtx::responses::Notifications> ¬ifs); + void syncUI(const mtx::responses::Rooms &rooms); + void dropToLoginPageCb(const QString &msg); + + void notifyMessage(const QString &roomid, + const QString &eventid, + const QString &roomname, + const QString &sender, + const QString &message, + const QImage &icon); + + void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); + void themeChanged(); + void decryptSidebarChanged(); + void chatFocusChanged(const bool focused); + + //! Signals for device verificaiton + void receivedDeviceVerificationAccept(const mtx::events::msg::KeyVerificationAccept &message); + void receivedDeviceVerificationRequest(const mtx::events::msg::KeyVerificationRequest &message, + std::string sender); + void receivedRoomDeviceVerificationRequest( + const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, + TimelineModel *model); + void receivedDeviceVerificationCancel(const mtx::events::msg::KeyVerificationCancel &message); + void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message); + void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message); + void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message, + std::string sender); + void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message); + void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message); + + void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, + const SecretsToDecrypt &secrets); private slots: - void logout(); - void removeRoom(const QString &room_id); - void changeRoom(const QString &room_id); - void dropToLoginPage(const QString &msg); + void logout(); + void removeRoom(const QString &room_id); + void changeRoom(const QString &room_id); + void dropToLoginPage(const QString &msg); - void handleSyncResponse(const mtx::responses::Sync &res, - const std::string &prev_batch_token); + void handleSyncResponse(const mtx::responses::Sync &res, const std::string &prev_batch_token); private: - static ChatPage *instance_; + static ChatPage *instance_; - void startInitialSync(); - void tryInitialSync(); - void trySync(); - void verifyOneTimeKeyCountAfterStartup(); - void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts); - void getProfileInfo(); - void getBackupVersion(); + void startInitialSync(); + void tryInitialSync(); + void trySync(); + void verifyOneTimeKeyCountAfterStartup(); + void ensureOneTimeKeyCount(const std::map<std::string, uint16_t> &counts); + void getProfileInfo(); + void getBackupVersion(); - //! Check if the given room is currently open. - bool isRoomActive(const QString &room_id); + //! Check if the given room is currently open. + bool isRoomActive(const QString &room_id); - using UserID = QString; - using Membership = mtx::events::StateEvent<mtx::events::state::Member>; - using Memberships = std::map<std::string, Membership>; + using UserID = QString; + using Membership = mtx::events::StateEvent<mtx::events::state::Member>; + using Memberships = std::map<std::string, Membership>; - void loadStateFromCache(); - void resetUI(); + void loadStateFromCache(); + void resetUI(); - template<class Collection> - Memberships getMemberships(const std::vector<Collection> &events) const; + template<class Collection> + Memberships getMemberships(const std::vector<Collection> &events) const; - //! Send desktop notification for the received messages. - void sendNotifications(const mtx::responses::Notifications &); + //! Send desktop notification for the received messages. + void sendNotifications(const mtx::responses::Notifications &); - template<typename T> - void connectCallMessage(); + template<typename T> + void connectCallMessage(); - QHBoxLayout *topLayout_; + QHBoxLayout *topLayout_; - TimelineViewManager *view_manager_; + TimelineViewManager *view_manager_; - QTimer connectivityTimer_; - std::atomic_bool isConnected_; + QTimer connectivityTimer_; + std::atomic_bool isConnected_; - // Global user settings. - QSharedPointer<UserSettings> userSettings_; + // Global user settings. + QSharedPointer<UserSettings> userSettings_; - NotificationsManager notificationsManager; - CallManager *callManager_; + NotificationsManager notificationsManager; + CallManager *callManager_; }; template<class Collection> std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> ChatPage::getMemberships(const std::vector<Collection> &collection) const { - std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships; + std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> memberships; - using Member = mtx::events::StateEvent<mtx::events::state::Member>; + using Member = mtx::events::StateEvent<mtx::events::state::Member>; - for (const auto &event : collection) { - if (auto member = std::get_if<Member>(event)) { - memberships.emplace(member->state_key, *member); - } + for (const auto &event : collection) { + if (auto member = std::get_if<Member>(event)) { + memberships.emplace(member->state_key, *member); } + } - return memberships; + return memberships; } diff --git a/src/Clipboard.cpp b/src/Clipboard.cpp index d4d5bab7..93d913be 100644 --- a/src/Clipboard.cpp +++ b/src/Clipboard.cpp @@ -10,18 +10,17 @@ Clipboard::Clipboard(QObject *parent) : QObject(parent) { - connect( - QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged); + connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &Clipboard::textChanged); } void Clipboard::setText(QString text) { - QGuiApplication::clipboard()->setText(text); + QGuiApplication::clipboard()->setText(text); } QString Clipboard::text() const { - return QGuiApplication::clipboard()->text(); + return QGuiApplication::clipboard()->text(); } diff --git a/src/Clipboard.h b/src/Clipboard.h index fa74da22..1a6584ca 100644 --- a/src/Clipboard.h +++ b/src/Clipboard.h @@ -9,14 +9,14 @@ class Clipboard : public QObject { - Q_OBJECT - Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) public: - Clipboard(QObject *parent = nullptr); + Clipboard(QObject *parent = nullptr); - QString text() const; - void setText(QString text_); + QString text() const; + void setText(QString text_); signals: - void textChanged(); + void textChanged(); }; diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp index 41fd5d8f..9c371c8c 100644 --- a/src/ColorImageProvider.cpp +++ b/src/ColorImageProvider.cpp @@ -9,23 +9,23 @@ QPixmap ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) { - auto args = id.split('?'); + auto args = id.split('?'); - QPixmap source(args[0]); + QPixmap source(args[0]); - if (size) - *size = QSize(source.width(), source.height()); + if (size) + *size = QSize(source.width(), source.height()); - if (args.size() < 2) - return source; + if (args.size() < 2) + return source; - QColor color(args[1]); + QColor color(args[1]); - QPixmap colorized = source; - QPainter painter(&colorized); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(colorized.rect(), color); - painter.end(); + QPixmap colorized = source; + QPainter painter(&colorized); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colorized.rect(), color); + painter.end(); - return colorized; + return colorized; } diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h index 9ae8c85e..f2997e0a 100644 --- a/src/ColorImageProvider.h +++ b/src/ColorImageProvider.h @@ -7,9 +7,9 @@ class ColorImageProvider : public QQuickImageProvider { public: - ColorImageProvider() - : QQuickImageProvider(QQuickImageProvider::Pixmap) - {} + ColorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} - QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; }; diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp index 341a34ec..9a52f810 100644 --- a/src/CombinedImagePackModel.cpp +++ b/src/CombinedImagePackModel.cpp @@ -13,65 +13,65 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, : QAbstractListModel(parent) , room_id(roomId) { - auto packs = cache::client()->getImagePacks(room_id, stickers); + auto packs = cache::client()->getImagePacks(room_id, stickers); - for (const auto &pack : packs) { - QString packname = - pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : ""; + for (const auto &pack : packs) { + QString packname = + pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : ""; - for (const auto &img : pack.pack.images) { - ImageDesc i{}; - i.shortcode = QString::fromStdString(img.first); - i.packname = packname; - i.image = img.second; - images.push_back(std::move(i)); - } + for (const auto &img : pack.pack.images) { + ImageDesc i{}; + i.shortcode = QString::fromStdString(img.first); + i.packname = packname; + i.image = img.second; + images.push_back(std::move(i)); } + } } int CombinedImagePackModel::rowCount(const QModelIndex &) const { - return (int)images.size(); + return (int)images.size(); } QHash<int, QByteArray> CombinedImagePackModel::roleNames() const { - return { - {CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::Url, "url"}, - {Roles::ShortCode, "shortcode"}, - {Roles::Body, "body"}, - {Roles::PackName, "packname"}, - {Roles::OriginalRow, "originalRow"}, - }; + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::Url, "url"}, + {Roles::ShortCode, "shortcode"}, + {Roles::Body, "body"}, + {Roles::PackName, "packname"}, + {Roles::OriginalRow, "originalRow"}, + }; } QVariant CombinedImagePackModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: - return QString::fromStdString(images[index.row()].image.url); - case Roles::Url: - return QString::fromStdString(images[index.row()].image.url); - case CompletionModel::SearchRole: - case Roles::ShortCode: - return images[index.row()].shortcode; - case CompletionModel::SearchRole2: - case Roles::Body: - return QString::fromStdString(images[index.row()].image.body); - case Roles::PackName: - return images[index.row()].packname; - case Roles::OriginalRow: - return index.row(); - default: - return {}; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + return QString::fromStdString(images[index.row()].image.url); + case Roles::Url: + return QString::fromStdString(images[index.row()].image.url); + case CompletionModel::SearchRole: + case Roles::ShortCode: + return images[index.row()].shortcode; + case CompletionModel::SearchRole2: + case Roles::Body: + return QString::fromStdString(images[index.row()].image.body); + case Roles::PackName: + return images[index.row()].packname; + case Roles::OriginalRow: + return index.row(); + default: + return {}; } - return {}; + } + return {}; } diff --git a/src/CombinedImagePackModel.h b/src/CombinedImagePackModel.h index f0f69799..ec49b325 100644 --- a/src/CombinedImagePackModel.h +++ b/src/CombinedImagePackModel.h @@ -10,39 +10,39 @@ class CombinedImagePackModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT public: - enum Roles - { - Url = Qt::UserRole, - ShortCode, - Body, - PackName, - OriginalRow, - }; - - CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - - mtx::events::msc2545::PackImage imageAt(int row) - { - if (row < 0 || static_cast<size_t>(row) >= images.size()) - return {}; - return images.at(static_cast<size_t>(row)).image; - } + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + PackName, + OriginalRow, + }; + + CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + mtx::events::msc2545::PackImage imageAt(int row) + { + if (row < 0 || static_cast<size_t>(row) >= images.size()) + return {}; + return images.at(static_cast<size_t>(row)).image; + } private: - std::string room_id; + std::string room_id; - struct ImageDesc - { - QString shortcode; - QString packname; + struct ImageDesc + { + QString shortcode; + QString packname; - mtx::events::msc2545::PackImage image; - }; + mtx::events::msc2545::PackImage image; + }; - std::vector<ImageDesc> images; + std::vector<ImageDesc> images; }; diff --git a/src/CompletionModelRoles.h b/src/CompletionModelRoles.h index 8505e761..9a735d60 100644 --- a/src/CompletionModelRoles.h +++ b/src/CompletionModelRoles.h @@ -12,8 +12,8 @@ namespace CompletionModel { // Start at Qt::UserRole * 2 to prevent clashes enum Roles { - CompletionRole = Qt::UserRole * 2, // The string to replace the active completion - SearchRole, // String completer uses for search - SearchRole2, // Secondary string completer uses for search + CompletionRole = Qt::UserRole * 2, // The string to replace the active completion + SearchRole, // String completer uses for search + SearchRole2, // Secondary string completer uses for search }; } diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp index e68944c7..454f54b7 100644 --- a/src/CompletionProxyModel.cpp +++ b/src/CompletionProxyModel.cpp @@ -18,154 +18,154 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, , maxMistakes_(max_mistakes) , max_completions_(max_completions) { - setSourceModel(model); - QChar splitPoints(' '); - - // insert all the full texts - for (int i = 0; i < sourceModel()->rowCount(); i++) { - if (static_cast<size_t>(i) < max_completions_) - mapping.push_back(i); - - auto string1 = sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) - .toString() - .toLower(); - if (!string1.isEmpty()) - trie_.insert(string1.toUcs4(), i); - - auto string2 = sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) - .toString() - .toLower(); - if (!string2.isEmpty()) - trie_.insert(string2.toUcs4(), i); + setSourceModel(model); + QChar splitPoints(' '); + + // insert all the full texts + for (int i = 0; i < sourceModel()->rowCount(); i++) { + if (static_cast<size_t>(i) < max_completions_) + mapping.push_back(i); + + auto string1 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) + .toString() + .toLower(); + if (!string1.isEmpty()) + trie_.insert(string1.toUcs4(), i); + + auto string2 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) + .toString() + .toLower(); + if (!string2.isEmpty()) + trie_.insert(string2.toUcs4(), i); + } + + // insert the partial matches + for (int i = 0; i < sourceModel()->rowCount(); i++) { + auto string1 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) + .toString() + .toLower(); + + for (const auto &e : string1.splitRef(splitPoints)) { + if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 + trie_.insert(e.toUcs4(), i); } - // insert the partial matches - for (int i = 0; i < sourceModel()->rowCount(); i++) { - auto string1 = sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) - .toString() - .toLower(); - - for (const auto &e : string1.splitRef(splitPoints)) { - if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 - trie_.insert(e.toUcs4(), i); - } - - auto string2 = sourceModel() - ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) - .toString() - .toLower(); - - if (!string2.isEmpty()) { - for (const auto &e : string2.splitRef(splitPoints)) { - if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 - trie_.insert(e.toUcs4(), i); - } - } - } + auto string2 = sourceModel() + ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) + .toString() + .toLower(); - connect( - this, - &CompletionProxyModel::newSearchString, - this, - [this](QString s) { - s.remove(":"); - s.remove("@"); - searchString_ = s.toLower(); - invalidate(); - }, - Qt::QueuedConnection); + if (!string2.isEmpty()) { + for (const auto &e : string2.splitRef(splitPoints)) { + if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 + trie_.insert(e.toUcs4(), i); + } + } + } + + connect( + this, + &CompletionProxyModel::newSearchString, + this, + [this](QString s) { + s.remove(":"); + s.remove("@"); + searchString_ = s.toLower(); + invalidate(); + }, + Qt::QueuedConnection); } void CompletionProxyModel::invalidate() { - auto key = searchString_.toUcs4(); - beginResetModel(); - if (!key.empty()) // return default model data, if no search string - mapping = trie_.search(key, max_completions_, maxMistakes_); - endResetModel(); + auto key = searchString_.toUcs4(); + beginResetModel(); + if (!key.empty()) // return default model data, if no search string + mapping = trie_.search(key, max_completions_, maxMistakes_); + endResetModel(); } QHash<int, QByteArray> CompletionProxyModel::roleNames() const { - return this->sourceModel()->roleNames(); + return this->sourceModel()->roleNames(); } int CompletionProxyModel::rowCount(const QModelIndex &) const { - if (searchString_.isEmpty()) - return std::min(static_cast<int>(std::min<size_t>(max_completions_, - std::numeric_limits<int>::max())), - sourceModel()->rowCount()); - else - return (int)mapping.size(); + if (searchString_.isEmpty()) + return std::min( + static_cast<int>(std::min<size_t>(max_completions_, std::numeric_limits<int>::max())), + sourceModel()->rowCount()); + else + return (int)mapping.size(); } QModelIndex CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { - // return default model data, if no search string - if (searchString_.isEmpty()) { - return index(sourceIndex.row(), 0); - } - - for (int i = 0; i < (int)mapping.size(); i++) { - if (mapping[i] == sourceIndex.row()) { - return index(i, 0); - } + // return default model data, if no search string + if (searchString_.isEmpty()) { + return index(sourceIndex.row(), 0); + } + + for (int i = 0; i < (int)mapping.size(); i++) { + if (mapping[i] == sourceIndex.row()) { + return index(i, 0); } - return QModelIndex(); + } + return QModelIndex(); } QModelIndex CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const { - auto row = proxyIndex.row(); + auto row = proxyIndex.row(); - // return default model data, if no search string - if (searchString_.isEmpty()) { - return index(row, 0); - } + // return default model data, if no search string + if (searchString_.isEmpty()) { + return index(row, 0); + } - if (row < 0 || row >= (int)mapping.size()) - return QModelIndex(); + if (row < 0 || row >= (int)mapping.size()) + return QModelIndex(); - return sourceModel()->index(mapping[row], 0); + return sourceModel()->index(mapping[row], 0); } QModelIndex CompletionProxyModel::index(int row, int column, const QModelIndex &) const { - return createIndex(row, column); + return createIndex(row, column); } QModelIndex CompletionProxyModel::parent(const QModelIndex &) const { - return QModelIndex{}; + return QModelIndex{}; } int CompletionProxyModel::columnCount(const QModelIndex &) const { - return sourceModel()->columnCount(); + return sourceModel()->columnCount(); } QVariant CompletionProxyModel::completionAt(int i) const { - if (i >= 0 && i < rowCount()) - return data(index(i, 0), CompletionModel::CompletionRole); - else - return {}; + if (i >= 0 && i < rowCount()) + return data(index(i, 0), CompletionModel::CompletionRole); + else + return {}; } void CompletionProxyModel::setSearchString(QString s) { - emit newSearchString(s); + emit newSearchString(s); } diff --git a/src/CompletionProxyModel.h b/src/CompletionProxyModel.h index d85d9343..c6331a2d 100644 --- a/src/CompletionProxyModel.h +++ b/src/CompletionProxyModel.h @@ -11,179 +11,176 @@ template<typename Key, typename Value> struct trie { - std::vector<Value> values; - std::map<Key, trie> next; - - void insert(const QVector<Key> &keys, const Value &v) - { - auto t = this; - for (const auto k : keys) { - t = &t->next[k]; - } - - t->values.push_back(v); + std::vector<Value> values; + std::map<Key, trie> next; + + void insert(const QVector<Key> &keys, const Value &v) + { + auto t = this; + for (const auto k : keys) { + t = &t->next[k]; } - std::vector<Value> valuesAndSubvalues(size_t limit = -1) const - { - std::vector<Value> ret; - if (limit < 200) - ret.reserve(limit); - - for (const auto &v : values) { - if (ret.size() >= limit) - return ret; - else - ret.push_back(v); - } + t->values.push_back(v); + } - for (const auto &[k, t] : next) { - (void)k; - if (ret.size() >= limit) - return ret; - else { - auto temp = t.valuesAndSubvalues(limit - ret.size()); - for (auto &&v : temp) { - if (ret.size() >= limit) - return ret; - - if (std::find(ret.begin(), ret.end(), v) == ret.end()) { - ret.push_back(std::move(v)); - } - } - } - } + std::vector<Value> valuesAndSubvalues(size_t limit = -1) const + { + std::vector<Value> ret; + if (limit < 200) + ret.reserve(limit); + for (const auto &v : values) { + if (ret.size() >= limit) return ret; + else + ret.push_back(v); } - std::vector<Value> search(const QVector<Key> &keys, //< TODO(Nico): replace this with a span - size_t result_count_limit, - size_t max_edit_distance_ = 2) const - { - std::vector<Value> ret; - if (!result_count_limit) + for (const auto &[k, t] : next) { + (void)k; + if (ret.size() >= limit) + return ret; + else { + auto temp = t.valuesAndSubvalues(limit - ret.size()); + for (auto &&v : temp) { + if (ret.size() >= limit) return ret; - if (keys.isEmpty()) - return valuesAndSubvalues(result_count_limit); + if (std::find(ret.begin(), ret.end(), v) == ret.end()) { + ret.push_back(std::move(v)); + } + } + } + } - auto append = [&ret, result_count_limit](std::vector<Value> &&in) { - for (auto &&v : in) { - if (ret.size() >= result_count_limit) - return; + return ret; + } - if (std::find(ret.begin(), ret.end(), v) == ret.end()) { - ret.push_back(std::move(v)); - } - } - }; - - auto limit = [&ret, result_count_limit] { - return std::min(result_count_limit, (result_count_limit - ret.size()) * 2); - }; - - // Try first exact matches, then with maximum errors - for (size_t max_edit_distance = 0; - max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit; - max_edit_distance += 1) { - if (max_edit_distance && ret.size() < result_count_limit) { - max_edit_distance -= 1; - - // swap chars case - if (keys.size() >= 2) { - auto t = this; - for (int i = 1; i >= 0; i--) { - if (auto e = t->next.find(keys[i]); - e != t->next.end()) { - t = &e->second; - } else { - t = nullptr; - break; - } - } - - if (t) { - append(t->search( - keys.mid(2), limit(), max_edit_distance)); - } - } - - // insert case - for (const auto &[k, t] : this->next) { - if (k == keys[0]) - continue; - if (ret.size() >= limit()) - break; - - // insert - append(t.search(keys, limit(), max_edit_distance)); - } - - // delete character case - append(this->search(keys.mid(1), limit(), max_edit_distance)); - - // substitute case - for (const auto &[k, t] : this->next) { - if (k == keys[0]) - continue; - if (ret.size() >= limit()) - break; - - // substitute - append(t.search(keys.mid(1), limit(), max_edit_distance)); - } - - max_edit_distance += 1; - } + std::vector<Value> search(const QVector<Key> &keys, //< TODO(Nico): replace this with a span + size_t result_count_limit, + size_t max_edit_distance_ = 2) const + { + std::vector<Value> ret; + if (!result_count_limit) + return ret; + + if (keys.isEmpty()) + return valuesAndSubvalues(result_count_limit); + + auto append = [&ret, result_count_limit](std::vector<Value> &&in) { + for (auto &&v : in) { + if (ret.size() >= result_count_limit) + return; - if (auto e = this->next.find(keys[0]); e != this->next.end()) { - append(e->second.search(keys.mid(1), limit(), max_edit_distance)); + if (std::find(ret.begin(), ret.end(), v) == ret.end()) { + ret.push_back(std::move(v)); + } + } + }; + + auto limit = [&ret, result_count_limit] { + return std::min(result_count_limit, (result_count_limit - ret.size()) * 2); + }; + + // Try first exact matches, then with maximum errors + for (size_t max_edit_distance = 0; + max_edit_distance <= max_edit_distance_ && ret.size() < result_count_limit; + max_edit_distance += 1) { + if (max_edit_distance && ret.size() < result_count_limit) { + max_edit_distance -= 1; + + // swap chars case + if (keys.size() >= 2) { + auto t = this; + for (int i = 1; i >= 0; i--) { + if (auto e = t->next.find(keys[i]); e != t->next.end()) { + t = &e->second; + } else { + t = nullptr; + break; } + } + + if (t) { + append(t->search(keys.mid(2), limit(), max_edit_distance)); + } } - return ret; + // insert case + for (const auto &[k, t] : this->next) { + if (k == keys[0]) + continue; + if (ret.size() >= limit()) + break; + + // insert + append(t.search(keys, limit(), max_edit_distance)); + } + + // delete character case + append(this->search(keys.mid(1), limit(), max_edit_distance)); + + // substitute case + for (const auto &[k, t] : this->next) { + if (k == keys[0]) + continue; + if (ret.size() >= limit()) + break; + + // substitute + append(t.search(keys.mid(1), limit(), max_edit_distance)); + } + + max_edit_distance += 1; + } + + if (auto e = this->next.find(keys[0]); e != this->next.end()) { + append(e->second.search(keys.mid(1), limit(), max_edit_distance)); + } } + + return ret; + } }; class CompletionProxyModel : public QAbstractProxyModel { - Q_OBJECT - Q_PROPERTY( - QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString) + Q_OBJECT + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY newSearchString) public: - CompletionProxyModel(QAbstractItemModel *model, - int max_mistakes = 2, - size_t max_completions = 7, - QObject *parent = nullptr); + CompletionProxyModel(QAbstractItemModel *model, + int max_mistakes = 2, + size_t max_completions = 7, + QObject *parent = nullptr); - void invalidate(); + void invalidate(); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &) const override; + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &) const override; - QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; - QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; - QModelIndex index(int row, - int column, - const QModelIndex &parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex &) const override; + QModelIndex index(int row, + int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &) const override; public slots: - QVariant completionAt(int i) const; + QVariant completionAt(int i) const; - void setSearchString(QString s); - QString searchString() const { return searchString_; } + void setSearchString(QString s); + QString searchString() const { return searchString_; } signals: - void newSearchString(QString); + void newSearchString(QString); private: - QString searchString_; - trie<uint, int> trie_; - std::vector<int> mapping; - int maxMistakes_; - size_t max_completions_; + QString searchString_; + trie<uint, int> trie_; + std::vector<int> mapping; + int maxMistakes_; + size_t max_completions_; }; diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp index 1760ea9a..d4e27022 100644 --- a/src/DeviceVerificationFlow.cpp +++ b/src/DeviceVerificationFlow.cpp @@ -38,685 +38,653 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, , deviceId(deviceId_) , model_(model) { - timeout = new QTimer(this); - timeout->setSingleShot(true); - this->sas = olm::client()->sas_init(); - this->isMacVerified = false; - - auto user_id = userID.toStdString(); - this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id); - cache::client()->query_keys( - user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {},{}", - mtx::errors::to_string(err->matrix_error.errcode), - static_cast<int>(err->status_code)); - return; - } - - if (!this->deviceId.isEmpty() && - (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) { - nhlog::net()->warn("no devices retrieved {}", user_id); - return; - } - - this->their_keys = res; - }); - - cache::client()->query_keys( - http::client()->user_id().to_string(), - [this](const UserKeyCache &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {},{}", - mtx::errors::to_string(err->matrix_error.errcode), - static_cast<int>(err->status_code)); - return; - } - - if (res.master_keys.keys.empty()) - return; - - if (auto status = - cache::verificationStatus(http::client()->user_id().to_string()); - status && status->user_verified == crypto::Trust::Verified) - this->our_trusted_master_key = res.master_keys.keys.begin()->second; - }); - - if (model) { - connect(this->model_, - &TimelineModel::updateFlowEventId, - this, - [this](std::string event_id_) { - this->relation.rel_type = mtx::common::RelationType::Reference; - this->relation.event_id = event_id_; - this->transaction_id = event_id_; - }); - } - - connect(timeout, &QTimer::timeout, this, [this]() { - nhlog::crypto()->info("verification: timeout"); - if (state_ != Success && state_ != Failed) - this->cancelVerification(DeviceVerificationFlow::Error::Timeout); - }); - - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationStart, - this, - &DeviceVerificationFlow::handleStartMessage); - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationAccept, - this, - [this](const mtx::events::msg::KeyVerificationAccept &msg) { - nhlog::crypto()->info("verification: received accept"); - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && - (msg.hash == "sha256") && - (msg.message_authentication_code == "hkdf-hmac-sha256")) { - this->commitment = msg.commitment; - if (std::find(msg.short_authentication_string.begin(), - msg.short_authentication_string.end(), - mtx::events::msg::SASMethods::Emoji) != - msg.short_authentication_string.end()) { - this->method = mtx::events::msg::SASMethods::Emoji; - } else { - this->method = mtx::events::msg::SASMethods::Decimal; - } - this->mac_method = msg.message_authentication_code; - this->sendVerificationKey(); - } else { - this->cancelVerification( - DeviceVerificationFlow::Error::UnknownMethod); - } - }); - - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationCancel, - this, - [this](const mtx::events::msg::KeyVerificationCancel &msg) { - nhlog::crypto()->info("verification: received cancel"); - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - error_ = User; - emit errorChanged(); - setState(Failed); - }); - - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationKey, - this, - [this](const mtx::events::msg::KeyVerificationKey &msg) { - nhlog::crypto()->info("verification: received key"); - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - - if (sender) { - if (state_ != WaitingForOtherToAccept) { - this->cancelVerification(OutOfOrder); - return; - } - } else { - if (state_ != WaitingForKeys) { - this->cancelVerification(OutOfOrder); - return; - } - } - - this->sas->set_their_key(msg.key); - std::string info; - if (this->sender == true) { - info = "MATRIX_KEY_VERIFICATION_SAS|" + - http::client()->user_id().to_string() + "|" + - http::client()->device_id() + "|" + this->sas->public_key() + - "|" + this->toClient.to_string() + "|" + - this->deviceId.toStdString() + "|" + msg.key + "|" + - this->transaction_id; - } else { - info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() + - "|" + this->deviceId.toStdString() + "|" + msg.key + "|" + - http::client()->user_id().to_string() + "|" + - http::client()->device_id() + "|" + this->sas->public_key() + - "|" + this->transaction_id; - } - - nhlog::ui()->info("Info is: '{}'", info); - - if (this->sender == false) { - this->sendVerificationKey(); - } else { - if (this->commitment != - mtx::crypto::bin2base64_unpadded( - mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) { - this->cancelVerification( - DeviceVerificationFlow::Error::MismatchedCommitment); - return; - } - } - - if (this->method == mtx::events::msg::SASMethods::Emoji) { - this->sasList = this->sas->generate_bytes_emoji(info); - setState(CompareEmoji); - } else if (this->method == mtx::events::msg::SASMethods::Decimal) { - this->sasList = this->sas->generate_bytes_decimal(info); - setState(CompareNumber); - } - }); - + timeout = new QTimer(this); + timeout->setSingleShot(true); + this->sas = olm::client()->sas_init(); + this->isMacVerified = false; + + auto user_id = userID.toStdString(); + this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id); + cache::client()->query_keys( + user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + return; + } + + if (!this->deviceId.isEmpty() && + (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) { + nhlog::net()->warn("no devices retrieved {}", user_id); + return; + } + + this->their_keys = res; + }); + + cache::client()->query_keys( + http::client()->user_id().to_string(), + [this](const UserKeyCache &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + return; + } + + if (res.master_keys.keys.empty()) + return; + + if (auto status = cache::verificationStatus(http::client()->user_id().to_string()); + status && status->user_verified == crypto::Trust::Verified) + this->our_trusted_master_key = res.master_keys.keys.begin()->second; + }); + + if (model) { connect( - ChatPage::instance(), - &ChatPage::receivedDeviceVerificationMac, - this, - [this](const mtx::events::msg::KeyVerificationMac &msg) { - nhlog::crypto()->info("verification: received mac"); - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - - std::map<std::string, std::string> key_list; - std::string key_string; + this->model_, &TimelineModel::updateFlowEventId, this, [this](std::string event_id_) { + this->relation.rel_type = mtx::common::RelationType::Reference; + this->relation.event_id = event_id_; + this->transaction_id = event_id_; + }); + } + + connect(timeout, &QTimer::timeout, this, [this]() { + nhlog::crypto()->info("verification: timeout"); + if (state_ != Success && state_ != Failed) + this->cancelVerification(DeviceVerificationFlow::Error::Timeout); + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationStart, + this, + &DeviceVerificationFlow::handleStartMessage); + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationAccept, + this, + [this](const mtx::events::msg::KeyVerificationAccept &msg) { + nhlog::crypto()->info("verification: received accept"); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && + (msg.hash == "sha256") && + (msg.message_authentication_code == "hkdf-hmac-sha256")) { + this->commitment = msg.commitment; + if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Emoji) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Emoji; + } else { + this->method = mtx::events::msg::SASMethods::Decimal; + } + this->mac_method = msg.message_authentication_code; + this->sendVerificationKey(); + } else { + this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + } + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationCancel, + this, + [this](const mtx::events::msg::KeyVerificationCancel &msg) { + nhlog::crypto()->info("verification: received cancel"); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + error_ = User; + emit errorChanged(); + setState(Failed); + }); + + connect( + ChatPage::instance(), + &ChatPage::receivedDeviceVerificationKey, + this, + [this](const mtx::events::msg::KeyVerificationKey &msg) { + nhlog::crypto()->info("verification: received key"); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + + if (sender) { + if (state_ != WaitingForOtherToAccept) { + this->cancelVerification(OutOfOrder); + return; + } + } else { + if (state_ != WaitingForKeys) { + this->cancelVerification(OutOfOrder); + return; + } + } + + this->sas->set_their_key(msg.key); + std::string info; + if (this->sender == true) { + info = "MATRIX_KEY_VERIFICATION_SAS|" + http::client()->user_id().to_string() + "|" + + http::client()->device_id() + "|" + this->sas->public_key() + "|" + + this->toClient.to_string() + "|" + this->deviceId.toStdString() + "|" + + msg.key + "|" + this->transaction_id; + } else { + info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() + "|" + + this->deviceId.toStdString() + "|" + msg.key + "|" + + http::client()->user_id().to_string() + "|" + http::client()->device_id() + + "|" + this->sas->public_key() + "|" + this->transaction_id; + } + + nhlog::ui()->info("Info is: '{}'", info); + + if (this->sender == false) { + this->sendVerificationKey(); + } else { + if (this->commitment != mtx::crypto::bin2base64_unpadded(mtx::crypto::sha256( + msg.key + this->canonical_json.dump()))) { + this->cancelVerification(DeviceVerificationFlow::Error::MismatchedCommitment); + return; + } + } + + if (this->method == mtx::events::msg::SASMethods::Emoji) { + this->sasList = this->sas->generate_bytes_emoji(info); + setState(CompareEmoji); + } else if (this->method == mtx::events::msg::SASMethods::Decimal) { + this->sasList = this->sas->generate_bytes_decimal(info); + setState(CompareNumber); + } + }); + + connect( + ChatPage::instance(), + &ChatPage::receivedDeviceVerificationMac, + this, + [this](const mtx::events::msg::KeyVerificationMac &msg) { + nhlog::crypto()->info("verification: received mac"); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + + std::map<std::string, std::string> key_list; + std::string key_string; + for (const auto &mac : msg.mac) { + for (const auto &[deviceid, key] : their_keys.device_keys) { + (void)deviceid; + if (key.keys.count(mac.first)) + key_list[mac.first] = key.keys.at(mac.first); + } + + if (their_keys.master_keys.keys.count(mac.first)) + key_list[mac.first] = their_keys.master_keys.keys[mac.first]; + if (their_keys.user_signing_keys.keys.count(mac.first)) + key_list[mac.first] = their_keys.user_signing_keys.keys[mac.first]; + if (their_keys.self_signing_keys.keys.count(mac.first)) + key_list[mac.first] = their_keys.self_signing_keys.keys[mac.first]; + } + auto macs = key_verification_mac(sas.get(), + toClient, + this->deviceId.toStdString(), + http::client()->user_id(), + http::client()->device_id(), + this->transaction_id, + key_list); + + for (const auto &[key, mac] : macs.mac) { + if (mac != msg.mac.at(key)) { + this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch); + return; + } + } + + if (msg.keys == macs.keys) { + mtx::requests::KeySignaturesUpload req; + if (utils::localUser().toStdString() == this->toClient.to_string()) { + // self verification, sign master key with device key, if we + // verified it for (const auto &mac : msg.mac) { - for (const auto &[deviceid, key] : their_keys.device_keys) { - (void)deviceid; - if (key.keys.count(mac.first)) - key_list[mac.first] = key.keys.at(mac.first); + if (their_keys.master_keys.keys.count(mac.first)) { + json j = their_keys.master_keys; + j.erase("signatures"); + j.erase("unsigned"); + mtx::crypto::CrossSigningKeys master_key = j; + master_key.signatures[utils::localUser().toStdString()] + ["ed25519:" + http::client()->device_id()] = + olm::client()->sign_message(j.dump()); + req.signatures[utils::localUser().toStdString()] + [master_key.keys.at(mac.first)] = master_key; + } else if (mac.first == "ed25519:" + this->deviceId.toStdString()) { + // Sign their device key with self signing key + + auto device_id = this->deviceId.toStdString(); + + if (their_keys.device_keys.count(device_id)) { + json j = their_keys.device_keys.at(device_id); + j.erase("signatures"); + j.erase("unsigned"); + + auto secret = cache::secret( + mtx::secret_storage::secrets::cross_signing_self_signing); + if (!secret) + continue; + auto ssk = mtx::crypto::PkSigning::from_seed(*secret); + + mtx::crypto::DeviceKeys dev = j; + dev.signatures[utils::localUser().toStdString()] + ["ed25519:" + ssk.public_key()] = ssk.sign(j.dump()); + + req.signatures[utils::localUser().toStdString()][device_id] = dev; } - - if (their_keys.master_keys.keys.count(mac.first)) - key_list[mac.first] = their_keys.master_keys.keys[mac.first]; - if (their_keys.user_signing_keys.keys.count(mac.first)) - key_list[mac.first] = - their_keys.user_signing_keys.keys[mac.first]; - if (their_keys.self_signing_keys.keys.count(mac.first)) - key_list[mac.first] = - their_keys.self_signing_keys.keys[mac.first]; + } } - auto macs = key_verification_mac(sas.get(), - toClient, - this->deviceId.toStdString(), - http::client()->user_id(), - http::client()->device_id(), - this->transaction_id, - key_list); - - for (const auto &[key, mac] : macs.mac) { - if (mac != msg.mac.at(key)) { - this->cancelVerification( - DeviceVerificationFlow::Error::KeyMismatch); - return; - } + } else { + // Sign their master key with user signing key + for (const auto &mac : msg.mac) { + if (their_keys.master_keys.keys.count(mac.first)) { + json j = their_keys.master_keys; + j.erase("signatures"); + j.erase("unsigned"); + + auto secret = + cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing); + if (!secret) + continue; + auto usk = mtx::crypto::PkSigning::from_seed(*secret); + + mtx::crypto::CrossSigningKeys master_key = j; + master_key.signatures[utils::localUser().toStdString()] + ["ed25519:" + usk.public_key()] = usk.sign(j.dump()); + + req.signatures[toClient.to_string()][master_key.keys.at(mac.first)] = + master_key; + } } + } + + if (!req.signatures.empty()) { + http::client()->keys_signatures_upload( + req, + [](const mtx::responses::KeySignaturesUpload &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to upload signatures: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + } - if (msg.keys == macs.keys) { - mtx::requests::KeySignaturesUpload req; - if (utils::localUser().toStdString() == this->toClient.to_string()) { - // self verification, sign master key with device key, if we - // verified it - for (const auto &mac : msg.mac) { - if (their_keys.master_keys.keys.count(mac.first)) { - json j = their_keys.master_keys; - j.erase("signatures"); - j.erase("unsigned"); - mtx::crypto::CrossSigningKeys master_key = j; - master_key - .signatures[utils::localUser().toStdString()] - ["ed25519:" + - http::client()->device_id()] = - olm::client()->sign_message(j.dump()); - req.signatures[utils::localUser().toStdString()] - [master_key.keys.at(mac.first)] = - master_key; - } else if (mac.first == - "ed25519:" + this->deviceId.toStdString()) { - // Sign their device key with self signing key - - auto device_id = this->deviceId.toStdString(); - - if (their_keys.device_keys.count(device_id)) { - json j = - their_keys.device_keys.at(device_id); - j.erase("signatures"); - j.erase("unsigned"); - - auto secret = cache::secret( - mtx::secret_storage::secrets:: - cross_signing_self_signing); - if (!secret) - continue; - auto ssk = - mtx::crypto::PkSigning::from_seed( - *secret); - - mtx::crypto::DeviceKeys dev = j; - dev.signatures - [utils::localUser().toStdString()] - ["ed25519:" + ssk.public_key()] = - ssk.sign(j.dump()); - - req.signatures[utils::localUser() - .toStdString()] - [device_id] = dev; - } - } - } - } else { - // Sign their master key with user signing key - for (const auto &mac : msg.mac) { - if (their_keys.master_keys.keys.count(mac.first)) { - json j = their_keys.master_keys; - j.erase("signatures"); - j.erase("unsigned"); - - auto secret = - cache::secret(mtx::secret_storage::secrets:: - cross_signing_user_signing); - if (!secret) - continue; - auto usk = - mtx::crypto::PkSigning::from_seed(*secret); - - mtx::crypto::CrossSigningKeys master_key = j; - master_key - .signatures[utils::localUser().toStdString()] - ["ed25519:" + usk.public_key()] = - usk.sign(j.dump()); - - req.signatures[toClient.to_string()] - [master_key.keys.at(mac.first)] = - master_key; - } - } - } - - if (!req.signatures.empty()) { - http::client()->keys_signatures_upload( - req, - [](const mtx::responses::KeySignaturesUpload &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "failed to upload signatures: {},{}", - mtx::errors::to_string( - err->matrix_error.errcode), - static_cast<int>(err->status_code)); - } - - for (const auto &[user_id, tmp] : res.errors) - for (const auto &[key_id, e] : tmp) - nhlog::net()->error( - "signature error for user {} and key " - "id {}: {}, {}", - user_id, - key_id, - mtx::errors::to_string(e.errcode), - e.error); - }); - } - - this->isMacVerified = true; - this->acceptDevice(); - } else { - this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch); - } - }); + for (const auto &[user_id, tmp] : res.errors) + for (const auto &[key_id, e] : tmp) + nhlog::net()->error("signature error for user {} and key " + "id {}: {}, {}", + user_id, + key_id, + mtx::errors::to_string(e.errcode), + e.error); + }); + } + + this->isMacVerified = true; + this->acceptDevice(); + } else { + this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch); + } + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationReady, + this, + [this](const mtx::events::msg::KeyVerificationReady &msg) { + nhlog::crypto()->info("verification: received ready"); + if (!sender) { + if (msg.from_device != http::client()->device_id()) { + error_ = User; + emit errorChanged(); + setState(Failed); + } - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationReady, - this, - [this](const mtx::events::msg::KeyVerificationReady &msg) { - nhlog::crypto()->info("verification: received ready"); - if (!sender) { - if (msg.from_device != http::client()->device_id()) { - error_ = User; - emit errorChanged(); - setState(Failed); - } - - return; - } + return; + } - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - else { - this->deviceId = QString::fromStdString(msg.from_device); - } - } - this->startVerificationRequest(); - }); - - connect(ChatPage::instance(), - &ChatPage::receivedDeviceVerificationDone, - this, - [this](const mtx::events::msg::KeyVerificationDone &msg) { - nhlog::crypto()->info("verification: receoved done"); - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - nhlog::ui()->info("Flow done on other side"); - }); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + else { + this->deviceId = QString::fromStdString(msg.from_device); + } + } + this->startVerificationRequest(); + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationDone, + this, + [this](const mtx::events::msg::KeyVerificationDone &msg) { + nhlog::crypto()->info("verification: receoved done"); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + nhlog::ui()->info("Flow done on other side"); + }); - timeout->start(TIMEOUT); + timeout->start(TIMEOUT); } QString DeviceVerificationFlow::state() { + switch (state_) { + case PromptStartVerification: + return "PromptStartVerification"; + case CompareEmoji: + return "CompareEmoji"; + case CompareNumber: + return "CompareNumber"; + case WaitingForKeys: + return "WaitingForKeys"; + case WaitingForOtherToAccept: + return "WaitingForOtherToAccept"; + case WaitingForMac: + return "WaitingForMac"; + case Success: + return "Success"; + case Failed: + return "Failed"; + default: + return ""; + } +} + +void +DeviceVerificationFlow::next() +{ + if (sender) { switch (state_) { case PromptStartVerification: - return "PromptStartVerification"; + sendVerificationRequest(); + break; case CompareEmoji: - return "CompareEmoji"; case CompareNumber: - return "CompareNumber"; + sendVerificationMac(); + break; case WaitingForKeys: - return "WaitingForKeys"; case WaitingForOtherToAccept: - return "WaitingForOtherToAccept"; case WaitingForMac: - return "WaitingForMac"; case Success: - return "Success"; case Failed: - return "Failed"; - default: - return ""; + nhlog::db()->error("verification: Invalid state transition!"); + break; } -} - -void -DeviceVerificationFlow::next() -{ - if (sender) { - switch (state_) { - case PromptStartVerification: - sendVerificationRequest(); - break; - case CompareEmoji: - case CompareNumber: - sendVerificationMac(); - break; - case WaitingForKeys: - case WaitingForOtherToAccept: - case WaitingForMac: - case Success: - case Failed: - nhlog::db()->error("verification: Invalid state transition!"); - break; - } - } else { - switch (state_) { - case PromptStartVerification: - if (canonical_json.is_null()) - sendVerificationReady(); - else // legacy path without request and ready - acceptVerificationRequest(); - break; - case CompareEmoji: - [[fallthrough]]; - case CompareNumber: - sendVerificationMac(); - break; - case WaitingForKeys: - case WaitingForOtherToAccept: - case WaitingForMac: - case Success: - case Failed: - nhlog::db()->error("verification: Invalid state transition!"); - break; - } + } else { + switch (state_) { + case PromptStartVerification: + if (canonical_json.is_null()) + sendVerificationReady(); + else // legacy path without request and ready + acceptVerificationRequest(); + break; + case CompareEmoji: + [[fallthrough]]; + case CompareNumber: + sendVerificationMac(); + break; + case WaitingForKeys: + case WaitingForOtherToAccept: + case WaitingForMac: + case Success: + case Failed: + nhlog::db()->error("verification: Invalid state transition!"); + break; } + } } QString DeviceVerificationFlow::getUserId() { - return QString::fromStdString(this->toClient.to_string()); + return QString::fromStdString(this->toClient.to_string()); } QString DeviceVerificationFlow::getDeviceId() { - return this->deviceId; + return this->deviceId; } bool DeviceVerificationFlow::getSender() { - return this->sender; + return this->sender; } std::vector<int> DeviceVerificationFlow::getSasList() { - return this->sasList; + return this->sasList; } bool DeviceVerificationFlow::isSelfVerification() const { - return this->toClient.to_string() == http::client()->user_id().to_string(); + return this->toClient.to_string() == http::client()->user_id().to_string(); } void DeviceVerificationFlow::setEventId(std::string event_id_) { - this->relation.rel_type = mtx::common::RelationType::Reference; - this->relation.event_id = event_id_; - this->transaction_id = event_id_; + this->relation.rel_type = mtx::common::RelationType::Reference; + this->relation.event_id = event_id_; + this->transaction_id = event_id_; } void DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string) { - if (msg.transaction_id.has_value()) { - if (msg.transaction_id.value() != this->transaction_id) - return; - } else if (msg.relations.references()) { - if (msg.relations.references() != this->relation.event_id) - return; - } - if ((std::find(msg.key_agreement_protocols.begin(), - msg.key_agreement_protocols.end(), - "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) && - (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) && - (std::find(msg.message_authentication_codes.begin(), - msg.message_authentication_codes.end(), - "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) { - if (std::find(msg.short_authentication_string.begin(), - msg.short_authentication_string.end(), - mtx::events::msg::SASMethods::Emoji) != - msg.short_authentication_string.end()) { - this->method = mtx::events::msg::SASMethods::Emoji; - } else if (std::find(msg.short_authentication_string.begin(), - msg.short_authentication_string.end(), - mtx::events::msg::SASMethods::Decimal) != - msg.short_authentication_string.end()) { - this->method = mtx::events::msg::SASMethods::Decimal; - } else { - this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); - return; - } - if (!sender) - this->canonical_json = nlohmann::json(msg); - else { - if (utils::localUser().toStdString() < this->toClient.to_string()) { - this->canonical_json = nlohmann::json(msg); - } - } - - if (state_ != PromptStartVerification) - this->acceptVerificationRequest(); + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) + return; + } + if ((std::find(msg.key_agreement_protocols.begin(), + msg.key_agreement_protocols.end(), + "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) && + (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) && + (std::find(msg.message_authentication_codes.begin(), + msg.message_authentication_codes.end(), + "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) { + if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Emoji) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Emoji; + } else if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Decimal) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Decimal; } else { - this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + return; + } + if (!sender) + this->canonical_json = nlohmann::json(msg); + else { + if (utils::localUser().toStdString() < this->toClient.to_string()) { + this->canonical_json = nlohmann::json(msg); + } } + + if (state_ != PromptStartVerification) + this->acceptVerificationRequest(); + } else { + this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + } } //! accepts a verification void DeviceVerificationFlow::acceptVerificationRequest() { - mtx::events::msg::KeyVerificationAccept req; - - req.method = mtx::events::msg::VerificationMethods::SASv1; - req.key_agreement_protocol = "curve25519-hkdf-sha256"; - req.hash = "sha256"; - req.message_authentication_code = "hkdf-hmac-sha256"; - if (this->method == mtx::events::msg::SASMethods::Emoji) - req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji}; - else if (this->method == mtx::events::msg::SASMethods::Decimal) - req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal}; - req.commitment = mtx::crypto::bin2base64_unpadded( - mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump())); - - send(req); - setState(WaitingForKeys); + mtx::events::msg::KeyVerificationAccept req; + + req.method = mtx::events::msg::VerificationMethods::SASv1; + req.key_agreement_protocol = "curve25519-hkdf-sha256"; + req.hash = "sha256"; + req.message_authentication_code = "hkdf-hmac-sha256"; + if (this->method == mtx::events::msg::SASMethods::Emoji) + req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji}; + else if (this->method == mtx::events::msg::SASMethods::Decimal) + req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal}; + req.commitment = mtx::crypto::bin2base64_unpadded( + mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump())); + + send(req); + setState(WaitingForKeys); } //! responds verification request void DeviceVerificationFlow::sendVerificationReady() { - mtx::events::msg::KeyVerificationReady req; + mtx::events::msg::KeyVerificationReady req; - req.from_device = http::client()->device_id(); - req.methods = {mtx::events::msg::VerificationMethods::SASv1}; + req.from_device = http::client()->device_id(); + req.methods = {mtx::events::msg::VerificationMethods::SASv1}; - send(req); - setState(WaitingForKeys); + send(req); + setState(WaitingForKeys); } //! accepts a verification void DeviceVerificationFlow::sendVerificationDone() { - mtx::events::msg::KeyVerificationDone req; + mtx::events::msg::KeyVerificationDone req; - send(req); + send(req); } //! starts the verification flow void DeviceVerificationFlow::startVerificationRequest() { - mtx::events::msg::KeyVerificationStart req; - - req.from_device = http::client()->device_id(); - req.method = mtx::events::msg::VerificationMethods::SASv1; - req.key_agreement_protocols = {"curve25519-hkdf-sha256"}; - req.hashes = {"sha256"}; - req.message_authentication_codes = {"hkdf-hmac-sha256"}; - req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal, - mtx::events::msg::SASMethods::Emoji}; - - if (this->type == DeviceVerificationFlow::Type::ToDevice) { - mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body; - req.transaction_id = this->transaction_id; - this->canonical_json = nlohmann::json(req); - } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - req.relations.relations.push_back(this->relation); - // Set synthesized to surpress the nheko relation extensions - req.relations.synthesized = true; - this->canonical_json = nlohmann::json(req); - } - send(req); - setState(WaitingForOtherToAccept); + mtx::events::msg::KeyVerificationStart req; + + req.from_device = http::client()->device_id(); + req.method = mtx::events::msg::VerificationMethods::SASv1; + req.key_agreement_protocols = {"curve25519-hkdf-sha256"}; + req.hashes = {"sha256"}; + req.message_authentication_codes = {"hkdf-hmac-sha256"}; + req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal, + mtx::events::msg::SASMethods::Emoji}; + + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body; + req.transaction_id = this->transaction_id; + this->canonical_json = nlohmann::json(req); + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + req.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + req.relations.synthesized = true; + this->canonical_json = nlohmann::json(req); + } + send(req); + setState(WaitingForOtherToAccept); } //! sends a verification request void DeviceVerificationFlow::sendVerificationRequest() { - mtx::events::msg::KeyVerificationRequest req; + mtx::events::msg::KeyVerificationRequest req; - req.from_device = http::client()->device_id(); - req.methods = {mtx::events::msg::VerificationMethods::SASv1}; + req.from_device = http::client()->device_id(); + req.methods = {mtx::events::msg::VerificationMethods::SASv1}; - if (this->type == DeviceVerificationFlow::Type::ToDevice) { - QDateTime currentTime = QDateTime::currentDateTimeUtc(); + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + QDateTime currentTime = QDateTime::currentDateTimeUtc(); - req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch(); + req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch(); - } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - req.to = this->toClient.to_string(); - req.msgtype = "m.key.verification.request"; - req.body = "User is requesting to verify keys with you. However, your client does " - "not support this method, so you will need to use the legacy method of " - "key verification."; - } + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + req.to = this->toClient.to_string(); + req.msgtype = "m.key.verification.request"; + req.body = "User is requesting to verify keys with you. However, your client does " + "not support this method, so you will need to use the legacy method of " + "key verification."; + } - send(req); - setState(WaitingForOtherToAccept); + send(req); + setState(WaitingForOtherToAccept); } //! cancels a verification flow void DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code) { - if (state_ == State::Success || state_ == State::Failed) - return; - - mtx::events::msg::KeyVerificationCancel req; - - if (error_code == DeviceVerificationFlow::Error::UnknownMethod) { - req.code = "m.unknown_method"; - req.reason = "unknown method received"; - } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) { - req.code = "m.mismatched_commitment"; - req.reason = "commitment didn't match"; - } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) { - req.code = "m.mismatched_sas"; - req.reason = "sas didn't match"; - } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) { - req.code = "m.key_match"; - req.reason = "keys did not match"; - } else if (error_code == DeviceVerificationFlow::Error::Timeout) { - req.code = "m.timeout"; - req.reason = "timed out"; - } else if (error_code == DeviceVerificationFlow::Error::User) { - req.code = "m.user"; - req.reason = "user cancelled the verification"; - } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) { - req.code = "m.unexpected_message"; - req.reason = "received messages out of order"; - } - - this->error_ = error_code; - emit errorChanged(); - this->setState(Failed); - - send(req); + if (state_ == State::Success || state_ == State::Failed) + return; + + mtx::events::msg::KeyVerificationCancel req; + + if (error_code == DeviceVerificationFlow::Error::UnknownMethod) { + req.code = "m.unknown_method"; + req.reason = "unknown method received"; + } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) { + req.code = "m.mismatched_commitment"; + req.reason = "commitment didn't match"; + } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) { + req.code = "m.mismatched_sas"; + req.reason = "sas didn't match"; + } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) { + req.code = "m.key_match"; + req.reason = "keys did not match"; + } else if (error_code == DeviceVerificationFlow::Error::Timeout) { + req.code = "m.timeout"; + req.reason = "timed out"; + } else if (error_code == DeviceVerificationFlow::Error::User) { + req.code = "m.user"; + req.reason = "user cancelled the verification"; + } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) { + req.code = "m.unexpected_message"; + req.reason = "received messages out of order"; + } + + this->error_ = error_code; + emit errorChanged(); + this->setState(Failed); + + send(req); } //! sends the verification key void DeviceVerificationFlow::sendVerificationKey() { - mtx::events::msg::KeyVerificationKey req; + mtx::events::msg::KeyVerificationKey req; - req.key = this->sas->public_key(); + req.key = this->sas->public_key(); - send(req); + send(req); } mtx::events::msg::KeyVerificationMac @@ -728,79 +696,78 @@ key_verification_mac(mtx::crypto::SAS *sas, const std::string &transactionId, std::map<std::string, std::string> keys) { - mtx::events::msg::KeyVerificationMac req; + mtx::events::msg::KeyVerificationMac req; - std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice + - receiver.to_string() + receiverDevice + transactionId; + std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice + + receiver.to_string() + receiverDevice + transactionId; - std::string key_list; - bool first = true; - for (const auto &[key_id, key] : keys) { - req.mac[key_id] = sas->calculate_mac(key, info + key_id); + std::string key_list; + bool first = true; + for (const auto &[key_id, key] : keys) { + req.mac[key_id] = sas->calculate_mac(key, info + key_id); - if (!first) - key_list += ","; - key_list += key_id; - first = false; - } + if (!first) + key_list += ","; + key_list += key_id; + first = false; + } - req.keys = sas->calculate_mac(key_list, info + "KEY_IDS"); + req.keys = sas->calculate_mac(key_list, info + "KEY_IDS"); - return req; + return req; } //! sends the mac of the keys void DeviceVerificationFlow::sendVerificationMac() { - std::map<std::string, std::string> key_list; - key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519; + std::map<std::string, std::string> key_list; + key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519; - // send our master key, if we trust it - if (!this->our_trusted_master_key.empty()) - key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key; + // send our master key, if we trust it + if (!this->our_trusted_master_key.empty()) + key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key; - mtx::events::msg::KeyVerificationMac req = - key_verification_mac(sas.get(), - http::client()->user_id(), - http::client()->device_id(), - this->toClient, - this->deviceId.toStdString(), - this->transaction_id, - key_list); + mtx::events::msg::KeyVerificationMac req = key_verification_mac(sas.get(), + http::client()->user_id(), + http::client()->device_id(), + this->toClient, + this->deviceId.toStdString(), + this->transaction_id, + key_list); - send(req); + send(req); - setState(WaitingForMac); - acceptDevice(); + setState(WaitingForMac); + acceptDevice(); } //! Completes the verification flow void DeviceVerificationFlow::acceptDevice() { - if (!isMacVerified) { - setState(WaitingForMac); - } else if (state_ == WaitingForMac) { - cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString()); - this->sendVerificationDone(); - setState(Success); - - // Request secrets. We should probably check somehow, if a device knowns about the - // secrets. - if (utils::localUser().toStdString() == this->toClient.to_string() && - (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) || - !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) { - olm::request_cross_signing_keys(); - } + if (!isMacVerified) { + setState(WaitingForMac); + } else if (state_ == WaitingForMac) { + cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString()); + this->sendVerificationDone(); + setState(Success); + + // Request secrets. We should probably check somehow, if a device knowns about the + // secrets. + if (utils::localUser().toStdString() == this->toClient.to_string() && + (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) || + !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) { + olm::request_cross_signing_keys(); } + } } void DeviceVerificationFlow::unverify() { - cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString()); + cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString()); - emit refreshProfile(); + emit refreshProfile(); } QSharedPointer<DeviceVerificationFlow> @@ -810,22 +777,22 @@ DeviceVerificationFlow::NewInRoomVerification(QObject *parent_, QString other_user_, QString event_id_) { - QSharedPointer<DeviceVerificationFlow> flow( - new DeviceVerificationFlow(parent_, - Type::RoomMsg, - timelineModel_, - other_user_, - QString::fromStdString(msg.from_device))); - - flow->setEventId(event_id_.toStdString()); - - if (std::find(msg.methods.begin(), - msg.methods.end(), - mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { - flow->cancelVerification(UnknownMethod); - } - - return flow; + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, + Type::RoomMsg, + timelineModel_, + other_user_, + QString::fromStdString(msg.from_device))); + + flow->setEventId(event_id_.toStdString()); + + if (std::find(msg.methods.begin(), + msg.methods.end(), + mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { + flow->cancelVerification(UnknownMethod); + } + + return flow; } QSharedPointer<DeviceVerificationFlow> DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, @@ -833,17 +800,17 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, QString other_user_, QString txn_id_) { - QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( - parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); - flow->transaction_id = txn_id_.toStdString(); - - if (std::find(msg.methods.begin(), - msg.methods.end(), - mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { - flow->cancelVerification(UnknownMethod); - } + QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( + parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); + flow->transaction_id = txn_id_.toStdString(); + + if (std::find(msg.methods.begin(), + msg.methods.end(), + mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { + flow->cancelVerification(UnknownMethod); + } - return flow; + return flow; } QSharedPointer<DeviceVerificationFlow> DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, @@ -851,32 +818,32 @@ DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, QString other_user_, QString txn_id_) { - QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( - parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); - flow->transaction_id = txn_id_.toStdString(); + QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( + parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); + flow->transaction_id = txn_id_.toStdString(); - flow->handleStartMessage(msg, ""); + flow->handleStartMessage(msg, ""); - return flow; + return flow; } QSharedPointer<DeviceVerificationFlow> DeviceVerificationFlow::InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid) { - QSharedPointer<DeviceVerificationFlow> flow( - new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, "")); - flow->sender = true; - return flow; + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, "")); + flow->sender = true; + return flow; } QSharedPointer<DeviceVerificationFlow> DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device) { - QSharedPointer<DeviceVerificationFlow> flow( - new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device)); + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device)); - flow->sender = true; - flow->transaction_id = http::client()->generate_txn_id(); + flow->sender = true; + flow->transaction_id = http::client()->generate_txn_id(); - return flow; + return flow; } diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h index 4685a450..f71fa337 100644 --- a/src/DeviceVerificationFlow.h +++ b/src/DeviceVerificationFlow.h @@ -60,192 +60,189 @@ using sas_ptr = std::unique_ptr<mtx::crypto::SAS>; // clang-format on class DeviceVerificationFlow : public QObject { - Q_OBJECT - Q_PROPERTY(QString state READ state NOTIFY stateChanged) - Q_PROPERTY(Error error READ error NOTIFY errorChanged) - Q_PROPERTY(QString userId READ getUserId CONSTANT) - Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT) - Q_PROPERTY(bool sender READ getSender CONSTANT) - Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT) - Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT) - Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT) + Q_OBJECT + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + Q_PROPERTY(QString userId READ getUserId CONSTANT) + Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT) + Q_PROPERTY(bool sender READ getSender CONSTANT) + Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT) + Q_PROPERTY(bool isDeviceVerification READ isDeviceVerification CONSTANT) + Q_PROPERTY(bool isSelfVerification READ isSelfVerification CONSTANT) public: - enum State - { - PromptStartVerification, - WaitingForOtherToAccept, - WaitingForKeys, - CompareEmoji, - CompareNumber, - WaitingForMac, - Success, - Failed, - }; - Q_ENUM(State) - - enum Type - { - ToDevice, - RoomMsg - }; - - enum Error - { - UnknownMethod, - MismatchedCommitment, - MismatchedSAS, - KeyMismatch, - Timeout, - User, - OutOfOrder, - }; - Q_ENUM(Error) - - static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification( - QObject *parent_, - TimelineModel *timelineModel_, - const mtx::events::msg::KeyVerificationRequest &msg, - QString other_user_, - QString event_id_); - static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( - QObject *parent_, - const mtx::events::msg::KeyVerificationRequest &msg, - QString other_user_, - QString txn_id_); - static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( - QObject *parent_, - const mtx::events::msg::KeyVerificationStart &msg, - QString other_user_, - QString txn_id_); - static QSharedPointer<DeviceVerificationFlow> - InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid); - static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent, - QString userid, - QString device); - - // getters - QString state(); - Error error() { return error_; } - QString getUserId(); - QString getDeviceId(); - bool getSender(); - std::vector<int> getSasList(); - QString transactionId() { return QString::fromStdString(this->transaction_id); } - // setters - void setDeviceId(QString deviceID); - void setEventId(std::string event_id); - bool isDeviceVerification() const - { - return this->type == DeviceVerificationFlow::Type::ToDevice; - } - bool isSelfVerification() const; - - void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id); + enum State + { + PromptStartVerification, + WaitingForOtherToAccept, + WaitingForKeys, + CompareEmoji, + CompareNumber, + WaitingForMac, + Success, + Failed, + }; + Q_ENUM(State) + + enum Type + { + ToDevice, + RoomMsg + }; + + enum Error + { + UnknownMethod, + MismatchedCommitment, + MismatchedSAS, + KeyMismatch, + Timeout, + User, + OutOfOrder, + }; + Q_ENUM(Error) + + static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification( + QObject *parent_, + TimelineModel *timelineModel_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString event_id_); + static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( + QObject *parent_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString txn_id_); + static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( + QObject *parent_, + const mtx::events::msg::KeyVerificationStart &msg, + QString other_user_, + QString txn_id_); + static QSharedPointer<DeviceVerificationFlow> + InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid); + static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent, + QString userid, + QString device); + + // getters + QString state(); + Error error() { return error_; } + QString getUserId(); + QString getDeviceId(); + bool getSender(); + std::vector<int> getSasList(); + QString transactionId() { return QString::fromStdString(this->transaction_id); } + // setters + void setDeviceId(QString deviceID); + void setEventId(std::string event_id); + bool isDeviceVerification() const + { + return this->type == DeviceVerificationFlow::Type::ToDevice; + } + bool isSelfVerification() const; + + void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id); public slots: - //! unverifies a device - void unverify(); - //! Continues the flow - void next(); - //! Cancel the flow - void cancel() { cancelVerification(User); } + //! unverifies a device + void unverify(); + //! Continues the flow + void next(); + //! Cancel the flow + void cancel() { cancelVerification(User); } signals: - void refreshProfile(); - void stateChanged(); - void errorChanged(); + void refreshProfile(); + void stateChanged(); + void errorChanged(); private: - DeviceVerificationFlow(QObject *, - DeviceVerificationFlow::Type flow_type, - TimelineModel *model, - QString userID, - QString deviceId_); - void setState(State state) - { - if (state != state_) { - state_ = state; - emit stateChanged(); - } + DeviceVerificationFlow(QObject *, + DeviceVerificationFlow::Type flow_type, + TimelineModel *model, + QString userID, + QString deviceId_); + void setState(State state) + { + if (state != state_) { + state_ = state; + emit stateChanged(); } - - void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string); - //! sends a verification request - void sendVerificationRequest(); - //! accepts a verification request - void sendVerificationReady(); - //! completes the verification flow(); - void sendVerificationDone(); - //! accepts a verification - void acceptVerificationRequest(); - //! starts the verification flow - void startVerificationRequest(); - //! cancels a verification flow - void cancelVerification(DeviceVerificationFlow::Error error_code); - //! sends the verification key - void sendVerificationKey(); - //! sends the mac of the keys - void sendVerificationMac(); - //! Completes the verification flow - void acceptDevice(); - - std::string transaction_id; - - bool sender; - Type type; - mtx::identifiers::User toClient; - QString deviceId; - - // public part of our master key, when trusted or empty - std::string our_trusted_master_key; - - mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji; - QTimer *timeout = nullptr; - sas_ptr sas; - std::string mac_method; - std::string commitment; - nlohmann::json canonical_json; - - std::vector<int> sasList; - UserKeyCache their_keys; - TimelineModel *model_; - mtx::common::Relation relation; - - State state_ = PromptStartVerification; - Error error_ = UnknownMethod; - - bool isMacVerified = false; - - template<typename T> - void send(T msg) - { - if (this->type == DeviceVerificationFlow::Type::ToDevice) { - mtx::requests::ToDeviceMessages<T> body; - msg.transaction_id = this->transaction_id; - body[this->toClient][deviceId.toStdString()] = msg; - - http::client()->send_to_device<T>( - this->transaction_id, body, [](mtx::http::RequestErr err) { - if (err) - nhlog::net()->warn( - "failed to send verification to_device message: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - }); - } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - if constexpr (!std::is_same_v<T, - mtx::events::msg::KeyVerificationRequest>) { - msg.relations.relations.push_back(this->relation); - // Set synthesized to surpress the nheko relation extensions - msg.relations.synthesized = true; - } - (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>); - } - - nhlog::net()->debug( - "Sent verification step: {} in state: {}", - mtx::events::to_string(mtx::events::to_device_content_to_type<T>), - state().toStdString()); + } + + void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string); + //! sends a verification request + void sendVerificationRequest(); + //! accepts a verification request + void sendVerificationReady(); + //! completes the verification flow(); + void sendVerificationDone(); + //! accepts a verification + void acceptVerificationRequest(); + //! starts the verification flow + void startVerificationRequest(); + //! cancels a verification flow + void cancelVerification(DeviceVerificationFlow::Error error_code); + //! sends the verification key + void sendVerificationKey(); + //! sends the mac of the keys + void sendVerificationMac(); + //! Completes the verification flow + void acceptDevice(); + + std::string transaction_id; + + bool sender; + Type type; + mtx::identifiers::User toClient; + QString deviceId; + + // public part of our master key, when trusted or empty + std::string our_trusted_master_key; + + mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji; + QTimer *timeout = nullptr; + sas_ptr sas; + std::string mac_method; + std::string commitment; + nlohmann::json canonical_json; + + std::vector<int> sasList; + UserKeyCache their_keys; + TimelineModel *model_; + mtx::common::Relation relation; + + State state_ = PromptStartVerification; + Error error_ = UnknownMethod; + + bool isMacVerified = false; + + template<typename T> + void send(T msg) + { + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + mtx::requests::ToDeviceMessages<T> body; + msg.transaction_id = this->transaction_id; + body[this->toClient][deviceId.toStdString()] = msg; + + http::client()->send_to_device<T>( + this->transaction_id, body, [](mtx::http::RequestErr err) { + if (err) + nhlog::net()->warn("failed to send verification to_device message: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + }); + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>) { + msg.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + msg.relations.synthesized = true; + } + (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>); } + + nhlog::net()->debug("Sent verification step: {} in state: {}", + mtx::events::to_string(mtx::events::to_device_content_to_type<T>), + state().toStdString()); + } }; diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 362bf4e9..d794a384 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -16,463 +16,460 @@ using is_detected = typename nheko::detail::detector<nheko::nonesuch, void, Op, struct IsStateEvent { - template<class T> - bool operator()(const mtx::events::StateEvent<T> &) - { - return true; - } - template<class T> - bool operator()(const mtx::events::Event<T> &) - { - return false; - } + template<class T> + bool operator()(const mtx::events::StateEvent<T> &) + { + return true; + } + template<class T> + bool operator()(const mtx::events::Event<T> &) + { + return false; + } }; struct EventMsgType { - template<class E> - using msgtype_t = decltype(E::msgtype); - template<class T> - mtx::events::MessageType operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<msgtype_t, T>::value) { - if constexpr (std::is_same_v<std::optional<std::string>, - std::remove_cv_t<decltype(e.content.msgtype)>>) - return mtx::events::getMessageType(e.content.msgtype.value()); - else if constexpr (std::is_same_v< - std::string, - std::remove_cv_t<decltype(e.content.msgtype)>>) - return mtx::events::getMessageType(e.content.msgtype); - } - return mtx::events::MessageType::Unknown; + template<class E> + using msgtype_t = decltype(E::msgtype); + template<class T> + mtx::events::MessageType operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<msgtype_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.msgtype)>>) + return mtx::events::getMessageType(e.content.msgtype.value()); + else if constexpr (std::is_same_v<std::string, + std::remove_cv_t<decltype(e.content.msgtype)>>) + return mtx::events::getMessageType(e.content.msgtype); } + return mtx::events::MessageType::Unknown; + } }; struct EventRoomName { - template<class T> - std::string operator()(const T &e) - { - if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>) - return e.content.name; - return ""; - } + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>) + return e.content.name; + return ""; + } }; struct EventRoomTopic { - template<class T> - std::string operator()(const T &e) - { - if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>) - return e.content.topic; - return ""; - } + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>) + return e.content.topic; + return ""; + } }; struct CallType { - template<class T> - std::string operator()(const T &e) - { - if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, - T>) { - const char video[] = "m=video"; - const std::string &sdp = e.content.sdp; - return std::search(sdp.cbegin(), - sdp.cend(), - std::cbegin(video), - std::cend(video) - 1, - [](unsigned char c1, unsigned char c2) { - return std::tolower(c1) == std::tolower(c2); - }) != sdp.cend() - ? "video" - : "voice"; - } - return std::string(); + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) { + const char video[] = "m=video"; + const std::string &sdp = e.content.sdp; + return std::search(sdp.cbegin(), + sdp.cend(), + std::cbegin(video), + std::cend(video) - 1, + [](unsigned char c1, unsigned char c2) { + return std::tolower(c1) == std::tolower(c2); + }) != sdp.cend() + ? "video" + : "voice"; } + return std::string(); + } }; struct EventBody { - template<class C> - using body_t = decltype(C::body); - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<body_t, T>::value) { - if constexpr (std::is_same_v<std::optional<std::string>, - std::remove_cv_t<decltype(e.content.body)>>) - return e.content.body ? e.content.body.value() : ""; - else if constexpr (std::is_same_v< - std::string, - std::remove_cv_t<decltype(e.content.body)>>) - return e.content.body; - } - return ""; + template<class C> + using body_t = decltype(C::body); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<body_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.body)>>) + return e.content.body ? e.content.body.value() : ""; + else if constexpr (std::is_same_v<std::string, + std::remove_cv_t<decltype(e.content.body)>>) + return e.content.body; } + return ""; + } }; struct EventFormattedBody { - template<class C> - using formatted_body_t = decltype(C::formatted_body); - template<class T> - std::string operator()(const mtx::events::RoomEvent<T> &e) - { - if constexpr (is_detected<formatted_body_t, T>::value) { - if (e.content.format == "org.matrix.custom.html") - return e.content.formatted_body; - } - return ""; + template<class C> + using formatted_body_t = decltype(C::formatted_body); + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<formatted_body_t, T>::value) { + if (e.content.format == "org.matrix.custom.html") + return e.content.formatted_body; } + return ""; + } }; struct EventFile { - template<class Content> - using file_t = decltype(Content::file); - template<class T> - std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<file_t, T>::value) - return e.content.file; - return std::nullopt; - } + template<class Content> + using file_t = decltype(Content::file); + template<class T> + std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<file_t, T>::value) + return e.content.file; + return std::nullopt; + } }; struct EventUrl { - template<class Content> - using url_t = decltype(Content::url); - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<url_t, T>::value) { - if (auto file = EventFile{}(e)) - return file->url; - return e.content.url; - } - return ""; + template<class Content> + using url_t = decltype(Content::url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<url_t, T>::value) { + if (auto file = EventFile{}(e)) + return file->url; + return e.content.url; } + return ""; + } }; struct EventThumbnailUrl { - template<class Content> - using thumbnail_url_t = decltype(Content::info.thumbnail_url); - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<thumbnail_url_t, T>::value) { - return e.content.info.thumbnail_url; - } - return ""; + template<class Content> + using thumbnail_url_t = decltype(Content::info.thumbnail_url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<thumbnail_url_t, T>::value) { + return e.content.info.thumbnail_url; } + return ""; + } }; struct EventBlurhash { - template<class Content> - using blurhash_t = decltype(Content::info.blurhash); - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<blurhash_t, T>::value) { - return e.content.info.blurhash; - } - return ""; + template<class Content> + using blurhash_t = decltype(Content::info.blurhash); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<blurhash_t, T>::value) { + return e.content.info.blurhash; } + return ""; + } }; struct EventFilename { - template<class T> - std::string operator()(const mtx::events::Event<T> &) - { - return ""; - } - std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e) - { - // body may be the original filename - return e.content.body; - } - std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e) - { - // body may be the original filename - return e.content.body; - } - std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e) - { - // body may be the original filename - return e.content.body; - } - std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e) - { - // body may be the original filename - if (!e.content.filename.empty()) - return e.content.filename; - return e.content.body; - } + template<class T> + std::string operator()(const mtx::events::Event<T> &) + { + return ""; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e) + { + // body may be the original filename + if (!e.content.filename.empty()) + return e.content.filename; + return e.content.body; + } }; struct EventMimeType { - template<class Content> - using mimetype_t = decltype(Content::info.mimetype); - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<mimetype_t, T>::value) { - return e.content.info.mimetype; - } - return ""; + template<class Content> + using mimetype_t = decltype(Content::info.mimetype); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<mimetype_t, T>::value) { + return e.content.info.mimetype; } + return ""; + } }; struct EventFilesize { - template<class Content> - using filesize_t = decltype(Content::info.size); - template<class T> - int64_t operator()(const mtx::events::RoomEvent<T> &e) - { - if constexpr (is_detected<filesize_t, T>::value) { - return e.content.info.size; - } - return 0; + template<class Content> + using filesize_t = decltype(Content::info.size); + template<class T> + int64_t operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<filesize_t, T>::value) { + return e.content.info.size; } + return 0; + } }; struct EventRelations { - template<class Content> - using related_ev_id_t = decltype(Content::relations); - template<class T> - mtx::common::Relations operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<related_ev_id_t, T>::value) { - return e.content.relations; - } - return {}; + template<class Content> + using related_ev_id_t = decltype(Content::relations); + template<class T> + mtx::common::Relations operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<related_ev_id_t, T>::value) { + return e.content.relations; } + return {}; + } }; struct SetEventRelations { - mtx::common::Relations new_relations; - template<class Content> - using related_ev_id_t = decltype(Content::relations); - template<class T> - void operator()(mtx::events::Event<T> &e) - { - if constexpr (is_detected<related_ev_id_t, T>::value) { - e.content.relations = std::move(new_relations); - } + mtx::common::Relations new_relations; + template<class Content> + using related_ev_id_t = decltype(Content::relations); + template<class T> + void operator()(mtx::events::Event<T> &e) + { + if constexpr (is_detected<related_ev_id_t, T>::value) { + e.content.relations = std::move(new_relations); } + } }; struct EventTransactionId { - template<class T> - std::string operator()(const mtx::events::RoomEvent<T> &e) - { - return e.unsigned_data.transaction_id; - } - template<class T> - std::string operator()(const mtx::events::Event<T> &e) - { - return e.unsigned_data.transaction_id; - } + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + return e.unsigned_data.transaction_id; + } + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + return e.unsigned_data.transaction_id; + } }; struct EventMediaHeight { - template<class Content> - using h_t = decltype(Content::info.h); - template<class T> - uint64_t operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<h_t, T>::value) { - return e.content.info.h; - } - return -1; + template<class Content> + using h_t = decltype(Content::info.h); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<h_t, T>::value) { + return e.content.info.h; } + return -1; + } }; struct EventMediaWidth { - template<class Content> - using w_t = decltype(Content::info.w); - template<class T> - uint64_t operator()(const mtx::events::Event<T> &e) - { - if constexpr (is_detected<w_t, T>::value) { - return e.content.info.w; - } - return -1; + template<class Content> + using w_t = decltype(Content::info.w); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<w_t, T>::value) { + return e.content.info.w; } + return -1; + } }; template<class T> double eventPropHeight(const mtx::events::RoomEvent<T> &e) { - auto w = eventWidth(e); - if (w == 0) - w = 1; + auto w = eventWidth(e); + if (w == 0) + w = 1; - double prop = eventHeight(e) / (double)w; + double prop = eventHeight(e) / (double)w; - return prop > 0 ? prop : 1.; + return prop > 0 ? prop : 1.; } } std::string mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto e) { return e.event_id; }, event); + return std::visit([](const auto e) { return e.event_id; }, event); } std::string mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto e) { return e.room_id; }, event); + return std::visit([](const auto e) { return e.room_id; }, event); } std::string mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto e) { return e.sender; }, event); + return std::visit([](const auto e) { return e.sender; }, event); } QDateTime mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event) { - return QDateTime::fromMSecsSinceEpoch( - std::visit([](const auto e) { return e.origin_server_ts; }, event)); + return QDateTime::fromMSecsSinceEpoch( + std::visit([](const auto e) { return e.origin_server_ts; }, event)); } std::string mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFilename{}, event); + return std::visit(EventFilename{}, event); } mtx::events::MessageType mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMsgType{}, event); + return std::visit(EventMsgType{}, event); } std::string mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventRoomName{}, event); + return std::visit(EventRoomName{}, event); } std::string mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventRoomTopic{}, event); + return std::visit(EventRoomTopic{}, event); } std::string mtx::accessors::call_type(const mtx::events::collections::TimelineEvents &event) { - return std::visit(CallType{}, event); + return std::visit(CallType{}, event); } std::string mtx::accessors::body(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventBody{}, event); + return std::visit(EventBody{}, event); } std::string mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFormattedBody{}, event); + return std::visit(EventFormattedBody{}, event); } QString mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event) { - auto formatted = formatted_body(event); - if (!formatted.empty()) - return QString::fromStdString(formatted); - else - return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>"); + auto formatted = formatted_body(event); + if (!formatted.empty()) + return QString::fromStdString(formatted); + else + return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>"); } std::optional<mtx::crypto::EncryptedFile> mtx::accessors::file(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFile{}, event); + return std::visit(EventFile{}, event); } std::string mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventUrl{}, event); + return std::visit(EventUrl{}, event); } std::string mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventThumbnailUrl{}, event); + return std::visit(EventThumbnailUrl{}, event); } std::string mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventBlurhash{}, event); + return std::visit(EventBlurhash{}, event); } std::string mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMimeType{}, event); + return std::visit(EventMimeType{}, event); } mtx::common::Relations mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventRelations{}, event); + return std::visit(EventRelations{}, event); } void mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations) { - std::visit(SetEventRelations{std::move(relations)}, event); + std::visit(SetEventRelations{std::move(relations)}, event); } std::string mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventTransactionId{}, event); + return std::visit(EventTransactionId{}, event); } int64_t mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventFilesize{}, event); + return std::visit(EventFilesize{}, event); } uint64_t mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMediaHeight{}, event); + return std::visit(EventMediaHeight{}, event); } uint64_t mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventMediaWidth{}, event); + return std::visit(EventMediaWidth{}, event); } nlohmann::json mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto &e) { return nlohmann::json(e); }, event); + return std::visit([](const auto &e) { return nlohmann::json(e); }, event); } bool mtx::accessors::is_state_event(const mtx::events::collections::TimelineEvents &event) { - return std::visit(IsStateEvent{}, event); + return std::visit(IsStateEvent{}, event); } diff --git a/src/EventAccessors.h b/src/EventAccessors.h index a58c7de0..c6b8e854 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -14,24 +14,24 @@ namespace nheko { struct nonesuch { - ~nonesuch() = delete; - nonesuch(nonesuch const &) = delete; - void operator=(nonesuch const &) = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const &) = delete; + void operator=(nonesuch const &) = delete; }; namespace detail { template<class Default, class AlwaysVoid, template<class...> class Op, class... Args> struct detector { - using value_t = std::false_type; - using type = Default; + using value_t = std::false_type; + using type = Default; }; template<class Default, template<class...> class Op, class... Args> struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> { - using value_t = std::true_type; - using type = Op<Args...>; + using value_t = std::true_type; + using type = Op<Args...>; }; } // namespace detail diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp index 6392de22..39e46f01 100644 --- a/src/ImagePackListModel.cpp +++ b/src/ImagePackListModel.cpp @@ -13,82 +13,81 @@ ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *paren : QAbstractListModel(parent) , room_id(roomId) { - auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); + auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); - for (const auto &pack : packs_) { - packs.push_back( - QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack))); - } + for (const auto &pack : packs_) { + packs.push_back(QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack))); + } } int ImagePackListModel::rowCount(const QModelIndex &) const { - return (int)packs.size(); + return (int)packs.size(); } QHash<int, QByteArray> ImagePackListModel::roleNames() const { - return { - {Roles::DisplayName, "displayName"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::FromAccountData, "fromAccountData"}, - {Roles::FromCurrentRoom, "fromCurrentRoom"}, - {Roles::StateKey, "statekey"}, - {Roles::RoomId, "roomid"}, - }; + return { + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::FromAccountData, "fromAccountData"}, + {Roles::FromCurrentRoom, "fromCurrentRoom"}, + {Roles::StateKey, "statekey"}, + {Roles::RoomId, "roomid"}, + }; } QVariant ImagePackListModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - const auto &pack = packs.at(index.row()); - switch (role) { - case Roles::DisplayName: - return pack->packname(); - case Roles::AvatarUrl: - return pack->avatarUrl(); - case Roles::FromAccountData: - return pack->roomid().isEmpty(); - case Roles::FromCurrentRoom: - return pack->roomid().toStdString() == this->room_id; - case Roles::StateKey: - return pack->statekey(); - case Roles::RoomId: - return pack->roomid(); - default: - return {}; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &pack = packs.at(index.row()); + switch (role) { + case Roles::DisplayName: + return pack->packname(); + case Roles::AvatarUrl: + return pack->avatarUrl(); + case Roles::FromAccountData: + return pack->roomid().isEmpty(); + case Roles::FromCurrentRoom: + return pack->roomid().toStdString() == this->room_id; + case Roles::StateKey: + return pack->statekey(); + case Roles::RoomId: + return pack->roomid(); + default: + return {}; } - return {}; + } + return {}; } SingleImagePackModel * ImagePackListModel::packAt(int row) { - if (row < 0 || static_cast<size_t>(row) >= packs.size()) - return {}; - auto e = packs.at(row).get(); - QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); - return e; + if (row < 0 || static_cast<size_t>(row) >= packs.size()) + return {}; + auto e = packs.at(row).get(); + QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); + return e; } SingleImagePackModel * ImagePackListModel::newPack(bool inRoom) { - ImagePackInfo info{}; - if (inRoom) - info.source_room = room_id; - return new SingleImagePackModel(info); + ImagePackInfo info{}; + if (inRoom) + info.source_room = room_id; + return new SingleImagePackModel(info); } bool ImagePackListModel::containsAccountPack() const { - for (const auto &p : packs) - if (p->roomid().isEmpty()) - return true; - return false; + for (const auto &p : packs) + if (p->roomid().isEmpty()) + return true; + return false; } diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h index 2aa5abb2..0b39729a 100644 --- a/src/ImagePackListModel.h +++ b/src/ImagePackListModel.h @@ -11,31 +11,31 @@ class SingleImagePackModel; class ImagePackListModel : public QAbstractListModel { - Q_OBJECT - Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) + Q_OBJECT + Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT) public: - enum Roles - { - DisplayName = Qt::UserRole, - AvatarUrl, - FromAccountData, - FromCurrentRoom, - StateKey, - RoomId, - }; - - ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - - Q_INVOKABLE SingleImagePackModel *packAt(int row); - Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); - - bool containsAccountPack() const; + enum Roles + { + DisplayName = Qt::UserRole, + AvatarUrl, + FromAccountData, + FromCurrentRoom, + StateKey, + RoomId, + }; + + ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + Q_INVOKABLE SingleImagePackModel *packAt(int row); + Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom); + + bool containsAccountPack() const; private: - std::string room_id; + std::string room_id; - std::vector<QSharedPointer<SingleImagePackModel>> packs; + std::vector<QSharedPointer<SingleImagePackModel>> packs; }; diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 27b2116f..e045581a 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -16,69 +16,68 @@ InviteesModel::InviteesModel(QObject *parent) void InviteesModel::addUser(QString mxid) { - beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); + beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); - auto invitee = new Invitee{mxid, this}; - auto indexOfInvitee = invitees_.count(); - connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { - emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); - }); + auto invitee = new Invitee{mxid, this}; + auto indexOfInvitee = invitees_.count(); + connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { + emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); + }); - invitees_.push_back(invitee); + invitees_.push_back(invitee); - endInsertRows(); - emit countChanged(); + endInsertRows(); + emit countChanged(); } QHash<int, QByteArray> InviteesModel::roleNames() const { - return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; } QVariant InviteesModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) - return {}; + if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) + return {}; - switch (role) { - case Mxid: - return invitees_[index.row()]->mxid_; - case DisplayName: - return invitees_[index.row()]->displayName_; - case AvatarUrl: - return invitees_[index.row()]->avatarUrl_; - default: - return {}; - } + switch (role) { + case Mxid: + return invitees_[index.row()]->mxid_; + case DisplayName: + return invitees_[index.row()]->displayName_; + case AvatarUrl: + return invitees_[index.row()]->avatarUrl_; + default: + return {}; + } } QStringList InviteesModel::mxids() { - QStringList mxidList; - for (int i = 0; i < invitees_.length(); ++i) - mxidList.push_back(invitees_[i]->mxid_); - return mxidList; + QStringList mxidList; + for (int i = 0; i < invitees_.length(); ++i) + mxidList.push_back(invitees_[i]->mxid_); + return mxidList; } Invitee::Invitee(const QString &mxid, QObject *parent) : QObject{parent} , mxid_{mxid} { - http::client()->get_profile( - mxid_.toStdString(), - [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve profile info"); - emit userInfoLoaded(); - return; - } + http::client()->get_profile( + mxid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve profile info"); + emit userInfoLoaded(); + return; + } - displayName_ = QString::fromStdString(res.display_name); - avatarUrl_ = QString::fromStdString(res.avatar_url); + displayName_ = QString::fromStdString(res.display_name); + avatarUrl_ = QString::fromStdString(res.avatar_url); - emit userInfoLoaded(); - }); + emit userInfoLoaded(); + }); } diff --git a/src/InviteesModel.h b/src/InviteesModel.h index a4e19ebb..fd64116b 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -10,54 +10,54 @@ class Invitee : public QObject { - Q_OBJECT + Q_OBJECT public: - Invitee(const QString &mxid, QObject *parent = nullptr); + Invitee(const QString &mxid, QObject *parent = nullptr); signals: - void userInfoLoaded(); + void userInfoLoaded(); private: - const QString mxid_; - QString displayName_; - QString avatarUrl_; + const QString mxid_; + QString displayName_; + QString avatarUrl_; - friend class InviteesModel; + friend class InviteesModel; }; class InviteesModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - }; + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; - InviteesModel(QObject *parent = nullptr); + InviteesModel(QObject *parent = nullptr); - Q_INVOKABLE void addUser(QString mxid); + Q_INVOKABLE void addUser(QString mxid); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex & = QModelIndex()) const override - { - return (int)invitees_.size(); - } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QStringList mxids(); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return (int)invitees_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QStringList mxids(); signals: - void accept(); - void countChanged(); + void accept(); + void countChanged(); private: - QVector<Invitee *> invitees_; + QVector<Invitee *> invitees_; }; #endif // INVITEESMODEL_H diff --git a/src/JdenticonProvider.cpp b/src/JdenticonProvider.cpp index 23b601fc..e2828286 100644 --- a/src/JdenticonProvider.cpp +++ b/src/JdenticonProvider.cpp @@ -22,20 +22,20 @@ static QPixmap clipRadius(QPixmap img, double radius) { - QPixmap out(img.size()); - out.fill(Qt::transparent); + QPixmap out(img.size()); + out.fill(Qt::transparent); - QPainter painter(&out); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - QPainterPath ppath; - ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); + QPainterPath ppath; + ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); - painter.setClipPath(ppath); - painter.drawPixmap(img.rect(), img); + painter.setClipPath(ppath); + painter.drawPixmap(img.rect(), img); - return out; + return out; } JdenticonResponse::JdenticonResponse(const QString &key, @@ -49,64 +49,64 @@ JdenticonResponse::JdenticonResponse(const QString &key, , m_pixmap{m_requestedSize} , jdenticonInterface_{Jdenticon::getJdenticonInterface()} { - setAutoDelete(false); + setAutoDelete(false); } void JdenticonResponse::run() { - m_pixmap.fill(Qt::transparent); - - QPainter painter; - painter.begin(&m_pixmap); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - - try { - QSvgRenderer renderer{ - jdenticonInterface_->generate(m_key, m_requestedSize.width()).toUtf8()}; - renderer.render(&painter); - } catch (std::exception &e) { - nhlog::ui()->error( - "caught {} in jdenticonprovider, key '{}'", e.what(), m_key.toStdString()); - } + m_pixmap.fill(Qt::transparent); + + QPainter painter; + painter.begin(&m_pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + try { + QSvgRenderer renderer{ + jdenticonInterface_->generate(m_key, m_requestedSize.width()).toUtf8()}; + renderer.render(&painter); + } catch (std::exception &e) { + nhlog::ui()->error( + "caught {} in jdenticonprovider, key '{}'", e.what(), m_key.toStdString()); + } - painter.end(); + painter.end(); - m_pixmap = clipRadius(m_pixmap, m_radius); + m_pixmap = clipRadius(m_pixmap, m_radius); - emit finished(); + emit finished(); } namespace Jdenticon { JdenticonInterface * getJdenticonInterface() { - static JdenticonInterface *interface = nullptr; - static bool interfaceExists{true}; - - if (interface == nullptr && interfaceExists) { - QDir pluginsDir(qApp->applicationDirPath()); - - bool plugins = pluginsDir.cd("plugins"); - if (plugins) { - for (const QString &fileName : pluginsDir.entryList(QDir::Files)) { - QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); - QObject *plugin = pluginLoader.instance(); - if (plugin) { - interface = qobject_cast<JdenticonInterface *>(plugin); - if (interface) { - nhlog::ui()->info("Loaded jdenticon plugin."); - break; - } - } - } - } else { - nhlog::ui()->info("jdenticon plugin not found."); - interfaceExists = false; + static JdenticonInterface *interface = nullptr; + static bool interfaceExists{true}; + + if (interface == nullptr && interfaceExists) { + QDir pluginsDir(qApp->applicationDirPath()); + + bool plugins = pluginsDir.cd("plugins"); + if (plugins) { + for (const QString &fileName : pluginsDir.entryList(QDir::Files)) { + QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = pluginLoader.instance(); + if (plugin) { + interface = qobject_cast<JdenticonInterface *>(plugin); + if (interface) { + nhlog::ui()->info("Loaded jdenticon plugin."); + break; + } } + } + } else { + nhlog::ui()->info("jdenticon plugin not found."); + interfaceExists = false; } + } - return interface; + return interface; } } diff --git a/src/JdenticonProvider.h b/src/JdenticonProvider.h index bcda29c8..f4ef6d10 100644 --- a/src/JdenticonProvider.h +++ b/src/JdenticonProvider.h @@ -23,59 +23,58 @@ class JdenticonResponse , public QRunnable { public: - JdenticonResponse(const QString &key, bool crop, double radius, const QSize &requestedSize); + JdenticonResponse(const QString &key, bool crop, double radius, const QSize &requestedSize); - QQuickTextureFactory *textureFactory() const override - { - return QQuickTextureFactory::textureFactoryForImage(m_pixmap.toImage()); - } + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_pixmap.toImage()); + } - void run() override; + void run() override; - QString m_key; - bool m_crop; - double m_radius; - QSize m_requestedSize; - QPixmap m_pixmap; - JdenticonInterface *jdenticonInterface_ = nullptr; + QString m_key; + bool m_crop; + double m_radius; + QSize m_requestedSize; + QPixmap m_pixmap; + JdenticonInterface *jdenticonInterface_ = nullptr; }; class JdenticonProvider : public QObject , public QQuickAsyncImageProvider { - Q_OBJECT + Q_OBJECT public: - static bool isAvailable() { return Jdenticon::getJdenticonInterface() != nullptr; } + static bool isAvailable() { return Jdenticon::getJdenticonInterface() != nullptr; } public slots: - QQuickImageResponse *requestImageResponse(const QString &id, - const QSize &requestedSize) override - { - auto id_ = id; - bool crop = true; - double radius = 0; - - auto queryStart = id.lastIndexOf('?'); - if (queryStart != -1) { - id_ = id.left(queryStart); - auto query = id.midRef(queryStart + 1); - auto queryBits = query.split('&'); - - for (auto b : queryBits) { - if (b.startsWith("radius=")) { - radius = b.mid(7).toDouble(); - } - } + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + auto id_ = id; + bool crop = true; + double radius = 0; + + auto queryStart = id.lastIndexOf('?'); + if (queryStart != -1) { + id_ = id.left(queryStart); + auto query = id.midRef(queryStart + 1); + auto queryBits = query.split('&'); + + for (auto b : queryBits) { + if (b.startsWith("radius=")) { + radius = b.mid(7).toDouble(); } - - JdenticonResponse *response = - new JdenticonResponse(id_, crop, radius, requestedSize); - pool.start(response); - return response; + } } + JdenticonResponse *response = new JdenticonResponse(id_, crop, radius, requestedSize); + pool.start(response); + return response; + } + private: - QThreadPool pool; + QThreadPool pool; }; diff --git a/src/Logging.cpp b/src/Logging.cpp index 67bcaf7a..a18a1cee 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -25,35 +25,35 @@ constexpr auto MAX_LOG_FILES = 3; void qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - std::string localMsg = msg.toStdString(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; - - if ( - // The default style has the point size set. If you use pixel size anywhere, you get - // that warning, which is useless, since sometimes you need the pixel size to match the - // text to the size of the outer element for example. This is done in the avatar and - // without that you get one warning for every Avatar displayed, which is stupid! - msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size."))) - return; - - switch (type) { - case QtDebugMsg: - nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtInfoMsg: - nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtWarningMsg: - nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtCriticalMsg: - nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - case QtFatalMsg: - nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); - break; - } + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + if ( + // The default style has the point size set. If you use pixel size anywhere, you get + // that warning, which is useless, since sometimes you need the pixel size to match the + // text to the size of the outer element for example. This is done in the avatar and + // without that you get one warning for every Avatar displayed, which is stupid! + msg.endsWith(QStringLiteral("Both point size and pixel size set. Using pixel size."))) + return; + + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } } } @@ -63,60 +63,59 @@ bool enable_debug_log_from_commandline = false; void init(const std::string &file_path) { - auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>( - file_path, MAX_FILE_SIZE, MAX_LOG_FILES); - - auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); - - std::vector<spdlog::sink_ptr> sinks; - sinks.push_back(file_sink); - sinks.push_back(console_sink); - - net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks)); - ui_logger = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks)); - db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks)); - crypto_logger = - std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); - qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks)); - - if (nheko::enable_debug_log || enable_debug_log_from_commandline) { - db_logger->set_level(spdlog::level::trace); - ui_logger->set_level(spdlog::level::trace); - crypto_logger->set_level(spdlog::level::trace); - net_logger->set_level(spdlog::level::trace); - qml_logger->set_level(spdlog::level::trace); - } - - qInstallMessageHandler(qmlMessageHandler); + auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>( + file_path, MAX_FILE_SIZE, MAX_LOG_FILES); + + auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); + + std::vector<spdlog::sink_ptr> sinks; + sinks.push_back(file_sink); + sinks.push_back(console_sink); + + net_logger = std::make_shared<spdlog::logger>("net", std::begin(sinks), std::end(sinks)); + ui_logger = std::make_shared<spdlog::logger>("ui", std::begin(sinks), std::end(sinks)); + db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks)); + crypto_logger = std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks)); + + if (nheko::enable_debug_log || enable_debug_log_from_commandline) { + db_logger->set_level(spdlog::level::trace); + ui_logger->set_level(spdlog::level::trace); + crypto_logger->set_level(spdlog::level::trace); + net_logger->set_level(spdlog::level::trace); + qml_logger->set_level(spdlog::level::trace); + } + + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr<spdlog::logger> ui() { - return ui_logger; + return ui_logger; } std::shared_ptr<spdlog::logger> net() { - return net_logger; + return net_logger; } std::shared_ptr<spdlog::logger> db() { - return db_logger; + return db_logger; } std::shared_ptr<spdlog::logger> crypto() { - return crypto_logger; + return crypto_logger; } std::shared_ptr<spdlog::logger> qml() { - return qml_logger; + return qml_logger; } } diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp index f53d81ba..64e9c865 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp @@ -34,477 +34,468 @@ LoginPage::LoginPage(QWidget *parent) : QWidget(parent) , inferredServerAddress_() { - qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod"); + qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod"); - top_layout_ = new QVBoxLayout(); + top_layout_ = new QVBoxLayout(); + + top_bar_layout_ = new QHBoxLayout(); + top_bar_layout_->setSpacing(0); + top_bar_layout_->setMargin(0); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); - top_bar_layout_ = new QHBoxLayout(); - top_bar_layout_->setSpacing(0); - top_bar_layout_->setMargin(0); - - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); - - top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - top_bar_layout_->addStretch(1); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); - - QIcon logo; - logo.addFile(":/logos/login.png"); - - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); - - logo_layout_ = new QHBoxLayout(); - logo_layout_->setContentsMargins(0, 0, 0, 20); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 200)); - - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 30); - form_widget_->setLayout(form_layout_); - - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); - - matrixid_input_ = new TextField(this); - matrixid_input_->setLabel(tr("Matrix ID")); - matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}")); - matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); - matrixid_input_->setToolTip( - tr("Your login name. A mxid should start with @ followed by the user id. After the user " - "id you need to include your server name after a :.\nYou can also put your homeserver " - "address there, if your server doesn't support .well-known lookup.\nExample: " - "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a " - "field to enter the server manually.")); - - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(40); - spinner_->setFixedWidth(40); - spinner_->hide(); - - errorIcon_ = new QLabel(this); - errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); - errorIcon_->hide(); - - matrixidLayout_ = new QHBoxLayout(); - matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); - - QFont font; - - error_matrixid_label_ = new QLabel(this); - error_matrixid_label_->setFont(font); - error_matrixid_label_->setWordWrap(true); - - password_input_ = new TextField(this); - password_input_->setLabel(tr("Password")); - password_input_->setEchoMode(QLineEdit::Password); - password_input_->setToolTip(tr("Your password.")); - - deviceName_ = new TextField(this); - deviceName_->setLabel(tr("Device name")); - deviceName_->setToolTip( - tr("A name for this device, which will be shown to others, when verifying your devices. " - "If none is provided a default is used.")); - - serverInput_ = new TextField(this); - serverInput_->setLabel(tr("Homeserver address")); - serverInput_->setPlaceholderText(tr("server.my:8787")); - serverInput_->setToolTip(tr("The address that can be used to contact you homeservers " - "client API.\nExample: https://server.my:8787")); - serverInput_->hide(); - - serverLayout_ = new QHBoxLayout(); - serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); - - form_layout_->addLayout(matrixidLayout_); - form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter); - form_layout_->addWidget(password_input_); - form_layout_->addWidget(deviceName_, Qt::AlignHCenter); - form_layout_->addLayout(serverLayout_); - - error_matrixid_label_->hide(); - - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(20); - button_layout_->setContentsMargins(0, 0, 0, 30); - - login_button_ = new RaisedButton(tr("LOGIN"), this); - login_button_->setMinimumSize(150, 65); - login_button_->setFontSize(20); - login_button_->setCornerRadius(3); - - sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this); - sso_login_button_->setMinimumSize(150, 65); - sso_login_button_->setFontSize(20); - sso_login_button_->setCornerRadius(3); - sso_login_button_->setVisible(false); - - button_layout_->addStretch(1); - button_layout_->addWidget(login_button_); - button_layout_->addWidget(sso_login_button_); - button_layout_->addStretch(1); - - error_label_ = new QLabel(this); - error_label_->setFont(font); - error_label_->setWordWrap(true); - - top_layout_->addLayout(top_bar_layout_); - top_layout_->addStretch(1); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); - - setLayout(top_layout_); - - connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); - connect( - this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); - - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(login_button_, &RaisedButton::clicked, this, [this]() { - onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO); - }); - connect(sso_login_button_, &RaisedButton::clicked, this, [this]() { - onLoginButtonClicked(LoginMethod::SSO); - }); - connect(this, - &LoginPage::showErrorMessage, - this, - static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError), - Qt::QueuedConnection); - connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); - connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); + top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + top_bar_layout_->addStretch(1); + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); + + QIcon logo; + logo.addFile(":/logos/login.png"); + + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 0, 0, 20); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 200)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 30); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + matrixid_input_ = new TextField(this); + matrixid_input_->setLabel(tr("Matrix ID")); + matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}")); + matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); + matrixid_input_->setToolTip( + tr("Your login name. A mxid should start with @ followed by the user id. After the user " + "id you need to include your server name after a :.\nYou can also put your homeserver " + "address there, if your server doesn't support .well-known lookup.\nExample: " + "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a " + "field to enter the server manually.")); + + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(40); + spinner_->setFixedWidth(40); + spinner_->hide(); + + errorIcon_ = new QLabel(this); + errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); + errorIcon_->hide(); + + matrixidLayout_ = new QHBoxLayout(); + matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); + + QFont font; + + error_matrixid_label_ = new QLabel(this); + error_matrixid_label_->setFont(font); + error_matrixid_label_->setWordWrap(true); + + password_input_ = new TextField(this); + password_input_->setLabel(tr("Password")); + password_input_->setEchoMode(QLineEdit::Password); + password_input_->setToolTip(tr("Your password.")); + + deviceName_ = new TextField(this); + deviceName_->setLabel(tr("Device name")); + deviceName_->setToolTip( + tr("A name for this device, which will be shown to others, when verifying your devices. " + "If none is provided a default is used.")); + + serverInput_ = new TextField(this); + serverInput_->setLabel(tr("Homeserver address")); + serverInput_->setPlaceholderText(tr("server.my:8787")); + serverInput_->setToolTip(tr("The address that can be used to contact you homeservers " + "client API.\nExample: https://server.my:8787")); + serverInput_->hide(); + + serverLayout_ = new QHBoxLayout(); + serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); + + form_layout_->addLayout(matrixidLayout_); + form_layout_->addWidget(error_matrixid_label_, 0, Qt::AlignHCenter); + form_layout_->addWidget(password_input_); + form_layout_->addWidget(deviceName_, Qt::AlignHCenter); + form_layout_->addLayout(serverLayout_); + + error_matrixid_label_->hide(); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(20); + button_layout_->setContentsMargins(0, 0, 0, 30); + + login_button_ = new RaisedButton(tr("LOGIN"), this); + login_button_->setMinimumSize(150, 65); + login_button_->setFontSize(20); + login_button_->setCornerRadius(3); + + sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this); + sso_login_button_->setMinimumSize(150, 65); + sso_login_button_->setFontSize(20); + sso_login_button_->setCornerRadius(3); + sso_login_button_->setVisible(false); + + button_layout_->addStretch(1); + button_layout_->addWidget(login_button_); + button_layout_->addWidget(sso_login_button_); + button_layout_->addStretch(1); + + error_label_ = new QLabel(this); + error_label_->setFont(font); + error_label_->setWordWrap(true); + + top_layout_->addLayout(top_bar_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + + setLayout(top_layout_); + + connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk, Qt::QueuedConnection); + connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(login_button_, &RaisedButton::clicked, this, [this]() { + onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO); + }); + connect(sso_login_button_, &RaisedButton::clicked, this, [this]() { + onLoginButtonClicked(LoginMethod::SSO); + }); + connect(this, + &LoginPage::showErrorMessage, + this, + static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError), + Qt::QueuedConnection); + connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); + connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); } void LoginPage::showError(const QString &msg) { - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - error_label_->setFixedHeight((int)qCeil(width / 200.0) * height); - error_label_->setText(msg); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + error_label_->setFixedHeight((int)qCeil(width / 200.0) * height); + error_label_->setText(msg); } void LoginPage::showError(QLabel *label, const QString &msg) { - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - label->setFixedHeight((int)qCeil(width / 200.0) * height); - label->setText(msg); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); } void LoginPage::onMatrixIdEntered() { - error_label_->setText(""); + error_label_->setText(""); - User user; + User user; - if (!matrixid_input_->isValid()) { - error_matrixid_label_->show(); - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } else { + error_matrixid_label_->setText(""); + error_matrixid_label_->hide(); + } + + try { + user = parse<User>(matrixid_input_->text().toStdString()); + } catch (const std::exception &) { + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } + + QString homeServer = QString::fromStdString(user.hostname()); + if (homeServer != inferredServerAddress_) { + serverInput_->hide(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + if (serverInput_->isVisible()) { + matrixidLayout_->removeWidget(spinner_); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } else { - error_matrixid_label_->setText(""); - error_matrixid_label_->hide(); + serverLayout_->removeWidget(spinner_); + matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } - try { - user = parse<User>(matrixid_input_->text().toStdString()); - } catch (const std::exception &) { - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } + inferredServerAddress_ = homeServer; + serverInput_->setText(homeServer); - QString homeServer = QString::fromStdString(user.hostname()); - if (homeServer != inferredServerAddress_) { - serverInput_->hide(); - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - if (serverInput_->isVisible()) { - matrixidLayout_->removeWidget(spinner_); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } else { - serverLayout_->removeWidget(spinner_); - matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } - - inferredServerAddress_ = homeServer; - serverInput_->setText(homeServer); - - http::client()->set_server(user.hostname()); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); - - http::client()->well_known([this](const mtx::responses::WellKnown &res, - mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - checkHomeserverVersion(); - return; - } - - if (!err->parse_error.empty()) { - emit versionErrorCb( - tr("Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + - "'"); - http::client()->set_server(res.homeserver.base_url); - checkHomeserverVersion(); - }); - } + http::client()->set_server(user.hostname()); + http::client()->verify_certificates( + !UserSettings::instance()->disableCertificateValidation()); + + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + nhlog::net()->info("Autodiscovery: No .well-known."); + checkHomeserverVersion(); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error("Autodiscovery failed. Received malformed response."); + return; + } + + emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); + return; + } + + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + checkHomeserverVersion(); + }); + } } void LoginPage::checkHomeserverVersion() { - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - emit versionErrorCb(tr("The required endpoints were not found. " - "Possibly not a Matrix server.")); - return; - } - - if (!err->parse_error.empty()) { - emit versionErrorCb(tr("Received malformed response. Make sure " - "the homeserver domain is valid.")); - return; - } - - emit versionErrorCb(tr( - "An unknown error occured. Make sure the homeserver domain is valid.")); - return; - } + http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit versionErrorCb(tr("The required endpoints were not found. " + "Possibly not a Matrix server.")); + return; + } - http::client()->get_login( - [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) { - if (err || flows.flows.empty()) - emit versionOkCb(true, false); - - bool ssoSupported_ = false; - bool passwordSupported_ = false; - for (const auto &flow : flows.flows) { - if (flow.type == mtx::user_interactive::auth_types::sso) { - ssoSupported_ = true; - } else if (flow.type == - mtx::user_interactive::auth_types::password) { - passwordSupported_ = true; - } - } - emit versionOkCb(passwordSupported_, ssoSupported_); - }); + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Received malformed response. Make sure " + "the homeserver domain is valid.")); + return; + } + + emit versionErrorCb( + tr("An unknown error occured. Make sure the homeserver domain is valid.")); + return; + } + + http::client()->get_login( + [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) { + if (err || flows.flows.empty()) + emit versionOkCb(true, false); + + bool ssoSupported_ = false; + bool passwordSupported_ = false; + for (const auto &flow : flows.flows) { + if (flow.type == mtx::user_interactive::auth_types::sso) { + ssoSupported_ = true; + } else if (flow.type == mtx::user_interactive::auth_types::password) { + passwordSupported_ = true; + } + } + emit versionOkCb(passwordSupported_, ssoSupported_); }); + }); } void LoginPage::onServerAddressEntered() { - error_label_->setText(""); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); - http::client()->set_server(serverInput_->text().toStdString()); - checkHomeserverVersion(); - - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); + error_label_->setText(""); + http::client()->verify_certificates(!UserSettings::instance()->disableCertificateValidation()); + http::client()->set_server(serverInput_->text().toStdString()); + checkHomeserverVersion(); + + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } void LoginPage::versionError(const QString &error) { - showError(error_label_, error); - serverInput_->show(); - - spinner_->stop(); - serverLayout_->removeWidget(spinner_); - serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); - errorIcon_->show(); - matrixidLayout_->removeWidget(spinner_); + showError(error_label_, error); + serverInput_->show(); + + spinner_->stop(); + serverLayout_->removeWidget(spinner_); + serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); + errorIcon_->show(); + matrixidLayout_->removeWidget(spinner_); } void LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_) { - passwordSupported = passwordSupported_; - ssoSupported = ssoSupported_; + passwordSupported = passwordSupported_; + ssoSupported = ssoSupported_; - serverLayout_->removeWidget(spinner_); - matrixidLayout_->removeWidget(spinner_); - spinner_->stop(); + serverLayout_->removeWidget(spinner_); + matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); - sso_login_button_->setVisible(ssoSupported); - login_button_->setVisible(passwordSupported); + sso_login_button_->setVisible(ssoSupported); + login_button_->setVisible(passwordSupported); - if (serverInput_->isVisible()) - serverInput_->hide(); + if (serverInput_->isVisible()) + serverInput_->hide(); } void LoginPage::onLoginButtonClicked(LoginMethod loginMethod) { - error_label_->setText(""); - User user; + error_label_->setText(""); + User user; + + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } else { + error_matrixid_label_->setText(""); + error_matrixid_label_->hide(); + } + + try { + user = parse<User>(matrixid_input_->text().toStdString()); + } catch (const std::exception &) { + showError(error_matrixid_label_, + tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); + return; + } + + if (loginMethod == LoginMethod::Password) { + if (password_input_->text().isEmpty()) + return showError(error_label_, tr("Empty password")); + + http::client()->login( + user.localpart(), + password_input_->text().toStdString(), + deviceName_->text().trimmed().isEmpty() ? initialDeviceName() + : deviceName_->text().toStdString(), + [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + auto error = err->matrix_error.error; + if (error.empty()) + error = err->parse_error; + + showErrorMessage(error_label_, QString::fromStdString(error)); + emit errorOccurred(); + return; + } + + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } + + emit loginOk(res); + }); + } else { + auto sso = new SSOHandler(); + connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) { + mtx::requests::Login req{}; + req.token = token; + req.type = mtx::user_interactive::auth_types::token; + req.device_id = deviceName_->text().trimmed().isEmpty() + ? initialDeviceName() + : deviceName_->text().toStdString(); + http::client()->login( + req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + showErrorMessage(error_label_, + QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + return; + } - if (!matrixid_input_->isValid()) { - error_matrixid_label_->show(); - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } else { - error_matrixid_label_->setText(""); - error_matrixid_label_->hide(); - } + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } - try { - user = parse<User>(matrixid_input_->text().toStdString()); - } catch (const std::exception &) { - showError(error_matrixid_label_, - tr("You have entered an invalid Matrix ID e.g @joe:matrix.org")); - return; - } + emit loginOk(res); + }); + sso->deleteLater(); + }); + connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() { + showErrorMessage(error_label_, tr("SSO login failed")); + emit errorOccurred(); + sso->deleteLater(); + }); - if (loginMethod == LoginMethod::Password) { - if (password_input_->text().isEmpty()) - return showError(error_label_, tr("Empty password")); - - http::client()->login( - user.localpart(), - password_input_->text().toStdString(), - deviceName_->text().trimmed().isEmpty() ? initialDeviceName() - : deviceName_->text().toStdString(), - [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { - if (err) { - auto error = err->matrix_error.error; - if (error.empty()) - error = err->parse_error; - - showErrorMessage(error_label_, QString::fromStdString(error)); - emit errorOccurred(); - return; - } - - if (res.well_known) { - http::client()->set_server(res.well_known->homeserver.base_url); - nhlog::net()->info("Login requested to user server: " + - res.well_known->homeserver.base_url); - } - - emit loginOk(res); - }); - } else { - auto sso = new SSOHandler(); - connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) { - mtx::requests::Login req{}; - req.token = token; - req.type = mtx::user_interactive::auth_types::token; - req.device_id = deviceName_->text().trimmed().isEmpty() - ? initialDeviceName() - : deviceName_->text().toStdString(); - http::client()->login( - req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { - if (err) { - showErrorMessage( - error_label_, - QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - return; - } - - if (res.well_known) { - http::client()->set_server( - res.well_known->homeserver.base_url); - nhlog::net()->info("Login requested to user server: " + - res.well_known->homeserver.base_url); - } - - emit loginOk(res); - }); - sso->deleteLater(); - }); - connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() { - showErrorMessage(error_label_, tr("SSO login failed")); - emit errorOccurred(); - sso->deleteLater(); - }); - - QDesktopServices::openUrl( - QString::fromStdString(http::client()->login_sso_redirect(sso->url()))); - } + QDesktopServices::openUrl( + QString::fromStdString(http::client()->login_sso_redirect(sso->url()))); + } - emit loggingIn(); + emit loggingIn(); } void LoginPage::reset() { - matrixid_input_->clear(); - password_input_->clear(); - password_input_->show(); - serverInput_->clear(); - - spinner_->stop(); - errorIcon_->hide(); - serverLayout_->removeWidget(spinner_); - serverLayout_->removeWidget(errorIcon_); - matrixidLayout_->removeWidget(spinner_); - - inferredServerAddress_.clear(); + matrixid_input_->clear(); + password_input_->clear(); + password_input_->show(); + serverInput_->clear(); + + spinner_->stop(); + errorIcon_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->removeWidget(errorIcon_); + matrixidLayout_->removeWidget(spinner_); + + inferredServerAddress_.clear(); } void LoginPage::onBackButtonClicked() { - emit backButtonClicked(); + emit backButtonClicked(); } void LoginPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/LoginPage.h b/src/LoginPage.h index 2e1eb9b9..01dd27e1 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h @@ -24,101 +24,101 @@ struct Login; class LoginPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - enum class LoginMethod - { - Password, - SSO, - }; + enum class LoginMethod + { + Password, + SSO, + }; - LoginPage(QWidget *parent = nullptr); + LoginPage(QWidget *parent = nullptr); - void reset(); + void reset(); signals: - void backButtonClicked(); - void loggingIn(); - void errorOccurred(); + void backButtonClicked(); + void loggingIn(); + void errorOccurred(); - //! Used to trigger the corresponding slot outside of the main thread. - void versionErrorCb(const QString &err); - void versionOkCb(bool passwordSupported, bool ssoSupported); + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void versionOkCb(bool passwordSupported, bool ssoSupported); - void loginOk(const mtx::responses::Login &res); - void showErrorMessage(QLabel *label, const QString &msg); + void loginOk(const mtx::responses::Login &res); + void showErrorMessage(QLabel *label, const QString &msg); protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; public slots: - // Displays errors produced during the login. - void showError(const QString &msg); - void showError(QLabel *label, const QString &msg); + // Displays errors produced during the login. + void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); private slots: - // Callback for the back button. - void onBackButtonClicked(); + // Callback for the back button. + void onBackButtonClicked(); - // Callback for the login button. - void onLoginButtonClicked(LoginMethod loginMethod); + // Callback for the login button. + void onLoginButtonClicked(LoginMethod loginMethod); - // Callback for probing the server found in the mxid - void onMatrixIdEntered(); + // Callback for probing the server found in the mxid + void onMatrixIdEntered(); - // Callback for probing the manually entered server - void onServerAddressEntered(); + // Callback for probing the manually entered server + void onServerAddressEntered(); - // Callback for errors produced during server probing - void versionError(const QString &error_message); - // Callback for successful server probing - void versionOk(bool passwordSupported, bool ssoSupported); + // Callback for errors produced during server probing + void versionError(const QString &error_message); + // Callback for successful server probing + void versionOk(bool passwordSupported, bool ssoSupported); private: - void checkHomeserverVersion(); - std::string initialDeviceName() - { + void checkHomeserverVersion(); + std::string initialDeviceName() + { #if defined(Q_OS_MAC) - return "Nheko on macOS"; + return "Nheko on macOS"; #elif defined(Q_OS_LINUX) - return "Nheko on Linux"; + return "Nheko on Linux"; #elif defined(Q_OS_WIN) - return "Nheko on Windows"; + return "Nheko on Windows"; #elif defined(Q_OS_FREEBSD) - return "Nheko on FreeBSD"; + return "Nheko on FreeBSD"; #else - return "Nheko"; + return "Nheko"; #endif - } + } - QVBoxLayout *top_layout_; + QVBoxLayout *top_layout_; - QHBoxLayout *top_bar_layout_; - QHBoxLayout *logo_layout_; - QHBoxLayout *button_layout_; + QHBoxLayout *top_bar_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; - QLabel *logo_; - QLabel *error_label_; - QLabel *error_matrixid_label_; + QLabel *logo_; + QLabel *error_label_; + QLabel *error_matrixid_label_; - QHBoxLayout *serverLayout_; - QHBoxLayout *matrixidLayout_; - LoadingIndicator *spinner_; - QLabel *errorIcon_; - QString inferredServerAddress_; + QHBoxLayout *serverLayout_; + QHBoxLayout *matrixidLayout_; + LoadingIndicator *spinner_; + QLabel *errorIcon_; + QString inferredServerAddress_; - FlatButton *back_button_; - RaisedButton *login_button_, *sso_login_button_; + FlatButton *back_button_; + RaisedButton *login_button_, *sso_login_button_; - QWidget *form_widget_; - QHBoxLayout *form_wrapper_; - QVBoxLayout *form_layout_; + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; - TextField *matrixid_input_; - TextField *password_input_; - TextField *deviceName_; - TextField *serverInput_; - bool passwordSupported = true; - bool ssoSupported = false; + TextField *matrixid_input_; + TextField *password_input_; + TextField *deviceName_; + TextField *serverInput_; + bool passwordSupported = true; + bool ssoSupported = false; }; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index b423304f..bc53b906 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -43,415 +43,407 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , userSettings_{UserSettings::instance()} { - instance_ = this; - - setWindowTitle(0); - setObjectName("MainWindow"); - - modal_ = new OverlayModal(this); - - restoreWindowSize(); - - QFont font; - font.setStyleStrategy(QFont::PreferAntialias); - setFont(font); - - trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); - - welcome_page_ = new WelcomePage(this); - login_page_ = new LoginPage(this); - register_page_ = new RegisterPage(this); - chat_page_ = new ChatPage(userSettings_, this); - userSettingsPage_ = new UserSettingsPage(userSettings_, this); - - // Initialize sliding widget manager. - pageStack_ = new QStackedWidget(this); - pageStack_->addWidget(welcome_page_); - pageStack_->addWidget(login_page_); - pageStack_->addWidget(register_page_); - pageStack_->addWidget(chat_page_); - pageStack_->addWidget(userSettingsPage_); - - setCentralWidget(pageStack_); - - connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); - connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); - - connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); - connect( - register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); - connect( - login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); - connect(register_page_, &RegisterPage::errorOccurred, this, [this]() { - removeOverlayProgressBar(); - }); - connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - - connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); - connect( - chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); - connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle); - connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); - connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { - login_page_->showError(msg); - showLoginPage(); - }); - - connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { - pageStack_->setCurrentWidget(chat_page_); - }); - - connect( - userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - connect( - userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); - connect(trayIcon_, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - this, - SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); - - connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); - - connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged); - - connect( - chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); - - connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { - http::client()->set_user(res.user_id); - showChatPage(); - }); - - connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); - - QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); - connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); - - trayIcon_->setVisible(userSettings_->tray()); - - // load cache on event loop - QTimer::singleShot(0, this, [this] { - if (hasActiveUser()) { - QString token = userSettings_->accessToken(); - QString home_server = userSettings_->homeserver(); - QString user_id = userSettings_->userId(); - QString device_id = userSettings_->deviceId(); - - http::client()->set_access_token(token.toStdString()); - http::client()->set_server(home_server.toStdString()); - http::client()->set_device_id(device_id.toStdString()); - - try { - using namespace mtx::identifiers; - http::client()->set_user(parse<User>(user_id.toStdString())); - } catch (const std::invalid_argument &) { - nhlog::ui()->critical("bootstrapped with invalid user_id: {}", - user_id.toStdString()); - } - - showChatPage(); - } - }); + instance_ = this; + + setWindowTitle(0); + setObjectName("MainWindow"); + + modal_ = new OverlayModal(this); + + restoreWindowSize(); + + QFont font; + font.setStyleStrategy(QFont::PreferAntialias); + setFont(font); + + trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); + + welcome_page_ = new WelcomePage(this); + login_page_ = new LoginPage(this); + register_page_ = new RegisterPage(this); + chat_page_ = new ChatPage(userSettings_, this); + userSettingsPage_ = new UserSettingsPage(userSettings_, this); + + // Initialize sliding widget manager. + pageStack_ = new QStackedWidget(this); + pageStack_->addWidget(welcome_page_); + pageStack_->addWidget(login_page_); + pageStack_->addWidget(register_page_); + pageStack_->addWidget(chat_page_); + pageStack_->addWidget(userSettingsPage_); + + setCentralWidget(pageStack_); + + connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); + connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); + + connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); + connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); + connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect( + register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + + connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); + connect( + chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); + connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle); + connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); + connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { + login_page_->showError(msg); + showLoginPage(); + }); + + connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { + pageStack_->setCurrentWidget(chat_page_); + }); + + connect(userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); + connect( + userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); + connect(trayIcon_, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, + SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); + + connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); + + connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged); + + connect(chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); + + connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { + http::client()->set_user(res.user_id); + showChatPage(); + }); + + connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); + + QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); + connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); + + trayIcon_->setVisible(userSettings_->tray()); + + // load cache on event loop + QTimer::singleShot(0, this, [this] { + if (hasActiveUser()) { + QString token = userSettings_->accessToken(); + QString home_server = userSettings_->homeserver(); + QString user_id = userSettings_->userId(); + QString device_id = userSettings_->deviceId(); + + http::client()->set_access_token(token.toStdString()); + http::client()->set_server(home_server.toStdString()); + http::client()->set_device_id(device_id.toStdString()); + + try { + using namespace mtx::identifiers; + http::client()->set_user(parse<User>(user_id.toStdString())); + } catch (const std::invalid_argument &) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", + user_id.toStdString()); + } + + showChatPage(); + } + }); } void MainWindow::setWindowTitle(int notificationCount) { - QString name = "nheko"; - - if (!userSettings_.data()->profile().isEmpty()) - name += " | " + userSettings_.data()->profile(); - if (notificationCount > 0) { - name.append(QString{" (%1)"}.arg(notificationCount)); - } - QMainWindow::setWindowTitle(name); + QString name = "nheko"; + + if (!userSettings_.data()->profile().isEmpty()) + name += " | " + userSettings_.data()->profile(); + if (notificationCount > 0) { + name.append(QString{" (%1)"}.arg(notificationCount)); + } + QMainWindow::setWindowTitle(name); } bool MainWindow::event(QEvent *event) { - auto type = event->type(); - if (type == QEvent::WindowActivate) { - emit focusChanged(true); - } else if (type == QEvent::WindowDeactivate) { - emit focusChanged(false); - } - - return QMainWindow::event(event); + auto type = event->type(); + if (type == QEvent::WindowActivate) { + emit focusChanged(true); + } else if (type == QEvent::WindowDeactivate) { + emit focusChanged(false); + } + + return QMainWindow::event(event); } void MainWindow::restoreWindowSize() { - int savedWidth = userSettings_->qsettings()->value("window/width").toInt(); - int savedheight = userSettings_->qsettings()->value("window/height").toInt(); + int savedWidth = userSettings_->qsettings()->value("window/width").toInt(); + int savedheight = userSettings_->qsettings()->value("window/height").toInt(); - nhlog::ui()->info("Restoring window size {}x{}", savedWidth, savedheight); + nhlog::ui()->info("Restoring window size {}x{}", savedWidth, savedheight); - if (savedWidth == 0 || savedheight == 0) - resize(conf::window::width, conf::window::height); - else - resize(savedWidth, savedheight); + if (savedWidth == 0 || savedheight == 0) + resize(conf::window::width, conf::window::height); + else + resize(savedWidth, savedheight); } void MainWindow::saveCurrentWindowSize() { - auto settings = userSettings_->qsettings(); - QSize current = size(); + auto settings = userSettings_->qsettings(); + QSize current = size(); - settings->setValue("window/width", current.width()); - settings->setValue("window/height", current.height()); + settings->setValue("window/width", current.width()); + settings->setValue("window/height", current.height()); } void MainWindow::removeOverlayProgressBar() { - QTimer *timer = new QTimer(this); - timer->setSingleShot(true); + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); - connect(timer, &QTimer::timeout, [this, timer]() { - timer->deleteLater(); + connect(timer, &QTimer::timeout, [this, timer]() { + timer->deleteLater(); - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); - if (spinner_) - spinner_->stop(); - }); + if (spinner_) + spinner_->stop(); + }); - // FIXME: Snackbar doesn't work if it's initialized in the constructor. - QTimer::singleShot(0, this, [this]() { - snackBar_ = new SnackBar(this); - connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage); - }); + // FIXME: Snackbar doesn't work if it's initialized in the constructor. + QTimer::singleShot(0, this, [this]() { + snackBar_ = new SnackBar(this); + connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage); + }); - timer->start(50); + timer->start(50); } void MainWindow::showChatPage() { - auto userid = QString::fromStdString(http::client()->user_id().to_string()); - auto device_id = QString::fromStdString(http::client()->device_id()); - auto homeserver = QString::fromStdString(http::client()->server() + ":" + - std::to_string(http::client()->port())); - auto token = QString::fromStdString(http::client()->access_token()); - - userSettings_.data()->setUserId(userid); - userSettings_.data()->setAccessToken(token); - userSettings_.data()->setDeviceId(device_id); - userSettings_.data()->setHomeserver(homeserver); - - showOverlayProgressBar(); - - pageStack_->setCurrentWidget(chat_page_); - - pageStack_->removeWidget(welcome_page_); - pageStack_->removeWidget(login_page_); - pageStack_->removeWidget(register_page_); - - login_page_->reset(); - chat_page_->bootstrap(userid, homeserver, token); - connect(cache::client(), - &Cache::secretChanged, - userSettingsPage_, - &UserSettingsPage::updateSecretStatus); - emit reload(); + auto userid = QString::fromStdString(http::client()->user_id().to_string()); + auto device_id = QString::fromStdString(http::client()->device_id()); + auto homeserver = QString::fromStdString(http::client()->server() + ":" + + std::to_string(http::client()->port())); + auto token = QString::fromStdString(http::client()->access_token()); + + userSettings_.data()->setUserId(userid); + userSettings_.data()->setAccessToken(token); + userSettings_.data()->setDeviceId(device_id); + userSettings_.data()->setHomeserver(homeserver); + + showOverlayProgressBar(); + + pageStack_->setCurrentWidget(chat_page_); + + pageStack_->removeWidget(welcome_page_); + pageStack_->removeWidget(login_page_); + pageStack_->removeWidget(register_page_); + + login_page_->reset(); + chat_page_->bootstrap(userid, homeserver, token); + connect(cache::client(), + &Cache::secretChanged, + userSettingsPage_, + &UserSettingsPage::updateSecretStatus); + emit reload(); } void MainWindow::closeEvent(QCloseEvent *event) { - if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { - if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") != - QMessageBox::Yes) { - event->ignore(); - return; - } + if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { + if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") != + QMessageBox::Yes) { + event->ignore(); + return; } + } - if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && - userSettings_->tray()) { - event->ignore(); - hide(); - } + if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && userSettings_->tray()) { + event->ignore(); + hide(); + } } void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) { - switch (reason) { - case QSystemTrayIcon::Trigger: - if (!isVisible()) { - show(); - } else { - hide(); - } - break; - default: - break; + switch (reason) { + case QSystemTrayIcon::Trigger: + if (!isVisible()) { + show(); + } else { + hide(); } + break; + default: + break; + } } bool MainWindow::hasActiveUser() { - auto settings = userSettings_->qsettings(); - QString prefix; - if (userSettings_->profile() != "") - prefix = "profile/" + userSettings_->profile() + "/"; - - return settings->contains(prefix + "auth/access_token") && - settings->contains(prefix + "auth/home_server") && - settings->contains(prefix + "auth/user_id"); + auto settings = userSettings_->qsettings(); + QString prefix; + if (userSettings_->profile() != "") + prefix = "profile/" + userSettings_->profile() + "/"; + + return settings->contains(prefix + "auth/access_token") && + settings->contains(prefix + "auth/home_server") && + settings->contains(prefix + "auth/user_id"); } void MainWindow::openLeaveRoomDialog(const QString &room_id) { - auto dialog = new dialogs::LeaveRoom(this); - connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, room_id]() { - chat_page_->leaveRoom(room_id); - }); + auto dialog = new dialogs::LeaveRoom(this); + connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, room_id]() { + chat_page_->leaveRoom(room_id); + }); - showDialog(dialog); + showDialog(dialog); } void MainWindow::showOverlayProgressBar() { - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(100); - spinner_->setFixedWidth(100); - spinner_->setObjectName("ChatPageLoadSpinner"); - spinner_->start(); + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(100); + spinner_->setFixedWidth(100); + spinner_->setObjectName("ChatPageLoadSpinner"); + spinner_->start(); - showSolidOverlayModal(spinner_); + showSolidOverlayModal(spinner_); } void MainWindow::openJoinRoomDialog(std::function<void(const QString &room_id)> callback) { - auto dialog = new dialogs::JoinRoom(this); - connect(dialog, &dialogs::JoinRoom::joinRoom, this, [callback](const QString &room) { - if (!room.isEmpty()) - callback(room); - }); + auto dialog = new dialogs::JoinRoom(this); + connect(dialog, &dialogs::JoinRoom::joinRoom, this, [callback](const QString &room) { + if (!room.isEmpty()) + callback(room); + }); - showDialog(dialog); + showDialog(dialog); } void MainWindow::openCreateRoomDialog( std::function<void(const mtx::requests::CreateRoom &request)> callback) { - auto dialog = new dialogs::CreateRoom(this); - connect(dialog, - &dialogs::CreateRoom::createRoom, - this, - [callback](const mtx::requests::CreateRoom &request) { callback(request); }); + auto dialog = new dialogs::CreateRoom(this); + connect(dialog, + &dialogs::CreateRoom::createRoom, + this, + [callback](const mtx::requests::CreateRoom &request) { callback(request); }); - showDialog(dialog); + showDialog(dialog); } void MainWindow::showTransparentOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags) { - modal_->setWidget(content); - modal_->setColor(QColor(30, 30, 30, 150)); - modal_->setDismissible(true); - modal_->setContentAlignment(flags); - modal_->raise(); - modal_->show(); + modal_->setWidget(content); + modal_->setColor(QColor(30, 30, 30, 150)); + modal_->setDismissible(true); + modal_->setContentAlignment(flags); + modal_->raise(); + modal_->show(); } void MainWindow::showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags) { - modal_->setWidget(content); - modal_->setColor(QColor(30, 30, 30)); - modal_->setDismissible(false); - modal_->setContentAlignment(flags); - modal_->raise(); - modal_->show(); + modal_->setWidget(content); + modal_->setColor(QColor(30, 30, 30)); + modal_->setDismissible(false); + modal_->setContentAlignment(flags); + modal_->raise(); + modal_->show(); } void MainWindow::openLogoutDialog() { - auto dialog = new dialogs::Logout(this); - connect(dialog, &dialogs::Logout::loggingOut, this, [this]() { - if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { - if (QMessageBox::question( - this, "nheko", "A call is in progress. Log out?") != - QMessageBox::Yes) { - return; - } - WebRTCSession::instance().end(); - } - chat_page_->initiateLogout(); - }); - - showDialog(dialog); + auto dialog = new dialogs::Logout(this); + connect(dialog, &dialogs::Logout::loggingOut, this, [this]() { + if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { + if (QMessageBox::question(this, "nheko", "A call is in progress. Log out?") != + QMessageBox::Yes) { + return; + } + WebRTCSession::instance().end(); + } + chat_page_->initiateLogout(); + }); + + showDialog(dialog); } bool MainWindow::hasActiveDialogs() const { - return !modal_ && modal_->isVisible(); + return !modal_ && modal_->isVisible(); } bool MainWindow::pageSupportsTray() const { - return !welcome_page_->isVisible() && !login_page_->isVisible() && - !register_page_->isVisible(); + return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible(); } void MainWindow::hideOverlay() { - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); } inline void MainWindow::showDialog(QWidget *dialog) { - utils::centerWidget(dialog, this); - dialog->raise(); - dialog->show(); + utils::centerWidget(dialog, this); + dialog->raise(); + dialog->show(); } void MainWindow::showWelcomePage() { - removeOverlayProgressBar(); - pageStack_->addWidget(welcome_page_); - pageStack_->setCurrentWidget(welcome_page_); + removeOverlayProgressBar(); + pageStack_->addWidget(welcome_page_); + pageStack_->setCurrentWidget(welcome_page_); } void MainWindow::showLoginPage() { - if (modal_) - modal_->hide(); + if (modal_) + modal_->hide(); - pageStack_->addWidget(login_page_); - pageStack_->setCurrentWidget(login_page_); + pageStack_->addWidget(login_page_); + pageStack_->setCurrentWidget(login_page_); } void MainWindow::showRegisterPage() { - pageStack_->addWidget(register_page_); - pageStack_->setCurrentWidget(register_page_); + pageStack_->addWidget(register_page_); + pageStack_->setCurrentWidget(register_page_); } void MainWindow::showUserSettingsPage() { - pageStack_->setCurrentWidget(userSettingsPage_); + pageStack_->setCurrentWidget(userSettingsPage_); } diff --git a/src/MainWindow.h b/src/MainWindow.h index d9ffb9b1..eff8fbe7 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -46,93 +46,92 @@ class ReCaptcha; class MainWindow : public QMainWindow { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(int x READ x CONSTANT) - Q_PROPERTY(int y READ y CONSTANT) - Q_PROPERTY(int width READ width CONSTANT) - Q_PROPERTY(int height READ height CONSTANT) + Q_PROPERTY(int x READ x CONSTANT) + Q_PROPERTY(int y READ y CONSTANT) + Q_PROPERTY(int width READ width CONSTANT) + Q_PROPERTY(int height READ height CONSTANT) public: - explicit MainWindow(QWidget *parent = nullptr); + explicit MainWindow(QWidget *parent = nullptr); - static MainWindow *instance() { return instance_; } - void saveCurrentWindowSize(); + static MainWindow *instance() { return instance_; } + void saveCurrentWindowSize(); - void openLeaveRoomDialog(const QString &room_id); - void openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback); - void openCreateRoomDialog( - std::function<void(const mtx::requests::CreateRoom &request)> callback); - void openJoinRoomDialog(std::function<void(const QString &room_id)> callback); - void openLogoutDialog(); + void openLeaveRoomDialog(const QString &room_id); + void openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback); + void openCreateRoomDialog( + std::function<void(const mtx::requests::CreateRoom &request)> callback); + void openJoinRoomDialog(std::function<void(const QString &room_id)> callback); + void openLogoutDialog(); - void hideOverlay(); - void showSolidOverlayModal(QWidget *content, - QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter); - void showTransparentOverlayModal(QWidget *content, - QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop | - Qt::AlignHCenter); + void hideOverlay(); + void showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags = Qt::AlignCenter); + void showTransparentOverlayModal(QWidget *content, + QFlags<Qt::AlignmentFlag> flags = Qt::AlignTop | + Qt::AlignHCenter); protected: - void closeEvent(QCloseEvent *event) override; - bool event(QEvent *event) override; + void closeEvent(QCloseEvent *event) override; + bool event(QEvent *event) override; private slots: - //! Handle interaction with the tray icon. - void iconActivated(QSystemTrayIcon::ActivationReason reason); + //! Handle interaction with the tray icon. + void iconActivated(QSystemTrayIcon::ActivationReason reason); - //! Show the welcome page in the main window. - void showWelcomePage(); + //! Show the welcome page in the main window. + void showWelcomePage(); - //! Show the login page in the main window. - void showLoginPage(); + //! Show the login page in the main window. + void showLoginPage(); - //! Show the register page in the main window. - void showRegisterPage(); + //! Show the register page in the main window. + void showRegisterPage(); - //! Show user settings page. - void showUserSettingsPage(); + //! Show user settings page. + void showUserSettingsPage(); - //! Show the chat page and start communicating with the given access token. - void showChatPage(); + //! Show the chat page and start communicating with the given access token. + void showChatPage(); - void showOverlayProgressBar(); - void removeOverlayProgressBar(); + void showOverlayProgressBar(); + void removeOverlayProgressBar(); - virtual void setWindowTitle(int notificationCount); + virtual void setWindowTitle(int notificationCount); signals: - void focusChanged(const bool focused); - void reload(); + void focusChanged(const bool focused); + void reload(); private: - void showDialog(QWidget *dialog); - bool hasActiveUser(); - void restoreWindowSize(); - //! Check if there is an open dialog. - bool hasActiveDialogs() const; - //! Check if the current page supports the "minimize to tray" functionality. - bool pageSupportsTray() const; - - static MainWindow *instance_; - - //! The initial welcome screen. - WelcomePage *welcome_page_; - //! The login screen. - LoginPage *login_page_; - //! The register page. - RegisterPage *register_page_; - //! A stacked widget that handles the transitions between widgets. - QStackedWidget *pageStack_; - //! The main chat area. - ChatPage *chat_page_; - UserSettingsPage *userSettingsPage_; - QSharedPointer<UserSettings> userSettings_; - //! Tray icon that shows the unread message count. - TrayIcon *trayIcon_; - //! Notifications display. - SnackBar *snackBar_ = nullptr; - //! Overlay modal used to project other widgets. - OverlayModal *modal_ = nullptr; - LoadingIndicator *spinner_ = nullptr; + void showDialog(QWidget *dialog); + bool hasActiveUser(); + void restoreWindowSize(); + //! Check if there is an open dialog. + bool hasActiveDialogs() const; + //! Check if the current page supports the "minimize to tray" functionality. + bool pageSupportsTray() const; + + static MainWindow *instance_; + + //! The initial welcome screen. + WelcomePage *welcome_page_; + //! The login screen. + LoginPage *login_page_; + //! The register page. + RegisterPage *register_page_; + //! A stacked widget that handles the transitions between widgets. + QStackedWidget *pageStack_; + //! The main chat area. + ChatPage *chat_page_; + UserSettingsPage *userSettingsPage_; + QSharedPointer<UserSettings> userSettings_; + //! Tray icon that shows the unread message count. + TrayIcon *trayIcon_; + //! Notifications display. + SnackBar *snackBar_ = nullptr; + //! Overlay modal used to project other widgets. + OverlayModal *modal_ = nullptr; + LoadingIndicator *spinner_ = nullptr; }; diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp index 196a9322..2ceb53a8 100644 --- a/src/MatrixClient.cpp +++ b/src/MatrixClient.cpp @@ -37,31 +37,31 @@ namespace http { mtx::http::Client * client() { - return client_.get(); + return client_.get(); } bool is_logged_in() { - return !client_->access_token().empty(); + return !client_->access_token().empty(); } void init() { - qRegisterMetaType<mtx::responses::Login>(); - qRegisterMetaType<mtx::responses::Messages>(); - qRegisterMetaType<mtx::responses::Notifications>(); - qRegisterMetaType<mtx::responses::Rooms>(); - qRegisterMetaType<mtx::responses::Sync>(); - qRegisterMetaType<mtx::responses::JoinedGroups>(); - qRegisterMetaType<mtx::responses::GroupProfile>(); - qRegisterMetaType<std::string>(); - qRegisterMetaType<nlohmann::json>(); - qRegisterMetaType<std::vector<std::string>>(); - qRegisterMetaType<std::vector<QString>>(); - qRegisterMetaType<std::map<QString, bool>>("std::map<QString, bool>"); - qRegisterMetaType<std::set<QString>>(); + qRegisterMetaType<mtx::responses::Login>(); + qRegisterMetaType<mtx::responses::Messages>(); + qRegisterMetaType<mtx::responses::Notifications>(); + qRegisterMetaType<mtx::responses::Rooms>(); + qRegisterMetaType<mtx::responses::Sync>(); + qRegisterMetaType<mtx::responses::JoinedGroups>(); + qRegisterMetaType<mtx::responses::GroupProfile>(); + qRegisterMetaType<std::string>(); + qRegisterMetaType<nlohmann::json>(); + qRegisterMetaType<std::vector<std::string>>(); + qRegisterMetaType<std::vector<QString>>(); + qRegisterMetaType<std::map<QString, bool>>("std::map<QString, bool>"); + qRegisterMetaType<std::set<QString>>(); } } // namespace http diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 0c0f0cdd..34730e9a 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -15,98 +15,96 @@ MemberList::MemberList(const QString &room_id, QObject *parent) : QAbstractListModel{parent} , room_id_{room_id} { - try { - info_ = cache::singleRoomInfo(room_id_.toStdString()); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve room info from cache: {}", - room_id_.toStdString()); - } + try { + info_ = cache::singleRoomInfo(room_id_.toStdString()); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); + } - try { - auto members = cache::getMembers(room_id_.toStdString()); - addUsers(members); - numUsersLoaded_ = members.size(); - } catch (const lmdb::error &e) { - nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); - } + try { + auto members = cache::getMembers(room_id_.toStdString()); + addUsers(members); + numUsersLoaded_ = members.size(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); + } } void MemberList::addUsers(const std::vector<RoomMember> &members) { - beginInsertRows( - QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); - for (const auto &member : members) - m_memberList.push_back( - {member, - ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( - member.user_id)}); + for (const auto &member : members) + m_memberList.push_back( + {member, + ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( + member.user_id)}); - endInsertRows(); + endInsertRows(); } QHash<int, QByteArray> MemberList::roleNames() const { - return { - {Mxid, "mxid"}, - {DisplayName, "displayName"}, - {AvatarUrl, "avatarUrl"}, - {Trustlevel, "trustlevel"}, - }; + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Trustlevel, "trustlevel"}, + }; } QVariant MemberList::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) - return {}; + if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) + return {}; - switch (role) { - case Mxid: - return m_memberList[index.row()].first.user_id; - case DisplayName: - return m_memberList[index.row()].first.display_name; - case AvatarUrl: - return m_memberList[index.row()].second; - case Trustlevel: { - auto stat = - cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString()); + switch (role) { + case Mxid: + return m_memberList[index.row()].first.user_id; + case DisplayName: + return m_memberList[index.row()].first.display_name; + case AvatarUrl: + return m_memberList[index.row()].second; + case Trustlevel: { + auto stat = + cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString()); - if (!stat) - return crypto::Unverified; - if (stat->unverified_device_count) - return crypto::Unverified; - else - return stat->user_verified; - } - default: - return {}; - } + if (!stat) + return crypto::Unverified; + if (stat->unverified_device_count) + return crypto::Unverified; + else + return stat->user_verified; + } + default: + return {}; + } } bool MemberList::canFetchMore(const QModelIndex &) const { - const size_t numMembers = rowCount(); - if (numMembers > 1 && numMembers < info_.member_count) - return true; - else - return false; + const size_t numMembers = rowCount(); + if (numMembers > 1 && numMembers < info_.member_count) + return true; + else + return false; } void MemberList::fetchMore(const QModelIndex &) { - loadingMoreMembers_ = true; - emit loadingMoreMembersChanged(); + loadingMoreMembers_ = true; + emit loadingMoreMembersChanged(); - auto members = cache::getMembers(room_id_.toStdString(), rowCount()); - addUsers(members); - numUsersLoaded_ += members.size(); - emit numUsersLoadedChanged(); + auto members = cache::getMembers(room_id_.toStdString(), rowCount()); + addUsers(members); + numUsersLoaded_ += members.size(); + emit numUsersLoadedChanged(); - loadingMoreMembers_ = false; - emit loadingMoreMembersChanged(); + loadingMoreMembers_ = false; + emit loadingMoreMembersChanged(); } diff --git a/src/MemberList.h b/src/MemberList.h index cffcd83d..b16ac983 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -10,59 +10,59 @@ class MemberList : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) - Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) - Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) - Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) + Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - Trustlevel, - }; - MemberList(const QString &room_id, QObject *parent = nullptr); + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Trustlevel, + }; + MemberList(const QString &room_id, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - Q_UNUSED(parent) - return static_cast<int>(m_memberList.size()); - } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return static_cast<int>(m_memberList.size()); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QString roomName() const { return QString::fromStdString(info_.name); } - int memberCount() const { return info_.member_count; } - QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } - QString roomId() const { return room_id_; } - int numUsersLoaded() const { return numUsersLoaded_; } - bool loadingMoreMembers() const { return loadingMoreMembers_; } + QString roomName() const { return QString::fromStdString(info_.name); } + int memberCount() const { return info_.member_count; } + QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } + QString roomId() const { return room_id_; } + int numUsersLoaded() const { return numUsersLoaded_; } + bool loadingMoreMembers() const { return loadingMoreMembers_; } signals: - void roomNameChanged(); - void memberCountChanged(); - void avatarUrlChanged(); - void roomIdChanged(); - void numUsersLoadedChanged(); - void loadingMoreMembersChanged(); + void roomNameChanged(); + void memberCountChanged(); + void avatarUrlChanged(); + void roomIdChanged(); + void numUsersLoadedChanged(); + void loadingMoreMembersChanged(); public slots: - void addUsers(const std::vector<RoomMember> &users); + void addUsers(const std::vector<RoomMember> &users); protected: - bool canFetchMore(const QModelIndex &) const override; - void fetchMore(const QModelIndex &) override; + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; private: - QVector<QPair<RoomMember, QString>> m_memberList; - QString room_id_; - RoomInfo info_; - int numUsersLoaded_{0}; - bool loadingMoreMembers_{false}; + QVector<QPair<RoomMember, QString>> m_memberList; + QString room_id_; + RoomInfo info_; + int numUsersLoaded_{0}; + bool loadingMoreMembers_{false}; }; diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 056374a9..5d0ee0be 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -24,70 +24,70 @@ QHash<QString, mtx::crypto::EncryptedFile> infos; QQuickImageResponse * MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { - auto id_ = id; - bool crop = true; - double radius = 0; - - auto queryStart = id.lastIndexOf('?'); - if (queryStart != -1) { - id_ = id.left(queryStart); - auto query = id.midRef(queryStart + 1); - auto queryBits = query.split('&'); - - for (auto b : queryBits) { - if (b == "scale") { - crop = false; - } else if (b.startsWith("radius=")) { - radius = b.mid(7).toDouble(); - } - } + auto id_ = id; + bool crop = true; + double radius = 0; + + auto queryStart = id.lastIndexOf('?'); + if (queryStart != -1) { + id_ = id.left(queryStart); + auto query = id.midRef(queryStart + 1); + auto queryBits = query.split('&'); + + for (auto b : queryBits) { + if (b == "scale") { + crop = false; + } else if (b.startsWith("radius=")) { + radius = b.mid(7).toDouble(); + } } + } - MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize); - pool.start(response); - return response; + MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize); + pool.start(response); + return response; } void MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info) { - infos.insert(QString::fromStdString(info.url), info); + infos.insert(QString::fromStdString(info.url), info); } void MxcImageResponse::run() { - MxcImageProvider::download( - m_id, - m_requestedSize, - [this](QString, QSize, QImage image, QString) { - if (image.isNull()) { - m_error = "Failed to download image."; - } else { - m_image = image; - } - emit finished(); - }, - m_crop, - m_radius); + MxcImageProvider::download( + m_id, + m_requestedSize, + [this](QString, QSize, QImage image, QString) { + if (image.isNull()) { + m_error = "Failed to download image."; + } else { + m_image = image; + } + emit finished(); + }, + m_crop, + m_radius); } static QImage clipRadius(QImage img, double radius) { - QImage out(img.size(), QImage::Format_ARGB32_Premultiplied); - out.fill(Qt::transparent); + QImage out(img.size(), QImage::Format_ARGB32_Premultiplied); + out.fill(Qt::transparent); - QPainter painter(&out); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - QPainterPath ppath; - ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); + QPainterPath ppath; + ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); - painter.setClipPath(ppath); - painter.drawImage(img.rect(), img); + painter.setClipPath(ppath); + painter.drawImage(img.rect(), img); - return out; + return out; } void @@ -97,187 +97,165 @@ MxcImageProvider::download(const QString &id, bool crop, double radius) { - std::optional<mtx::crypto::EncryptedFile> encryptionInfo; - auto temp = infos.find("mxc://" + id); - if (temp != infos.end()) - encryptionInfo = *temp; - - if (requestedSize.isValid() && !encryptionInfo) { - QString fileName = - QString("%1_%2x%3_%4_radius%5") - .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | - QByteArray::OmitTrailingEquals))) - .arg(requestedSize.width()) - .arg(requestedSize.height()) - .arg(crop ? "crop" : "scale") - .arg(radius); - QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/media_cache", - fileName); - QDir().mkpath(fileInfo.absolutePath()); - - if (fileInfo.exists()) { - QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (!image.isNull()) { - image = image.scaled( - requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - if (!image.isNull()) { - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - return; - } - } + std::optional<mtx::crypto::EncryptedFile> encryptionInfo; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + encryptionInfo = *temp; + + if (requestedSize.isValid() && !encryptionInfo) { + QString fileName = QString("%1_%2x%3_%4_radius%5") + .arg(QString::fromUtf8(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) + .arg(requestedSize.width()) + .arg(requestedSize.height()) + .arg(crop ? "crop" : "scale") + .arg(radius); + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); + + if (fileInfo.exists()) { + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + image = image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (radius != 0) { + image = clipRadius(std::move(image), radius); } - mtx::http::ThumbOpts opts; - opts.mxc_url = "mxc://" + id.toStdString(); - opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; - opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; - opts.method = crop ? "crop" : "scale"; - http::client()->get_thumbnail( - opts, - [fileInfo, requestedSize, radius, then, id](const std::string &res, - mtx::http::RequestErr err) { - if (err || res.empty()) { - then(id, QSize(), {}, ""); - - return; - } - - auto data = QByteArray(res.data(), (int)res.size()); - QImage image = utils::readImage(data); - if (!image.isNull()) { - image = image.scaled( - requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - } - image.setText("mxc url", "mxc://" + id); - if (image.save(fileInfo.absoluteFilePath(), "png")) - nhlog::ui()->debug("Wrote: {}", - fileInfo.absoluteFilePath().toStdString()); - else - nhlog::ui()->debug("Failed to write: {}", - fileInfo.absoluteFilePath().toStdString()); - - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - }); - } else { - try { - QString fileName = - QString("%1_radius%2") - .arg(QString::fromUtf8(id.toUtf8().toBase64( - QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) - .arg(radius); - - QFileInfo fileInfo( - QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - "/media_cache", - fileName); - QDir().mkpath(fileInfo.absolutePath()); - - if (fileInfo.exists()) { - if (encryptionInfo) { - QFile f(fileInfo.absoluteFilePath()); - f.open(QIODevice::ReadOnly); - - QByteArray fileData = f.readAll(); - auto tempData = - mtx::crypto::to_string(mtx::crypto::decrypt_file( - fileData.toStdString(), encryptionInfo.value())); - auto data = - QByteArray(tempData.data(), (int)tempData.size()); - QImage image = utils::readImage(data); - image.setText("mxc url", "mxc://" + id); - if (!image.isNull()) { - if (radius != 0) { - image = - clipRadius(std::move(image), radius); - } - - then(id, - requestedSize, - image, - fileInfo.absoluteFilePath()); - return; - } - } else { - QImage image = - utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (!image.isNull()) { - if (radius != 0) { - image = - clipRadius(std::move(image), radius); - } - - then(id, - requestedSize, - image, - fileInfo.absoluteFilePath()); - return; - } - } + if (!image.isNull()) { + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + } + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + id.toStdString(); + opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; + opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; + opts.method = crop ? "crop" : "scale"; + http::client()->get_thumbnail( + opts, + [fileInfo, requestedSize, radius, then, id](const std::string &res, + mtx::http::RequestErr err) { + if (err || res.empty()) { + then(id, QSize(), {}, ""); + + return; + } + + auto data = QByteArray(res.data(), (int)res.size()); + QImage image = utils::readImage(data); + if (!image.isNull()) { + image = + image.scaled(requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + } + image.setText("mxc url", "mxc://" + id); + if (image.save(fileInfo.absoluteFilePath(), "png")) + nhlog::ui()->debug("Wrote: {}", fileInfo.absoluteFilePath().toStdString()); + else + nhlog::ui()->debug("Failed to write: {}", + fileInfo.absoluteFilePath().toStdString()); + + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + }); + } else { + try { + QString fileName = QString("%1_radius%2") + .arg(QString::fromUtf8(id.toUtf8().toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) + .arg(radius); + + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/media_cache", + fileName); + QDir().mkpath(fileInfo.absolutePath()); + + if (fileInfo.exists()) { + if (encryptionInfo) { + QFile f(fileInfo.absoluteFilePath()); + f.open(QIODevice::ReadOnly); + + QByteArray fileData = f.readAll(); + auto tempData = mtx::crypto::to_string( + mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value())); + auto data = QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + image.setText("mxc url", "mxc://" + id); + if (!image.isNull()) { + if (radius != 0) { + image = clipRadius(std::move(image), radius); } - http::client()->download( - "mxc://" + id.toStdString(), - [fileInfo, requestedSize, then, id, radius, encryptionInfo]( - const std::string &res, - const std::string &, - const std::string &originalFilename, - mtx::http::RequestErr err) { - if (err) { - then(id, QSize(), {}, ""); - return; - } - - auto tempData = res; - QFile f(fileInfo.absoluteFilePath()); - if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) { - then(id, QSize(), {}, ""); - return; - } - f.write(tempData.data(), tempData.size()); - f.close(); - - if (encryptionInfo) { - tempData = - mtx::crypto::to_string(mtx::crypto::decrypt_file( - tempData, encryptionInfo.value())); - auto data = - QByteArray(tempData.data(), (int)tempData.size()); - QImage image = utils::readImage(data); - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - image.setText("original filename", - QString::fromStdString(originalFilename)); - image.setText("mxc url", "mxc://" + id); - then( - id, requestedSize, image, fileInfo.absoluteFilePath()); - return; - } - - QImage image = - utils::readImageFromFile(fileInfo.absoluteFilePath()); - if (radius != 0) { - image = clipRadius(std::move(image), radius); - } - - image.setText("original filename", - QString::fromStdString(originalFilename)); - image.setText("mxc url", "mxc://" + id); - then(id, requestedSize, image, fileInfo.absoluteFilePath()); - }); - } catch (std::exception &e) { - nhlog::net()->error("Exception while downloading media: {}", e.what()); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + } else { + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (!image.isNull()) { + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } } + } + + http::client()->download( + "mxc://" + id.toStdString(), + [fileInfo, requestedSize, then, id, radius, encryptionInfo]( + const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + then(id, QSize(), {}, ""); + return; + } + + auto tempData = res; + QFile f(fileInfo.absoluteFilePath()); + if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) { + then(id, QSize(), {}, ""); + return; + } + f.write(tempData.data(), tempData.size()); + f.close(); + + if (encryptionInfo) { + tempData = mtx::crypto::to_string( + mtx::crypto::decrypt_file(tempData, encryptionInfo.value())); + auto data = QByteArray(tempData.data(), (int)tempData.size()); + QImage image = utils::readImage(data); + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + image.setText("original filename", QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + return; + } + + QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath()); + if (radius != 0) { + image = clipRadius(std::move(image), radius); + } + + image.setText("original filename", QString::fromStdString(originalFilename)); + image.setText("mxc url", "mxc://" + id); + then(id, requestedSize, image, fileInfo.absoluteFilePath()); + }); + } catch (std::exception &e) { + nhlog::net()->error("Exception while downloading media: {}", e.what()); } + } } diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 6de83c0e..3cf5bbf4 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -19,46 +19,46 @@ class MxcImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize) - : m_id(id) - , m_requestedSize(requestedSize) - , m_crop(crop) - , m_radius(radius) - { - setAutoDelete(false); - } + MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + , m_crop(crop) + , m_radius(radius) + { + setAutoDelete(false); + } - QQuickTextureFactory *textureFactory() const override - { - return QQuickTextureFactory::textureFactoryForImage(m_image); - } - QString errorString() const override { return m_error; } + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } - void run() override; + void run() override; - QString m_id, m_error; - QSize m_requestedSize; - QImage m_image; - bool m_crop; - double m_radius; + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; + bool m_crop; + double m_radius; }; class MxcImageProvider : public QObject , public QQuickAsyncImageProvider { - Q_OBJECT + Q_OBJECT public slots: - QQuickImageResponse *requestImageResponse(const QString &id, - const QSize &requestedSize) override; + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override; - static void addEncryptionInfo(mtx::crypto::EncryptedFile info); - static void download(const QString &id, - const QSize &requestedSize, - std::function<void(QString, QSize, QImage, QString)> then, - bool crop = true, - double radius = 0); + static void addEncryptionInfo(mtx::crypto::EncryptedFile info); + static void download(const QString &id, + const QSize &requestedSize, + std::function<void(QString, QSize, QImage, QString)> then, + bool crop = true, + double radius = 0); private: - QThreadPool pool; + QThreadPool pool; }; diff --git a/src/Olm.cpp b/src/Olm.cpp index 72dc582f..60460b5c 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -39,433 +39,385 @@ backup_session_key(const MegolmSessionIndex &idx, void from_json(const nlohmann::json &obj, OlmMessage &msg) { - if (obj.at("type") != "m.room.encrypted") - throw std::invalid_argument("invalid type for olm message"); + if (obj.at("type") != "m.room.encrypted") + throw std::invalid_argument("invalid type for olm message"); - if (obj.at("content").at("algorithm") != OLM_ALGO) - throw std::invalid_argument("invalid algorithm for olm message"); + if (obj.at("content").at("algorithm") != OLM_ALGO) + throw std::invalid_argument("invalid algorithm for olm message"); - msg.sender = obj.at("sender"); - msg.sender_key = obj.at("content").at("sender_key"); - msg.ciphertext = obj.at("content") - .at("ciphertext") - .get<std::map<std::string, mtx::events::msg::OlmCipherContent>>(); + msg.sender = obj.at("sender"); + msg.sender_key = obj.at("content").at("sender_key"); + msg.ciphertext = obj.at("content") + .at("ciphertext") + .get<std::map<std::string, mtx::events::msg::OlmCipherContent>>(); } mtx::crypto::OlmClient * client() { - return client_.get(); + return client_.get(); } static void handle_secret_request(const mtx::events::DeviceEvent<mtx::events::msg::SecretRequest> *e, const std::string &sender) { - using namespace mtx::events; + using namespace mtx::events; - if (e->content.action != mtx::events::msg::RequestAction::Request) - return; + if (e->content.action != mtx::events::msg::RequestAction::Request) + return; - auto local_user = http::client()->user_id(); + auto local_user = http::client()->user_id(); - if (sender != local_user.to_string()) - return; + if (sender != local_user.to_string()) + return; - auto verificationStatus = cache::verificationStatus(local_user.to_string()); + auto verificationStatus = cache::verificationStatus(local_user.to_string()); - if (!verificationStatus) - return; + if (!verificationStatus) + return; - auto deviceKeys = cache::userKeys(local_user.to_string()); - if (!deviceKeys) - return; + auto deviceKeys = cache::userKeys(local_user.to_string()); + if (!deviceKeys) + return; - if (std::find(verificationStatus->verified_devices.begin(), - verificationStatus->verified_devices.end(), - e->content.requesting_device_id) == - verificationStatus->verified_devices.end()) - return; + if (std::find(verificationStatus->verified_devices.begin(), + verificationStatus->verified_devices.end(), + e->content.requesting_device_id) == verificationStatus->verified_devices.end()) + return; - // this is a verified device - mtx::events::DeviceEvent<mtx::events::msg::SecretSend> secretSend; - secretSend.type = EventType::SecretSend; - secretSend.content.request_id = e->content.request_id; + // this is a verified device + mtx::events::DeviceEvent<mtx::events::msg::SecretSend> secretSend; + secretSend.type = EventType::SecretSend; + secretSend.content.request_id = e->content.request_id; - auto secret = cache::client()->secret(e->content.name); - if (!secret) - return; - secretSend.content.secret = secret.value(); + auto secret = cache::client()->secret(e->content.name); + if (!secret) + return; + secretSend.content.secret = secret.value(); - send_encrypted_to_device_messages( - {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend); + send_encrypted_to_device_messages( + {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend); - nhlog::net()->info("Sent secret '{}' to ({},{})", - e->content.name, - local_user.to_string(), - e->content.requesting_device_id); + nhlog::net()->info("Sent secret '{}' to ({},{})", + e->content.name, + local_user.to_string(), + e->content.requesting_device_id); } void handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs) { - if (msgs.empty()) - return; - nhlog::crypto()->info("received {} to_device messages", msgs.size()); - nlohmann::json j_msg; - - for (const auto &msg : msgs) { - j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg)); - if (j_msg.count("type") == 0) { - nhlog::crypto()->warn("received message with no type field: {}", - j_msg.dump(2)); - continue; - } + if (msgs.empty()) + return; + nhlog::crypto()->info("received {} to_device messages", msgs.size()); + nlohmann::json j_msg; + + for (const auto &msg : msgs) { + j_msg = std::visit([](auto &e) { return json(e); }, std::move(msg)); + if (j_msg.count("type") == 0) { + nhlog::crypto()->warn("received message with no type field: {}", j_msg.dump(2)); + continue; + } - std::string msg_type = j_msg.at("type"); - - if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { - try { - olm::OlmMessage olm_msg = j_msg; - cache::client()->query_keys( - olm_msg.sender, - [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) { - if (e) { - nhlog::crypto()->error( - "Failed to query user keys, dropping olm " - "message"); - return; - } - handle_olm_message(std::move(olm_msg), userKeys); - }); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->warn( - "parsing error for olm message: {} {}", e.what(), j_msg.dump(2)); - } catch (const std::invalid_argument &e) { - nhlog::crypto()->warn("validation error for olm message: {} {}", - e.what(), - j_msg.dump(2)); - } + std::string msg_type = j_msg.at("type"); - } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { - nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2)); - try { - mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> req = j_msg; - if (req.content.action == mtx::events::msg::RequestAction::Request) - handle_key_request_message(req); - else - nhlog::crypto()->warn( - "ignore key request (unhandled action): {}", + if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { + try { + olm::OlmMessage olm_msg = j_msg; + cache::client()->query_keys( + olm_msg.sender, [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) { + if (e) { + nhlog::crypto()->error("Failed to query user keys, dropping olm " + "message"); + return; + } + handle_olm_message(std::move(olm_msg), userKeys); + }); + } catch (const nlohmann::json::exception &e) { + nhlog::crypto()->warn( + "parsing error for olm message: {} {}", e.what(), j_msg.dump(2)); + } catch (const std::invalid_argument &e) { + nhlog::crypto()->warn( + "validation error for olm message: {} {}", e.what(), j_msg.dump(2)); + } + + } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { + nhlog::crypto()->warn("handling key request event: {}", j_msg.dump(2)); + try { + mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> req = j_msg; + if (req.content.action == mtx::events::msg::RequestAction::Request) + handle_key_request_message(req); + else + nhlog::crypto()->warn("ignore key request (unhandled action): {}", req.content.request_id); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->warn( - "parsing error for key_request message: {} {}", - e.what(), - j_msg.dump(2)); - } - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) { - auto message = std::get< - mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg); - ChatPage::instance()->receivedDeviceVerificationAccept(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) { - auto message = std::get< - mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg); - ChatPage::instance()->receivedDeviceVerificationRequest(message.content, - message.sender); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) { - auto message = std::get< - mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg); - ChatPage::instance()->receivedDeviceVerificationCancel(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) { - auto message = - std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>( - msg); - ChatPage::instance()->receivedDeviceVerificationKey(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) { - auto message = - std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>( - msg); - ChatPage::instance()->receivedDeviceVerificationMac(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) { - auto message = std::get< - mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg); - ChatPage::instance()->receivedDeviceVerificationStart(message.content, - message.sender); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) { - auto message = std::get< - mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg); - ChatPage::instance()->receivedDeviceVerificationReady(message.content); - } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) { - auto message = - std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>( - msg); - ChatPage::instance()->receivedDeviceVerificationDone(message.content); - } else if (auto e = - std::get_if<mtx::events::DeviceEvent<mtx::events::msg::SecretRequest>>( - &msg)) { - handle_secret_request(e, e->sender); - } else { - nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2)); - } + } catch (const nlohmann::json::exception &e) { + nhlog::crypto()->warn( + "parsing error for key_request message: {} {}", e.what(), j_msg.dump(2)); + } + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg); + ChatPage::instance()->receivedDeviceVerificationAccept(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg); + ChatPage::instance()->receivedDeviceVerificationRequest(message.content, + message.sender); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg); + ChatPage::instance()->receivedDeviceVerificationCancel(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>(msg); + ChatPage::instance()->receivedDeviceVerificationKey(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>(msg); + ChatPage::instance()->receivedDeviceVerificationMac(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg); + ChatPage::instance()->receivedDeviceVerificationStart(message.content, message.sender); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg); + ChatPage::instance()->receivedDeviceVerificationReady(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(msg); + ChatPage::instance()->receivedDeviceVerificationDone(message.content); + } else if (auto e = + std::get_if<mtx::events::DeviceEvent<mtx::events::msg::SecretRequest>>(&msg)) { + handle_secret_request(e, e->sender); + } else { + nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2)); } + } } void handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKeys) { - nhlog::crypto()->info("sender : {}", msg.sender); - nhlog::crypto()->info("sender_key: {}", msg.sender_key); + nhlog::crypto()->info("sender : {}", msg.sender); + nhlog::crypto()->info("sender_key: {}", msg.sender_key); - if (msg.sender_key == olm::client()->identity_keys().ed25519) { - nhlog::crypto()->warn("Ignoring olm message from ourselves!"); - return; + if (msg.sender_key == olm::client()->identity_keys().ed25519) { + nhlog::crypto()->warn("Ignoring olm message from ourselves!"); + return; + } + + const auto my_key = olm::client()->identity_keys().curve25519; + + bool failed_decryption = false; + + for (const auto &cipher : msg.ciphertext) { + // We skip messages not meant for the current device. + if (cipher.first != my_key) { + nhlog::crypto()->debug( + "Skipping message for {} since we are {}.", cipher.first, my_key); + continue; } - const auto my_key = olm::client()->identity_keys().curve25519; + const auto type = cipher.second.type; + nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); - bool failed_decryption = false; + auto payload = try_olm_decryption(msg.sender_key, cipher.second); - for (const auto &cipher : msg.ciphertext) { - // We skip messages not meant for the current device. - if (cipher.first != my_key) { - nhlog::crypto()->debug( - "Skipping message for {} since we are {}.", cipher.first, my_key); - continue; - } + if (payload.is_null()) { + // Check for PRE_KEY message + if (cipher.second.type == 0) { + payload = handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); + } else { + nhlog::crypto()->error("Undecryptable olm message!"); + failed_decryption = true; + continue; + } + } - const auto type = cipher.second.type; - nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); + if (!payload.is_null()) { + mtx::events::collections::DeviceEvents device_event; + + // Other properties are included in order to prevent an attacker from + // publishing someone else's curve25519 keys as their own and subsequently + // claiming to have sent messages which they didn't. sender must correspond + // to the user who sent the event, recipient to the local user, and + // recipient_keys to the local ed25519 key. + std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"]; + if (receiver_ed25519.empty() || + receiver_ed25519 != olm::client()->identity_keys().ed25519) { + nhlog::crypto()->warn("Decrypted event doesn't include our ed25519: {}", + payload.dump()); + return; + } + std::string receiver = payload["recipient"]; + if (receiver.empty() || receiver != http::client()->user_id().to_string()) { + nhlog::crypto()->warn("Decrypted event doesn't include our user_id: {}", + payload.dump()); + return; + } + + // Clients must confirm that the sender_key and the ed25519 field value + // under the keys property match the keys returned by /keys/query for the + // given user, and must also verify the signature of the payload. Without + // this check, a client cannot be sure that the sender device owns the + // private part of the ed25519 key it claims to have in the Olm payload. + // This is crucial when the ed25519 key corresponds to a verified device. + std::string sender_ed25519 = payload["keys"]["ed25519"]; + if (sender_ed25519.empty()) { + nhlog::crypto()->warn("Decrypted event doesn't include sender ed25519: {}", + payload.dump()); + return; + } - auto payload = try_olm_decryption(msg.sender_key, cipher.second); + bool from_their_device = false; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + auto c_key = key.keys.find("curve25519:" + device_id); + auto e_key = key.keys.find("ed25519:" + device_id); - if (payload.is_null()) { - // Check for PRE_KEY message - if (cipher.second.type == 0) { - payload = handle_pre_key_olm_message( - msg.sender, msg.sender_key, cipher.second); - } else { - nhlog::crypto()->error("Undecryptable olm message!"); - failed_decryption = true; - continue; - } + if (c_key == key.keys.end() || e_key == key.keys.end()) { + nhlog::crypto()->warn("Skipping device {} as we have no keys for it.", + device_id); + } else if (c_key->second == msg.sender_key && e_key->second == sender_ed25519) { + from_their_device = true; + break; } + } + if (!from_their_device) { + nhlog::crypto()->warn("Decrypted event isn't sent from a device " + "listed by that user! {}", + payload.dump()); + return; + } + + { + std::string msg_type = payload["type"]; + json event_array = json::array(); + event_array.push_back(payload); + + std::vector<mtx::events::collections::DeviceEvents> temp_events; + mtx::responses::utils::parse_device_events(event_array, temp_events); + if (temp_events.empty()) { + nhlog::crypto()->warn("Decrypted unknown event: {}", payload.dump()); + return; + } + device_event = temp_events.at(0); + } + + using namespace mtx::events; + if (auto e1 = std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationAccept(e1->content); + } else if (auto e2 = + std::get_if<DeviceEvent<msg::KeyVerificationRequest>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationRequest(e2->content, e2->sender); + } else if (auto e3 = + std::get_if<DeviceEvent<msg::KeyVerificationCancel>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationCancel(e3->content); + } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationKey(e4->content); + } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationMac(e5->content); + } else if (auto e6 = + std::get_if<DeviceEvent<msg::KeyVerificationStart>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationStart(e6->content, e6->sender); + } else if (auto e7 = + std::get_if<DeviceEvent<msg::KeyVerificationReady>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationReady(e7->content); + } else if (auto e8 = + std::get_if<DeviceEvent<msg::KeyVerificationDone>>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationDone(e8->content); + } else if (auto roomKey = std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) { + create_inbound_megolm_session(*roomKey, msg.sender_key, sender_ed25519); + } else if (auto forwardedRoomKey = + std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(&device_event)) { + forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back(msg.sender_key); + import_inbound_megolm_session(*forwardedRoomKey); + } else if (auto e = std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) { + auto local_user = http::client()->user_id(); + + if (msg.sender != local_user.to_string()) + return; + + auto secret_name = request_id_to_secret_name.find(e->content.request_id); + + if (secret_name != request_id_to_secret_name.end()) { + nhlog::crypto()->info("Received secret: {}", secret_name->second); + + mtx::events::msg::SecretRequest secretRequest{}; + secretRequest.action = mtx::events::msg::RequestAction::Cancellation; + secretRequest.requesting_device_id = http::client()->device_id(); + secretRequest.request_id = e->content.request_id; + + auto verificationStatus = cache::verificationStatus(local_user.to_string()); + + if (!verificationStatus) + return; - if (!payload.is_null()) { - mtx::events::collections::DeviceEvents device_event; - - // Other properties are included in order to prevent an attacker from - // publishing someone else's curve25519 keys as their own and subsequently - // claiming to have sent messages which they didn't. sender must correspond - // to the user who sent the event, recipient to the local user, and - // recipient_keys to the local ed25519 key. - std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"]; - if (receiver_ed25519.empty() || - receiver_ed25519 != olm::client()->identity_keys().ed25519) { - nhlog::crypto()->warn( - "Decrypted event doesn't include our ed25519: {}", - payload.dump()); - return; - } - std::string receiver = payload["recipient"]; - if (receiver.empty() || receiver != http::client()->user_id().to_string()) { - nhlog::crypto()->warn( - "Decrypted event doesn't include our user_id: {}", - payload.dump()); - return; - } - - // Clients must confirm that the sender_key and the ed25519 field value - // under the keys property match the keys returned by /keys/query for the - // given user, and must also verify the signature of the payload. Without - // this check, a client cannot be sure that the sender device owns the - // private part of the ed25519 key it claims to have in the Olm payload. - // This is crucial when the ed25519 key corresponds to a verified device. - std::string sender_ed25519 = payload["keys"]["ed25519"]; - if (sender_ed25519.empty()) { - nhlog::crypto()->warn( - "Decrypted event doesn't include sender ed25519: {}", - payload.dump()); - return; + auto deviceKeys = cache::userKeys(local_user.to_string()); + std::string sender_device_id; + if (deviceKeys) { + for (auto &[dev, key] : deviceKeys->device_keys) { + if (key.keys["curve25519:" + dev] == msg.sender_key) { + sender_device_id = dev; + break; + } } + } - bool from_their_device = false; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - auto c_key = key.keys.find("curve25519:" + device_id); - auto e_key = key.keys.find("ed25519:" + device_id); + std::map<mtx::identifiers::User, + std::map<std::string, mtx::events::msg::SecretRequest>> + body; - if (c_key == key.keys.end() || e_key == key.keys.end()) { - nhlog::crypto()->warn( - "Skipping device {} as we have no keys for it.", - device_id); - } else if (c_key->second == msg.sender_key && - e_key->second == sender_ed25519) { - from_their_device = true; - break; - } - } - if (!from_their_device) { - nhlog::crypto()->warn("Decrypted event isn't sent from a device " - "listed by that user! {}", - payload.dump()); - return; - } + for (const auto &dev : verificationStatus->verified_devices) { + if (dev != secretRequest.requesting_device_id && dev != sender_device_id) + body[local_user][dev] = secretRequest; + } - { - std::string msg_type = payload["type"]; - json event_array = json::array(); - event_array.push_back(payload); - - std::vector<mtx::events::collections::DeviceEvents> temp_events; - mtx::responses::utils::parse_device_events(event_array, - temp_events); - if (temp_events.empty()) { - nhlog::crypto()->warn("Decrypted unknown event: {}", - payload.dump()); - return; - } - device_event = temp_events.at(0); - } + http::client()->send_to_device<mtx::events::msg::SecretRequest>( + http::client()->generate_txn_id(), + body, + [name = secret_name->second](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to send request cancellation " + "for secrect " + "'{}'", + name); + } + }); - using namespace mtx::events; - if (auto e1 = - std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) { - ChatPage::instance()->receivedDeviceVerificationAccept(e1->content); - } else if (auto e2 = std::get_if<DeviceEvent<msg::KeyVerificationRequest>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationRequest(e2->content, - e2->sender); - } else if (auto e3 = std::get_if<DeviceEvent<msg::KeyVerificationCancel>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationCancel(e3->content); - } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationKey(e4->content); - } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationMac(e5->content); - } else if (auto e6 = std::get_if<DeviceEvent<msg::KeyVerificationStart>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationStart(e6->content, - e6->sender); - } else if (auto e7 = std::get_if<DeviceEvent<msg::KeyVerificationReady>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationReady(e7->content); - } else if (auto e8 = std::get_if<DeviceEvent<msg::KeyVerificationDone>>( - &device_event)) { - ChatPage::instance()->receivedDeviceVerificationDone(e8->content); - } else if (auto roomKey = - std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) { - create_inbound_megolm_session( - *roomKey, msg.sender_key, sender_ed25519); - } else if (auto forwardedRoomKey = - std::get_if<DeviceEvent<msg::ForwardedRoomKey>>( - &device_event)) { - forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back( - msg.sender_key); - import_inbound_megolm_session(*forwardedRoomKey); - } else if (auto e = - std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) { - auto local_user = http::client()->user_id(); - - if (msg.sender != local_user.to_string()) - return; - - auto secret_name = - request_id_to_secret_name.find(e->content.request_id); - - if (secret_name != request_id_to_secret_name.end()) { - nhlog::crypto()->info("Received secret: {}", - secret_name->second); - - mtx::events::msg::SecretRequest secretRequest{}; - secretRequest.action = - mtx::events::msg::RequestAction::Cancellation; - secretRequest.requesting_device_id = - http::client()->device_id(); - secretRequest.request_id = e->content.request_id; - - auto verificationStatus = - cache::verificationStatus(local_user.to_string()); - - if (!verificationStatus) - return; - - auto deviceKeys = cache::userKeys(local_user.to_string()); - std::string sender_device_id; - if (deviceKeys) { - for (auto &[dev, key] : deviceKeys->device_keys) { - if (key.keys["curve25519:" + dev] == - msg.sender_key) { - sender_device_id = dev; - break; - } - } - } - - std::map< - mtx::identifiers::User, - std::map<std::string, mtx::events::msg::SecretRequest>> - body; - - for (const auto &dev : - verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id && - dev != sender_device_id) - body[local_user][dev] = secretRequest; - } - - http::client() - ->send_to_device<mtx::events::msg::SecretRequest>( - http::client()->generate_txn_id(), - body, - [name = - secret_name->second](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "Failed to send request cancellation " - "for secrect " - "'{}'", - name); - } - }); - - nhlog::crypto()->info("Storing secret {}", - secret_name->second); - cache::client()->storeSecret(secret_name->second, - e->content.secret); - - request_id_to_secret_name.erase(secret_name); - } - - } else if (auto sec_req = - std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) { - handle_secret_request(sec_req, msg.sender); - } + nhlog::crypto()->info("Storing secret {}", secret_name->second); + cache::client()->storeSecret(secret_name->second, e->content.secret); - return; - } else { - failed_decryption = true; + request_id_to_secret_name.erase(secret_name); } - } - if (failed_decryption) { - try { - std::map<std::string, std::vector<std::string>> targets; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) - targets[msg.sender].push_back(device_id); - } + } else if (auto sec_req = std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) { + handle_secret_request(sec_req, msg.sender); + } - send_encrypted_to_device_messages( - targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true); - nhlog::crypto()->info("Recovering from broken olm channel with {}:{}", - msg.sender, - msg.sender_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", - e.what()); - } + return; + } else { + failed_decryption = true; } + } + + if (failed_decryption) { + try { + std::map<std::string, std::vector<std::string>> targets; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + if (key.keys.at("curve25519:" + device_id) == msg.sender_key) + targets[msg.sender].push_back(device_id); + } + + send_encrypted_to_device_messages( + targets, mtx::events::DeviceEvent<mtx::events::msg::Dummy>{}, true); + nhlog::crypto()->info( + "Recovering from broken olm channel with {}:{}", msg.sender, msg.sender_key); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", e.what()); + } + } } nlohmann::json @@ -473,320 +425,291 @@ handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const mtx::events::msg::OlmCipherContent &content) { - nhlog::crypto()->info("opening olm session with {}", sender); + nhlog::crypto()->info("opening olm session with {}", sender); - mtx::crypto::OlmSessionPtr inbound_session = nullptr; - try { - inbound_session = - olm::client()->create_inbound_session_from(sender_key, content.body); + mtx::crypto::OlmSessionPtr inbound_session = nullptr; + try { + inbound_session = olm::client()->create_inbound_session_from(sender_key, content.body); - // We also remove the one time key used to establish that - // session so we'll have to update our copy of the account object. - cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to create inbound session with {}: {}", sender, e.what()); - return {}; - } + // We also remove the one time key used to establish that + // session so we'll have to update our copy of the account object. + cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create inbound session with {}: {}", sender, e.what()); + return {}; + } - if (!mtx::crypto::matches_inbound_session_from( - inbound_session.get(), sender_key, content.body)) { - nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", - sender); - return {}; - } + if (!mtx::crypto::matches_inbound_session_from( + inbound_session.get(), sender_key, content.body)) { + nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender); + return {}; + } - mtx::crypto::BinaryBuf output; - try { - output = - olm::client()->decrypt_message(inbound_session.get(), content.type, content.body); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to decrypt olm message {}: {}", content.body, e.what()); - return {}; - } + mtx::crypto::BinaryBuf output; + try { + output = olm::client()->decrypt_message(inbound_session.get(), content.type, content.body); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt olm message {}: {}", content.body, e.what()); + return {}; + } - auto plaintext = json::parse(std::string((char *)output.data(), output.size())); - nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); + auto plaintext = json::parse(std::string((char *)output.data(), output.size())); + nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); - try { - nhlog::crypto()->debug("New olm session: {}", - mtx::crypto::session_id(inbound_session.get())); - cache::saveOlmSession( - sender_key, std::move(inbound_session), QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->warn( - "failed to save inbound olm session from {}: {}", sender, e.what()); - } + try { + nhlog::crypto()->debug("New olm session: {}", + mtx::crypto::session_id(inbound_session.get())); + cache::saveOlmSession( + sender_key, std::move(inbound_session), QDateTime::currentMSecsSinceEpoch()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("failed to save inbound olm session from {}: {}", sender, e.what()); + } - return plaintext; + return plaintext; } mtx::events::msg::Encrypted encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) { - using namespace mtx::events; - using namespace mtx::identifiers; - - auto own_user_id = http::client()->user_id().to_string(); - - auto members = cache::client()->getMembersWithKeys( - room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); - - std::map<std::string, std::vector<std::string>> sendSessionTo; - mtx::crypto::OutboundGroupSessionPtr session = nullptr; - GroupSessionData group_session_data; - - if (cache::outboundMegolmSessionExists(room_id)) { - auto res = cache::getOutboundMegolmSession(room_id); - auto encryptionSettings = cache::client()->roomEncryptionSettings(room_id); - mtx::events::state::Encryption defaultSettings; - - // rotate if we crossed the limits for this key - if (res.data.message_index < - encryptionSettings.value_or(defaultSettings).rotation_period_msgs && - (QDateTime::currentMSecsSinceEpoch() - res.data.timestamp) < - encryptionSettings.value_or(defaultSettings).rotation_period_ms) { - auto member_it = members.begin(); - auto session_member_it = res.data.currently.keys.begin(); - auto session_member_it_end = res.data.currently.keys.end(); - - while (member_it != members.end() || - session_member_it != session_member_it_end) { - if (member_it == members.end()) { - // a member left, purge session! - nhlog::crypto()->debug( - "Rotating megolm session because of left member"); - break; - } - - if (session_member_it == session_member_it_end) { - // share with all remaining members - while (member_it != members.end()) { - sendSessionTo[member_it->first] = {}; - - if (member_it->second) - for (const auto &dev : - member_it->second->device_keys) - if (member_it->first != - own_user_id || - dev.first != device_id) - sendSessionTo[member_it - ->first] - .push_back(dev.first); - - ++member_it; - } - - session = std::move(res.session); - break; - } - - if (member_it->first > session_member_it->first) { - // a member left, purge session - nhlog::crypto()->debug( - "Rotating megolm session because of left member"); - break; - } else if (member_it->first < session_member_it->first) { - // new member, send them the session at this index - sendSessionTo[member_it->first] = {}; - - if (member_it->second) { - for (const auto &dev : - member_it->second->device_keys) - if (member_it->first != own_user_id || - dev.first != device_id) - sendSessionTo[member_it->first] - .push_back(dev.first); - } - - ++member_it; - } else { - // compare devices - bool device_removed = false; - for (const auto &dev : - session_member_it->second.deviceids) { - if (!member_it->second || - !member_it->second->device_keys.count( - dev.first)) { - device_removed = true; - break; - } - } - - if (device_removed) { - // device removed, rotate session! - nhlog::crypto()->debug( - "Rotating megolm session because of removed " - "device of {}", - member_it->first); - break; - } - - // check for new devices to share with - if (member_it->second) - for (const auto &dev : - member_it->second->device_keys) - if (!session_member_it->second.deviceids - .count(dev.first) && - (member_it->first != own_user_id || - dev.first != device_id)) - sendSessionTo[member_it->first] - .push_back(dev.first); - - ++member_it; - ++session_member_it; - if (member_it == members.end() && - session_member_it == session_member_it_end) { - // all devices match or are newly added - session = std::move(res.session); - } - } - } + using namespace mtx::events; + using namespace mtx::identifiers; + + auto own_user_id = http::client()->user_id().to_string(); + + auto members = cache::client()->getMembersWithKeys( + room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); + + std::map<std::string, std::vector<std::string>> sendSessionTo; + mtx::crypto::OutboundGroupSessionPtr session = nullptr; + GroupSessionData group_session_data; + + if (cache::outboundMegolmSessionExists(room_id)) { + auto res = cache::getOutboundMegolmSession(room_id); + auto encryptionSettings = cache::client()->roomEncryptionSettings(room_id); + mtx::events::state::Encryption defaultSettings; + + // rotate if we crossed the limits for this key + if (res.data.message_index < + encryptionSettings.value_or(defaultSettings).rotation_period_msgs && + (QDateTime::currentMSecsSinceEpoch() - res.data.timestamp) < + encryptionSettings.value_or(defaultSettings).rotation_period_ms) { + auto member_it = members.begin(); + auto session_member_it = res.data.currently.keys.begin(); + auto session_member_it_end = res.data.currently.keys.end(); + + while (member_it != members.end() || session_member_it != session_member_it_end) { + if (member_it == members.end()) { + // a member left, purge session! + nhlog::crypto()->debug("Rotating megolm session because of left member"); + break; } - group_session_data = std::move(res.data); - } + if (session_member_it == session_member_it_end) { + // share with all remaining members + while (member_it != members.end()) { + sendSessionTo[member_it->first] = {}; - if (!session) { - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(session.get()); - const auto session_key = mtx::crypto::session_key(session.get()); - - // Saving the new megolm session. - GroupSessionData session_data{}; - session_data.message_index = 0; - session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); - session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; - - sendSessionTo.clear(); - - for (const auto &[user, devices] : members) { - sendSessionTo[user] = {}; - session_data.currently.keys[user] = {}; - if (devices) { - for (const auto &[device_id_, key] : devices->device_keys) { - (void)key; - if (device_id != device_id_ || user != own_user_id) { - sendSessionTo[user].push_back(device_id_); - session_data.currently.keys[user] - .deviceids[device_id_] = 0; - } - } + if (member_it->second) + for (const auto &dev : member_it->second->device_keys) + if (member_it->first != own_user_id || dev.first != device_id) + sendSessionTo[member_it->first].push_back(dev.first); + + ++member_it; + } + + session = std::move(res.session); + break; + } + + if (member_it->first > session_member_it->first) { + // a member left, purge session + nhlog::crypto()->debug("Rotating megolm session because of left member"); + break; + } else if (member_it->first < session_member_it->first) { + // new member, send them the session at this index + sendSessionTo[member_it->first] = {}; + + if (member_it->second) { + for (const auto &dev : member_it->second->device_keys) + if (member_it->first != own_user_id || dev.first != device_id) + sendSessionTo[member_it->first].push_back(dev.first); + } + + ++member_it; + } else { + // compare devices + bool device_removed = false; + for (const auto &dev : session_member_it->second.deviceids) { + if (!member_it->second || + !member_it->second->device_keys.count(dev.first)) { + device_removed = true; + break; } + } + + if (device_removed) { + // device removed, rotate session! + nhlog::crypto()->debug("Rotating megolm session because of removed " + "device of {}", + member_it->first); + break; + } + + // check for new devices to share with + if (member_it->second) + for (const auto &dev : member_it->second->device_keys) + if (!session_member_it->second.deviceids.count(dev.first) && + (member_it->first != own_user_id || dev.first != device_id)) + sendSessionTo[member_it->first].push_back(dev.first); + + ++member_it; + ++session_member_it; + if (member_it == members.end() && session_member_it == session_member_it_end) { + // all devices match or are newly added + session = std::move(res.session); + } } + } + } - { - MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = session_id; - index.sender_key = olm::client()->identity_keys().curve25519; - auto megolm_session = - olm::client()->init_inbound_group_session(session_key); - backup_session_key(index, session_data, megolm_session); - cache::saveInboundMegolmSession( - index, std::move(megolm_session), session_data); + group_session_data = std::move(res.data); + } + + if (!session) { + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(session.get()); + const auto session_key = mtx::crypto::session_key(session.get()); + + // Saving the new megolm session. + GroupSessionData session_data{}; + session_data.message_index = 0; + session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); + session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; + + sendSessionTo.clear(); + + for (const auto &[user, devices] : members) { + sendSessionTo[user] = {}; + session_data.currently.keys[user] = {}; + if (devices) { + for (const auto &[device_id_, key] : devices->device_keys) { + (void)key; + if (device_id != device_id_ || user != own_user_id) { + sendSessionTo[user].push_back(device_id_); + session_data.currently.keys[user].deviceids[device_id_] = 0; + } } + } + } - cache::saveOutboundMegolmSession(room_id, session_data, session); - group_session_data = std::move(session_data); + { + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = session_id; + index.sender_key = olm::client()->identity_keys().curve25519; + auto megolm_session = olm::client()->init_inbound_group_session(session_key); + backup_session_key(index, session_data, megolm_session); + cache::saveInboundMegolmSession(index, std::move(megolm_session), session_data); } - mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{}; - megolm_payload.content.algorithm = MEGOLM_ALGO; - megolm_payload.content.room_id = room_id; - megolm_payload.content.session_id = mtx::crypto::session_id(session.get()); - megolm_payload.content.session_key = mtx::crypto::session_key(session.get()); - megolm_payload.type = mtx::events::EventType::RoomKey; - - if (!sendSessionTo.empty()) - olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - - // relations shouldn't be encrypted... - mtx::common::Relations relations = mtx::common::parse_relations(body["content"]); - - auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); - - // Prepare the m.room.encrypted event. - msg::Encrypted data; - data.ciphertext = std::string((char *)payload.data(), payload.size()); - data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = mtx::crypto::session_id(session.get()); - data.device_id = device_id; - data.algorithm = MEGOLM_ALGO; - data.relations = relations; - - group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); - nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); - - // update current set of members for the session with the new members and that message_index - for (const auto &[user, devices] : sendSessionTo) { - if (!group_session_data.currently.keys.count(user)) - group_session_data.currently.keys[user] = {}; - - for (const auto &device_id_ : devices) { - if (!group_session_data.currently.keys[user].deviceids.count(device_id_)) - group_session_data.currently.keys[user].deviceids[device_id_] = - group_session_data.message_index; - } + cache::saveOutboundMegolmSession(room_id, session_data, session); + group_session_data = std::move(session_data); + } + + mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{}; + megolm_payload.content.algorithm = MEGOLM_ALGO; + megolm_payload.content.room_id = room_id; + megolm_payload.content.session_id = mtx::crypto::session_id(session.get()); + megolm_payload.content.session_key = mtx::crypto::session_key(session.get()); + megolm_payload.type = mtx::events::EventType::RoomKey; + + if (!sendSessionTo.empty()) + olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); + + // relations shouldn't be encrypted... + mtx::common::Relations relations = mtx::common::parse_relations(body["content"]); + + auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); + + // Prepare the m.room.encrypted event. + msg::Encrypted data; + data.ciphertext = std::string((char *)payload.data(), payload.size()); + data.sender_key = olm::client()->identity_keys().curve25519; + data.session_id = mtx::crypto::session_id(session.get()); + data.device_id = device_id; + data.algorithm = MEGOLM_ALGO; + data.relations = relations; + + group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); + nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); + + // update current set of members for the session with the new members and that message_index + for (const auto &[user, devices] : sendSessionTo) { + if (!group_session_data.currently.keys.count(user)) + group_session_data.currently.keys[user] = {}; + + for (const auto &device_id_ : devices) { + if (!group_session_data.currently.keys[user].deviceids.count(device_id_)) + group_session_data.currently.keys[user].deviceids[device_id_] = + group_session_data.message_index; } + } - // We need to re-pickle the session after we send a message to save the new message_index. - cache::updateOutboundMegolmSession(room_id, group_session_data, session); + // We need to re-pickle the session after we send a message to save the new message_index. + cache::updateOutboundMegolmSession(room_id, group_session_data, session); - return data; + return data; } nlohmann::json try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg) { - auto session_ids = cache::getOlmSessions(sender_key); + auto session_ids = cache::getOlmSessions(sender_key); - nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", - session_ids.size()); + nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", + session_ids.size()); - for (const auto &id : session_ids) { - auto session = cache::getOlmSession(sender_key, id); + for (const auto &id : session_ids) { + auto session = cache::getOlmSession(sender_key, id); - if (!session) { - nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); - continue; - } + if (!session) { + nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); + continue; + } - mtx::crypto::BinaryBuf text; + mtx::crypto::BinaryBuf text; - try { - text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - nhlog::crypto()->debug("Updated olm session: {}", - mtx::crypto::session_id(session->get())); - cache::saveOlmSession( - id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", - msg.type, - sender_key, - id, - e.what()); - continue; - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save session: {}", e.what()); - return {}; - } + try { + text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); + nhlog::crypto()->debug("Updated olm session: {}", + mtx::crypto::session_id(session->get())); + cache::saveOlmSession( + id, std::move(session.value()), QDateTime::currentMSecsSinceEpoch()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", + msg.type, + sender_key, + id, + e.what()); + continue; + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save session: {}", e.what()); + return {}; + } - try { - return json::parse(std::string_view((char *)text.data(), text.size())); - } catch (const json::exception &e) { - nhlog::crypto()->critical( - "failed to parse the decrypted session msg: {} {}", - e.what(), - std::string_view((char *)text.data(), text.size())); - } + try { + return json::parse(std::string_view((char *)text.data(), text.size())); + } catch (const json::exception &e) { + nhlog::crypto()->critical("failed to parse the decrypted session msg: {} {}", + e.what(), + std::string_view((char *)text.data(), text.size())); } + } - return {}; + return {}; } void @@ -794,75 +717,73 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R const std::string &sender_key, const std::string &sender_ed25519) { - MegolmSessionIndex index; - index.room_id = roomKey.content.room_id; - index.session_id = roomKey.content.session_id; - index.sender_key = sender_key; - - try { - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = {sender_key}; - data.sender_claimed_ed25519_key = sender_ed25519; - - auto megolm_session = - olm::client()->init_inbound_group_session(roomKey.content.session_key); - backup_session_key(index, data, megolm_session); - cache::saveInboundMegolmSession(index, std::move(megolm_session), data); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); - return; - } - - nhlog::crypto()->info( - "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); - - ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); + MegolmSessionIndex index; + index.room_id = roomKey.content.room_id; + index.session_id = roomKey.content.session_id; + index.sender_key = sender_key; + + try { + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = {sender_key}; + data.sender_claimed_ed25519_key = sender_ed25519; + + auto megolm_session = + olm::client()->init_inbound_group_session(roomKey.content.session_key); + backup_session_key(index, data, megolm_session); + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); + return; + } + + nhlog::crypto()->info( + "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); + + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } void import_inbound_megolm_session( const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey) { - MegolmSessionIndex index; - index.room_id = roomKey.content.room_id; - index.session_id = roomKey.content.session_id; - index.sender_key = roomKey.content.sender_key; - - try { - auto megolm_session = - olm::client()->import_inbound_group_session(roomKey.content.session_key); - - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = - roomKey.content.forwarding_curve25519_key_chain; - data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key; - // may have come from online key backup, so we can't trust it... - data.trusted = false; - // if we got it forwarded from the sender, assume it is trusted. They may still have - // used key backup, but it is unlikely. - if (roomKey.content.forwarding_curve25519_key_chain.size() == 1 && - roomKey.content.forwarding_curve25519_key_chain.back() == - roomKey.content.sender_key) { - data.trusted = true; - } - - backup_session_key(index, data, megolm_session); - cache::saveInboundMegolmSession(index, std::move(megolm_session), data); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what()); - return; + MegolmSessionIndex index; + index.room_id = roomKey.content.room_id; + index.session_id = roomKey.content.session_id; + index.sender_key = roomKey.content.sender_key; + + try { + auto megolm_session = + olm::client()->import_inbound_group_session(roomKey.content.session_key); + + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = roomKey.content.forwarding_curve25519_key_chain; + data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key; + // may have come from online key backup, so we can't trust it... + data.trusted = false; + // if we got it forwarded from the sender, assume it is trusted. They may still have + // used key backup, but it is unlikely. + if (roomKey.content.forwarding_curve25519_key_chain.size() == 1 && + roomKey.content.forwarding_curve25519_key_chain.back() == roomKey.content.sender_key) { + data.trusted = true; } - nhlog::crypto()->info( - "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); + backup_session_key(index, data, megolm_session); + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what()); + return; + } + + nhlog::crypto()->info( + "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); - ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } void @@ -870,165 +791,156 @@ backup_session_key(const MegolmSessionIndex &idx, const GroupSessionData &data, mtx::crypto::InboundGroupSessionPtr &session) { - try { - if (!UserSettings::instance()->useOnlineKeyBackup()) { - // Online key backup disabled - return; - } - - auto backupVersion = cache::client()->backupVersion(); - if (!backupVersion) { - // no trusted OKB - return; - } - - using namespace mtx::crypto; - - auto decryptedSecret = - cache::secret(mtx::secret_storage::secrets::megolm_backup_v1); - if (!decryptedSecret) { - // no backup key available - return; - } - auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret)); - - auto public_key = - mtx::crypto::CURVE25519_public_key_from_private(sessionDecryptionKey); - - mtx::responses::backup::SessionData sessionData; - sessionData.algorithm = mtx::crypto::MEGOLM_ALGO; - sessionData.forwarding_curve25519_key_chain = data.forwarding_curve25519_key_chain; - sessionData.sender_claimed_keys["ed25519"] = data.sender_claimed_ed25519_key; - sessionData.sender_key = idx.sender_key; - sessionData.session_key = mtx::crypto::export_session(session.get(), -1); - - auto encrypt_session = mtx::crypto::encrypt_session(sessionData, public_key); - - mtx::responses::backup::SessionBackup bk; - bk.first_message_index = olm_inbound_group_session_first_known_index(session.get()); - bk.forwarded_count = data.forwarding_curve25519_key_chain.size(); - bk.is_verified = false; - bk.session_data = std::move(encrypt_session); - - http::client()->put_room_keys( - backupVersion->version, - idx.room_id, - idx.session_id, - bk, - [idx](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to backup session key ({}:{}): {} ({})", - idx.room_id, - idx.session_id, - err->matrix_error.error, - static_cast<int>(err->status_code)); - } else { - nhlog::crypto()->debug( - "backed up session key ({}:{})", idx.room_id, idx.session_id); - } - }); - } catch (std::exception &e) { - nhlog::net()->warn("failed to backup session key: {}", e.what()); - } -} - -void -mark_keys_as_published() -{ - olm::client()->mark_keys_as_published(); - cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); -} - -void -lookup_keybackup(const std::string room, const std::string session_id) -{ + try { if (!UserSettings::instance()->useOnlineKeyBackup()) { - // Online key backup disabled - return; + // Online key backup disabled + return; } auto backupVersion = cache::client()->backupVersion(); if (!backupVersion) { - // no trusted OKB - return; + // no trusted OKB + return; } using namespace mtx::crypto; auto decryptedSecret = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1); if (!decryptedSecret) { - // no backup key available - return; + // no backup key available + return; } auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret)); - http::client()->room_keys( + auto public_key = mtx::crypto::CURVE25519_public_key_from_private(sessionDecryptionKey); + + mtx::responses::backup::SessionData sessionData; + sessionData.algorithm = mtx::crypto::MEGOLM_ALGO; + sessionData.forwarding_curve25519_key_chain = data.forwarding_curve25519_key_chain; + sessionData.sender_claimed_keys["ed25519"] = data.sender_claimed_ed25519_key; + sessionData.sender_key = idx.sender_key; + sessionData.session_key = mtx::crypto::export_session(session.get(), -1); + + auto encrypt_session = mtx::crypto::encrypt_session(sessionData, public_key); + + mtx::responses::backup::SessionBackup bk; + bk.first_message_index = olm_inbound_group_session_first_known_index(session.get()); + bk.forwarded_count = data.forwarding_curve25519_key_chain.size(); + bk.is_verified = false; + bk.session_data = std::move(encrypt_session); + + http::client()->put_room_keys( backupVersion->version, - room, - session_id, - [room, session_id, sessionDecryptionKey](const mtx::responses::backup::SessionBackup &bk, - mtx::http::RequestErr err) { - if (err) { - if (err->status_code != 404) - nhlog::crypto()->error( - "Failed to dowload key {}:{}: {} - {}", - room, - session_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - try { - auto session = decrypt_session(bk.session_data, sessionDecryptionKey); - - if (session.algorithm != mtx::crypto::MEGOLM_ALGO) - // don't know this algorithm - return; - - MegolmSessionIndex index; - index.room_id = room; - index.session_id = session_id; - index.sender_key = session.sender_key; - - GroupSessionData data{}; - data.forwarding_curve25519_key_chain = - session.forwarding_curve25519_key_chain; - data.sender_claimed_ed25519_key = session.sender_claimed_keys["ed25519"]; - // online key backup can't be trusted, because anyone can upload to it. - data.trusted = false; - - auto megolm_session = - olm::client()->import_inbound_group_session(session.session_key); - - if (!cache::inboundMegolmSessionExists(index) || - olm_inbound_group_session_first_known_index(megolm_session.get()) < - olm_inbound_group_session_first_known_index( - cache::getInboundMegolmSession(index).get())) { - cache::saveInboundMegolmSession( - index, std::move(megolm_session), data); - - nhlog::crypto()->info("imported inbound megolm session " - "from key backup ({}, {})", - room, - session_id); - - // call on UI thread - QTimer::singleShot(0, ChatPage::instance(), [index] { - ChatPage::instance()->receivedSessionKey( - index.room_id, index.session_id); - }); - } - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save inbound megolm session: {}", - e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to import inbound megolm session: {}", - e.what()); - return; - } + idx.room_id, + idx.session_id, + bk, + [idx](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to backup session key ({}:{}): {} ({})", + idx.room_id, + idx.session_id, + err->matrix_error.error, + static_cast<int>(err->status_code)); + } else { + nhlog::crypto()->debug( + "backed up session key ({}:{})", idx.room_id, idx.session_id); + } }); + } catch (std::exception &e) { + nhlog::net()->warn("failed to backup session key: {}", e.what()); + } +} + +void +mark_keys_as_published() +{ + olm::client()->mark_keys_as_published(); + cache::saveOlmAccount(olm::client()->save(cache::client()->pickleSecret())); +} + +void +lookup_keybackup(const std::string room, const std::string session_id) +{ + if (!UserSettings::instance()->useOnlineKeyBackup()) { + // Online key backup disabled + return; + } + + auto backupVersion = cache::client()->backupVersion(); + if (!backupVersion) { + // no trusted OKB + return; + } + + using namespace mtx::crypto; + + auto decryptedSecret = cache::secret(mtx::secret_storage::secrets::megolm_backup_v1); + if (!decryptedSecret) { + // no backup key available + return; + } + auto sessionDecryptionKey = to_binary_buf(base642bin(*decryptedSecret)); + + http::client()->room_keys( + backupVersion->version, + room, + session_id, + [room, session_id, sessionDecryptionKey](const mtx::responses::backup::SessionBackup &bk, + mtx::http::RequestErr err) { + if (err) { + if (err->status_code != 404) + nhlog::crypto()->error("Failed to dowload key {}:{}: {} - {}", + room, + session_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + try { + auto session = decrypt_session(bk.session_data, sessionDecryptionKey); + + if (session.algorithm != mtx::crypto::MEGOLM_ALGO) + // don't know this algorithm + return; + + MegolmSessionIndex index; + index.room_id = room; + index.session_id = session_id; + index.sender_key = session.sender_key; + + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = session.forwarding_curve25519_key_chain; + data.sender_claimed_ed25519_key = session.sender_claimed_keys["ed25519"]; + // online key backup can't be trusted, because anyone can upload to it. + data.trusted = false; + + auto megolm_session = + olm::client()->import_inbound_group_session(session.session_key); + + if (!cache::inboundMegolmSessionExists(index) || + olm_inbound_group_session_first_known_index(megolm_session.get()) < + olm_inbound_group_session_first_known_index( + cache::getInboundMegolmSession(index).get())) { + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); + + nhlog::crypto()->info("imported inbound megolm session " + "from key backup ({}, {})", + room, + session_id); + + // call on UI thread + QTimer::singleShot(0, ChatPage::instance(), [index] { + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); + }); + } + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what()); + return; + } + }); } void @@ -1036,158 +948,152 @@ send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e, const std::string &request_id, bool cancel) { - using namespace mtx::events; - - nhlog::crypto()->debug("sending key request: sender_key {}, session_id {}", - e.content.sender_key, - e.content.session_id); - - mtx::events::msg::KeyRequest request; - request.action = cancel ? mtx::events::msg::RequestAction::Cancellation - : mtx::events::msg::RequestAction::Request; - - request.algorithm = MEGOLM_ALGO; - request.room_id = e.room_id; - request.sender_key = e.content.sender_key; - request.session_id = e.content.session_id; - request.request_id = request_id; - request.requesting_device_id = http::client()->device_id(); - - nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2)); - - std::map<mtx::identifiers::User, std::map<std::string, decltype(request)>> body; - body[mtx::identifiers::parse<mtx::identifiers::User>(e.sender)][e.content.device_id] = - request; - body[http::client()->user_id()]["*"] = request; - - http::client()->send_to_device( - http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - nhlog::net()->info("m.room_key_request sent to {}:{} and your own devices", - e.sender, - e.content.device_id); - }); - - // http::client()->room_keys + using namespace mtx::events; + + nhlog::crypto()->debug("sending key request: sender_key {}, session_id {}", + e.content.sender_key, + e.content.session_id); + + mtx::events::msg::KeyRequest request; + request.action = cancel ? mtx::events::msg::RequestAction::Cancellation + : mtx::events::msg::RequestAction::Request; + + request.algorithm = MEGOLM_ALGO; + request.room_id = e.room_id; + request.sender_key = e.content.sender_key; + request.session_id = e.content.session_id; + request.request_id = request_id; + request.requesting_device_id = http::client()->device_id(); + + nhlog::crypto()->debug("m.room_key_request: {}", json(request).dump(2)); + + std::map<mtx::identifiers::User, std::map<std::string, decltype(request)>> body; + body[mtx::identifiers::parse<mtx::identifiers::User>(e.sender)][e.content.device_id] = request; + body[http::client()->user_id()]["*"] = request; + + http::client()->send_to_device( + http::client()->generate_txn_id(), body, [e](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + nhlog::net()->info( + "m.room_key_request sent to {}:{} and your own devices", e.sender, e.content.device_id); + }); + + // http::client()->room_keys } void handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &req) { - if (req.content.algorithm != MEGOLM_ALGO) { - nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}", - req.content.request_id, - req.content.algorithm); - return; - } - - // Check if we were the sender of the session being requested (unless it is actually us - // requesting the session). - if (req.sender != http::client()->user_id().to_string() && - req.content.sender_key != olm::client()->identity_keys().curve25519) { - nhlog::crypto()->debug( - "ignoring key request {} because we did not create the requested session: " - "\nrequested({}) ours({})", - req.content.request_id, - req.content.sender_key, - olm::client()->identity_keys().curve25519); - return; - } - - // Check that the requested session_id and the one we have saved match. - MegolmSessionIndex index{}; - index.room_id = req.content.room_id; - index.session_id = req.content.session_id; - index.sender_key = req.content.sender_key; - - // Check if we have the keys for the requested session. - auto sessionData = cache::getMegolmSessionData(index); - if (!sessionData) { - nhlog::crypto()->warn("requested session not found in room: {}", - req.content.room_id); - return; - } - - const auto session = cache::getInboundMegolmSession(index); - if (!session) { - nhlog::crypto()->warn("No session with id {} in db", req.content.session_id); - return; - } - - if (!cache::isRoomMember(req.sender, req.content.room_id)) { - nhlog::crypto()->warn( - "user {} that requested the session key is not member of the room {}", - req.sender, - req.content.room_id); - return; - } - - // check if device is verified - auto verificationStatus = cache::verificationStatus(req.sender); - bool verifiedDevice = false; - if (verificationStatus && - // Share keys, if the option to share with trusted users is enabled or with yourself - (ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers() || - req.sender == http::client()->user_id().to_string())) { - for (const auto &dev : verificationStatus->verified_devices) { - if (dev == req.content.requesting_device_id) { - verifiedDevice = true; - nhlog::crypto()->debug("Verified device: {}", dev); - break; - } - } - } - - bool shouldSeeKeys = false; - uint64_t minimumIndex = -1; - if (sessionData->currently.keys.count(req.sender)) { - if (sessionData->currently.keys.at(req.sender) - .deviceids.count(req.content.requesting_device_id)) { - shouldSeeKeys = true; - minimumIndex = sessionData->currently.keys.at(req.sender) - .deviceids.at(req.content.requesting_device_id); - } + if (req.content.algorithm != MEGOLM_ALGO) { + nhlog::crypto()->debug("ignoring key request {} with invalid algorithm: {}", + req.content.request_id, + req.content.algorithm); + return; + } + + // Check if we were the sender of the session being requested (unless it is actually us + // requesting the session). + if (req.sender != http::client()->user_id().to_string() && + req.content.sender_key != olm::client()->identity_keys().curve25519) { + nhlog::crypto()->debug( + "ignoring key request {} because we did not create the requested session: " + "\nrequested({}) ours({})", + req.content.request_id, + req.content.sender_key, + olm::client()->identity_keys().curve25519); + return; + } + + // Check that the requested session_id and the one we have saved match. + MegolmSessionIndex index{}; + index.room_id = req.content.room_id; + index.session_id = req.content.session_id; + index.sender_key = req.content.sender_key; + + // Check if we have the keys for the requested session. + auto sessionData = cache::getMegolmSessionData(index); + if (!sessionData) { + nhlog::crypto()->warn("requested session not found in room: {}", req.content.room_id); + return; + } + + const auto session = cache::getInboundMegolmSession(index); + if (!session) { + nhlog::crypto()->warn("No session with id {} in db", req.content.session_id); + return; + } + + if (!cache::isRoomMember(req.sender, req.content.room_id)) { + nhlog::crypto()->warn("user {} that requested the session key is not member of the room {}", + req.sender, + req.content.room_id); + return; + } + + // check if device is verified + auto verificationStatus = cache::verificationStatus(req.sender); + bool verifiedDevice = false; + if (verificationStatus && + // Share keys, if the option to share with trusted users is enabled or with yourself + (ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers() || + req.sender == http::client()->user_id().to_string())) { + for (const auto &dev : verificationStatus->verified_devices) { + if (dev == req.content.requesting_device_id) { + verifiedDevice = true; + nhlog::crypto()->debug("Verified device: {}", dev); + break; + } } - - if (!verifiedDevice && !shouldSeeKeys) { - nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id); - return; - } - - if (verifiedDevice) { - // share the minimum index we have - minimumIndex = -1; - } - - try { - auto session_key = mtx::crypto::export_session(session.get(), minimumIndex); - - // - // Prepare the m.room_key event. - // - mtx::events::msg::ForwardedRoomKey forward_key{}; - forward_key.algorithm = MEGOLM_ALGO; - forward_key.room_id = index.room_id; - forward_key.session_id = index.session_id; - forward_key.session_key = session_key; - forward_key.sender_key = index.sender_key; - - // TODO(Nico): Figure out if this is correct - forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key; - forward_key.forwarding_curve25519_key_chain = - sessionData->forwarding_curve25519_key_chain; - - send_megolm_key_to_device( - req.sender, req.content.requesting_device_id, forward_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to forward session key: {}", e.what()); + } + + bool shouldSeeKeys = false; + uint64_t minimumIndex = -1; + if (sessionData->currently.keys.count(req.sender)) { + if (sessionData->currently.keys.at(req.sender) + .deviceids.count(req.content.requesting_device_id)) { + shouldSeeKeys = true; + minimumIndex = sessionData->currently.keys.at(req.sender) + .deviceids.at(req.content.requesting_device_id); } + } + + if (!verifiedDevice && !shouldSeeKeys) { + nhlog::crypto()->debug("ignoring key request for room {}", req.content.room_id); + return; + } + + if (verifiedDevice) { + // share the minimum index we have + minimumIndex = -1; + } + + try { + auto session_key = mtx::crypto::export_session(session.get(), minimumIndex); + + // + // Prepare the m.room_key event. + // + mtx::events::msg::ForwardedRoomKey forward_key{}; + forward_key.algorithm = MEGOLM_ALGO; + forward_key.room_id = index.room_id; + forward_key.session_id = index.session_id; + forward_key.session_key = session_key; + forward_key.sender_key = index.sender_key; + + // TODO(Nico): Figure out if this is correct + forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key; + forward_key.forwarding_curve25519_key_chain = sessionData->forwarding_curve25519_key_chain; + + send_megolm_key_to_device(req.sender, req.content.requesting_device_id, forward_key); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to forward session key: {}", e.what()); + } } void @@ -1195,14 +1101,14 @@ send_megolm_key_to_device(const std::string &user_id, const std::string &device_id, const mtx::events::msg::ForwardedRoomKey &payload) { - mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> room_key; - room_key.content = payload; - room_key.type = mtx::events::EventType::ForwardedRoomKey; - - std::map<std::string, std::vector<std::string>> targets; - targets[user_id] = {device_id}; - send_encrypted_to_device_messages(targets, room_key); - nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id); + mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> room_key; + room_key.content = payload; + room_key.type = mtx::events::EventType::ForwardedRoomKey; + + std::map<std::string, std::vector<std::string>> targets; + targets[user_id] = {device_id}; + send_encrypted_to_device_messages(targets, room_key); + nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id); } DecryptionResult @@ -1210,79 +1116,74 @@ decryptEvent(const MegolmSessionIndex &index, const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &event, bool dont_write_db) { - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt}; - } - } catch (const lmdb::error &e) { - return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; - } - - // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto sessionData = - cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{}); - - auto res = - olm::client()->decrypt_group_message(session.get(), event.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - - if (!event.event_id.empty() && event.event_id[0] == '$') { - auto oldIdx = sessionData.indices.find(res.message_index); - if (oldIdx != sessionData.indices.end()) { - if (oldIdx->second != event.event_id) - return {DecryptionErrorCode::ReplayAttack, - std::nullopt, - std::nullopt}; - } else if (!dont_write_db) { - sessionData.indices[res.message_index] = event.event_id; - cache::client()->saveInboundMegolmSession( - index, std::move(session), sessionData); - } - } - } catch (const lmdb::error &e) { - return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; - } catch (const mtx::crypto::olm_exception &e) { - if (e.error_code() == mtx::crypto::OlmErrorCode::UNKNOWN_MESSAGE_INDEX) - return {DecryptionErrorCode::MissingSessionIndex, e.what(), std::nullopt}; - return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt}; + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + return {DecryptionErrorCode::MissingSession, std::nullopt, std::nullopt}; } - - try { - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = event.event_id; - body["sender"] = event.sender; - body["origin_server_ts"] = event.origin_server_ts; - body["unsigned"] = event.unsigned_data; - - // relations are unencrypted in content... - mtx::common::add_relations(body["content"], event.content.relations); - - mtx::events::collections::TimelineEvent te; - mtx::events::collections::from_json(body, te); - - return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; - } catch (std::exception &e) { - return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; + } catch (const lmdb::error &e) { + return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; + } + + // TODO: Lookup index,event_id,origin_server_ts tuple for replay attack errors + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto sessionData = + cache::client()->getMegolmSessionData(index).value_or(GroupSessionData{}); + + auto res = olm::client()->decrypt_group_message(session.get(), event.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + + if (!event.event_id.empty() && event.event_id[0] == '$') { + auto oldIdx = sessionData.indices.find(res.message_index); + if (oldIdx != sessionData.indices.end()) { + if (oldIdx->second != event.event_id) + return {DecryptionErrorCode::ReplayAttack, std::nullopt, std::nullopt}; + } else if (!dont_write_db) { + sessionData.indices[res.message_index] = event.event_id; + cache::client()->saveInboundMegolmSession(index, std::move(session), sessionData); + } } + } catch (const lmdb::error &e) { + return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; + } catch (const mtx::crypto::olm_exception &e) { + if (e.error_code() == mtx::crypto::OlmErrorCode::UNKNOWN_MESSAGE_INDEX) + return {DecryptionErrorCode::MissingSessionIndex, e.what(), std::nullopt}; + return {DecryptionErrorCode::DecryptionFailed, e.what(), std::nullopt}; + } + + try { + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = event.event_id; + body["sender"] = event.sender; + body["origin_server_ts"] = event.origin_server_ts; + body["unsigned"] = event.unsigned_data; + + // relations are unencrypted in content... + mtx::common::add_relations(body["content"], event.content.relations); + + mtx::events::collections::TimelineEvent te; + mtx::events::collections::from_json(body, te); + + return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)}; + } catch (std::exception &e) { + return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt}; + } } crypto::Trust calculate_trust(const std::string &user_id, const MegolmSessionIndex &index) { - auto status = cache::client()->verificationStatus(user_id); - auto megolmData = cache::client()->getMegolmSessionData(index); - crypto::Trust trustlevel = crypto::Trust::Unverified; + auto status = cache::client()->verificationStatus(user_id); + auto megolmData = cache::client()->getMegolmSessionData(index); + crypto::Trust trustlevel = crypto::Trust::Unverified; - if (megolmData && megolmData->trusted && - status.verified_device_keys.count(index.sender_key)) - trustlevel = status.verified_device_keys.at(index.sender_key); + if (megolmData && megolmData->trusted && status.verified_device_keys.count(index.sender_key)) + trustlevel = status.verified_device_keys.at(index.sender_key); - return trustlevel; + return trustlevel; } //! Send encrypted to device messages, targets is a map from userid to device ids or {} for all @@ -1292,397 +1193,352 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s const mtx::events::collections::DeviceEvents &event, bool force_new_session) { - static QMap<QPair<std::string, std::string>, qint64> rateLimit; + static QMap<QPair<std::string, std::string>, qint64> rateLimit; - nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event); + nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event); - std::map<std::string, std::vector<std::string>> keysToQuery; - mtx::requests::ClaimKeys claims; - std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>> - messages; - std::map<std::string, std::map<std::string, DevicePublicKeys>> pks; + std::map<std::string, std::vector<std::string>> keysToQuery; + mtx::requests::ClaimKeys claims; + std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>> + messages; + std::map<std::string, std::map<std::string, DevicePublicKeys>> pks; - auto our_curve = olm::client()->identity_keys().curve25519; + auto our_curve = olm::client()->identity_keys().curve25519; - for (const auto &[user, devices] : targets) { - auto deviceKeys = cache::client()->userKeys(user); + for (const auto &[user, devices] : targets) { + auto deviceKeys = cache::client()->userKeys(user); - // no keys for user, query them - if (!deviceKeys) { - keysToQuery[user] = devices; - continue; + // no keys for user, query them + if (!deviceKeys) { + keysToQuery[user] = devices; + continue; + } + + auto deviceTargets = devices; + if (devices.empty()) { + deviceTargets.clear(); + for (const auto &[device, keys] : deviceKeys->device_keys) { + (void)keys; + deviceTargets.push_back(device); + } + } + + for (const auto &device : deviceTargets) { + if (!deviceKeys->device_keys.count(device)) { + keysToQuery[user] = {}; + break; + } + + auto d = deviceKeys->device_keys.at(device); + + if (!d.keys.count("curve25519:" + device) || !d.keys.count("ed25519:" + device)) { + nhlog::crypto()->warn("Skipping device {} since it has no keys!", device); + continue; + } + + auto device_curve = d.keys.at("curve25519:" + device); + if (device_curve == our_curve) { + nhlog::crypto()->warn("Skipping our own device, since sending " + "ourselves olm messages makes no sense."); + continue; + } + + auto session = cache::getLatestOlmSession(device_curve); + if (!session || force_new_session) { + auto currentTime = QDateTime::currentSecsSinceEpoch(); + if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < currentTime) { + claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519; + pks[user][device].ed25519 = d.keys.at("ed25519:" + device); + pks[user][device].curve25519 = d.keys.at("curve25519:" + device); + + rateLimit.insert(QPair(user, device), currentTime); + } else { + nhlog::crypto()->warn("Not creating new session with {}:{} " + "because of rate limit", + user, + device); } + continue; + } + + messages[mtx::identifiers::parse<mtx::identifiers::User>(user)][device] = + olm::client() + ->create_olm_encrypted_content(session->get(), + ev_json, + UserId(user), + d.keys.at("ed25519:" + device), + device_curve) + .get<mtx::events::msg::OlmEncrypted>(); + + try { + nhlog::crypto()->debug("Updated olm session: {}", + mtx::crypto::session_id(session->get())); + cache::saveOlmSession(d.keys.at("curve25519:" + device), + std::move(*session), + QDateTime::currentMSecsSinceEpoch()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", e.what()); + } + } + } + + if (!messages.empty()) + http::client()->send_to_device<mtx::events::msg::OlmEncrypted>( + http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + }); - auto deviceTargets = devices; - if (devices.empty()) { - deviceTargets.clear(); - for (const auto &[device, keys] : deviceKeys->device_keys) { - (void)keys; - deviceTargets.push_back(device); - } + auto BindPks = [ev_json](decltype(pks) pks_temp) { + return [pks = pks_temp, ev_json](const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr) { + std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::OlmEncrypted>> + messages; + for (const auto &[user_id, retrieved_devices] : res.one_time_keys) { + nhlog::net()->debug("claimed keys for {}", user_id); + if (retrieved_devices.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + continue; } - for (const auto &device : deviceTargets) { - if (!deviceKeys->device_keys.count(device)) { - keysToQuery[user] = {}; - break; - } + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; - auto d = deviceKeys->device_keys.at(device); + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - if (!d.keys.count("curve25519:" + device) || - !d.keys.count("ed25519:" + device)) { - nhlog::crypto()->warn("Skipping device {} since it has no keys!", - device); - continue; - } + if (rd.second.empty() || !rd.second.begin()->contains("key")) { + nhlog::net()->warn("Skipping device {} as it has no key.", device_id); + continue; + } - auto device_curve = d.keys.at("curve25519:" + device); - if (device_curve == our_curve) { - nhlog::crypto()->warn("Skipping our own device, since sending " - "ourselves olm messages makes no sense."); - continue; - } + auto otk = rd.second.begin()->at("key"); - auto session = cache::getLatestOlmSession(device_curve); - if (!session || force_new_session) { - auto currentTime = QDateTime::currentSecsSinceEpoch(); - if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < - currentTime) { - claims.one_time_keys[user][device] = - mtx::crypto::SIGNED_CURVE25519; - pks[user][device].ed25519 = d.keys.at("ed25519:" + device); - pks[user][device].curve25519 = - d.keys.at("curve25519:" + device); - - rateLimit.insert(QPair(user, device), currentTime); - } else { - nhlog::crypto()->warn("Not creating new session with {}:{} " - "because of rate limit", - user, - device); - } - continue; - } + auto sign_key = pks.at(user_id).at(device_id).ed25519; + auto id_key = pks.at(user_id).at(device_id).curve25519; + + // Verify signature + { + auto signedKey = *rd.second.begin(); + std::string signature = + signedKey["signatures"][user_id].value("ed25519:" + device_id, ""); - messages[mtx::identifiers::parse<mtx::identifiers::User>(user)][device] = - olm::client() - ->create_olm_encrypted_content(session->get(), - ev_json, - UserId(user), - d.keys.at("ed25519:" + device), - device_curve) - .get<mtx::events::msg::OlmEncrypted>(); - - try { - nhlog::crypto()->debug("Updated olm session: {}", - mtx::crypto::session_id(session->get())); - cache::saveOlmSession(d.keys.at("curve25519:" + device), - std::move(*session), - QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", - e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to pickle outbound olm session: {}", e.what()); + if (signature.empty() || !mtx::crypto::ed25519_verify_signature( + sign_key, signedKey, signature)) { + nhlog::net()->warn("Skipping device {} as its one time key " + "has an invalid signature.", + device_id); + continue; } + } + + auto session = olm::client()->create_outbound_session(id_key, otk); + + messages[mtx::identifiers::parse<mtx::identifiers::User>(user_id)][device_id] = + olm::client() + ->create_olm_encrypted_content( + session.get(), ev_json, UserId(user_id), sign_key, id_key) + .get<mtx::events::msg::OlmEncrypted>(); + + try { + nhlog::crypto()->debug("Updated olm session: {}", + mtx::crypto::session_id(session.get())); + cache::saveOlmSession( + id_key, std::move(session), QDateTime::currentMSecsSinceEpoch()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } } - } + nhlog::net()->info("send_to_device: {}", user_id); + } - if (!messages.empty()) + if (!messages.empty()) http::client()->send_to_device<mtx::events::msg::OlmEncrypted>( http::client()->generate_txn_id(), messages, [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } }); - - auto BindPks = [ev_json](decltype(pks) pks_temp) { - return [pks = pks_temp, ev_json](const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr) { - std::map<mtx::identifiers::User, - std::map<std::string, mtx::events::msg::OlmEncrypted>> - messages; - for (const auto &[user_id, retrieved_devices] : res.one_time_keys) { - nhlog::net()->debug("claimed keys for {}", user_id); - if (retrieved_devices.size() == 0) { - nhlog::net()->debug( - "no one-time keys found for user_id: {}", user_id); - continue; - } - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - - nhlog::net()->debug( - "{} : \n {}", device_id, rd.second.dump(2)); - - if (rd.second.empty() || - !rd.second.begin()->contains("key")) { - nhlog::net()->warn( - "Skipping device {} as it has no key.", - device_id); - continue; - } - - auto otk = rd.second.begin()->at("key"); - - auto sign_key = pks.at(user_id).at(device_id).ed25519; - auto id_key = pks.at(user_id).at(device_id).curve25519; - - // Verify signature - { - auto signedKey = *rd.second.begin(); - std::string signature = - signedKey["signatures"][user_id].value( - "ed25519:" + device_id, ""); - - if (signature.empty() || - !mtx::crypto::ed25519_verify_signature( - sign_key, signedKey, signature)) { - nhlog::net()->warn( - "Skipping device {} as its one time key " - "has an invalid signature.", - device_id); - continue; - } - } - - auto session = - olm::client()->create_outbound_session(id_key, otk); - - messages[mtx::identifiers::parse<mtx::identifiers::User>( - user_id)][device_id] = - olm::client() - ->create_olm_encrypted_content(session.get(), - ev_json, - UserId(user_id), - sign_key, - id_key) - .get<mtx::events::msg::OlmEncrypted>(); - - try { - nhlog::crypto()->debug( - "Updated olm session: {}", - mtx::crypto::session_id(session.get())); - cache::saveOlmSession( - id_key, - std::move(session), - QDateTime::currentMSecsSinceEpoch()); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save outbound olm session: {}", - e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to pickle outbound olm session: {}", - e.what()); - } - } - nhlog::net()->info("send_to_device: {}", user_id); - } - - if (!messages.empty()) - http::client()->send_to_device<mtx::events::msg::OlmEncrypted>( - http::client()->generate_txn_id(), - messages, - [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - }); - }; }; - - if (!claims.one_time_keys.empty()) - http::client()->claim_keys(claims, BindPks(pks)); - - if (!keysToQuery.empty()) { - mtx::requests::QueryKeys req; - req.device_keys = keysToQuery; - http::client()->query_keys( - req, - [ev_json, BindPks, our_curve](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - nhlog::net()->info("queried keys"); - - cache::client()->updateUserKeys(cache::nextBatchToken(), res); - - mtx::requests::ClaimKeys claim_keys; - - std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys; - - for (const auto &user : res.device_keys) { - for (const auto &dev : user.second) { - const auto user_id = ::UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - if (user_id.get() == - http::client()->user_id().to_string() && - device_id.get() == http::client()->device_id()) - continue; - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - if (pks.curve25519 == our_curve) { - nhlog::crypto()->warn( - "Skipping our own device, since sending " - "ourselves olm messages makes no sense."); - continue; - } - - try { - if (!mtx::crypto::verify_identity_signature( - dev.second, device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto currentTime = QDateTime::currentSecsSinceEpoch(); - if (rateLimit.value(QPair(user.first, device_id.get())) + - 60 * 60 * 10 < - currentTime) { - deviceKeys[user_id].emplace(device_id, pks); - claim_keys.one_time_keys[user.first][device_id] = - mtx::crypto::SIGNED_CURVE25519; - - rateLimit.insert( - QPair(user.first, device_id.get()), - currentTime); - } else { - nhlog::crypto()->warn( - "Not creating new session with {}:{} " - "because of rate limit", - user.first, - device_id.get()); - continue; - } - - nhlog::net()->info("{}", device_id.get()); - nhlog::net()->info(" curve25519 {}", pks.curve25519); - nhlog::net()->info(" ed25519 {}", pks.ed25519); - } + }; + + if (!claims.one_time_keys.empty()) + http::client()->claim_keys(claims, BindPks(pks)); + + if (!keysToQuery.empty()) { + mtx::requests::QueryKeys req; + req.device_keys = keysToQuery; + http::client()->query_keys( + req, + [ev_json, BindPks, our_curve](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + nhlog::net()->info("queried keys"); + + cache::client()->updateUserKeys(cache::nextBatchToken(), res); + + mtx::requests::ClaimKeys claim_keys; + + std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys; + + for (const auto &user : res.device_keys) { + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + if (user_id.get() == http::client()->user_id().to_string() && + device_id.get() == http::client()->device_id()) + continue; + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug("ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + if (pks.curve25519 == our_curve) { + nhlog::crypto()->warn("Skipping our own device, since sending " + "ourselves olm messages makes no sense."); + continue; + } + + try { + if (!mtx::crypto::verify_identity_signature( + dev.second, device_id, user_id)) { + nhlog::crypto()->warn("failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; } + } catch (const json::exception &e) { + nhlog::crypto()->warn("failed to parse device key json: {}", e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn("failed to verify device key json: {}", e.what()); + continue; + } + + auto currentTime = QDateTime::currentSecsSinceEpoch(); + if (rateLimit.value(QPair(user.first, device_id.get())) + 60 * 60 * 10 < + currentTime) { + deviceKeys[user_id].emplace(device_id, pks); + claim_keys.one_time_keys[user.first][device_id] = + mtx::crypto::SIGNED_CURVE25519; + + rateLimit.insert(QPair(user.first, device_id.get()), currentTime); + } else { + nhlog::crypto()->warn("Not creating new session with {}:{} " + "because of rate limit", + user.first, + device_id.get()); + continue; + } + + nhlog::net()->info("{}", device_id.get()); + nhlog::net()->info(" curve25519 {}", pks.curve25519); + nhlog::net()->info(" ed25519 {}", pks.ed25519); + } + } - if (!claim_keys.one_time_keys.empty()) - http::client()->claim_keys(claim_keys, BindPks(deviceKeys)); - }); - } + if (!claim_keys.one_time_keys.empty()) + http::client()->claim_keys(claim_keys, BindPks(deviceKeys)); + }); + } } void request_cross_signing_keys() { - mtx::events::msg::SecretRequest secretRequest{}; - secretRequest.action = mtx::events::msg::RequestAction::Request; - secretRequest.requesting_device_id = http::client()->device_id(); + mtx::events::msg::SecretRequest secretRequest{}; + secretRequest.action = mtx::events::msg::RequestAction::Request; + secretRequest.requesting_device_id = http::client()->device_id(); - auto local_user = http::client()->user_id(); + auto local_user = http::client()->user_id(); - auto verificationStatus = cache::verificationStatus(local_user.to_string()); + auto verificationStatus = cache::verificationStatus(local_user.to_string()); - if (!verificationStatus) - return; + if (!verificationStatus) + return; - auto request = [&](std::string secretName) { - secretRequest.name = secretName; - secretRequest.request_id = "ss." + http::client()->generate_txn_id(); + auto request = [&](std::string secretName) { + secretRequest.name = secretName; + secretRequest.request_id = "ss." + http::client()->generate_txn_id(); - request_id_to_secret_name[secretRequest.request_id] = secretRequest.name; + request_id_to_secret_name[secretRequest.request_id] = secretRequest.name; - std::map<mtx::identifiers::User, - std::map<std::string, mtx::events::msg::SecretRequest>> - body; + std::map<mtx::identifiers::User, std::map<std::string, mtx::events::msg::SecretRequest>> + body; - for (const auto &dev : verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id) - body[local_user][dev] = secretRequest; - } + for (const auto &dev : verificationStatus->verified_devices) { + if (dev != secretRequest.requesting_device_id) + body[local_user][dev] = secretRequest; + } + + http::client()->send_to_device<mtx::events::msg::SecretRequest>( + http::client()->generate_txn_id(), + body, + [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to send request for secrect '{}'", secretName); + // Cancel request on UI thread + QTimer::singleShot(1, cache::client(), [request_id]() { + request_id_to_secret_name.erase(request_id); + }); + return; + } + }); + for (const auto &dev : verificationStatus->verified_devices) { + if (dev != secretRequest.requesting_device_id) + body[local_user][dev].action = mtx::events::msg::RequestAction::Cancellation; + } + + // timeout after 15 min + QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() { + if (request_id_to_secret_name.count(secretRequest.request_id)) { + request_id_to_secret_name.erase(secretRequest.request_id); http::client()->send_to_device<mtx::events::msg::SecretRequest>( http::client()->generate_txn_id(), body, - [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("Failed to send request for secrect '{}'", - secretName); - // Cancel request on UI thread - QTimer::singleShot(1, cache::client(), [request_id]() { - request_id_to_secret_name.erase(request_id); - }); - return; - } + [secretRequest](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to cancel request for secrect '{}'", + secretRequest.name); + return; + } }); + } + }); + }; - for (const auto &dev : verificationStatus->verified_devices) { - if (dev != secretRequest.requesting_device_id) - body[local_user][dev].action = - mtx::events::msg::RequestAction::Cancellation; - } - - // timeout after 15 min - QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() { - if (request_id_to_secret_name.count(secretRequest.request_id)) { - request_id_to_secret_name.erase(secretRequest.request_id); - http::client()->send_to_device<mtx::events::msg::SecretRequest>( - http::client()->generate_txn_id(), - body, - [secretRequest](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "Failed to cancel request for secrect '{}'", - secretRequest.name); - return; - } - }); - } - }); - }; - - request(mtx::secret_storage::secrets::cross_signing_self_signing); - request(mtx::secret_storage::secrets::cross_signing_user_signing); - request(mtx::secret_storage::secrets::megolm_backup_v1); + request(mtx::secret_storage::secrets::cross_signing_self_signing); + request(mtx::secret_storage::secrets::cross_signing_user_signing); + request(mtx::secret_storage::secrets::megolm_backup_v1); } namespace { @@ -1690,67 +1546,63 @@ void unlock_secrets(const std::string &key, const std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData> &secrets) { - http::client()->secret_storage_key( - key, - [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("Failed to download secret storage key"); - return; - } - - emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets); - }); + http::client()->secret_storage_key( + key, + [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download secret storage key"); + return; + } + + emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets); + }); } } void download_cross_signing_keys() { - using namespace mtx::secret_storage; - http::client()->secret_storage_secret( - secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) { - std::optional<Secret> backup_key; - if (!err) - backup_key = secret; - - http::client()->secret_storage_secret( - secrets::cross_signing_self_signing, - [backup_key](Secret secret, mtx::http::RequestErr err) { - std::optional<Secret> self_signing_key; - if (!err) - self_signing_key = secret; - - http::client()->secret_storage_secret( - secrets::cross_signing_user_signing, - [backup_key, self_signing_key](Secret secret, - mtx::http::RequestErr err) { - std::optional<Secret> user_signing_key; - if (!err) - user_signing_key = secret; - - std::map<std::string, - std::map<std::string, AesHmacSha2EncryptedData>> - secrets; - - if (backup_key && !backup_key->encrypted.empty()) - secrets[backup_key->encrypted.begin()->first] - [secrets::megolm_backup_v1] = - backup_key->encrypted.begin()->second; - if (self_signing_key && !self_signing_key->encrypted.empty()) - secrets[self_signing_key->encrypted.begin()->first] - [secrets::cross_signing_self_signing] = - self_signing_key->encrypted.begin()->second; - if (user_signing_key && !user_signing_key->encrypted.empty()) - secrets[user_signing_key->encrypted.begin()->first] - [secrets::cross_signing_user_signing] = - user_signing_key->encrypted.begin()->second; - - for (const auto &[key, secrets] : secrets) - unlock_secrets(key, secrets); - }); - }); - }); + using namespace mtx::secret_storage; + http::client()->secret_storage_secret( + secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) { + std::optional<Secret> backup_key; + if (!err) + backup_key = secret; + + http::client()->secret_storage_secret( + secrets::cross_signing_self_signing, + [backup_key](Secret secret, mtx::http::RequestErr err) { + std::optional<Secret> self_signing_key; + if (!err) + self_signing_key = secret; + + http::client()->secret_storage_secret( + secrets::cross_signing_user_signing, + [backup_key, self_signing_key](Secret secret, mtx::http::RequestErr err) { + std::optional<Secret> user_signing_key; + if (!err) + user_signing_key = secret; + + std::map<std::string, std::map<std::string, AesHmacSha2EncryptedData>> + secrets; + + if (backup_key && !backup_key->encrypted.empty()) + secrets[backup_key->encrypted.begin()->first][secrets::megolm_backup_v1] = + backup_key->encrypted.begin()->second; + if (self_signing_key && !self_signing_key->encrypted.empty()) + secrets[self_signing_key->encrypted.begin()->first] + [secrets::cross_signing_self_signing] = + self_signing_key->encrypted.begin()->second; + if (user_signing_key && !user_signing_key->encrypted.empty()) + secrets[user_signing_key->encrypted.begin()->first] + [secrets::cross_signing_user_signing] = + user_signing_key->encrypted.begin()->second; + + for (const auto &[key, secrets] : secrets) + unlock_secrets(key, secrets); + }); + }); + }); } } // namespace olm diff --git a/src/Olm.h b/src/Olm.h index eb60ae3a..44e2b8ed 100644 --- a/src/Olm.h +++ b/src/Olm.h @@ -18,32 +18,32 @@ Q_NAMESPACE 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, - // retrieve from backup or request from other devices and try again - DbError, // DB read failed - DecryptionFailed, // libolm error - ParsingFailed, // Failed to parse the actual event - ReplayAttack, // Megolm index reused + 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, + // retrieve from backup or request from other devices and try again + DbError, // DB read failed + DecryptionFailed, // libolm error + ParsingFailed, // Failed to parse the actual event + ReplayAttack, // Megolm index reused }; Q_ENUM_NS(DecryptionErrorCode) struct DecryptionResult { - DecryptionErrorCode error; - std::optional<std::string> error_message; - std::optional<mtx::events::collections::TimelineEvents> event; + DecryptionErrorCode error; + std::optional<std::string> error_message; + std::optional<mtx::events::collections::TimelineEvents> event; }; struct OlmMessage { - std::string sender_key; - std::string sender; + std::string sender_key; + std::string sender; - using RecipientKey = std::string; - std::map<RecipientKey, mtx::events::msg::OlmCipherContent> ciphertext; + using RecipientKey = std::string; + std::map<RecipientKey, mtx::events::msg::OlmCipherContent> ciphertext; }; void diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp index 25262c59..ff93f7d8 100644 --- a/src/ReadReceiptsModel.cpp +++ b/src/ReadReceiptsModel.cpp @@ -16,116 +16,115 @@ ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject , event_id_{event_id} , room_id_{room_id} { - try { - addUsers(cache::readReceipts(event_id_, room_id_)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id_.toStdString(), - room_id_.toStdString()); - - return; - } + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } - connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); + connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); } void ReadReceiptsModel::update() { - try { - addUsers(cache::readReceipts(event_id_, room_id_)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id_.toStdString(), - room_id_.toStdString()); - - return; - } + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } } QHash<int, QByteArray> ReadReceiptsModel::roleNames() const { - // Note: RawTimestamp is purposely not included here - return { - {Mxid, "mxid"}, - {DisplayName, "displayName"}, - {AvatarUrl, "avatarUrl"}, - {Timestamp, "timestamp"}, - }; + // Note: RawTimestamp is purposely not included here + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}, + }; } QVariant ReadReceiptsModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) - return {}; - - switch (role) { - case Mxid: - return readReceipts_[index.row()].first; - case DisplayName: - return cache::displayName(room_id_, readReceipts_[index.row()].first); - case AvatarUrl: - return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); - case Timestamp: - return dateFormat(readReceipts_[index.row()].second); - case RawTimestamp: - return readReceipts_[index.row()].second; - default: - return {}; - } + if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return readReceipts_[index.row()].first; + case DisplayName: + return cache::displayName(room_id_, readReceipts_[index.row()].first); + case AvatarUrl: + return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); + case Timestamp: + return dateFormat(readReceipts_[index.row()].second); + case RawTimestamp: + return readReceipts_[index.row()].second; + default: + return {}; + } } void ReadReceiptsModel::addUsers( const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users) { - auto newReceipts = users.size() - readReceipts_.size(); - - if (newReceipts > 0) { - beginInsertRows( - QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); + auto newReceipts = users.size() - readReceipts_.size(); - for (const auto &user : users) { - QPair<QString, QDateTime> item = { - QString::fromStdString(user.second), - QDateTime::fromMSecsSinceEpoch(user.first)}; - if (!readReceipts_.contains(item)) - readReceipts_.push_back(item); - } + if (newReceipts > 0) { + beginInsertRows( + QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); - endInsertRows(); + for (const auto &user : users) { + QPair<QString, QDateTime> item = {QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); } + + endInsertRows(); + } } QString ReadReceiptsModel::dateFormat(const QDateTime &then) const { - auto now = QDateTime::currentDateTime(); - auto days = then.daysTo(now); - - if (days == 0) - return QLocale::system().toString(then.time(), QLocale::ShortFormat); - else if (days < 2) - return tr("Yesterday, %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 7) - //: %1 is the name of the current day, %2 is the time the read receipt was read. The - //: result may look like this: Monday, 7:15 - return QString("%1, %2") - .arg(then.toString("dddd")) - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + if (days == 0) return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return tr("Yesterday, %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 7) + //: %1 is the name of the current day, %2 is the time the read receipt was read. The + //: result may look like this: Monday, 7:15 + return QString("%1, %2") + .arg(then.toString("dddd")) + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + + return QLocale::system().toString(then.time(), QLocale::ShortFormat); } ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent) : QSortFilterProxyModel{parent} , model_{event_id, room_id, this} { - setSourceModel(&model_); - setSortRole(ReadReceiptsModel::RawTimestamp); - sort(0, Qt::DescendingOrder); - setDynamicSortFilter(true); + setSourceModel(&model_); + setSortRole(ReadReceiptsModel::RawTimestamp); + sort(0, Qt::DescendingOrder); + setDynamicSortFilter(true); } diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h index 3b45716c..7487fff8 100644 --- a/src/ReadReceiptsModel.h +++ b/src/ReadReceiptsModel.h @@ -13,61 +13,61 @@ class ReadReceiptsModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT public: - enum Roles - { - Mxid, - DisplayName, - AvatarUrl, - Timestamp, - RawTimestamp, - }; - - explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); - - QString eventId() const { return event_id_; } - QString roomId() const { return room_id_; } - - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent) const override - { - Q_UNUSED(parent) - return readReceipts_.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Timestamp, + RawTimestamp, + }; + + explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent) + return readReceipts_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; public slots: - void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users); - void update(); + void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users); + void update(); private: - QString dateFormat(const QDateTime &then) const; + QString dateFormat(const QDateTime &then) const; - QString event_id_; - QString room_id_; - QVector<QPair<QString, QDateTime>> readReceipts_; + QString event_id_; + QString room_id_; + QVector<QPair<QString, QDateTime>> readReceipts_; }; class ReadReceiptsProxy : public QSortFilterProxyModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QString eventId READ eventId CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) public: - explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); + explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); - QString eventId() const { return event_id_; } - QString roomId() const { return room_id_; } + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } private: - QString event_id_; - QString room_id_; + QString event_id_; + QString room_id_; - ReadReceiptsModel model_; + ReadReceiptsModel model_; }; #endif // READRECEIPTSMODEL_H diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index fb6a1b97..0204a307 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -33,496 +33,483 @@ Q_DECLARE_METATYPE(mtx::user_interactive::Auth) RegisterPage::RegisterPage(QWidget *parent) : QWidget(parent) { - qRegisterMetaType<mtx::user_interactive::Unauthorized>(); - qRegisterMetaType<mtx::user_interactive::Auth>(); - top_layout_ = new QVBoxLayout(); - - back_layout_ = new QHBoxLayout(); - back_layout_->setSpacing(0); - back_layout_->setContentsMargins(5, 5, -1, -1); - - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); - - back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - back_layout_->addStretch(1); - - QIcon logo; - logo.addFile(":/logos/register.png"); - - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); - - logo_layout_ = new QHBoxLayout(); - logo_layout_->setMargin(0); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 300)); - - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 40); - form_widget_->setLayout(form_layout_); - - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); - - username_input_ = new TextField(); - username_input_->setLabel(tr("Username")); - username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+")); - username_input_->setToolTip(tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /.")); - - password_input_ = new TextField(); - password_input_->setLabel(tr("Password")); - password_input_->setRegexp(QRegularExpression("^.{8,}$")); - password_input_->setEchoMode(QLineEdit::Password); - password_input_->setToolTip(tr("Please choose a secure password. The exact requirements " - "for password strength may depend on your server.")); - - password_confirmation_ = new TextField(); - password_confirmation_->setLabel(tr("Password confirmation")); - password_confirmation_->setEchoMode(QLineEdit::Password); - - server_input_ = new TextField(); - server_input_->setLabel(tr("Homeserver")); - server_input_->setRegexp(QRegularExpression(".+")); - server_input_->setToolTip( - tr("A server that allows registration. Since matrix is decentralized, you need to first " - "find a server you can register on or host your own.")); - - error_username_label_ = new QLabel(this); - error_username_label_->setWordWrap(true); - error_username_label_->hide(); - - error_password_label_ = new QLabel(this); - error_password_label_->setWordWrap(true); - error_password_label_->hide(); - - error_password_confirmation_label_ = new QLabel(this); - error_password_confirmation_label_->setWordWrap(true); - error_password_confirmation_label_->hide(); - - error_server_label_ = new QLabel(this); - error_server_label_->setWordWrap(true); - error_server_label_->hide(); - - form_layout_->addWidget(username_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); - form_layout_->addWidget(password_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_password_label_, Qt::AlignHCenter); - form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); - form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); - form_layout_->addWidget(server_input_, Qt::AlignHCenter); - form_layout_->addWidget(error_server_label_, Qt::AlignHCenter); - - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(0); - button_layout_->setMargin(0); - - error_label_ = new QLabel(this); - error_label_->setWordWrap(true); - - register_button_ = new RaisedButton(tr("REGISTER"), this); - register_button_->setMinimumSize(350, 65); - register_button_->setFontSize(conf::btn::fontSize); - register_button_->setCornerRadius(conf::btn::cornerRadius); - - button_layout_->addStretch(1); - button_layout_->addWidget(register_button_); - button_layout_->addStretch(1); - - top_layout_->addLayout(back_layout_); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); - setLayout(top_layout_); - - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); - - connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); - connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); - connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_confirmation_, - &TextField::editingFinished, - this, - &RegisterPage::checkPasswordConfirmation); - connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); - - connect( - this, - &RegisterPage::serverError, - this, - [this](const QString &msg) { - server_input_->setValid(false); - showError(error_server_label_, msg); - }, - Qt::QueuedConnection); - - connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); - connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); - connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); - connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA); - connect( - this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth); + qRegisterMetaType<mtx::user_interactive::Unauthorized>(); + qRegisterMetaType<mtx::user_interactive::Auth>(); + top_layout_ = new QVBoxLayout(); + + back_layout_ = new QHBoxLayout(); + back_layout_->setSpacing(0); + back_layout_->setContentsMargins(5, 5, -1, -1); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); + + back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + back_layout_->addStretch(1); + + QIcon logo; + logo.addFile(":/logos/register.png"); + + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setMargin(0); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 300)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 40); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + username_input_ = new TextField(); + username_input_->setLabel(tr("Username")); + username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+")); + username_input_->setToolTip(tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); + + password_input_ = new TextField(); + password_input_->setLabel(tr("Password")); + password_input_->setRegexp(QRegularExpression("^.{8,}$")); + password_input_->setEchoMode(QLineEdit::Password); + password_input_->setToolTip(tr("Please choose a secure password. The exact requirements " + "for password strength may depend on your server.")); + + password_confirmation_ = new TextField(); + password_confirmation_->setLabel(tr("Password confirmation")); + password_confirmation_->setEchoMode(QLineEdit::Password); + + server_input_ = new TextField(); + server_input_->setLabel(tr("Homeserver")); + server_input_->setRegexp(QRegularExpression(".+")); + server_input_->setToolTip( + tr("A server that allows registration. Since matrix is decentralized, you need to first " + "find a server you can register on or host your own.")); + + error_username_label_ = new QLabel(this); + error_username_label_->setWordWrap(true); + error_username_label_->hide(); + + error_password_label_ = new QLabel(this); + error_password_label_->setWordWrap(true); + error_password_label_->hide(); + + error_password_confirmation_label_ = new QLabel(this); + error_password_confirmation_label_->setWordWrap(true); + error_password_confirmation_label_->hide(); + + error_server_label_ = new QLabel(this); + error_server_label_->setWordWrap(true); + error_server_label_->hide(); + + form_layout_->addWidget(username_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); + form_layout_->addWidget(password_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_label_, Qt::AlignHCenter); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); + form_layout_->addWidget(server_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_server_label_, Qt::AlignHCenter); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setMargin(0); + + error_label_ = new QLabel(this); + error_label_->setWordWrap(true); + + register_button_ = new RaisedButton(tr("REGISTER"), this); + register_button_->setMinimumSize(350, 65); + register_button_->setFontSize(conf::btn::fontSize); + register_button_->setCornerRadius(conf::btn::cornerRadius); + + button_layout_->addStretch(1); + button_layout_->addWidget(register_button_); + button_layout_->addStretch(1); + + top_layout_->addLayout(back_layout_); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + setLayout(top_layout_); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); + + connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername); + connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword); + connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_confirmation_, + &TextField::editingFinished, + this, + &RegisterPage::checkPasswordConfirmation); + connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer); + + connect( + this, + &RegisterPage::serverError, + this, + [this](const QString &msg) { + server_input_->setValid(false); + showError(error_server_label_, msg); + }, + Qt::QueuedConnection); + + connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup); + connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck); + connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration); + connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA); + connect(this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth); } void RegisterPage::onBackButtonClicked() { - emit backButtonClicked(); + emit backButtonClicked(); } void RegisterPage::showError(const QString &msg) { - emit errorOccurred(); - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - error_label_->setFixedHeight(qCeil(width / 200.0) * height); - error_label_->setText(msg); + emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + error_label_->setFixedHeight(qCeil(width / 200.0) * height); + error_label_->setText(msg); } void RegisterPage::showError(QLabel *label, const QString &msg) { - emit errorOccurred(); - auto rect = QFontMetrics(font()).boundingRect(msg); - int width = rect.width(); - int height = rect.height(); - label->setFixedHeight((int)qCeil(width / 200.0) * height); - label->setText(msg); - label->show(); + emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); + label->show(); } bool RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg) { - if (t_field->isValid()) { - label->hide(); - return true; - } else { - showError(label, msg); - return false; - } + if (t_field->isValid()) { + label->hide(); + return true; + } else { + showError(label, msg); + return false; + } } bool RegisterPage::checkUsername() { - return checkOneField(error_username_label_, - username_input_, - tr("The username must not be empty, and must contain only the " - "characters a-z, 0-9, ., _, =, -, and /.")); + return checkOneField(error_username_label_, + username_input_, + tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /.")); } bool RegisterPage::checkPassword() { - return checkOneField( - error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); + return checkOneField( + error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)")); } bool RegisterPage::checkPasswordConfirmation() { - if (password_input_->text() == password_confirmation_->text()) { - error_password_confirmation_label_->hide(); - password_confirmation_->setValid(true); - return true; - } else { - showError(error_password_confirmation_label_, tr("Passwords don't match")); - password_confirmation_->setValid(false); - return false; - } + if (password_input_->text() == password_confirmation_->text()) { + error_password_confirmation_label_->hide(); + password_confirmation_->setValid(true); + return true; + } else { + showError(error_password_confirmation_label_, tr("Passwords don't match")); + password_confirmation_->setValid(false); + return false; + } } bool RegisterPage::checkServer() { - // This doesn't check that the server is reachable, - // just that the input is not obviously wrong. - return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); + // This doesn't check that the server is reachable, + // just that the input is not obviously wrong. + return checkOneField(error_server_label_, server_input_, tr("Invalid server name")); } void RegisterPage::onRegisterButtonClicked() { - if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { - auto server = server_input_->text().toStdString(); - - http::client()->set_server(server); - http::client()->verify_certificates( - !UserSettings::instance()->disableCertificateValidation()); - - // This starts a chain of `emit`s which ends up doing the - // registration. Signals are used rather than normal function - // calls so that the dialogs used in UIA work correctly. - // - // The sequence of events looks something like this: - // - // dowellKnownLookup - // v - // doVersionsCheck - // v - // doRegistration - // v - // doUIA <-----------------+ - // v | More auth required - // doRegistrationWithAuth -+ - // | Success - // v - // registering - - emit wellKnownLookup(); - - emit registering(); - } + if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) { + auto server = server_input_->text().toStdString(); + + http::client()->set_server(server); + http::client()->verify_certificates( + !UserSettings::instance()->disableCertificateValidation()); + + // This starts a chain of `emit`s which ends up doing the + // registration. Signals are used rather than normal function + // calls so that the dialogs used in UIA work correctly. + // + // The sequence of events looks something like this: + // + // dowellKnownLookup + // v + // doVersionsCheck + // v + // doRegistration + // v + // doUIA <-----------------+ + // v | More auth required + // doRegistrationWithAuth -+ + // | Success + // v + // registering + + emit wellKnownLookup(); + + emit registering(); + } } void RegisterPage::doWellKnownLookup() { - http::client()->well_known( - [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - nhlog::net()->info("Autodiscovery: No .well-known."); - // Check that the homeserver can be reached - emit versionsCheck(); - return; - } - - if (!err->parse_error.empty()) { - emit serverError( - tr("Autodiscovery failed. Received malformed response.")); - nhlog::net()->error( - "Autodiscovery failed. Received malformed response."); - return; - } - - emit serverError(tr("Autodiscovery failed. Unknown error when " - "requesting .well-known.")); - nhlog::net()->error("Autodiscovery failed. Unknown error when " - "requesting .well-known. {} {}", - err->status_code, - err->error_code); - return; - } - - nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); - http::client()->set_server(res.homeserver.base_url); + http::client()->well_known( + [this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + nhlog::net()->info("Autodiscovery: No .well-known."); // Check that the homeserver can be reached emit versionsCheck(); - }); + return; + } + + if (!err->parse_error.empty()) { + emit serverError(tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error("Autodiscovery failed. Received malformed response."); + return; + } + + emit serverError(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known. {} {}", + err->status_code, + err->error_code); + return; + } + + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'"); + http::client()->set_server(res.homeserver.base_url); + // Check that the homeserver can be reached + emit versionsCheck(); + }); } void RegisterPage::doVersionsCheck() { - // Make a request to /_matrix/client/versions to check the address - // given is a Matrix homeserver. - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - if (err->status_code == 404) { - emit serverError( - tr("The required endpoints were not found. Possibly " - "not a Matrix server.")); - return; - } - - if (!err->parse_error.empty()) { - emit serverError( - tr("Received malformed response. Make sure the homeserver " - "domain is valid.")); - return; - } - - emit serverError(tr("An unknown error occured. Make sure the " - "homeserver domain is valid.")); - return; - } - - // Attempt registration without an `auth` dict - emit registration(); - }); + // Make a request to /_matrix/client/versions to check the address + // given is a Matrix homeserver. + http::client()->versions([this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + if (err->status_code == 404) { + emit serverError(tr("The required endpoints were not found. Possibly " + "not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit serverError(tr("Received malformed response. Make sure the homeserver " + "domain is valid.")); + return; + } + + emit serverError(tr("An unknown error occured. Make sure the " + "homeserver domain is valid.")); + return; + } + + // Attempt registration without an `auth` dict + emit registration(); + }); } void RegisterPage::doRegistration() { - // These inputs should still be alright, but check just in case - if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - http::client()->registration(username, password, registrationCb()); - } + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, registrationCb()); + } } void RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth) { - // These inputs should still be alright, but check just in case - if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - http::client()->registration(username, password, auth, registrationCb()); - } + // These inputs should still be alright, but check just in case + if (checkUsername() && checkPassword() && checkPasswordConfirmation()) { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + http::client()->registration(username, password, auth, registrationCb()); + } } mtx::http::Callback<mtx::responses::Register> RegisterPage::registrationCb() { - // Return a function to be used as the callback when an attempt at - // registration is made. - return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == 401) { - if (err->matrix_error.unauthorized.flows.empty()) { - nhlog::net()->warn("failed to retrieve registration flows: " - "status_code({}), matrix_error({}) ", - static_cast<int>(err->status_code), - err->matrix_error.error); - showError(QString::fromStdString(err->matrix_error.error)); - return; - } - - // Attempt to complete a UIA stage - emit UIA(err->matrix_error.unauthorized); - return; - } - - nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", - static_cast<int>(err->status_code), - err->matrix_error.error); + // Return a function to be used as the callback when an attempt at + // registration is made. + return [this](const mtx::responses::Register &res, mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + emit registerOk(); + return; + } + // The server requires registration flows. + if (err->status_code == 401) { + if (err->matrix_error.unauthorized.flows.empty()) { + nhlog::net()->warn("failed to retrieve registration flows: " + "status_code({}), matrix_error({}) ", + static_cast<int>(err->status_code), + err->matrix_error.error); showError(QString::fromStdString(err->matrix_error.error)); - }; + return; + } + + // Attempt to complete a UIA stage + emit UIA(err->matrix_error.unauthorized); + return; + } + + nhlog::net()->error("failed to register: status_code ({}), matrix_error({})", + static_cast<int>(err->status_code), + err->matrix_error.error); + + showError(QString::fromStdString(err->matrix_error.error)); + }; } void RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized) { - auto completed_stages = unauthorized.completed; - auto flows = unauthorized.flows; - auto session = - unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session; - - nhlog::ui()->info("Completed stages: {}", completed_stages.size()); - - if (!completed_stages.empty()) { - // Get rid of all flows which don't start with the sequence of - // stages that have already been completed. - flows.erase( - std::remove_if(flows.begin(), - flows.end(), - [completed_stages](auto flow) { - if (completed_stages.size() > flow.stages.size()) - return true; - for (size_t f = 0; f < completed_stages.size(); f++) - if (completed_stages[f] != flow.stages[f]) - return true; - return false; - }), - flows.end()); - } + auto completed_stages = unauthorized.completed; + auto flows = unauthorized.flows; + auto session = + unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session; + + nhlog::ui()->info("Completed stages: {}", completed_stages.size()); + + if (!completed_stages.empty()) { + // Get rid of all flows which don't start with the sequence of + // stages that have already been completed. + flows.erase(std::remove_if(flows.begin(), + flows.end(), + [completed_stages](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + } + + if (flows.empty()) { + nhlog::ui()->error("No available registration flows!"); + showError(tr("No supported registration flows!")); + return; + } + + auto current_stage = flows.front().stages.at(completed_stages.size()); + + if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this); - if (flows.empty()) { - nhlog::ui()->error("No available registration flows!"); - showError(tr("No supported registration flows!")); - return; - } + connect( + captchaDialog, &dialogs::ReCaptcha::confirmation, this, [this, session, captchaDialog]() { + captchaDialog->close(); + captchaDialog->deleteLater(); + doRegistrationWithAuth( + mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Fallback{}}); + }); - auto current_stage = flows.front().stages.at(completed_stages.size()); - - if (current_stage == mtx::user_interactive::auth_types::recaptcha) { - auto captchaDialog = new dialogs::ReCaptcha(QString::fromStdString(session), this); - - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); - doRegistrationWithAuth(mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); - - connect( - captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); - - QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); - - } else if (current_stage == mtx::user_interactive::auth_types::dummy) { - doRegistrationWithAuth( - mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); - - } else if (current_stage == mtx::user_interactive::auth_types::registration_token) { - bool ok; - QString token = - QInputDialog::getText(this, - tr("Registration token"), - tr("Please enter a valid registration token."), - QLineEdit::Normal, - QString(), - &ok); - - if (ok) { - emit registrationWithAuth(mtx::user_interactive::Auth{ - session, - mtx::user_interactive::auth::RegistrationToken{token.toStdString()}}); - } else { - emit errorOccurred(); - } - } else { - // use fallback - auto dialog = new dialogs::FallbackAuth( - QString::fromStdString(current_stage), QString::fromStdString(session), this); + connect(captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred); - connect( - dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() { - dialog->close(); - dialog->deleteLater(); - emit registrationWithAuth(mtx::user_interactive::Auth{ - session, mtx::user_interactive::auth::Fallback{}}); - }); + QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); - connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred); + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + doRegistrationWithAuth( + mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); - dialog->show(); + } else if (current_stage == mtx::user_interactive::auth_types::registration_token) { + bool ok; + QString token = QInputDialog::getText(this, + tr("Registration token"), + tr("Please enter a valid registration token."), + QLineEdit::Normal, + QString(), + &ok); + + if (ok) { + emit registrationWithAuth(mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::RegistrationToken{token.toStdString()}}); + } else { + emit errorOccurred(); } + } else { + // use fallback + auto dialog = new dialogs::FallbackAuth( + QString::fromStdString(current_stage), QString::fromStdString(session), this); + + connect(dialog, &dialogs::FallbackAuth::confirmation, this, [this, session, dialog]() { + dialog->close(); + dialog->deleteLater(); + emit registrationWithAuth( + mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Fallback{}}); + }); + + connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred); + + dialog->show(); + } } void RegisterPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/RegisterPage.h b/src/RegisterPage.h index 42ea00cb..b88808f9 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h @@ -21,76 +21,76 @@ class QHBoxLayout; class RegisterPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - RegisterPage(QWidget *parent = nullptr); + RegisterPage(QWidget *parent = nullptr); protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; signals: - void backButtonClicked(); - void errorOccurred(); + void backButtonClicked(); + void errorOccurred(); - //! Used to trigger the corresponding slot outside of the main thread. - void serverError(const QString &err); + //! Used to trigger the corresponding slot outside of the main thread. + void serverError(const QString &err); - void wellKnownLookup(); - void versionsCheck(); - void registration(); - void UIA(const mtx::user_interactive::Unauthorized &unauthorized); - void registrationWithAuth(const mtx::user_interactive::Auth &auth); + void wellKnownLookup(); + void versionsCheck(); + void registration(); + void UIA(const mtx::user_interactive::Unauthorized &unauthorized); + void registrationWithAuth(const mtx::user_interactive::Auth &auth); - void registering(); - void registerOk(); + void registering(); + void registerOk(); private slots: - void onBackButtonClicked(); - void onRegisterButtonClicked(); - - // function for showing different errors - void showError(const QString &msg); - void showError(QLabel *label, const QString &msg); - - bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); - bool checkUsername(); - bool checkPassword(); - bool checkPasswordConfirmation(); - bool checkServer(); - - void doWellKnownLookup(); - void doVersionsCheck(); - void doRegistration(); - void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); - void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); - mtx::http::Callback<mtx::responses::Register> registrationCb(); + void onBackButtonClicked(); + void onRegisterButtonClicked(); + + // function for showing different errors + void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); + + bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); + bool checkUsername(); + bool checkPassword(); + bool checkPasswordConfirmation(); + bool checkServer(); + + void doWellKnownLookup(); + void doVersionsCheck(); + void doRegistration(); + void doUIA(const mtx::user_interactive::Unauthorized &unauthorized); + void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth); + mtx::http::Callback<mtx::responses::Register> registrationCb(); private: - QVBoxLayout *top_layout_; - - QHBoxLayout *back_layout_; - QHBoxLayout *logo_layout_; - QHBoxLayout *button_layout_; - - QLabel *logo_; - QLabel *error_label_; - QLabel *error_username_label_; - QLabel *error_password_label_; - QLabel *error_password_confirmation_label_; - QLabel *error_server_label_; - QLabel *error_registration_token_label_; - - FlatButton *back_button_; - RaisedButton *register_button_; - - QWidget *form_widget_; - QHBoxLayout *form_wrapper_; - QVBoxLayout *form_layout_; - - TextField *username_input_; - TextField *password_input_; - TextField *password_confirmation_; - TextField *server_input_; - TextField *registration_token_input_; + QVBoxLayout *top_layout_; + + QHBoxLayout *back_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; + + QLabel *logo_; + QLabel *error_label_; + QLabel *error_username_label_; + QLabel *error_password_label_; + QLabel *error_password_confirmation_label_; + QLabel *error_server_label_; + QLabel *error_registration_token_label_; + + FlatButton *back_button_; + RaisedButton *register_button_; + + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; + + TextField *username_input_; + TextField *password_input_; + TextField *password_confirmation_; + TextField *server_input_; + TextField *registration_token_input_; }; diff --git a/src/RoomDirectoryModel.cpp b/src/RoomDirectoryModel.cpp index cfa2b623..707571d6 100644 --- a/src/RoomDirectoryModel.cpp +++ b/src/RoomDirectoryModel.cpp @@ -12,207 +12,205 @@ RoomDirectoryModel::RoomDirectoryModel(QObject *parent, const std::string &serve : QAbstractListModel(parent) , server_(server) { - connect(ChatPage::instance(), &ChatPage::newRoom, this, [this](const QString &roomid) { - auto roomid_ = roomid.toStdString(); - - int i = 0; - for (const auto &room : publicRoomsData_) { - if (room.room_id == roomid_) { - emit dataChanged(index(i), index(i), {Roles::CanJoin}); - break; - } - i++; - } - }); - - connect(this, - &RoomDirectoryModel::fetchedRoomsBatch, - this, - &RoomDirectoryModel::displayRooms, - Qt::QueuedConnection); + connect(ChatPage::instance(), &ChatPage::newRoom, this, [this](const QString &roomid) { + auto roomid_ = roomid.toStdString(); + + int i = 0; + for (const auto &room : publicRoomsData_) { + if (room.room_id == roomid_) { + emit dataChanged(index(i), index(i), {Roles::CanJoin}); + break; + } + i++; + } + }); + + connect(this, + &RoomDirectoryModel::fetchedRoomsBatch, + this, + &RoomDirectoryModel::displayRooms, + Qt::QueuedConnection); } QHash<int, QByteArray> RoomDirectoryModel::roleNames() const { - return { - {Roles::Name, "name"}, - {Roles::Id, "roomid"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::Topic, "topic"}, - {Roles::MemberCount, "numMembers"}, - {Roles::Previewable, "canPreview"}, - {Roles::CanJoin, "canJoin"}, - }; + return { + {Roles::Name, "name"}, + {Roles::Id, "roomid"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::Topic, "topic"}, + {Roles::MemberCount, "numMembers"}, + {Roles::Previewable, "canPreview"}, + {Roles::CanJoin, "canJoin"}, + }; } void RoomDirectoryModel::resetDisplayedData() { - beginResetModel(); + beginResetModel(); - prevBatch_ = ""; - nextBatch_ = ""; - canFetchMore_ = true; + prevBatch_ = ""; + nextBatch_ = ""; + canFetchMore_ = true; - publicRoomsData_.clear(); + publicRoomsData_.clear(); - endResetModel(); + endResetModel(); } void RoomDirectoryModel::setMatrixServer(const QString &s) { - server_ = s.toStdString(); + server_ = s.toStdString(); - nhlog::ui()->debug("Received matrix server: {}", server_); + nhlog::ui()->debug("Received matrix server: {}", server_); - resetDisplayedData(); + resetDisplayedData(); } void RoomDirectoryModel::setSearchTerm(const QString &f) { - userSearchString_ = f.toStdString(); + userSearchString_ = f.toStdString(); - nhlog::ui()->debug("Received user query: {}", userSearchString_); + nhlog::ui()->debug("Received user query: {}", userSearchString_); - resetDisplayedData(); + resetDisplayedData(); } bool RoomDirectoryModel::canJoinRoom(const QString &room) const { - return !room.isEmpty() && cache::getRoomInfo({room.toStdString()}).empty(); + return !room.isEmpty() && cache::getRoomInfo({room.toStdString()}).empty(); } std::vector<std::string> RoomDirectoryModel::getViasForRoom(const std::vector<std::string> &aliases) { - std::vector<std::string> vias; - - vias.reserve(aliases.size()); - - std::transform(aliases.begin(), - aliases.end(), - std::back_inserter(vias), - [](const auto &alias) { return alias.substr(alias.find(":") + 1); }); - - // When joining a room hosted on a homeserver other than the one the - // account has been registered on, the room's server has to be explicitly - // specified in the "server_name=..." URL parameter of the Matrix Join Room - // request. For more details consult the specs: - // https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias - if (!server_.empty()) { - vias.push_back(server_); - } + std::vector<std::string> vias; + + vias.reserve(aliases.size()); + + std::transform(aliases.begin(), aliases.end(), std::back_inserter(vias), [](const auto &alias) { + return alias.substr(alias.find(":") + 1); + }); - return vias; + // When joining a room hosted on a homeserver other than the one the + // account has been registered on, the room's server has to be explicitly + // specified in the "server_name=..." URL parameter of the Matrix Join Room + // request. For more details consult the specs: + // https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias + if (!server_.empty()) { + vias.push_back(server_); + } + + return vias; } void RoomDirectoryModel::joinRoom(const int &index) { - if (index >= 0 && static_cast<size_t>(index) < publicRoomsData_.size()) { - const auto &chunk = publicRoomsData_[index]; - nhlog::ui()->debug("'Joining room {}", chunk.room_id); - ChatPage::instance()->joinRoomVia(chunk.room_id, getViasForRoom(chunk.aliases)); - } + if (index >= 0 && static_cast<size_t>(index) < publicRoomsData_.size()) { + const auto &chunk = publicRoomsData_[index]; + nhlog::ui()->debug("'Joining room {}", chunk.room_id); + ChatPage::instance()->joinRoomVia(chunk.room_id, getViasForRoom(chunk.aliases)); + } } QVariant RoomDirectoryModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - const auto &room_chunk = publicRoomsData_[index.row()]; - switch (role) { - case Roles::Name: - return QString::fromStdString(room_chunk.name); - case Roles::Id: - return QString::fromStdString(room_chunk.room_id); - case Roles::AvatarUrl: - return QString::fromStdString(room_chunk.avatar_url); - case Roles::Topic: - return QString::fromStdString(room_chunk.topic); - case Roles::MemberCount: - return QVariant::fromValue(room_chunk.num_joined_members); - case Roles::Previewable: - return QVariant::fromValue(room_chunk.world_readable); - case Roles::CanJoin: - return canJoinRoom(QString::fromStdString(room_chunk.room_id)); - } + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &room_chunk = publicRoomsData_[index.row()]; + switch (role) { + case Roles::Name: + return QString::fromStdString(room_chunk.name); + case Roles::Id: + return QString::fromStdString(room_chunk.room_id); + case Roles::AvatarUrl: + return QString::fromStdString(room_chunk.avatar_url); + case Roles::Topic: + return QString::fromStdString(room_chunk.topic); + case Roles::MemberCount: + return QVariant::fromValue(room_chunk.num_joined_members); + case Roles::Previewable: + return QVariant::fromValue(room_chunk.world_readable); + case Roles::CanJoin: + return canJoinRoom(QString::fromStdString(room_chunk.room_id)); } - return {}; + } + return {}; } void RoomDirectoryModel::fetchMore(const QModelIndex &) { - if (!canFetchMore_) - return; - - nhlog::net()->debug("Fetching more rooms from mtxclient..."); - - mtx::requests::PublicRooms req; - req.limit = limit_; - req.since = prevBatch_; - req.filter.generic_search_term = userSearchString_; - // req.third_party_instance_id = third_party_instance_id; - auto requested_server = server_; - - reachedEndOfPagination_ = false; - emit reachedEndOfPaginationChanged(); - - loadingMoreRooms_ = true; - emit loadingMoreRoomsChanged(); - - http::client()->post_public_rooms( - req, - [requested_server, this, req](const mtx::responses::PublicRooms &res, - mtx::http::RequestErr err) { - loadingMoreRooms_ = false; - emit loadingMoreRoomsChanged(); - - if (err) { - nhlog::net()->error( - "Failed to retrieve rooms from mtxclient - {} - {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error, - err->parse_error); - } else if (req.filter.generic_search_term == this->userSearchString_ && - req.since == this->prevBatch_ && requested_server == this->server_) { - nhlog::net()->debug("signalling chunk to GUI thread"); - emit fetchedRoomsBatch(res.chunk, res.next_batch); - } - }, - requested_server); + if (!canFetchMore_) + return; + + nhlog::net()->debug("Fetching more rooms from mtxclient..."); + + mtx::requests::PublicRooms req; + req.limit = limit_; + req.since = prevBatch_; + req.filter.generic_search_term = userSearchString_; + // req.third_party_instance_id = third_party_instance_id; + auto requested_server = server_; + + reachedEndOfPagination_ = false; + emit reachedEndOfPaginationChanged(); + + loadingMoreRooms_ = true; + emit loadingMoreRoomsChanged(); + + http::client()->post_public_rooms( + req, + [requested_server, this, req](const mtx::responses::PublicRooms &res, + mtx::http::RequestErr err) { + loadingMoreRooms_ = false; + emit loadingMoreRoomsChanged(); + + if (err) { + nhlog::net()->error("Failed to retrieve rooms from mtxclient - {} - {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error, + err->parse_error); + } else if (req.filter.generic_search_term == this->userSearchString_ && + req.since == this->prevBatch_ && requested_server == this->server_) { + nhlog::net()->debug("signalling chunk to GUI thread"); + emit fetchedRoomsBatch(res.chunk, res.next_batch); + } + }, + requested_server); } void RoomDirectoryModel::displayRooms(std::vector<mtx::responses::PublicRoomsChunk> fetched_rooms, const std::string &next_batch) { - nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, next_batch); - - if (fetched_rooms.empty()) { - nhlog::net()->error("mtxclient helper thread yielded empty chunk!"); - return; - } - - beginInsertRows(QModelIndex(), - static_cast<int>(publicRoomsData_.size()), - static_cast<int>(publicRoomsData_.size() + fetched_rooms.size()) - 1); - this->publicRoomsData_.insert( - this->publicRoomsData_.end(), fetched_rooms.begin(), fetched_rooms.end()); - endInsertRows(); - - if (next_batch.empty()) { - canFetchMore_ = false; - reachedEndOfPagination_ = true; - emit reachedEndOfPaginationChanged(); - } + nhlog::net()->debug("Prev batch: {} | Next batch: {}", prevBatch_, next_batch); + + if (fetched_rooms.empty()) { + nhlog::net()->error("mtxclient helper thread yielded empty chunk!"); + return; + } + + beginInsertRows(QModelIndex(), + static_cast<int>(publicRoomsData_.size()), + static_cast<int>(publicRoomsData_.size() + fetched_rooms.size()) - 1); + this->publicRoomsData_.insert( + this->publicRoomsData_.end(), fetched_rooms.begin(), fetched_rooms.end()); + endInsertRows(); + + if (next_batch.empty()) { + canFetchMore_ = false; + reachedEndOfPagination_ = true; + emit reachedEndOfPaginationChanged(); + } - prevBatch_ = next_batch; + prevBatch_ = next_batch; - nhlog::ui()->debug("Finished loading rooms"); + nhlog::ui()->debug("Finished loading rooms"); } diff --git a/src/RoomDirectoryModel.h b/src/RoomDirectoryModel.h index 80c04612..4699474b 100644 --- a/src/RoomDirectoryModel.h +++ b/src/RoomDirectoryModel.h @@ -25,74 +25,74 @@ struct PublicRooms; class RoomDirectoryModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged) - Q_PROPERTY(bool reachedEndOfPagination READ reachedEndOfPagination NOTIFY - reachedEndOfPaginationChanged) + Q_PROPERTY(bool loadingMoreRooms READ loadingMoreRooms NOTIFY loadingMoreRoomsChanged) + Q_PROPERTY( + bool reachedEndOfPagination READ reachedEndOfPagination NOTIFY reachedEndOfPaginationChanged) public: - explicit RoomDirectoryModel(QObject *parent = nullptr, const std::string &server = ""); + explicit RoomDirectoryModel(QObject *parent = nullptr, const std::string &server = ""); - enum Roles - { - Name = Qt::UserRole, - Id, - AvatarUrl, - Topic, - MemberCount, - Previewable, - CanJoin, - }; - QHash<int, QByteArray> roleNames() const override; + enum Roles + { + Name = Qt::UserRole, + Id, + AvatarUrl, + Topic, + MemberCount, + Previewable, + CanJoin, + }; + QHash<int, QByteArray> roleNames() const override; - QVariant data(const QModelIndex &index, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; - inline int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return static_cast<int>(publicRoomsData_.size()); - } + inline int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return static_cast<int>(publicRoomsData_.size()); + } - bool canFetchMore(const QModelIndex &) const override { return canFetchMore_; } + bool canFetchMore(const QModelIndex &) const override { return canFetchMore_; } - bool loadingMoreRooms() const { return loadingMoreRooms_; } + bool loadingMoreRooms() const { return loadingMoreRooms_; } - bool reachedEndOfPagination() const { return reachedEndOfPagination_; } + bool reachedEndOfPagination() const { return reachedEndOfPagination_; } - void fetchMore(const QModelIndex &) override; + void fetchMore(const QModelIndex &) override; - Q_INVOKABLE void joinRoom(const int &index = -1); + Q_INVOKABLE void joinRoom(const int &index = -1); signals: - void fetchedRoomsBatch(std::vector<mtx::responses::PublicRoomsChunk> rooms, - const std::string &next_batch); - void loadingMoreRoomsChanged(); - void reachedEndOfPaginationChanged(); + void fetchedRoomsBatch(std::vector<mtx::responses::PublicRoomsChunk> rooms, + const std::string &next_batch); + void loadingMoreRoomsChanged(); + void reachedEndOfPaginationChanged(); public slots: - void setMatrixServer(const QString &s = ""); - void setSearchTerm(const QString &f); + void setMatrixServer(const QString &s = ""); + void setSearchTerm(const QString &f); private slots: - void displayRooms(std::vector<mtx::responses::PublicRoomsChunk> rooms, - const std::string &next_batch); + void displayRooms(std::vector<mtx::responses::PublicRoomsChunk> rooms, + const std::string &next_batch); private: - bool canJoinRoom(const QString &room) const; + bool canJoinRoom(const QString &room) const; - static constexpr size_t limit_ = 50; + static constexpr size_t limit_ = 50; - std::string server_; - std::string userSearchString_; - std::string prevBatch_; - std::string nextBatch_; - bool canFetchMore_{true}; - bool loadingMoreRooms_{false}; - bool reachedEndOfPagination_{false}; - std::vector<mtx::responses::PublicRoomsChunk> publicRoomsData_; + std::string server_; + std::string userSearchString_; + std::string prevBatch_; + std::string nextBatch_; + bool canFetchMore_{true}; + bool loadingMoreRooms_{false}; + bool reachedEndOfPagination_{false}; + std::vector<mtx::responses::PublicRoomsChunk> publicRoomsData_; - std::vector<std::string> getViasForRoom(const std::vector<std::string> &room); - void resetDisplayedData(); + std::vector<std::string> getViasForRoom(const std::vector<std::string> &room); + void resetDisplayedData(); }; diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp index 656a0deb..8c05b7bb 100644 --- a/src/RoomsModel.cpp +++ b/src/RoomsModel.cpp @@ -14,71 +14,67 @@ RoomsModel::RoomsModel(bool showOnlyRoomWithAliases, QObject *parent) : QAbstractListModel(parent) , showOnlyRoomWithAliases_(showOnlyRoomWithAliases) { - std::vector<std::string> rooms_ = cache::joinedRooms(); - roomInfos = cache::getRoomInfo(rooms_); - if (!showOnlyRoomWithAliases_) { - roomids.reserve(rooms_.size()); - roomAliases.reserve(rooms_.size()); - } + std::vector<std::string> rooms_ = cache::joinedRooms(); + roomInfos = cache::getRoomInfo(rooms_); + if (!showOnlyRoomWithAliases_) { + roomids.reserve(rooms_.size()); + roomAliases.reserve(rooms_.size()); + } - for (const auto &r : rooms_) { - auto roomAliasesList = cache::client()->getRoomAliases(r); + for (const auto &r : rooms_) { + auto roomAliasesList = cache::client()->getRoomAliases(r); - if (showOnlyRoomWithAliases_) { - if (roomAliasesList && !roomAliasesList->alias.empty()) { - roomids.push_back(QString::fromStdString(r)); - roomAliases.push_back( - QString::fromStdString(roomAliasesList->alias)); - } - } else { - roomids.push_back(QString::fromStdString(r)); - roomAliases.push_back( - roomAliasesList ? QString::fromStdString(roomAliasesList->alias) : ""); - } + if (showOnlyRoomWithAliases_) { + if (roomAliasesList && !roomAliasesList->alias.empty()) { + roomids.push_back(QString::fromStdString(r)); + roomAliases.push_back(QString::fromStdString(roomAliasesList->alias)); + } + } else { + roomids.push_back(QString::fromStdString(r)); + roomAliases.push_back(roomAliasesList ? QString::fromStdString(roomAliasesList->alias) + : ""); } + } } QHash<int, QByteArray> RoomsModel::roleNames() const { - return {{CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::RoomAlias, "roomAlias"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::RoomID, "roomid"}, - {Roles::RoomName, "roomName"}}; + return {{CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::RoomAlias, "roomAlias"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::RoomID, "roomid"}, + {Roles::RoomName, "roomName"}}; } QVariant RoomsModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: { - if (UserSettings::instance()->markdown()) { - QString percentEncoding = - QUrl::toPercentEncoding(roomAliases[index.row()]); - return QString("[%1](https://matrix.to/#/%2)") - .arg(roomAliases[index.row()], percentEncoding); - } else { - return roomAliases[index.row()]; - } - } - case CompletionModel::SearchRole: - case Qt::DisplayRole: - case Roles::RoomAlias: - return roomAliases[index.row()].toHtmlEscaped(); - case CompletionModel::SearchRole2: - case Roles::RoomName: - return QString::fromStdString(roomInfos.at(roomids[index.row()]).name) - .toHtmlEscaped(); - case Roles::AvatarUrl: - return QString::fromStdString( - roomInfos.at(roomids[index.row()]).avatar_url); - case Roles::RoomID: - return roomids[index.row()].toHtmlEscaped(); - } + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: { + if (UserSettings::instance()->markdown()) { + QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]); + return QString("[%1](https://matrix.to/#/%2)") + .arg(roomAliases[index.row()], percentEncoding); + } else { + return roomAliases[index.row()]; + } + } + case CompletionModel::SearchRole: + case Qt::DisplayRole: + case Roles::RoomAlias: + return roomAliases[index.row()].toHtmlEscaped(); + case CompletionModel::SearchRole2: + case Roles::RoomName: + return QString::fromStdString(roomInfos.at(roomids[index.row()]).name).toHtmlEscaped(); + case Roles::AvatarUrl: + return QString::fromStdString(roomInfos.at(roomids[index.row()]).avatar_url); + case Roles::RoomID: + return roomids[index.row()].toHtmlEscaped(); } - return {}; + } + return {}; } diff --git a/src/RoomsModel.h b/src/RoomsModel.h index 255f207c..b6e29974 100644 --- a/src/RoomsModel.h +++ b/src/RoomsModel.h @@ -12,26 +12,26 @@ class RoomsModel : public QAbstractListModel { public: - enum Roles - { - AvatarUrl = Qt::UserRole, - RoomAlias, - RoomID, - RoomName, - }; + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomAlias, + RoomID, + RoomName, + }; - RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)roomids.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; private: - std::vector<QString> roomids; - std::vector<QString> roomAliases; - std::map<QString, RoomInfo> roomInfos; - bool showOnlyRoomWithAliases_; + std::vector<QString> roomids; + std::vector<QString> roomAliases; + std::map<QString, RoomInfo> roomInfos; + bool showOnlyRoomWithAliases_; }; diff --git a/src/SSOHandler.cpp b/src/SSOHandler.cpp index 8fd0828c..a6f7ba11 100644 --- a/src/SSOHandler.cpp +++ b/src/SSOHandler.cpp @@ -12,46 +12,46 @@ SSOHandler::SSOHandler(QObject *) { - QTimer::singleShot(120000, this, &SSOHandler::ssoFailed); - - using namespace httplib; - - svr.set_logger([](const Request &req, const Response &res) { - nhlog::net()->info("req: {}, res: {}", req.path, res.status); - }); - - svr.Get("/sso", [this](const Request &req, Response &res) { - if (req.has_param("loginToken")) { - auto val = req.get_param_value("loginToken"); - res.set_content("SSO success", "text/plain"); - emit ssoSuccess(val); - } else { - res.set_content("Missing loginToken for SSO login!", "text/plain"); - emit ssoFailed(); - } - }); - - std::thread t([this]() { - this->port = svr.bind_to_any_port("localhost"); - svr.listen_after_bind(); - }); - t.detach(); - - while (!svr.is_running()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + QTimer::singleShot(120000, this, &SSOHandler::ssoFailed); + + using namespace httplib; + + svr.set_logger([](const Request &req, const Response &res) { + nhlog::net()->info("req: {}, res: {}", req.path, res.status); + }); + + svr.Get("/sso", [this](const Request &req, Response &res) { + if (req.has_param("loginToken")) { + auto val = req.get_param_value("loginToken"); + res.set_content("SSO success", "text/plain"); + emit ssoSuccess(val); + } else { + res.set_content("Missing loginToken for SSO login!", "text/plain"); + emit ssoFailed(); } + }); + + std::thread t([this]() { + this->port = svr.bind_to_any_port("localhost"); + svr.listen_after_bind(); + }); + t.detach(); + + while (!svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } } SSOHandler::~SSOHandler() { - svr.stop(); - while (svr.is_running()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + svr.stop(); + while (svr.is_running()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } } std::string SSOHandler::url() const { - return "http://localhost:" + std::to_string(port) + "/sso"; + return "http://localhost:" + std::to_string(port) + "/sso"; } diff --git a/src/SSOHandler.h b/src/SSOHandler.h index bd0d424d..ab652a06 100644 --- a/src/SSOHandler.h +++ b/src/SSOHandler.h @@ -9,20 +9,20 @@ class SSOHandler : public QObject { - Q_OBJECT + Q_OBJECT public: - SSOHandler(QObject *parent = nullptr); + SSOHandler(QObject *parent = nullptr); - ~SSOHandler(); + ~SSOHandler(); - std::string url() const; + std::string url() const; signals: - void ssoSuccess(std::string token); - void ssoFailed(); + void ssoSuccess(std::string token); + void ssoFailed(); private: - httplib::Server svr; - int port = 0; + httplib::Server svr; + int port = 0; }; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index 6d0f0ad9..978a0480 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -24,344 +24,336 @@ SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) , old_statekey_(statekey_) , pack(std::move(pack_.pack)) { - [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>(); + [[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>(); - if (!pack.pack) - pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; + if (!pack.pack) + pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; - for (const auto &e : pack.images) - shortcodes.push_back(e.first); + for (const auto &e : pack.images) + shortcodes.push_back(e.first); - connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); + connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb); } int SingleImagePackModel::rowCount(const QModelIndex &) const { - return (int)shortcodes.size(); + return (int)shortcodes.size(); } QHash<int, QByteArray> SingleImagePackModel::roleNames() const { - return { - {Roles::Url, "url"}, - {Roles::ShortCode, "shortCode"}, - {Roles::Body, "body"}, - {Roles::IsEmote, "isEmote"}, - {Roles::IsSticker, "isSticker"}, - }; + return { + {Roles::Url, "url"}, + {Roles::ShortCode, "shortCode"}, + {Roles::Body, "body"}, + {Roles::IsEmote, "isEmote"}, + {Roles::IsSticker, "isSticker"}, + }; } QVariant SingleImagePackModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - const auto &img = pack.images.at(shortcodes.at(index.row())); - switch (role) { - case Url: - return QString::fromStdString(img.url); - case ShortCode: - return QString::fromStdString(shortcodes.at(index.row())); - case Body: - return QString::fromStdString(img.body); - case IsEmote: - return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); - case IsSticker: - return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); - default: - return {}; - } + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case Url: + return QString::fromStdString(img.url); + case ShortCode: + return QString::fromStdString(shortcodes.at(index.row())); + case Body: + return QString::fromStdString(img.body); + case IsEmote: + return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + case IsSticker: + return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + default: + return {}; } - return {}; + } + return {}; } bool SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) { - using mtx::events::msc2545::PackUsage; - - if (hasIndex(index.row(), index.column(), index.parent())) { - auto &img = pack.images.at(shortcodes.at(index.row())); - switch (role) { - case ShortCode: { - auto newCode = value.toString().toStdString(); - - // otherwise we delete this by accident - if (pack.images.count(newCode)) - return false; - - auto tmp = img; - auto oldCode = shortcodes.at(index.row()); - pack.images.erase(oldCode); - shortcodes[index.row()] = newCode; - pack.images.insert({newCode, tmp}); - - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); - return true; - } - case Body: - img.body = value.toString().toStdString(); - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::Body}); - return true; - case IsEmote: { - bool isEmote = value.toBool(); - bool isSticker = - img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); - - img.usage.set(PackUsage::Emoji, isEmote); - img.usage.set(PackUsage::Sticker, isSticker); - - if (img.usage == pack.pack->usage) - img.usage.reset(); - - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); - - return true; - } - case IsSticker: { - bool isEmote = - img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); - bool isSticker = value.toBool(); - - img.usage.set(PackUsage::Emoji, isEmote); - img.usage.set(PackUsage::Sticker, isSticker); - - if (img.usage == pack.pack->usage) - img.usage.reset(); - - emit dataChanged( - this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); - - return true; - } - } + using mtx::events::msc2545::PackUsage; + + if (hasIndex(index.row(), index.column(), index.parent())) { + auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case ShortCode: { + auto newCode = value.toString().toStdString(); + + // otherwise we delete this by accident + if (pack.images.count(newCode)) + return false; + + auto tmp = img; + auto oldCode = shortcodes.at(index.row()); + pack.images.erase(oldCode); + shortcodes[index.row()] = newCode; + pack.images.insert({newCode, tmp}); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); + return true; } - return false; + case Body: + img.body = value.toString().toStdString(); + emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body}); + return true; + case IsEmote: { + bool isEmote = value.toBool(); + bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); + + return true; + } + case IsSticker: { + bool isEmote = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + bool isSticker = value.toBool(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); + + return true; + } + } + } + return false; } bool SingleImagePackModel::isGloballyEnabled() const { - if (auto roomPacks = - cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { - if (auto tmp = std::get_if< - mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( - &*roomPacks)) { - if (tmp->content.rooms.count(roomid_) && - tmp->content.rooms.at(roomid_).count(statekey_)) - return true; - } + if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = + std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( + &*roomPacks)) { + if (tmp->content.rooms.count(roomid_) && + tmp->content.rooms.at(roomid_).count(statekey_)) + return true; } - return false; + } + return false; } void SingleImagePackModel::setGloballyEnabled(bool enabled) { - mtx::events::msc2545::ImagePackRooms content{}; - if (auto roomPacks = - cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { - if (auto tmp = std::get_if< - mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( - &*roomPacks)) { - content = tmp->content; - } + mtx::events::msc2545::ImagePackRooms content{}; + if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = + std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>( + &*roomPacks)) { + content = tmp->content; } + } - if (enabled) - content.rooms[roomid_][statekey_] = {}; - else - content.rooms[roomid_].erase(statekey_); + if (enabled) + content.rooms[roomid_][statekey_] = {}; + else + content.rooms[roomid_].erase(statekey_); - http::client()->put_account_data(content, [](mtx::http::RequestErr) { - // emit this->globallyEnabledChanged(); - }); + http::client()->put_account_data(content, [](mtx::http::RequestErr) { + // emit this->globallyEnabledChanged(); + }); } bool SingleImagePackModel::canEdit() const { - if (roomid_.empty()) - return true; - else - return Permissions(QString::fromStdString(roomid_)) - .canChange(qml_mtx_events::ImagePackInRoom); + if (roomid_.empty()) + return true; + else + return Permissions(QString::fromStdString(roomid_)) + .canChange(qml_mtx_events::ImagePackInRoom); } void SingleImagePackModel::setPackname(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->display_name) { - this->pack.pack->display_name = val_; - emit packnameChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->display_name) { + this->pack.pack->display_name = val_; + emit packnameChanged(); + } } void SingleImagePackModel::setAttribution(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->attribution) { - this->pack.pack->attribution = val_; - emit attributionChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->attribution) { + this->pack.pack->attribution = val_; + emit attributionChanged(); + } } void SingleImagePackModel::setAvatarUrl(QString val) { - auto val_ = val.toStdString(); - if (val_ != this->pack.pack->avatar_url) { - this->pack.pack->avatar_url = val_; - emit avatarUrlChanged(); - } + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->avatar_url) { + this->pack.pack->avatar_url = val_; + emit avatarUrlChanged(); + } } void SingleImagePackModel::setStatekey(QString val) { - auto val_ = val.toStdString(); - if (val_ != statekey_) { - statekey_ = val_; - emit statekeyChanged(); - } + auto val_ = val.toStdString(); + if (val_ != statekey_) { + statekey_ = val_; + emit statekeyChanged(); + } } void SingleImagePackModel::setIsStickerPack(bool val) { - using mtx::events::msc2545::PackUsage; - if (val != pack.pack->is_sticker()) { - pack.pack->usage.set(PackUsage::Sticker, val); - emit isStickerPackChanged(); - } + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_sticker()) { + pack.pack->usage.set(PackUsage::Sticker, val); + emit isStickerPackChanged(); + } } void SingleImagePackModel::setIsEmotePack(bool val) { - using mtx::events::msc2545::PackUsage; - if (val != pack.pack->is_emoji()) { - pack.pack->usage.set(PackUsage::Emoji, val); - emit isEmotePackChanged(); - } + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_emoji()) { + pack.pack->usage.set(PackUsage::Emoji, val); + emit isEmotePackChanged(); + } } void SingleImagePackModel::save() { - if (roomid_.empty()) { - http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to update image pack: %1") - .arg(QString::fromStdString(e->matrix_error.error))); - }); - } else { - if (old_statekey_ != statekey_) { - http::client()->send_state_event( - roomid_, - to_string(mtx::events::EventType::ImagePackInRoom), - old_statekey_, - nlohmann::json::object(), - [](const mtx::responses::EventId &, mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to delete old image pack: %1") - .arg(QString::fromStdString(e->matrix_error.error))); - }); - } - - http::client()->send_state_event( - roomid_, - statekey_, - pack, - [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { - if (e) - ChatPage::instance()->showNotification( - tr("Failed to update image pack: %1") - .arg(QString::fromStdString(e->matrix_error.error))); - - nhlog::net()->info("Uploaded image pack: %1", statekey_); - }); + if (roomid_.empty()) { + http::client()->put_account_data(pack, [](mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } else { + if (old_statekey_ != statekey_) { + http::client()->send_state_event( + roomid_, + to_string(mtx::events::EventType::ImagePackInRoom), + old_statekey_, + nlohmann::json::object(), + [](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to delete old image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + }); } + + http::client()->send_state_event( + roomid_, + statekey_, + pack, + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + + nhlog::net()->info("Uploaded image pack: %1", statekey_); + }); + } } void SingleImagePackModel::addStickers(QList<QUrl> files) { - for (const auto &f : files) { - auto file = QFile(f.toLocalFile()); - if (!file.open(QFile::ReadOnly)) { - ChatPage::instance()->showNotification( - tr("Failed to open image: %1").arg(f.toLocalFile())); - return; - } - - auto bytes = file.readAll(); - auto img = utils::readImage(bytes); - - mtx::common::ImageInfo info{}; - - auto sz = img.size() / 2; - if (sz.width() > 512 || sz.height() > 512) { - sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); - } else if (img.height() < 128 && img.width() < 128) { - sz = img.size(); - } - - info.h = sz.height(); - info.w = sz.width(); - info.size = bytes.size(); - info.mimetype = - QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(); - - auto filename = f.fileName().toStdString(); - http::client()->upload( - bytes.toStdString(), - QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), - filename, - [this, filename, info](const mtx::responses::ContentURI &uri, - mtx::http::RequestErr e) { - if (e) { - ChatPage::instance()->showNotification( - tr("Failed to upload image: %1") - .arg(QString::fromStdString(e->matrix_error.error))); - return; - } - - emit addImage(uri.content_uri, filename, info); - }); + for (const auto &f : files) { + auto file = QFile(f.toLocalFile()); + if (!file.open(QFile::ReadOnly)) { + ChatPage::instance()->showNotification( + tr("Failed to open image: %1").arg(f.toLocalFile())); + return; } + + auto bytes = file.readAll(); + auto img = utils::readImage(bytes); + + mtx::common::ImageInfo info{}; + + auto sz = img.size() / 2; + if (sz.width() > 512 || sz.height() > 512) { + sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio); + } else if (img.height() < 128 && img.width() < 128) { + sz = img.size(); + } + + info.h = sz.height(); + info.w = sz.width(); + info.size = bytes.size(); + info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(); + + auto filename = f.fileName().toStdString(); + http::client()->upload( + bytes.toStdString(), + QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(), + filename, + [this, filename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) { + if (e) { + ChatPage::instance()->showNotification( + tr("Failed to upload image: %1") + .arg(QString::fromStdString(e->matrix_error.error))); + return; + } + + emit addImage(uri.content_uri, filename, info); + }); + } } void SingleImagePackModel::remove(int idx) { - if (idx < (int)shortcodes.size() && idx >= 0) { - beginRemoveRows(QModelIndex(), idx, idx); - auto s = shortcodes.at(idx); - shortcodes.erase(shortcodes.begin() + idx); - pack.images.erase(s); - endRemoveRows(); - } + if (idx < (int)shortcodes.size() && idx >= 0) { + beginRemoveRows(QModelIndex(), idx, idx); + auto s = shortcodes.at(idx); + shortcodes.erase(shortcodes.begin() + idx); + pack.images.erase(s); + endRemoveRows(); + } } void SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info) { - mtx::events::msc2545::PackImage img{}; - img.url = uri; - img.info = info; - beginInsertRows( - QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size())); + mtx::events::msc2545::PackImage img{}; + img.url = uri; + img.info = info; + beginInsertRows( + QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size())); - pack.images[filename] = img; - shortcodes.push_back(filename); + pack.images[filename] = img; + shortcodes.push_back(filename); - endInsertRows(); + endInsertRows(); } diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index 60138d36..cd8b0547 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -14,81 +14,78 @@ class SingleImagePackModel : public QAbstractListModel { - Q_OBJECT - - Q_PROPERTY(QString roomid READ roomid CONSTANT) - Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) - Q_PROPERTY( - QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) - Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) - Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY( - bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) - Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) - Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY - globallyEnabledChanged) - Q_PROPERTY(bool canEdit READ canEdit CONSTANT) + Q_OBJECT + + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) + Q_PROPERTY(QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) + Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY( + bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) + Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) + Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY + globallyEnabledChanged) + Q_PROPERTY(bool canEdit READ canEdit CONSTANT) public: - enum Roles - { - Url = Qt::UserRole, - ShortCode, - Body, - IsEmote, - IsSticker, - }; - Q_ENUM(Roles); - - SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, - const QVariant &value, - int role = Qt::EditRole) override; - - QString roomid() const { return QString::fromStdString(roomid_); } - QString statekey() const { return QString::fromStdString(statekey_); } - QString packname() const { return QString::fromStdString(pack.pack->display_name); } - QString attribution() const { return QString::fromStdString(pack.pack->attribution); } - QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } - bool isStickerPack() const { return pack.pack->is_sticker(); } - bool isEmotePack() const { return pack.pack->is_emoji(); } - - bool isGloballyEnabled() const; - bool canEdit() const; - void setGloballyEnabled(bool enabled); - - void setPackname(QString val); - void setAttribution(QString val); - void setAvatarUrl(QString val); - void setStatekey(QString val); - void setIsStickerPack(bool val); - void setIsEmotePack(bool val); - - Q_INVOKABLE void save(); - Q_INVOKABLE void addStickers(QList<QUrl> files); - Q_INVOKABLE void remove(int index); + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + IsEmote, + IsSticker, + }; + Q_ENUM(Roles); + + SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + QString roomid() const { return QString::fromStdString(roomid_); } + QString statekey() const { return QString::fromStdString(statekey_); } + QString packname() const { return QString::fromStdString(pack.pack->display_name); } + QString attribution() const { return QString::fromStdString(pack.pack->attribution); } + QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } + bool isStickerPack() const { return pack.pack->is_sticker(); } + bool isEmotePack() const { return pack.pack->is_emoji(); } + + bool isGloballyEnabled() const; + bool canEdit() const; + void setGloballyEnabled(bool enabled); + + void setPackname(QString val); + void setAttribution(QString val); + void setAvatarUrl(QString val); + void setStatekey(QString val); + void setIsStickerPack(bool val); + void setIsEmotePack(bool val); + + Q_INVOKABLE void save(); + Q_INVOKABLE void addStickers(QList<QUrl> files); + Q_INVOKABLE void remove(int index); signals: - void globallyEnabledChanged(); - void statekeyChanged(); - void attributionChanged(); - void packnameChanged(); - void avatarUrlChanged(); - void isEmotePackChanged(); - void isStickerPackChanged(); + void globallyEnabledChanged(); + void statekeyChanged(); + void attributionChanged(); + void packnameChanged(); + void avatarUrlChanged(); + void isEmotePackChanged(); + void isStickerPackChanged(); - void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); + void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info); private slots: - void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); + void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info); private: - std::string roomid_; - std::string statekey_, old_statekey_; + std::string roomid_; + std::string statekey_, old_statekey_; - mtx::events::msc2545::ImagePack pack; - std::vector<std::string> shortcodes; + mtx::events::msc2545::ImagePack pack; + std::vector<std::string> shortcodes; }; diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index db0130c8..98a1d242 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -19,7 +19,7 @@ MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename) : QIconEngine() { - icon_ = QIcon(filename); + icon_ = QIcon(filename); } void @@ -28,95 +28,95 @@ MsgCountComposedIcon::paint(QPainter *painter, QIcon::Mode mode, QIcon::State state) { - painter->setRenderHint(QPainter::TextAntialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - painter->setRenderHint(QPainter::Antialiasing); - - icon_.paint(painter, rect, Qt::AlignCenter, mode, state); - - if (msgCount <= 0) - return; - - QColor backgroundColor("red"); - QColor textColor("white"); - - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor); - - QFont f; - f.setPointSizeF(8); - f.setWeight(QFont::Thin); - - painter->setBrush(brush); - painter->setPen(Qt::NoPen); - painter->setFont(f); - - QRectF bubble(rect.width() - BubbleDiameter, - rect.height() - BubbleDiameter, - BubbleDiameter, - BubbleDiameter); - painter->drawEllipse(bubble); - painter->setPen(QPen(textColor)); - painter->setBrush(Qt::NoBrush); - painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); + painter->setRenderHint(QPainter::TextAntialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setRenderHint(QPainter::Antialiasing); + + icon_.paint(painter, rect, Qt::AlignCenter, mode, state); + + if (msgCount <= 0) + return; + + QColor backgroundColor("red"); + QColor textColor("white"); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor); + + QFont f; + f.setPointSizeF(8); + f.setWeight(QFont::Thin); + + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->setFont(f); + + QRectF bubble(rect.width() - BubbleDiameter, + rect.height() - BubbleDiameter, + BubbleDiameter, + BubbleDiameter); + painter->drawEllipse(bubble); + painter->setPen(QPen(textColor)); + painter->setBrush(Qt::NoBrush); + painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); } QIconEngine * MsgCountComposedIcon::clone() const { - return new MsgCountComposedIcon(*this); + return new MsgCountComposedIcon(*this); } QList<QSize> MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const { - Q_UNUSED(mode); - Q_UNUSED(state); - QList<QSize> sizes; - sizes.append(QSize(24, 24)); - sizes.append(QSize(32, 32)); - sizes.append(QSize(48, 48)); - sizes.append(QSize(64, 64)); - sizes.append(QSize(128, 128)); - sizes.append(QSize(256, 256)); - return sizes; + Q_UNUSED(mode); + Q_UNUSED(state); + QList<QSize> sizes; + sizes.append(QSize(24, 24)); + sizes.append(QSize(32, 32)); + sizes.append(QSize(48, 48)); + sizes.append(QSize(64, 64)); + sizes.append(QSize(128, 128)); + sizes.append(QSize(256, 256)); + return sizes; } QPixmap MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) { - QImage img(size, QImage::Format_ARGB32); - img.fill(qRgba(0, 0, 0, 0)); - QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); - { - QPainter painter(&result); - paint(&painter, QRect(QPoint(0, 0), size), mode, state); - } - return result; + QImage img(size, QImage::Format_ARGB32); + img.fill(qRgba(0, 0, 0, 0)); + QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); + { + QPainter painter(&result); + paint(&painter, QRect(QPoint(0, 0), size), mode, state); + } + return result; } TrayIcon::TrayIcon(const QString &filename, QWidget *parent) : QSystemTrayIcon(parent) { #if defined(Q_OS_MAC) || defined(Q_OS_WIN) - setIcon(QIcon(filename)); + setIcon(QIcon(filename)); #else - icon_ = new MsgCountComposedIcon(filename); - setIcon(QIcon(icon_)); + icon_ = new MsgCountComposedIcon(filename); + setIcon(QIcon(icon_)); #endif - QMenu *menu = new QMenu(parent); - setContextMenu(menu); + QMenu *menu = new QMenu(parent); + setContextMenu(menu); - viewAction_ = new QAction(tr("Show"), this); - quitAction_ = new QAction(tr("Quit"), this); + viewAction_ = new QAction(tr("Show"), this); + quitAction_ = new QAction(tr("Quit"), this); - connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); - connect(quitAction_, &QAction::triggered, this, QApplication::quit); + connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); + connect(quitAction_, &QAction::triggered, this, QApplication::quit); - menu->addAction(viewAction_); - menu->addAction(quitAction_); + menu->addAction(viewAction_); + menu->addAction(quitAction_); } void @@ -127,25 +127,25 @@ TrayIcon::setUnreadCount(int count) // currently, to avoid writing obj-c code, ignore deprecated warnings on the badge functions #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - auto labelText = count == 0 ? "" : QString::number(count); + auto labelText = count == 0 ? "" : QString::number(count); - if (labelText == QtMac::badgeLabelText()) - return; + if (labelText == QtMac::badgeLabelText()) + return; - QtMac::setBadgeLabelText(labelText); + QtMac::setBadgeLabelText(labelText); #pragma clang diagnostic pop #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else - if (count == icon_->msgCount) - return; + if (count == icon_->msgCount) + return; - // Custom drawing on Linux. - MsgCountComposedIcon *tmp = static_cast<MsgCountComposedIcon *>(icon_->clone()); - tmp->msgCount = count; + // Custom drawing on Linux. + MsgCountComposedIcon *tmp = static_cast<MsgCountComposedIcon *>(icon_->clone()); + tmp->msgCount = count; - setIcon(QIcon(tmp)); + setIcon(QIcon(tmp)); - icon_ = tmp; + icon_ = tmp; #endif } diff --git a/src/TrayIcon.h b/src/TrayIcon.h index 10dfafc5..1ce7fb0b 100644 --- a/src/TrayIcon.h +++ b/src/TrayIcon.h @@ -16,33 +16,33 @@ class QPainter; class MsgCountComposedIcon : public QIconEngine { public: - MsgCountComposedIcon(const QString &filename); + MsgCountComposedIcon(const QString &filename); - void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; - QIconEngine *clone() const override; - QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override; - QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + QIconEngine *clone() const override; + QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; - int msgCount = 0; + int msgCount = 0; private: - const int BubbleDiameter = 17; + const int BubbleDiameter = 17; - QIcon icon_; + QIcon icon_; }; class TrayIcon : public QSystemTrayIcon { - Q_OBJECT + Q_OBJECT public: - TrayIcon(const QString &filename, QWidget *parent); + TrayIcon(const QString &filename, QWidget *parent); public slots: - void setUnreadCount(int count); + void setUnreadCount(int count); private: - QAction *viewAction_; - QAction *quitAction_; + QAction *viewAction_; + QAction *quitAction_; - MsgCountComposedIcon *icon_; + MsgCountComposedIcon *icon_; }; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 7b01b0b8..cc1f8206 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -41,1499 +41,1484 @@ QSharedPointer<UserSettings> UserSettings::instance_; UserSettings::UserSettings() { - connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { - instance_.clear(); - }); + connect( + QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); }); } QSharedPointer<UserSettings> UserSettings::instance() { - return instance_; + return instance_; } void UserSettings::initialize(std::optional<QString> profile) { - instance_.reset(new UserSettings()); - instance_->load(profile); + instance_.reset(new UserSettings()); + instance_->load(profile); } void UserSettings::load(std::optional<QString> profile) { - tray_ = settings.value("user/window/tray", false).toBool(); - startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); - - roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); - communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); - - hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); - hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); - groupView_ = settings.value("user/group_view", true).toBool(); - hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); - buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); - timelineMaxWidth_ = settings.value("user/timeline/max_width", 0).toInt(); - messageHoverHighlight_ = - settings.value("user/timeline/message_hover_highlight", false).toBool(); - enlargeEmojiOnlyMessages_ = - settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); - markdown_ = settings.value("user/markdown_enabled", true).toBool(); - animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool(); - typingNotifications_ = settings.value("user/typing_notifications", true).toBool(); - sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); - readReceipts_ = settings.value("user/read_receipts", true).toBool(); - theme_ = settings.value("user/theme", defaultTheme_).toString(); - font_ = settings.value("user/font_family", "default").toString(); - avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); - useIdenticon_ = settings.value("user/use_identicon", true).toBool(); - decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); - privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); - privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); - auto tempPresence = settings.value("user/presence", "").toString().toStdString(); - auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str()); - if (presenceValue < 0) - presenceValue = 0; - presence_ = static_cast<Presence>(presenceValue); - ringtone_ = settings.value("user/ringtone", "Default").toString(); - microphone_ = settings.value("user/microphone", QString()).toString(); - camera_ = settings.value("user/camera", QString()).toString(); - cameraResolution_ = settings.value("user/camera_resolution", QString()).toString(); - cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); - screenShareFrameRate_ = settings.value("user/screen_share_frame_rate", 5).toInt(); - screenSharePiP_ = settings.value("user/screen_share_pip", true).toBool(); - screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool(); - screenShareHideCursor_ = settings.value("user/screen_share_hide_cursor", false).toBool(); - useStunServer_ = settings.value("user/use_stun_server", false).toBool(); - - if (profile) // set to "" if it's the default to maintain compatibility - profile_ = (*profile == "default") ? "" : *profile; - else - profile_ = settings.value("user/currentProfile", "").toString(); - - QString prefix = - (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; - accessToken_ = settings.value(prefix + "auth/access_token", "").toString(); - homeserver_ = settings.value(prefix + "auth/home_server", "").toString(); - userId_ = settings.value(prefix + "auth/user_id", "").toString(); - deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); - - shareKeysWithTrustedUsers_ = - settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) - .toBool(); - onlyShareKeysWithVerifiedUsers_ = - settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); - useOnlineKeyBackup_ = settings.value(prefix + "user/online_key_backup", false).toBool(); - - disableCertificateValidation_ = - settings.value("disable_certificate_validation", false).toBool(); - - applyTheme(); + tray_ = settings.value("user/window/tray", false).toBool(); + startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); + + roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); + communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); + + hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); + hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); + groupView_ = settings.value("user/group_view", true).toBool(); + hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); + buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); + timelineMaxWidth_ = settings.value("user/timeline/max_width", 0).toInt(); + messageHoverHighlight_ = + settings.value("user/timeline/message_hover_highlight", false).toBool(); + enlargeEmojiOnlyMessages_ = + settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); + markdown_ = settings.value("user/markdown_enabled", true).toBool(); + animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool(); + typingNotifications_ = settings.value("user/typing_notifications", true).toBool(); + sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); + readReceipts_ = settings.value("user/read_receipts", true).toBool(); + theme_ = settings.value("user/theme", defaultTheme_).toString(); + font_ = settings.value("user/font_family", "default").toString(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); + useIdenticon_ = settings.value("user/use_identicon", true).toBool(); + decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); + privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); + privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + auto tempPresence = settings.value("user/presence", "").toString().toStdString(); + auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str()); + if (presenceValue < 0) + presenceValue = 0; + presence_ = static_cast<Presence>(presenceValue); + ringtone_ = settings.value("user/ringtone", "Default").toString(); + microphone_ = settings.value("user/microphone", QString()).toString(); + camera_ = settings.value("user/camera", QString()).toString(); + cameraResolution_ = settings.value("user/camera_resolution", QString()).toString(); + cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); + screenShareFrameRate_ = settings.value("user/screen_share_frame_rate", 5).toInt(); + screenSharePiP_ = settings.value("user/screen_share_pip", true).toBool(); + screenShareRemoteVideo_ = settings.value("user/screen_share_remote_video", false).toBool(); + screenShareHideCursor_ = settings.value("user/screen_share_hide_cursor", false).toBool(); + useStunServer_ = settings.value("user/use_stun_server", false).toBool(); + + if (profile) // set to "" if it's the default to maintain compatibility + profile_ = (*profile == "default") ? "" : *profile; + else + profile_ = settings.value("user/currentProfile", "").toString(); + + QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; + accessToken_ = settings.value(prefix + "auth/access_token", "").toString(); + homeserver_ = settings.value(prefix + "auth/home_server", "").toString(); + userId_ = settings.value(prefix + "auth/user_id", "").toString(); + deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false).toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + useOnlineKeyBackup_ = settings.value(prefix + "user/online_key_backup", false).toBool(); + + disableCertificateValidation_ = + settings.value("disable_certificate_validation", false).toBool(); + + applyTheme(); } void UserSettings::setMessageHoverHighlight(bool state) { - if (state == messageHoverHighlight_) - return; - messageHoverHighlight_ = state; - emit messageHoverHighlightChanged(state); - save(); + if (state == messageHoverHighlight_) + return; + messageHoverHighlight_ = state; + emit messageHoverHighlightChanged(state); + save(); } void UserSettings::setEnlargeEmojiOnlyMessages(bool state) { - if (state == enlargeEmojiOnlyMessages_) - return; - enlargeEmojiOnlyMessages_ = state; - emit enlargeEmojiOnlyMessagesChanged(state); - save(); + if (state == enlargeEmojiOnlyMessages_) + return; + enlargeEmojiOnlyMessages_ = state; + emit enlargeEmojiOnlyMessagesChanged(state); + save(); } void UserSettings::setTray(bool state) { - if (state == tray_) - return; - tray_ = state; - emit trayChanged(state); - save(); + if (state == tray_) + return; + tray_ = state; + emit trayChanged(state); + save(); } void UserSettings::setStartInTray(bool state) { - if (state == startInTray_) - return; - startInTray_ = state; - emit startInTrayChanged(state); - save(); + if (state == startInTray_) + return; + startInTray_ = state; + emit startInTrayChanged(state); + save(); } void UserSettings::setMobileMode(bool state) { - if (state == mobileMode_) - return; - mobileMode_ = state; - emit mobileModeChanged(state); - save(); + if (state == mobileMode_) + return; + mobileMode_ = state; + emit mobileModeChanged(state); + save(); } void UserSettings::setGroupView(bool state) { - if (groupView_ == state) - return; + if (groupView_ == state) + return; - groupView_ = state; - emit groupViewStateChanged(state); - save(); + groupView_ = state; + emit groupViewStateChanged(state); + save(); } void UserSettings::setHiddenTags(QStringList hiddenTags) { - hiddenTags_ = hiddenTags; - save(); + hiddenTags_ = hiddenTags; + save(); } void UserSettings::setMarkdown(bool state) { - if (state == markdown_) - return; - markdown_ = state; - emit markdownChanged(state); - save(); + if (state == markdown_) + return; + markdown_ = state; + emit markdownChanged(state); + save(); } void UserSettings::setAnimateImagesOnHover(bool state) { - if (state == animateImagesOnHover_) - return; - animateImagesOnHover_ = state; - emit animateImagesOnHoverChanged(state); - save(); + if (state == animateImagesOnHover_) + return; + animateImagesOnHover_ = state; + emit animateImagesOnHoverChanged(state); + save(); } void UserSettings::setReadReceipts(bool state) { - if (state == readReceipts_) - return; - readReceipts_ = state; - emit readReceiptsChanged(state); - save(); + if (state == readReceipts_) + return; + readReceipts_ = state; + emit readReceiptsChanged(state); + save(); } void UserSettings::setTypingNotifications(bool state) { - if (state == typingNotifications_) - return; - typingNotifications_ = state; - emit typingNotificationsChanged(state); - save(); + if (state == typingNotifications_) + return; + typingNotifications_ = state; + emit typingNotificationsChanged(state); + save(); } void UserSettings::setSortByImportance(bool state) { - if (state == sortByImportance_) - return; - sortByImportance_ = state; - emit roomSortingChanged(state); - save(); + if (state == sortByImportance_) + return; + sortByImportance_ = state; + emit roomSortingChanged(state); + save(); } void UserSettings::setButtonsInTimeline(bool state) { - if (state == buttonsInTimeline_) - return; - buttonsInTimeline_ = state; - emit buttonInTimelineChanged(state); - save(); + if (state == buttonsInTimeline_) + return; + buttonsInTimeline_ = state; + emit buttonInTimelineChanged(state); + save(); } void UserSettings::setTimelineMaxWidth(int state) { - if (state == timelineMaxWidth_) - return; - timelineMaxWidth_ = state; - emit timelineMaxWidthChanged(state); - save(); + if (state == timelineMaxWidth_) + return; + timelineMaxWidth_ = state; + emit timelineMaxWidthChanged(state); + save(); } void UserSettings::setCommunityListWidth(int state) { - if (state == communityListWidth_) - return; - communityListWidth_ = state; - emit communityListWidthChanged(state); - save(); + if (state == communityListWidth_) + return; + communityListWidth_ = state; + emit communityListWidthChanged(state); + save(); } void UserSettings::setRoomListWidth(int state) { - if (state == roomListWidth_) - return; - roomListWidth_ = state; - emit roomListWidthChanged(state); - save(); + if (state == roomListWidth_) + return; + roomListWidth_ = state; + emit roomListWidthChanged(state); + save(); } void UserSettings::setDesktopNotifications(bool state) { - if (state == hasDesktopNotifications_) - return; - hasDesktopNotifications_ = state; - emit desktopNotificationsChanged(state); - save(); + if (state == hasDesktopNotifications_) + return; + hasDesktopNotifications_ = state; + emit desktopNotificationsChanged(state); + save(); } void UserSettings::setAlertOnNotification(bool state) { - if (state == hasAlertOnNotification_) - return; - hasAlertOnNotification_ = state; - emit alertOnNotificationChanged(state); - save(); + if (state == hasAlertOnNotification_) + return; + hasAlertOnNotification_ = state; + emit alertOnNotificationChanged(state); + save(); } void UserSettings::setAvatarCircles(bool state) { - if (state == avatarCircles_) - return; - avatarCircles_ = state; - emit avatarCirclesChanged(state); - save(); + if (state == avatarCircles_) + return; + avatarCircles_ = state; + emit avatarCirclesChanged(state); + save(); } void UserSettings::setDecryptSidebar(bool state) { - if (state == decryptSidebar_) - return; - decryptSidebar_ = state; - emit decryptSidebarChanged(state); - save(); + if (state == decryptSidebar_) + return; + decryptSidebar_ = state; + emit decryptSidebarChanged(state); + save(); } void UserSettings::setPrivacyScreen(bool state) { - if (state == privacyScreen_) { - return; - } - privacyScreen_ = state; - emit privacyScreenChanged(state); - save(); + if (state == privacyScreen_) { + return; + } + privacyScreen_ = state; + emit privacyScreenChanged(state); + save(); } void UserSettings::setPrivacyScreenTimeout(int state) { - if (state == privacyScreenTimeout_) { - return; - } - privacyScreenTimeout_ = state; - emit privacyScreenTimeoutChanged(state); - save(); + if (state == privacyScreenTimeout_) { + return; + } + privacyScreenTimeout_ = state; + emit privacyScreenTimeoutChanged(state); + save(); } void UserSettings::setFontSize(double size) { - if (size == baseFontSize_) - return; - baseFontSize_ = size; - emit fontSizeChanged(size); - save(); + if (size == baseFontSize_) + return; + baseFontSize_ = size; + emit fontSizeChanged(size); + save(); } void UserSettings::setFontFamily(QString family) { - if (family == font_) - return; - font_ = family; - emit fontChanged(family); - save(); + if (family == font_) + return; + font_ = family; + emit fontChanged(family); + save(); } void UserSettings::setEmojiFontFamily(QString family) { - if (family == emojiFont_) - return; + if (family == emojiFont_) + return; - if (family == tr("Default")) { - emojiFont_ = "default"; - } else { - emojiFont_ = family; - } + if (family == tr("Default")) { + emojiFont_ = "default"; + } else { + emojiFont_ = family; + } - emit emojiFontChanged(family); - save(); + emit emojiFontChanged(family); + save(); } void UserSettings::setPresence(Presence state) { - if (state == presence_) - return; - presence_ = state; - emit presenceChanged(state); - save(); + if (state == presence_) + return; + presence_ = state; + emit presenceChanged(state); + save(); } void UserSettings::setTheme(QString theme) { - if (theme == theme_) - return; - theme_ = theme; - save(); - applyTheme(); - emit themeChanged(theme); + if (theme == theme_) + return; + theme_ = theme; + save(); + applyTheme(); + emit themeChanged(theme); } void UserSettings::setUseStunServer(bool useStunServer) { - if (useStunServer == useStunServer_) - return; - useStunServer_ = useStunServer; - emit useStunServerChanged(useStunServer); - save(); + if (useStunServer == useStunServer_) + return; + useStunServer_ = useStunServer; + emit useStunServerChanged(useStunServer); + save(); } void UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) { - if (shareKeys == onlyShareKeysWithVerifiedUsers_) - return; + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; - onlyShareKeysWithVerifiedUsers_ = shareKeys; - emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); - save(); + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); } void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { - if (shareKeys == shareKeysWithTrustedUsers_) - return; + if (shareKeys == shareKeysWithTrustedUsers_) + return; - shareKeysWithTrustedUsers_ = shareKeys; - emit shareKeysWithTrustedUsersChanged(shareKeys); - save(); + shareKeysWithTrustedUsers_ = shareKeys; + emit shareKeysWithTrustedUsersChanged(shareKeys); + save(); } void UserSettings::setUseOnlineKeyBackup(bool useBackup) { - if (useBackup == useOnlineKeyBackup_) - return; + if (useBackup == useOnlineKeyBackup_) + return; - useOnlineKeyBackup_ = useBackup; - emit useOnlineKeyBackupChanged(useBackup); - save(); + useOnlineKeyBackup_ = useBackup; + emit useOnlineKeyBackupChanged(useBackup); + save(); } void UserSettings::setRingtone(QString ringtone) { - if (ringtone == ringtone_) - return; - ringtone_ = ringtone; - emit ringtoneChanged(ringtone); - save(); + if (ringtone == ringtone_) + return; + ringtone_ = ringtone; + emit ringtoneChanged(ringtone); + save(); } void UserSettings::setMicrophone(QString microphone) { - if (microphone == microphone_) - return; - microphone_ = microphone; - emit microphoneChanged(microphone); - save(); + if (microphone == microphone_) + return; + microphone_ = microphone; + emit microphoneChanged(microphone); + save(); } void UserSettings::setCamera(QString camera) { - if (camera == camera_) - return; - camera_ = camera; - emit cameraChanged(camera); - save(); + if (camera == camera_) + return; + camera_ = camera; + emit cameraChanged(camera); + save(); } void UserSettings::setCameraResolution(QString resolution) { - if (resolution == cameraResolution_) - return; - cameraResolution_ = resolution; - emit cameraResolutionChanged(resolution); - save(); + if (resolution == cameraResolution_) + return; + cameraResolution_ = resolution; + emit cameraResolutionChanged(resolution); + save(); } void UserSettings::setCameraFrameRate(QString frameRate) { - if (frameRate == cameraFrameRate_) - return; - cameraFrameRate_ = frameRate; - emit cameraFrameRateChanged(frameRate); - save(); + if (frameRate == cameraFrameRate_) + return; + cameraFrameRate_ = frameRate; + emit cameraFrameRateChanged(frameRate); + save(); } void UserSettings::setScreenShareFrameRate(int frameRate) { - if (frameRate == screenShareFrameRate_) - return; - screenShareFrameRate_ = frameRate; - emit screenShareFrameRateChanged(frameRate); - save(); + if (frameRate == screenShareFrameRate_) + return; + screenShareFrameRate_ = frameRate; + emit screenShareFrameRateChanged(frameRate); + save(); } void UserSettings::setScreenSharePiP(bool state) { - if (state == screenSharePiP_) - return; - screenSharePiP_ = state; - emit screenSharePiPChanged(state); - save(); + if (state == screenSharePiP_) + return; + screenSharePiP_ = state; + emit screenSharePiPChanged(state); + save(); } void UserSettings::setScreenShareRemoteVideo(bool state) { - if (state == screenShareRemoteVideo_) - return; - screenShareRemoteVideo_ = state; - emit screenShareRemoteVideoChanged(state); - save(); + if (state == screenShareRemoteVideo_) + return; + screenShareRemoteVideo_ = state; + emit screenShareRemoteVideoChanged(state); + save(); } void UserSettings::setScreenShareHideCursor(bool state) { - if (state == screenShareHideCursor_) - return; - screenShareHideCursor_ = state; - emit screenShareHideCursorChanged(state); - save(); + if (state == screenShareHideCursor_) + return; + screenShareHideCursor_ = state; + emit screenShareHideCursorChanged(state); + save(); } void UserSettings::setProfile(QString profile) { - if (profile == profile_) - return; - profile_ = profile; - emit profileChanged(profile_); - save(); + if (profile == profile_) + return; + profile_ = profile; + emit profileChanged(profile_); + save(); } void UserSettings::setUserId(QString userId) { - if (userId == userId_) - return; - userId_ = userId; - emit userIdChanged(userId_); - save(); + if (userId == userId_) + return; + userId_ = userId; + emit userIdChanged(userId_); + save(); } void UserSettings::setAccessToken(QString accessToken) { - if (accessToken == accessToken_) - return; - accessToken_ = accessToken; - emit accessTokenChanged(accessToken_); - save(); + if (accessToken == accessToken_) + return; + accessToken_ = accessToken; + emit accessTokenChanged(accessToken_); + save(); } void UserSettings::setDeviceId(QString deviceId) { - if (deviceId == deviceId_) - return; - deviceId_ = deviceId; - emit deviceIdChanged(deviceId_); - save(); + if (deviceId == deviceId_) + return; + deviceId_ = deviceId; + emit deviceIdChanged(deviceId_); + save(); } void UserSettings::setHomeserver(QString homeserver) { - if (homeserver == homeserver_) - return; - homeserver_ = homeserver; - emit homeserverChanged(homeserver_); - save(); + if (homeserver == homeserver_) + return; + homeserver_ = homeserver; + emit homeserverChanged(homeserver_); + save(); } void UserSettings::setDisableCertificateValidation(bool disabled) { - if (disabled == disableCertificateValidation_) - return; - disableCertificateValidation_ = disabled; - http::client()->verify_certificates(!disabled); - emit disableCertificateValidationChanged(disabled); + if (disabled == disableCertificateValidation_) + return; + disableCertificateValidation_ = disabled; + http::client()->verify_certificates(!disabled); + emit disableCertificateValidationChanged(disabled); } void UserSettings::setUseIdenticon(bool state) { - if (state == useIdenticon_) - return; - useIdenticon_ = state; - emit useIdenticonChanged(useIdenticon_); - save(); + if (state == useIdenticon_) + return; + useIdenticon_ = state; + emit useIdenticonChanged(useIdenticon_); + save(); } void UserSettings::applyTheme() { - QFile stylefile; + QFile stylefile; - if (this->theme() == "light") { - stylefile.setFileName(":/styles/styles/nheko.qss"); - } else if (this->theme() == "dark") { - stylefile.setFileName(":/styles/styles/nheko-dark.qss"); - } else { - stylefile.setFileName(":/styles/styles/system.qss"); - } - QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); + if (this->theme() == "light") { + stylefile.setFileName(":/styles/styles/nheko.qss"); + } else if (this->theme() == "dark") { + stylefile.setFileName(":/styles/styles/nheko-dark.qss"); + } else { + stylefile.setFileName(":/styles/styles/system.qss"); + } + QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); - stylefile.open(QFile::ReadOnly); - QString stylesheet = QString(stylefile.readAll()); + stylefile.open(QFile::ReadOnly); + QString stylesheet = QString(stylefile.readAll()); - qobject_cast<QApplication *>(QApplication::instance())->setStyleSheet(stylesheet); + qobject_cast<QApplication *>(QApplication::instance())->setStyleSheet(stylesheet); } void UserSettings::save() { - settings.beginGroup("user"); - - settings.beginGroup("window"); - settings.setValue("tray", tray_); - settings.setValue("start_in_tray", startInTray_); - settings.endGroup(); // window - - settings.beginGroup("sidebar"); - settings.setValue("community_list_width", communityListWidth_); - settings.setValue("room_list_width", roomListWidth_); - settings.endGroup(); // window - - settings.beginGroup("timeline"); - settings.setValue("buttons", buttonsInTimeline_); - settings.setValue("message_hover_highlight", messageHoverHighlight_); - settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_); - settings.setValue("max_width", timelineMaxWidth_); - settings.endGroup(); // timeline - - settings.setValue("avatar_circles", avatarCircles_); - settings.setValue("decrypt_sidebar", decryptSidebar_); - settings.setValue("privacy_screen", privacyScreen_); - settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("mobile_mode", mobileMode_); - settings.setValue("font_size", baseFontSize_); - settings.setValue("typing_notifications", typingNotifications_); - settings.setValue("sort_by_unread", sortByImportance_); - settings.setValue("minor_events", sortByImportance_); - settings.setValue("read_receipts", readReceipts_); - settings.setValue("group_view", groupView_); - settings.setValue("hidden_tags", hiddenTags_); - settings.setValue("markdown_enabled", markdown_); - settings.setValue("animate_images_on_hover", animateImagesOnHover_); - settings.setValue("desktop_notifications", hasDesktopNotifications_); - settings.setValue("alert_on_notification", hasAlertOnNotification_); - settings.setValue("theme", theme()); - settings.setValue("font_family", font_); - settings.setValue("emoji_font_family", emojiFont_); - settings.setValue("presence", - QString::fromUtf8(QMetaEnum::fromType<Presence>().valueToKey( - static_cast<int>(presence_)))); - settings.setValue("ringtone", ringtone_); - settings.setValue("microphone", microphone_); - settings.setValue("camera", camera_); - settings.setValue("camera_resolution", cameraResolution_); - settings.setValue("camera_frame_rate", cameraFrameRate_); - settings.setValue("screen_share_frame_rate", screenShareFrameRate_); - settings.setValue("screen_share_pip", screenSharePiP_); - settings.setValue("screen_share_remote_video", screenShareRemoteVideo_); - settings.setValue("screen_share_hide_cursor", screenShareHideCursor_); - settings.setValue("use_stun_server", useStunServer_); - settings.setValue("currentProfile", profile_); - settings.setValue("use_identicon", useIdenticon_); - - settings.endGroup(); // user - - QString prefix = - (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; - settings.setValue(prefix + "auth/access_token", accessToken_); - settings.setValue(prefix + "auth/home_server", homeserver_); - settings.setValue(prefix + "auth/user_id", userId_); - settings.setValue(prefix + "auth/device_id", deviceId_); - - settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); - settings.setValue(prefix + "user/only_share_keys_with_verified_users", - onlyShareKeysWithVerifiedUsers_); - settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_); - - settings.setValue("disable_certificate_validation", disableCertificateValidation_); - - settings.sync(); + settings.beginGroup("user"); + + settings.beginGroup("window"); + settings.setValue("tray", tray_); + settings.setValue("start_in_tray", startInTray_); + settings.endGroup(); // window + + settings.beginGroup("sidebar"); + settings.setValue("community_list_width", communityListWidth_); + settings.setValue("room_list_width", roomListWidth_); + settings.endGroup(); // window + + settings.beginGroup("timeline"); + settings.setValue("buttons", buttonsInTimeline_); + settings.setValue("message_hover_highlight", messageHoverHighlight_); + settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_); + settings.setValue("max_width", timelineMaxWidth_); + settings.endGroup(); // timeline + + settings.setValue("avatar_circles", avatarCircles_); + settings.setValue("decrypt_sidebar", decryptSidebar_); + settings.setValue("privacy_screen", privacyScreen_); + settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); + settings.setValue("mobile_mode", mobileMode_); + settings.setValue("font_size", baseFontSize_); + settings.setValue("typing_notifications", typingNotifications_); + settings.setValue("sort_by_unread", sortByImportance_); + settings.setValue("minor_events", sortByImportance_); + settings.setValue("read_receipts", readReceipts_); + settings.setValue("group_view", groupView_); + settings.setValue("hidden_tags", hiddenTags_); + settings.setValue("markdown_enabled", markdown_); + settings.setValue("animate_images_on_hover", animateImagesOnHover_); + settings.setValue("desktop_notifications", hasDesktopNotifications_); + settings.setValue("alert_on_notification", hasAlertOnNotification_); + settings.setValue("theme", theme()); + settings.setValue("font_family", font_); + settings.setValue("emoji_font_family", emojiFont_); + settings.setValue( + "presence", + QString::fromUtf8(QMetaEnum::fromType<Presence>().valueToKey(static_cast<int>(presence_)))); + settings.setValue("ringtone", ringtone_); + settings.setValue("microphone", microphone_); + settings.setValue("camera", camera_); + settings.setValue("camera_resolution", cameraResolution_); + settings.setValue("camera_frame_rate", cameraFrameRate_); + settings.setValue("screen_share_frame_rate", screenShareFrameRate_); + settings.setValue("screen_share_pip", screenSharePiP_); + settings.setValue("screen_share_remote_video", screenShareRemoteVideo_); + settings.setValue("screen_share_hide_cursor", screenShareHideCursor_); + settings.setValue("use_stun_server", useStunServer_); + settings.setValue("currentProfile", profile_); + settings.setValue("use_identicon", useIdenticon_); + + settings.endGroup(); // user + + QString prefix = (profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : ""; + settings.setValue(prefix + "auth/access_token", accessToken_); + settings.setValue(prefix + "auth/home_server", homeserver_); + settings.setValue(prefix + "auth/user_id", userId_); + settings.setValue(prefix + "auth/device_id", deviceId_); + + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_); + + settings.setValue("disable_certificate_validation", disableCertificateValidation_); + + settings.sync(); } HorizontalLine::HorizontalLine(QWidget *parent) : QFrame{parent} { - setFrameShape(QFrame::HLine); - setFrameShadow(QFrame::Sunken); + setFrameShape(QFrame::HLine); + setFrameShadow(QFrame::Sunken); } UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent) : QWidget{parent} , settings_{settings} { - topLayout_ = new QVBoxLayout{this}; - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - auto backBtn_ = new FlatButton{this}; - backBtn_->setMinimumSize(QSize(24, 24)); - backBtn_->setIcon(icon); - backBtn_->setIconSize(QSize(24, 24)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.1); - - auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); - if (QCoreApplication::applicationName() != "nheko") - versionInfo->setText(versionInfo->text() + " | " + - tr("profile: %1").arg(QCoreApplication::applicationName())); - versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); - - topBarLayout_ = new QHBoxLayout; - topBarLayout_->setSpacing(0); - topBarLayout_->setMargin(0); - topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); - topBarLayout_->addStretch(1); - - formLayout_ = new QFormLayout; - - formLayout_->setLabelAlignment(Qt::AlignLeft); - formLayout_->setFormAlignment(Qt::AlignRight); - formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); - formLayout_->setHorizontalSpacing(0); - - auto general_ = new QLabel{tr("GENERAL"), this}; - general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - general_->setFont(font); - - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - useIdenticon_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - onlyShareKeysWithVerifiedUsers_ = new Toggle(this); - shareKeysWithTrustedUsers_ = new Toggle(this); - useOnlineKeyBackup_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - animateImagesOnHover_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; - - trayToggle_->setChecked(settings_->tray()); - startInTrayToggle_->setChecked(settings_->startInTray()); - avatarCircles_->setChecked(settings_->avatarCircles()); - useIdenticon_->setChecked(settings_->useIdenticon()); - decryptSidebar_->setChecked(settings_->decryptSidebar()); - privacyScreen_->setChecked(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); - useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup()); - groupViewToggle_->setChecked(settings_->groupView()); - timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); - typingNotifications_->setChecked(settings_->typingNotifications()); - messageHoverHighlight_->setChecked(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages()); - sortByImportance_->setChecked(settings_->sortByImportance()); - readReceipts_->setChecked(settings_->readReceipts()); - markdown_->setChecked(settings_->markdown()); - animateImagesOnHover_->setChecked(settings_->animateImagesOnHover()); - desktopNotifications_->setChecked(settings_->hasDesktopNotifications()); - alertOnNotification_->setChecked(settings_->hasAlertOnNotification()); - useStunServer_->setChecked(settings_->useStunServer()); - mobileMode_->setChecked(settings_->mobileMode()); - - if (!settings_->tray()) { - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); - } - - if (!settings_->privacyScreen()) { - privacyScreenTimeout_->setDisabled(true); - } - - avatarCircles_->setFixedSize(64, 48); - - auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; - uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); - uiLabel_->setAlignment(Qt::AlignBottom); - uiLabel_->setFont(font); - - for (double option = 1; option <= 3; option += 0.25) - scaleFactorCombo_->addItem(QString::number(option)); - for (double option = 6; option <= 24; option += 0.5) - fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); - - QFontDatabase fontDb; - - // TODO: Is there a way to limit to just emojis, rather than - // all emoji fonts? - auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); - emojiFontSelectionCombo_->addItem(tr("Default")); - for (const auto &family : emojiFamilies) { - emojiFontSelectionCombo_->addItem(family); - } - - QString currentFont = settings_->font(); - if (currentFont != "default" || currentFont != "") { - fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); + topLayout_ = new QVBoxLayout{this}; + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + auto backBtn_ = new FlatButton{this}; + backBtn_->setMinimumSize(QSize(24, 24)); + backBtn_->setIcon(icon); + backBtn_->setIconSize(QSize(24, 24)); + + QFont font; + font.setPointSizeF(font.pointSizeF() * 1.1); + + auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); + if (QCoreApplication::applicationName() != "nheko") + versionInfo->setText(versionInfo->text() + " | " + + tr("profile: %1").arg(QCoreApplication::applicationName())); + versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); + + topBarLayout_ = new QHBoxLayout; + topBarLayout_->setSpacing(0); + topBarLayout_->setMargin(0); + topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); + topBarLayout_->addStretch(1); + + formLayout_ = new QFormLayout; + + formLayout_->setLabelAlignment(Qt::AlignLeft); + formLayout_->setFormAlignment(Qt::AlignRight); + formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); + formLayout_->setHorizontalSpacing(0); + + auto general_ = new QLabel{tr("GENERAL"), this}; + general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + general_->setFont(font); + + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + useIdenticon_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + useOnlineKeyBackup_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + animateImagesOnHover_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; + + trayToggle_->setChecked(settings_->tray()); + startInTrayToggle_->setChecked(settings_->startInTray()); + avatarCircles_->setChecked(settings_->avatarCircles()); + useIdenticon_->setChecked(settings_->useIdenticon()); + decryptSidebar_->setChecked(settings_->decryptSidebar()); + privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); + shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); + useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup()); + groupViewToggle_->setChecked(settings_->groupView()); + timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); + typingNotifications_->setChecked(settings_->typingNotifications()); + messageHoverHighlight_->setChecked(settings_->messageHoverHighlight()); + enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages()); + sortByImportance_->setChecked(settings_->sortByImportance()); + readReceipts_->setChecked(settings_->readReceipts()); + markdown_->setChecked(settings_->markdown()); + animateImagesOnHover_->setChecked(settings_->animateImagesOnHover()); + desktopNotifications_->setChecked(settings_->hasDesktopNotifications()); + alertOnNotification_->setChecked(settings_->hasAlertOnNotification()); + useStunServer_->setChecked(settings_->useStunServer()); + mobileMode_->setChecked(settings_->mobileMode()); + + if (!settings_->tray()) { + startInTrayToggle_->setState(false); + startInTrayToggle_->setDisabled(true); + } + + if (!settings_->privacyScreen()) { + privacyScreenTimeout_->setDisabled(true); + } + + avatarCircles_->setFixedSize(64, 48); + + auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; + uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); + uiLabel_->setAlignment(Qt::AlignBottom); + uiLabel_->setFont(font); + + for (double option = 1; option <= 3; option += 0.25) + scaleFactorCombo_->addItem(QString::number(option)); + for (double option = 6; option <= 24; option += 0.5) + fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); + + QFontDatabase fontDb; + + // TODO: Is there a way to limit to just emojis, rather than + // all emoji fonts? + auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + emojiFontSelectionCombo_->addItem(tr("Default")); + for (const auto &family : emojiFamilies) { + emojiFontSelectionCombo_->addItem(family); + } + + QString currentFont = settings_->font(); + if (currentFont != "default" || currentFont != "") { + fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); + } + + emojiFontSelectionCombo_->setCurrentIndex( + emojiFontSelectionCombo_->findText(settings_->emojiFont())); + + themeCombo_ = new QComboBox{this}; + themeCombo_->addItem("Light"); + themeCombo_->addItem("Dark"); + themeCombo_->addItem("System"); + + QString themeStr = settings_->theme(); + themeStr.replace(0, 1, themeStr[0].toUpper()); + int themeIndex = themeCombo_->findText(themeStr); + themeCombo_->setCurrentIndex(themeIndex); + + timelineMaxWidthSpin_->setMinimum(0); + timelineMaxWidthSpin_->setMaximum(100'000'000); + timelineMaxWidthSpin_->setSingleStep(10); + + privacyScreenTimeout_->setMinimum(0); + privacyScreenTimeout_->setMaximum(3600); + privacyScreenTimeout_->setSingleStep(10); + + auto callsLabel = new QLabel{tr("CALLS"), this}; + callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); + callsLabel->setAlignment(Qt::AlignBottom); + callsLabel->setFont(font); + + auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; + encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); + encryptionLabel_->setAlignment(Qt::AlignBottom); + encryptionLabel_->setFont(font); + + QFont monospaceFont; + monospaceFont.setFamily("Monospace"); + monospaceFont.setStyleHint(QFont::Monospace); + monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); + + deviceIdValue_ = new QLabel{this}; + deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); + deviceIdValue_->setFont(monospaceFont); + + deviceFingerprintValue_ = new QLabel{this}; + deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); + deviceFingerprintValue_->setFont(monospaceFont); + + deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); + + backupSecretCached = new QLabel{this}; + masterSecretCached = new QLabel{this}; + selfSigningSecretCached = new QLabel{this}; + userSigningSecretCached = new QLabel{this}; + backupSecretCached->setFont(monospaceFont); + masterSecretCached->setFont(monospaceFont); + selfSigningSecretCached->setFont(monospaceFont); + userSigningSecretCached->setFont(monospaceFont); + + auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; + sessionKeysLabel->setFont(font); + sessionKeysLabel->setMargin(OptionMargin); + + auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; + auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; + + auto sessionKeysLayout = new QHBoxLayout; + sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); + sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); + sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); + + auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this}; + crossSigningKeysLabel->setFont(font); + crossSigningKeysLabel->setMargin(OptionMargin); + + auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this}; + auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this}; + + auto crossSigningKeysLayout = new QHBoxLayout; + crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); + crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight); + crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight); + + auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") { + auto label = new QLabel{labelText, this}; + label->setFont(font); + label->setMargin(OptionMargin); + + if (!tooltipText.isEmpty()) { + label->setToolTip(tooltipText); } - emojiFontSelectionCombo_->setCurrentIndex( - emojiFontSelectionCombo_->findText(settings_->emojiFont())); - - themeCombo_ = new QComboBox{this}; - themeCombo_->addItem("Light"); - themeCombo_->addItem("Dark"); - themeCombo_->addItem("System"); - - QString themeStr = settings_->theme(); - themeStr.replace(0, 1, themeStr[0].toUpper()); - int themeIndex = themeCombo_->findText(themeStr); - themeCombo_->setCurrentIndex(themeIndex); - - timelineMaxWidthSpin_->setMinimum(0); - timelineMaxWidthSpin_->setMaximum(100'000'000); - timelineMaxWidthSpin_->setSingleStep(10); - - privacyScreenTimeout_->setMinimum(0); - privacyScreenTimeout_->setMaximum(3600); - privacyScreenTimeout_->setSingleStep(10); - - auto callsLabel = new QLabel{tr("CALLS"), this}; - callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); - callsLabel->setAlignment(Qt::AlignBottom); - callsLabel->setFont(font); - - auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; - encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); - encryptionLabel_->setAlignment(Qt::AlignBottom); - encryptionLabel_->setFont(font); - - QFont monospaceFont; - monospaceFont.setFamily("Monospace"); - monospaceFont.setStyleHint(QFont::Monospace); - monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); - - deviceIdValue_ = new QLabel{this}; - deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceIdValue_->setFont(monospaceFont); - - deviceFingerprintValue_ = new QLabel{this}; - deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceFingerprintValue_->setFont(monospaceFont); - - deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); - - backupSecretCached = new QLabel{this}; - masterSecretCached = new QLabel{this}; - selfSigningSecretCached = new QLabel{this}; - userSigningSecretCached = new QLabel{this}; - backupSecretCached->setFont(monospaceFont); - masterSecretCached->setFont(monospaceFont); - selfSigningSecretCached->setFont(monospaceFont); - userSigningSecretCached->setFont(monospaceFont); - - auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; - sessionKeysLabel->setFont(font); - sessionKeysLabel->setMargin(OptionMargin); - - auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; - auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; - - auto sessionKeysLayout = new QHBoxLayout; - sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); - - auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this}; - crossSigningKeysLabel->setFont(font); - crossSigningKeysLabel->setMargin(OptionMargin); - - auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this}; - auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this}; - - auto crossSigningKeysLayout = new QHBoxLayout; - crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight); - - auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") { - auto label = new QLabel{labelText, this}; - label->setFont(font); - label->setMargin(OptionMargin); - - if (!tooltipText.isEmpty()) { - label->setToolTip(tooltipText); - } - - auto layout = new QHBoxLayout; - layout->addWidget(field, 0, Qt::AlignRight); - - formLayout_->addRow(label, layout); - }; - - formLayout_->addRow(general_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap( - tr("Minimize to tray"), - trayToggle_, - tr("Keep the application running in the background after closing the client window.")); - boxWrap(tr("Start in tray"), - startInTrayToggle_, - tr("Start the application in the background without showing the client window.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Circular Avatars"), - avatarCircles_, - 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.")); - boxWrap(tr("Group's sidebar"), - groupViewToggle_, - tr("Show a column containing groups and tags next to the room list.")); - boxWrap(tr("Decrypt messages in sidebar"), - decryptSidebar_, - tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " - "encrypted chats.")); - boxWrap(tr("Privacy Screen"), - privacyScreen_, - tr("When the window loses focus, the timeline will\nbe blurred.")); - boxWrap( - tr("Privacy screen timeout (in seconds [0 - 3600])"), - privacyScreenTimeout_, - tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen" - " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " - "hour (3600 seconds)")); - boxWrap(tr("Show buttons in timeline"), - timelineButtonsToggle_, - tr("Show buttons to quickly reply, react or access additional options next to each " - "message.")); - boxWrap(tr("Limit width of timeline"), - timelineMaxWidthSpin_, - tr("Set the max width of messages in the timeline (in pixels). This can help " - "readability on wide screen, when Nheko is maximised")); - boxWrap(tr("Typing notifications"), - typingNotifications_, - tr("Show who is typing in a room.\nThis will also enable or disable sending typing " - "notifications to others.")); - boxWrap( - tr("Sort rooms by unreads"), - sortByImportance_, - tr( - "Display rooms with new messages first.\nIf this is off, the list of rooms will only " - "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which " - "have active notifications (the small circle with a number in it) will be sorted on " - "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " - "seem to consider them as important as the other rooms.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Read receipts"), - readReceipts_, - tr("Show if your message was read.\nStatus is displayed next to timestamps.")); - boxWrap( - tr("Send messages as Markdown"), - markdown_, - tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " - "text.")); - boxWrap(tr("Play animated images only on hover"), - animateImagesOnHover_, - tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.")); - boxWrap(tr("Desktop notifications"), - desktopNotifications_, - tr("Notify about received message when the client is not currently focused.")); - boxWrap(tr("Alert on notification"), - alertOnNotification_, - tr("Show an alert when a message is received.\nThis usually causes the application " - "icon in the task bar to animate in some fashion.")); - boxWrap(tr("Highlight message on hover"), - messageHoverHighlight_, - tr("Change the background color of messages when you hover over them.")); - boxWrap(tr("Large Emoji in timeline"), - enlargeEmojiOnlyMessages_, - tr("Make font size larger if messages with only a few emojis are displayed.")); - formLayout_->addRow(uiLabel_); - formLayout_->addRow(new HorizontalLine{this}); - - boxWrap(tr("Touchscreen mode"), - mobileMode_, - tr("Will prevent text selection in the timeline to make touch scrolling easier.")); + auto layout = new QHBoxLayout; + layout->addWidget(field, 0, Qt::AlignRight); + + formLayout_->addRow(label, layout); + }; + + formLayout_->addRow(general_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Minimize to tray"), + trayToggle_, + tr("Keep the application running in the background after closing the client window.")); + boxWrap(tr("Start in tray"), + startInTrayToggle_, + tr("Start the application in the background without showing the client window.")); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Circular Avatars"), + avatarCircles_, + 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.")); + boxWrap(tr("Group's sidebar"), + groupViewToggle_, + tr("Show a column containing groups and tags next to the room list.")); + boxWrap(tr("Decrypt messages in sidebar"), + decryptSidebar_, + tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " + "encrypted chats.")); + boxWrap(tr("Privacy Screen"), + privacyScreen_, + tr("When the window loses focus, the timeline will\nbe blurred.")); + boxWrap(tr("Privacy screen timeout (in seconds [0 - 3600])"), + privacyScreenTimeout_, + tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen" + " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " + "hour (3600 seconds)")); + boxWrap(tr("Show buttons in timeline"), + timelineButtonsToggle_, + tr("Show buttons to quickly reply, react or access additional options next to each " + "message.")); + boxWrap(tr("Limit width of timeline"), + timelineMaxWidthSpin_, + tr("Set the max width of messages in the timeline (in pixels). This can help " + "readability on wide screen, when Nheko is maximised")); + boxWrap(tr("Typing notifications"), + typingNotifications_, + tr("Show who is typing in a room.\nThis will also enable or disable sending typing " + "notifications to others.")); + boxWrap( + tr("Sort rooms by unreads"), + sortByImportance_, + tr("Display rooms with new messages first.\nIf this is off, the list of rooms will only " + "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which " + "have active notifications (the small circle with a number in it) will be sorted on " + "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " + "seem to consider them as important as the other rooms.")); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Read receipts"), + readReceipts_, + tr("Show if your message was read.\nStatus is displayed next to timestamps.")); + boxWrap(tr("Send messages as Markdown"), + markdown_, + tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " + "text.")); + boxWrap(tr("Play animated images only on hover"), + animateImagesOnHover_, + tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.")); + boxWrap(tr("Desktop notifications"), + desktopNotifications_, + tr("Notify about received message when the client is not currently focused.")); + boxWrap(tr("Alert on notification"), + alertOnNotification_, + tr("Show an alert when a message is received.\nThis usually causes the application " + "icon in the task bar to animate in some fashion.")); + boxWrap(tr("Highlight message on hover"), + messageHoverHighlight_, + tr("Change the background color of messages when you hover over them.")); + boxWrap(tr("Large Emoji in timeline"), + enlargeEmojiOnlyMessages_, + tr("Make font size larger if messages with only a few emojis are displayed.")); + formLayout_->addRow(uiLabel_); + formLayout_->addRow(new HorizontalLine{this}); + + boxWrap(tr("Touchscreen mode"), + mobileMode_, + tr("Will prevent text selection in the timeline to make touch scrolling easier.")); #if !defined(Q_OS_MAC) - boxWrap(tr("Scale factor"), - scaleFactorCombo_, - tr("Change the scale factor of the whole user interface.")); + boxWrap(tr("Scale factor"), + scaleFactorCombo_, + tr("Change the scale factor of the whole user interface.")); #else - scaleFactorCombo_->hide(); + scaleFactorCombo_->hide(); #endif - boxWrap(tr("Font size"), fontSizeCombo_); - boxWrap(tr("Font Family"), fontSelectionCombo_); + boxWrap(tr("Font size"), fontSizeCombo_); + boxWrap(tr("Font Family"), fontSelectionCombo_); #if !defined(Q_OS_MAC) - boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); + boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); #else - emojiFontSelectionCombo_->hide(); + emojiFontSelectionCombo_->hide(); #endif - boxWrap(tr("Theme"), themeCombo_); - - formLayout_->addRow(callsLabel); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Ringtone"), - ringtoneCombo_, - tr("Set the notification sound to play when a call invite arrives")); - boxWrap(tr("Microphone"), microphoneCombo_); - boxWrap(tr("Camera"), cameraCombo_); - boxWrap(tr("Camera resolution"), cameraResolutionCombo_); - boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_); - - ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - ringtoneCombo_->addItem("Mute"); - ringtoneCombo_->addItem("Default"); - ringtoneCombo_->addItem("Other..."); - const QString &ringtone = settings_->ringtone(); - if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default") - ringtoneCombo_->addItem(ringtone); - microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - - boxWrap(tr("Allow fallback call assist server"), - useStunServer_, - tr("Will use turn.matrix.org as assist when your home server does not offer one.")); - - formLayout_->addRow(encryptionLabel_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Device ID"), deviceIdValue_); - boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap(tr("Send encrypted messages to verified users only"), - onlyShareKeysWithVerifiedUsers_, - tr("Requires a user to be verified to send encrypted messages to them. This " - "improves safety but makes E2EE more tedious.")); - boxWrap(tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified, " - "even if that device shouldn't have access to those keys otherwise.")); - boxWrap(tr("Online Key Backup"), - useOnlineKeyBackup_, - tr("Download message encryption keys from and upload to the encrypted online key " - "backup.")); - formLayout_->addRow(new HorizontalLine{this}); - formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); - formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); - - boxWrap(tr("Master signing key"), - masterSecretCached, - tr("Your most important key. You don't need to have it cached, since not caching " - "it makes it less likely it can be stolen and it is only needed to rotate your " - "other signing keys.")); - boxWrap(tr("User signing key"), - userSigningSecretCached, - tr("The key to verify other users. If it is cached, verifying a user will verify " - "all their devices.")); - boxWrap( - tr("Self signing key"), - selfSigningSecretCached, - tr("The key to verify your own devices. If it is cached, verifying one of your devices " - "will mark it verified for all your other devices and for users, that have verified " - "you.")); - boxWrap(tr("Backup key"), - 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(); - - auto scrollArea_ = new QScrollArea{this}; - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - - QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); - - auto spacingAroundForm = new QHBoxLayout; - spacingAroundForm->addStretch(1); - spacingAroundForm->addLayout(formLayout_, 0); - spacingAroundForm->addStretch(1); - - auto scrollAreaContents_ = new QWidget{this}; - scrollAreaContents_->setObjectName("UserSettingScrollWidget"); - scrollAreaContents_->setLayout(spacingAroundForm); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addLayout(topBarLayout_); - topLayout_->addWidget(scrollArea_, Qt::AlignTop); - topLayout_->addStretch(1); - topLayout_->addWidget(versionInfo); - - connect(themeCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &text) { - settings_->setTheme(text.toLower()); - emit themeChanged(); - }); - connect(scaleFactorCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); - connect(fontSizeCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); - connect(fontSelectionCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); - connect(emojiFontSelectionCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); - - connect(ringtoneCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &ringtone) { - if (ringtone == "Other...") { - QString homeFolder = - QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - auto filepath = QFileDialog::getOpenFileName( - this, tr("Select a file"), homeFolder, tr("All Files (*)")); - if (!filepath.isEmpty()) { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != "Mute" && oldSetting != "Default") - ringtoneCombo_->removeItem( - ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(filepath); - ringtoneCombo_->addItem(filepath); - ringtoneCombo_->setCurrentText(filepath); - } else { - ringtoneCombo_->setCurrentText(settings_->ringtone()); - } - } else if (ringtone == "Mute" || ringtone == "Default") { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != "Mute" && oldSetting != "Default") - ringtoneCombo_->removeItem( - ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(ringtone); - } - }); - - connect(microphoneCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString µphone) { settings_->setMicrophone(microphone); }); - - connect(cameraCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &camera) { - settings_->setCamera(camera); - std::vector<std::string> resolutions = - CallDevices::instance().resolutions(camera.toStdString()); - cameraResolutionCombo_->clear(); - for (const auto &resolution : resolutions) - cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); - }); - - connect(cameraResolutionCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &resolution) { - settings_->setCameraResolution(resolution); - std::vector<std::string> frameRates = CallDevices::instance().frameRates( - settings_->camera().toStdString(), resolution.toStdString()); - cameraFrameRateCombo_->clear(); - for (const auto &frameRate : frameRates) - cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); - }); - - connect(cameraFrameRateCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); }); - - connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTray(enabled); - if (enabled) { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setEnabled(true); - startInTrayToggle_->setState(false); - settings_->setStartInTray(false); - } else { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); - settings_->setStartInTray(false); + boxWrap(tr("Theme"), themeCombo_); + + formLayout_->addRow(callsLabel); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Ringtone"), + ringtoneCombo_, + tr("Set the notification sound to play when a call invite arrives")); + boxWrap(tr("Microphone"), microphoneCombo_); + boxWrap(tr("Camera"), cameraCombo_); + boxWrap(tr("Camera resolution"), cameraResolutionCombo_); + boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_); + + ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + ringtoneCombo_->addItem("Mute"); + ringtoneCombo_->addItem("Default"); + ringtoneCombo_->addItem("Other..."); + const QString &ringtone = settings_->ringtone(); + if (!ringtone.isEmpty() && ringtone != "Mute" && ringtone != "Default") + ringtoneCombo_->addItem(ringtone); + microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); + + boxWrap(tr("Allow fallback call assist server"), + useStunServer_, + tr("Will use turn.matrix.org as assist when your home server does not offer one.")); + + formLayout_->addRow(encryptionLabel_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Device ID"), deviceIdValue_); + boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); + boxWrap(tr("Online Key Backup"), + useOnlineKeyBackup_, + tr("Download message encryption keys from and upload to the encrypted online key " + "backup.")); + formLayout_->addRow(new HorizontalLine{this}); + formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); + formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); + + boxWrap(tr("Master signing key"), + masterSecretCached, + tr("Your most important key. You don't need to have it cached, since not caching " + "it makes it less likely it can be stolen and it is only needed to rotate your " + "other signing keys.")); + boxWrap(tr("User signing key"), + userSigningSecretCached, + tr("The key to verify other users. If it is cached, verifying a user will verify " + "all their devices.")); + boxWrap(tr("Self signing key"), + selfSigningSecretCached, + tr("The key to verify your own devices. If it is cached, verifying one of your devices " + "will mark it verified for all your other devices and for users, that have verified " + "you.")); + boxWrap(tr("Backup key"), + 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(); + + auto scrollArea_ = new QScrollArea{this}; + scrollArea_->setFrameShape(QFrame::NoFrame); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->setWidgetResizable(true); + scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); + + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + + auto spacingAroundForm = new QHBoxLayout; + spacingAroundForm->addStretch(1); + spacingAroundForm->addLayout(formLayout_, 0); + spacingAroundForm->addStretch(1); + + auto scrollAreaContents_ = new QWidget{this}; + scrollAreaContents_->setObjectName("UserSettingScrollWidget"); + scrollAreaContents_->setLayout(spacingAroundForm); + + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addLayout(topBarLayout_); + topLayout_->addWidget(scrollArea_, Qt::AlignTop); + topLayout_->addStretch(1); + topLayout_->addWidget(versionInfo); + + connect(themeCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &text) { + settings_->setTheme(text.toLower()); + emit themeChanged(); + }); + connect(scaleFactorCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); + connect(fontSizeCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); + connect(fontSelectionCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); + connect(emojiFontSelectionCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); + + connect(ringtoneCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &ringtone) { + if (ringtone == "Other...") { + QString homeFolder = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + auto filepath = QFileDialog::getOpenFileName( + this, tr("Select a file"), homeFolder, tr("All Files (*)")); + if (!filepath.isEmpty()) { + const auto &oldSetting = settings_->ringtone(); + if (oldSetting != "Mute" && oldSetting != "Default") + ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); + settings_->setRingtone(filepath); + ringtoneCombo_->addItem(filepath); + ringtoneCombo_->setCurrentText(filepath); + } else { + ringtoneCombo_->setCurrentText(settings_->ringtone()); + } + } else if (ringtone == "Mute" || ringtone == "Default") { + const auto &oldSetting = settings_->ringtone(); + if (oldSetting != "Mute" && oldSetting != "Default") + ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); + settings_->setRingtone(ringtone); } - emit trayOptionChanged(enabled); - }); - - connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setStartInTray(enabled); - }); - - connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMobileMode(enabled); - }); + }); + + connect(microphoneCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString µphone) { settings_->setMicrophone(microphone); }); + + connect(cameraCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &camera) { + settings_->setCamera(camera); + std::vector<std::string> resolutions = + CallDevices::instance().resolutions(camera.toStdString()); + cameraResolutionCombo_->clear(); + for (const auto &resolution : resolutions) + cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); + }); + + connect(cameraResolutionCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &resolution) { + settings_->setCameraResolution(resolution); + std::vector<std::string> frameRates = CallDevices::instance().frameRates( + settings_->camera().toStdString(), resolution.toStdString()); + cameraFrameRateCombo_->clear(); + for (const auto &frameRate : frameRates) + cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); + }); + + connect(cameraFrameRateCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); }); + + connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setTray(enabled); + if (enabled) { + startInTrayToggle_->setChecked(false); + startInTrayToggle_->setEnabled(true); + startInTrayToggle_->setState(false); + settings_->setStartInTray(false); + } else { + startInTrayToggle_->setChecked(false); + startInTrayToggle_->setState(false); + startInTrayToggle_->setDisabled(true); + settings_->setStartInTray(false); + } + emit trayOptionChanged(enabled); + }); - connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setGroupView(enabled); - }); + connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setStartInTray(enabled); + }); - connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDecryptSidebar(enabled); - }); + connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setMobileMode(enabled); + }); - connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setPrivacyScreen(enabled); - if (enabled) { - privacyScreenTimeout_->setEnabled(true); - } else { - privacyScreenTimeout_->setDisabled(true); - } - }); + connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setGroupView(enabled); + }); - connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setOnlyShareKeysWithVerifiedUsers(enabled); - }); + connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setDecryptSidebar(enabled); + }); - connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setShareKeysWithTrustedUsers(enabled); - }); + connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setPrivacyScreen(enabled); + if (enabled) { + privacyScreenTimeout_->setEnabled(true); + } else { + privacyScreenTimeout_->setDisabled(true); + } + }); + + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setShareKeysWithTrustedUsers(enabled); + }); + + connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) { + if (enabled) { + if (QMessageBox::question( + this, + tr("Enable online key backup"), + tr("The Nheko authors recommend not enabling online key backup until " + "symmetric online key backup is available. Enable anyway?")) != + QMessageBox::StandardButton::Yes) { + useOnlineKeyBackup_->setState(false); + return; + } + } + settings_->setUseOnlineKeyBackup(enabled); + }); - connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) { - if (enabled) { - if (QMessageBox::question( - this, - tr("Enable online key backup"), - tr("The Nheko authors recommend not enabling online key backup until " - "symmetric online key backup is available. Enable anyway?")) != - QMessageBox::StandardButton::Yes) { - useOnlineKeyBackup_->setState(false); - return; - } - } - settings_->setUseOnlineKeyBackup(enabled); - }); + connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAvatarCircles(enabled); + }); - connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAvatarCircles(enabled); + if (JdenticonProvider::isAvailable()) + connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setUseIdenticon(enabled); }); + else + useIdenticon_->setDisabled(true); - if (JdenticonProvider::isAvailable()) - connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setUseIdenticon(enabled); - }); - else - useIdenticon_->setDisabled(true); + connect( + markdown_, &Toggle::toggled, this, [this](bool enabled) { settings_->setMarkdown(enabled); }); - connect(markdown_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMarkdown(enabled); - }); + connect(animateImagesOnHover_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAnimateImagesOnHover(enabled); + }); - connect(animateImagesOnHover_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAnimateImagesOnHover(enabled); - }); + connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setTypingNotifications(enabled); + }); - connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTypingNotifications(enabled); - }); + connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setSortByImportance(enabled); + }); - connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setSortByImportance(enabled); - }); + connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setButtonsInTimeline(enabled); + }); - connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setButtonsInTimeline(enabled); - }); + connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setReadReceipts(enabled); + }); - connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setReadReceipts(enabled); - }); + connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setDesktopNotifications(enabled); + }); - connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDesktopNotifications(enabled); - }); - - connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAlertOnNotification(enabled); - }); + connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setAlertOnNotification(enabled); + }); - connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMessageHoverHighlight(enabled); - }); + connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setMessageHoverHighlight(enabled); + }); - connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setEnlargeEmojiOnlyMessages(enabled); - }); + connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setEnlargeEmojiOnlyMessages(enabled); + }); - connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setUseStunServer(enabled); - }); + connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setUseStunServer(enabled); + }); - connect(timelineMaxWidthSpin_, - qOverload<int>(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setTimelineMaxWidth(newValue); }); + connect(timelineMaxWidthSpin_, + qOverload<int>(&QSpinBox::valueChanged), + this, + [this](int newValue) { settings_->setTimelineMaxWidth(newValue); }); - connect(privacyScreenTimeout_, - qOverload<int>(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); }); + connect(privacyScreenTimeout_, + qOverload<int>(&QSpinBox::valueChanged), + this, + [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); }); - connect( - sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); + connect( + sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); - connect( - sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); + connect( + sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); - connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() { - olm::request_cross_signing_keys(); - }); + connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() { + olm::request_cross_signing_keys(); + }); - connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() { - olm::download_cross_signing_keys(); - }); + connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() { + olm::download_cross_signing_keys(); + }); - connect(backBtn_, &QPushButton::clicked, this, [this]() { - settings_->save(); - emit moveBack(); - }); + connect(backBtn_, &QPushButton::clicked, this, [this]() { + settings_->save(); + emit moveBack(); + }); } void UserSettingsPage::showEvent(QShowEvent *) { - // FIXME macOS doesn't show the full option unless a space is added. - utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " "); - utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor())); - utils::restoreCombobox(themeCombo_, settings_->theme()); - utils::restoreCombobox(ringtoneCombo_, settings_->ringtone()); - - trayToggle_->setState(settings_->tray()); - startInTrayToggle_->setState(settings_->startInTray()); - groupViewToggle_->setState(settings_->groupView()); - decryptSidebar_->setState(settings_->decryptSidebar()); - privacyScreen_->setState(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); - useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup()); - avatarCircles_->setState(settings_->avatarCircles()); - typingNotifications_->setState(settings_->typingNotifications()); - sortByImportance_->setState(settings_->sortByImportance()); - timelineButtonsToggle_->setState(settings_->buttonsInTimeline()); - mobileMode_->setState(settings_->mobileMode()); - readReceipts_->setState(settings_->readReceipts()); - markdown_->setState(settings_->markdown()); - desktopNotifications_->setState(settings_->hasDesktopNotifications()); - alertOnNotification_->setState(settings_->hasAlertOnNotification()); - messageHoverHighlight_->setState(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages()); - deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); - timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); - privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); - - auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); - microphoneCombo_->clear(); - for (const auto &m : mics) - microphoneCombo_->addItem(QString::fromStdString(m)); - - auto cameraResolution = settings_->cameraResolution(); - auto cameraFrameRate = settings_->cameraFrameRate(); - - auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); - cameraCombo_->clear(); - for (const auto &c : cameras) - cameraCombo_->addItem(QString::fromStdString(c)); - - utils::restoreCombobox(cameraResolutionCombo_, cameraResolution); - utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate); - - useStunServer_->setState(settings_->useStunServer()); - - deviceFingerprintValue_->setText( - utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); + // FIXME macOS doesn't show the full option unless a space is added. + utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " "); + utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor())); + utils::restoreCombobox(themeCombo_, settings_->theme()); + utils::restoreCombobox(ringtoneCombo_, settings_->ringtone()); + + trayToggle_->setState(settings_->tray()); + startInTrayToggle_->setState(settings_->startInTray()); + groupViewToggle_->setState(settings_->groupView()); + decryptSidebar_->setState(settings_->decryptSidebar()); + privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); + shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); + useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup()); + avatarCircles_->setState(settings_->avatarCircles()); + typingNotifications_->setState(settings_->typingNotifications()); + sortByImportance_->setState(settings_->sortByImportance()); + timelineButtonsToggle_->setState(settings_->buttonsInTimeline()); + mobileMode_->setState(settings_->mobileMode()); + readReceipts_->setState(settings_->readReceipts()); + markdown_->setState(settings_->markdown()); + desktopNotifications_->setState(settings_->hasDesktopNotifications()); + alertOnNotification_->setState(settings_->hasAlertOnNotification()); + messageHoverHighlight_->setState(settings_->messageHoverHighlight()); + enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages()); + deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); + timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); + privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); + + auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); + microphoneCombo_->clear(); + for (const auto &m : mics) + microphoneCombo_->addItem(QString::fromStdString(m)); + + auto cameraResolution = settings_->cameraResolution(); + auto cameraFrameRate = settings_->cameraFrameRate(); + + auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); + cameraCombo_->clear(); + for (const auto &c : cameras) + cameraCombo_->addItem(QString::fromStdString(c)); + + utils::restoreCombobox(cameraResolutionCombo_, cameraResolution); + utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate); + + useStunServer_->setState(settings_->useStunServer()); + + deviceFingerprintValue_->setText( + utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); } void UserSettingsPage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } void UserSettingsPage::importSessionKeys() { - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); - return; - } - - auto bin = file.peek(file.size()); - auto payload = std::string(bin.data(), bin.size()); - - bool ok; - auto password = QInputDialog::getText(this, - tr("File Password"), - tr("Enter the passphrase to decrypt the file:"), - QLineEdit::Password, - "", - &ok); - if (!ok) - return; - - if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); - return; - } - - try { - auto sessions = - mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); - cache::importSessionKeys(std::move(sessions)); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); - } + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error"), file.errorString()); + return; + } + + auto bin = file.peek(file.size()); + auto payload = std::string(bin.data(), bin.size()); + + bool ok; + auto password = QInputDialog::getText(this, + tr("File Password"), + tr("Enter the passphrase to decrypt the file:"), + QLineEdit::Password, + "", + &ok); + if (!ok) + return; + + if (password.isEmpty()) { + QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + return; + } + + try { + auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); + cache::importSessionKeys(std::move(sessions)); + } catch (const std::exception &e) { + QMessageBox::warning(this, tr("Error"), e.what()); + } } void UserSettingsPage::exportSessionKeys() { - // Open password dialog. - bool ok; - auto password = QInputDialog::getText(this, - tr("File Password"), - tr("Enter passphrase to encrypt your session keys:"), - QLineEdit::Password, - "", - &ok); - if (!ok) - return; - - if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); - return; - } - - // Open file dialog to save the file. - const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); - - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); - return; - } - - // Export sessions & save to file. - try { - auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( - cache::exportSessionKeys(), password.toStdString()); - - QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); - - QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); - QString suffix("-----END MEGOLM SESSION DATA-----"); - QString newline("\n"); - QTextStream out(&file); - out << prefix << newline << b64 << newline << suffix << newline; - file.close(); - } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); - } + // Open password dialog. + bool ok; + auto password = QInputDialog::getText(this, + tr("File Password"), + tr("Enter passphrase to encrypt your session keys:"), + QLineEdit::Password, + "", + &ok); + if (!ok) + return; + + if (password.isEmpty()) { + QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + return; + } + + // Open file dialog to save the file. + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, tr("Error"), file.errorString()); + return; + } + + // Export sessions & save to file. + try { + auto encrypted_blob = mtx::crypto::encrypt_exported_sessions(cache::exportSessionKeys(), + password.toStdString()); + + QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); + + QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); + QString suffix("-----END MEGOLM SESSION DATA-----"); + QString newline("\n"); + QTextStream out(&file); + out << prefix << newline << b64 << newline << suffix << newline; + file.close(); + } catch (const std::exception &e) { + QMessageBox::warning(this, tr("Error"), e.what()); + } } void UserSettingsPage::updateSecretStatus() { - QString ok = "QLabel { color : #00cc66; }"; - QString notSoOk = "QLabel { color : #ff9933; }"; - - auto updateLabel = [&ok, ¬SoOk](QLabel *label, const std::string &secretName) { - if (cache::secret(secretName)) { - label->setStyleSheet(ok); - label->setText(tr("CACHED")); - } else { - if (secretName == mtx::secret_storage::secrets::cross_signing_master) - label->setStyleSheet(ok); - else - label->setStyleSheet(notSoOk); - label->setText(tr("NOT CACHED")); - } - }; - - updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master); - updateLabel(userSigningSecretCached, - mtx::secret_storage::secrets::cross_signing_user_signing); - updateLabel(selfSigningSecretCached, - mtx::secret_storage::secrets::cross_signing_self_signing); - updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1); + QString ok = "QLabel { color : #00cc66; }"; + QString notSoOk = "QLabel { color : #ff9933; }"; + + auto updateLabel = [&ok, ¬SoOk](QLabel *label, const std::string &secretName) { + if (cache::secret(secretName)) { + label->setStyleSheet(ok); + label->setText(tr("CACHED")); + } else { + if (secretName == mtx::secret_storage::secrets::cross_signing_master) + label->setStyleSheet(ok); + else + label->setStyleSheet(notSoOk); + label->setText(tr("NOT CACHED")); + } + }; + + updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master); + updateLabel(userSigningSecretCached, mtx::secret_storage::secrets::cross_signing_user_signing); + updateLabel(selfSigningSecretCached, mtx::secret_storage::secrets::cross_signing_self_signing); + updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index bcd9439b..31e28db2 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -30,402 +30,395 @@ constexpr int LayoutBottomMargin = LayoutTopMargin; class UserSettings : public QObject { - Q_OBJECT - - Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) - Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE - setMessageHoverHighlight NOTIFY messageHoverHighlightChanged) - Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE - setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged) - Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged) - Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged) - Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged) - Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) - Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover - NOTIFY animateImagesOnHoverChanged) - Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications - NOTIFY typingNotificationsChanged) - Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY - roomSortingChanged) - Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY - buttonInTimelineChanged) - Q_PROPERTY( - bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged) - Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE - setDesktopNotifications NOTIFY desktopNotificationsChanged) - Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification - NOTIFY alertOnNotificationChanged) - Q_PROPERTY( - bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged) - Q_PROPERTY(bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY - decryptSidebarChanged) - Q_PROPERTY( - bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged) - Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout - NOTIFY privacyScreenTimeoutChanged) - Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY - timelineMaxWidthChanged) - Q_PROPERTY( - int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) - Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY - communityListWidthChanged) - Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) - Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) - Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) - Q_PROPERTY( - QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged) - Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged) - Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) - Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged) - Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged) - Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY - cameraResolutionChanged) - Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY - cameraFrameRateChanged) - Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate - NOTIFY screenShareFrameRateChanged) - Q_PROPERTY(bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY - screenSharePiPChanged) - Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE - setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged) - Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE - setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) - Q_PROPERTY( - bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) - Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE - setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) - Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE - setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) - Q_PROPERTY(bool useOnlineKeyBackup READ useOnlineKeyBackup WRITE setUseOnlineKeyBackup - NOTIFY useOnlineKeyBackupChanged) - Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) - Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) - Q_PROPERTY( - QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged) - Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) - Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) - Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE - setDisableCertificateValidation NOTIFY disableCertificateValidationChanged) - Q_PROPERTY( - bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged) - - UserSettings(); + Q_OBJECT + + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(bool messageHoverHighlight READ messageHoverHighlight WRITE setMessageHoverHighlight + NOTIFY messageHoverHighlightChanged) + Q_PROPERTY(bool enlargeEmojiOnlyMessages READ enlargeEmojiOnlyMessages WRITE + setEnlargeEmojiOnlyMessages NOTIFY enlargeEmojiOnlyMessagesChanged) + Q_PROPERTY(bool tray READ tray WRITE setTray NOTIFY trayChanged) + Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged) + Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged) + Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) + Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover + NOTIFY animateImagesOnHoverChanged) + Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications NOTIFY + typingNotificationsChanged) + Q_PROPERTY(bool sortByImportance READ sortByImportance WRITE setSortByImportance NOTIFY + roomSortingChanged) + Q_PROPERTY(bool buttonsInTimeline READ buttonsInTimeline WRITE setButtonsInTimeline NOTIFY + buttonInTimelineChanged) + Q_PROPERTY(bool readReceipts READ readReceipts WRITE setReadReceipts NOTIFY readReceiptsChanged) + Q_PROPERTY(bool desktopNotifications READ hasDesktopNotifications WRITE setDesktopNotifications + NOTIFY desktopNotificationsChanged) + Q_PROPERTY(bool alertOnNotification READ hasAlertOnNotification WRITE setAlertOnNotification + NOTIFY alertOnNotificationChanged) + Q_PROPERTY( + bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged) + Q_PROPERTY( + bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY decryptSidebarChanged) + Q_PROPERTY( + bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged) + Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout + NOTIFY privacyScreenTimeoutChanged) + Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY + timelineMaxWidthChanged) + Q_PROPERTY( + int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) + Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY + communityListWidthChanged) + Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) + Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) + Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) + Q_PROPERTY(QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged) + Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged) + Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) + Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged) + Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged) + Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY + cameraResolutionChanged) + Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY + cameraFrameRateChanged) + Q_PROPERTY(int screenShareFrameRate READ screenShareFrameRate WRITE setScreenShareFrameRate + NOTIFY screenShareFrameRateChanged) + Q_PROPERTY( + bool screenSharePiP READ screenSharePiP WRITE setScreenSharePiP NOTIFY screenSharePiPChanged) + Q_PROPERTY(bool screenShareRemoteVideo READ screenShareRemoteVideo WRITE + setScreenShareRemoteVideo NOTIFY screenShareRemoteVideoChanged) + Q_PROPERTY(bool screenShareHideCursor READ screenShareHideCursor WRITE setScreenShareHideCursor + NOTIFY screenShareHideCursorChanged) + Q_PROPERTY( + bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) + Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE + setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) + Q_PROPERTY(bool useOnlineKeyBackup READ useOnlineKeyBackup WRITE setUseOnlineKeyBackup NOTIFY + useOnlineKeyBackupChanged) + Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) + Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged) + Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged) + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) + Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE + setDisableCertificateValidation NOTIFY disableCertificateValidationChanged) + Q_PROPERTY(bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged) + + UserSettings(); public: - static QSharedPointer<UserSettings> instance(); - static void initialize(std::optional<QString> profile); - - QSettings *qsettings() { return &settings; } - - enum class Presence - { - AutomaticPresence, - Online, - Unavailable, - Offline, - }; - Q_ENUM(Presence) - - void save(); - void load(std::optional<QString> profile); - void applyTheme(); - void setTheme(QString theme); - void setMessageHoverHighlight(bool state); - void setEnlargeEmojiOnlyMessages(bool state); - void setTray(bool state); - void setStartInTray(bool state); - void setMobileMode(bool mode); - void setFontSize(double size); - void setFontFamily(QString family); - void setEmojiFontFamily(QString family); - void setGroupView(bool state); - void setMarkdown(bool state); - void setAnimateImagesOnHover(bool state); - void setReadReceipts(bool state); - void setTypingNotifications(bool state); - void setSortByImportance(bool state); - void setButtonsInTimeline(bool state); - void setTimelineMaxWidth(int state); - void setCommunityListWidth(int state); - void setRoomListWidth(int state); - void setDesktopNotifications(bool state); - void setAlertOnNotification(bool state); - void setAvatarCircles(bool state); - void setDecryptSidebar(bool state); - void setPrivacyScreen(bool state); - void setPrivacyScreenTimeout(int state); - void setPresence(Presence state); - void setRingtone(QString ringtone); - void setMicrophone(QString microphone); - void setCamera(QString camera); - void setCameraResolution(QString resolution); - void setCameraFrameRate(QString frameRate); - void setScreenShareFrameRate(int frameRate); - void setScreenSharePiP(bool state); - void setScreenShareRemoteVideo(bool state); - void setScreenShareHideCursor(bool state); - void setUseStunServer(bool state); - void setOnlyShareKeysWithVerifiedUsers(bool state); - void setShareKeysWithTrustedUsers(bool state); - void setUseOnlineKeyBackup(bool state); - void setProfile(QString profile); - void setUserId(QString userId); - void setAccessToken(QString accessToken); - void setDeviceId(QString deviceId); - void setHomeserver(QString homeserver); - void setDisableCertificateValidation(bool disabled); - void setHiddenTags(QStringList hiddenTags); - void setUseIdenticon(bool state); - - QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } - bool messageHoverHighlight() const { return messageHoverHighlight_; } - bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; } - bool tray() const { return tray_; } - bool startInTray() const { return startInTray_; } - bool groupView() const { return groupView_; } - bool avatarCircles() const { return avatarCircles_; } - bool decryptSidebar() const { return decryptSidebar_; } - bool privacyScreen() const { return privacyScreen_; } - int privacyScreenTimeout() const { return privacyScreenTimeout_; } - bool markdown() const { return markdown_; } - bool animateImagesOnHover() const { return animateImagesOnHover_; } - bool typingNotifications() const { return typingNotifications_; } - bool sortByImportance() const { return sortByImportance_; } - bool buttonsInTimeline() const { return buttonsInTimeline_; } - bool mobileMode() const { return mobileMode_; } - bool readReceipts() const { return readReceipts_; } - bool hasDesktopNotifications() const { return hasDesktopNotifications_; } - bool hasAlertOnNotification() const { return hasAlertOnNotification_; } - bool hasNotifications() const - { - return hasDesktopNotifications() || hasAlertOnNotification(); + static QSharedPointer<UserSettings> instance(); + static void initialize(std::optional<QString> profile); + + QSettings *qsettings() { return &settings; } + + enum class Presence + { + AutomaticPresence, + Online, + Unavailable, + Offline, + }; + Q_ENUM(Presence) + + void save(); + void load(std::optional<QString> profile); + void applyTheme(); + void setTheme(QString theme); + void setMessageHoverHighlight(bool state); + void setEnlargeEmojiOnlyMessages(bool state); + void setTray(bool state); + void setStartInTray(bool state); + void setMobileMode(bool mode); + void setFontSize(double size); + void setFontFamily(QString family); + void setEmojiFontFamily(QString family); + void setGroupView(bool state); + void setMarkdown(bool state); + void setAnimateImagesOnHover(bool state); + void setReadReceipts(bool state); + void setTypingNotifications(bool state); + void setSortByImportance(bool state); + void setButtonsInTimeline(bool state); + void setTimelineMaxWidth(int state); + void setCommunityListWidth(int state); + void setRoomListWidth(int state); + void setDesktopNotifications(bool state); + void setAlertOnNotification(bool state); + void setAvatarCircles(bool state); + void setDecryptSidebar(bool state); + void setPrivacyScreen(bool state); + void setPrivacyScreenTimeout(int state); + void setPresence(Presence state); + void setRingtone(QString ringtone); + void setMicrophone(QString microphone); + void setCamera(QString camera); + void setCameraResolution(QString resolution); + void setCameraFrameRate(QString frameRate); + void setScreenShareFrameRate(int frameRate); + void setScreenSharePiP(bool state); + void setScreenShareRemoteVideo(bool state); + void setScreenShareHideCursor(bool state); + void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); + void setShareKeysWithTrustedUsers(bool state); + void setUseOnlineKeyBackup(bool state); + void setProfile(QString profile); + void setUserId(QString userId); + void setAccessToken(QString accessToken); + void setDeviceId(QString deviceId); + void setHomeserver(QString homeserver); + void setDisableCertificateValidation(bool disabled); + void setHiddenTags(QStringList hiddenTags); + void setUseIdenticon(bool state); + + QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } + bool messageHoverHighlight() const { return messageHoverHighlight_; } + bool enlargeEmojiOnlyMessages() const { return enlargeEmojiOnlyMessages_; } + bool tray() const { return tray_; } + bool startInTray() const { return startInTray_; } + bool groupView() const { return groupView_; } + bool avatarCircles() const { return avatarCircles_; } + bool decryptSidebar() const { return decryptSidebar_; } + bool privacyScreen() const { return privacyScreen_; } + int privacyScreenTimeout() const { return privacyScreenTimeout_; } + bool markdown() const { return markdown_; } + bool animateImagesOnHover() const { return animateImagesOnHover_; } + bool typingNotifications() const { return typingNotifications_; } + bool sortByImportance() const { return sortByImportance_; } + bool buttonsInTimeline() const { return buttonsInTimeline_; } + bool mobileMode() const { return mobileMode_; } + bool readReceipts() const { return readReceipts_; } + bool hasDesktopNotifications() const { return hasDesktopNotifications_; } + bool hasAlertOnNotification() const { return hasAlertOnNotification_; } + bool hasNotifications() const { return hasDesktopNotifications() || hasAlertOnNotification(); } + int timelineMaxWidth() const { return timelineMaxWidth_; } + int communityListWidth() const { return communityListWidth_; } + int roomListWidth() const { return roomListWidth_; } + double fontSize() const { return baseFontSize_; } + QString font() const { return font_; } + QString emojiFont() const + { + if (emojiFont_ == "Default") { + return tr("Default"); } - int timelineMaxWidth() const { return timelineMaxWidth_; } - int communityListWidth() const { return communityListWidth_; } - int roomListWidth() const { return roomListWidth_; } - double fontSize() const { return baseFontSize_; } - QString font() const { return font_; } - QString emojiFont() const - { - if (emojiFont_ == "Default") { - return tr("Default"); - } - - return emojiFont_; - } - Presence presence() const { return presence_; } - QString ringtone() const { return ringtone_; } - QString microphone() const { return microphone_; } - QString camera() const { return camera_; } - QString cameraResolution() const { return cameraResolution_; } - QString cameraFrameRate() const { return cameraFrameRate_; } - int screenShareFrameRate() const { return screenShareFrameRate_; } - bool screenSharePiP() const { return screenSharePiP_; } - bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; } - bool screenShareHideCursor() const { return screenShareHideCursor_; } - bool useStunServer() const { return useStunServer_; } - bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } - bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } - bool useOnlineKeyBackup() const { return useOnlineKeyBackup_; } - QString profile() const { return profile_; } - QString userId() const { return userId_; } - QString accessToken() const { return accessToken_; } - QString deviceId() const { return deviceId_; } - QString homeserver() const { return homeserver_; } - bool disableCertificateValidation() const { return disableCertificateValidation_; } - QStringList hiddenTags() const { return hiddenTags_; } - bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); } + + return emojiFont_; + } + Presence presence() const { return presence_; } + QString ringtone() const { return ringtone_; } + QString microphone() const { return microphone_; } + QString camera() const { return camera_; } + QString cameraResolution() const { return cameraResolution_; } + QString cameraFrameRate() const { return cameraFrameRate_; } + int screenShareFrameRate() const { return screenShareFrameRate_; } + bool screenSharePiP() const { return screenSharePiP_; } + bool screenShareRemoteVideo() const { return screenShareRemoteVideo_; } + bool screenShareHideCursor() const { return screenShareHideCursor_; } + bool useStunServer() const { return useStunServer_; } + bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } + bool useOnlineKeyBackup() const { return useOnlineKeyBackup_; } + QString profile() const { return profile_; } + QString userId() const { return userId_; } + QString accessToken() const { return accessToken_; } + QString deviceId() const { return deviceId_; } + QString homeserver() const { return homeserver_; } + bool disableCertificateValidation() const { return disableCertificateValidation_; } + QStringList hiddenTags() const { return hiddenTags_; } + bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); } signals: - void groupViewStateChanged(bool state); - void roomSortingChanged(bool state); - void themeChanged(QString state); - void messageHoverHighlightChanged(bool state); - void enlargeEmojiOnlyMessagesChanged(bool state); - void trayChanged(bool state); - void startInTrayChanged(bool state); - void markdownChanged(bool state); - void animateImagesOnHoverChanged(bool state); - void typingNotificationsChanged(bool state); - void buttonInTimelineChanged(bool state); - void readReceiptsChanged(bool state); - void desktopNotificationsChanged(bool state); - void alertOnNotificationChanged(bool state); - void avatarCirclesChanged(bool state); - void decryptSidebarChanged(bool state); - void privacyScreenChanged(bool state); - void privacyScreenTimeoutChanged(int state); - void timelineMaxWidthChanged(int state); - void roomListWidthChanged(int state); - void communityListWidthChanged(int state); - void mobileModeChanged(bool mode); - void fontSizeChanged(double state); - void fontChanged(QString state); - void emojiFontChanged(QString state); - void presenceChanged(Presence state); - void ringtoneChanged(QString ringtone); - void microphoneChanged(QString microphone); - void cameraChanged(QString camera); - void cameraResolutionChanged(QString resolution); - void cameraFrameRateChanged(QString frameRate); - void screenShareFrameRateChanged(int frameRate); - void screenSharePiPChanged(bool state); - void screenShareRemoteVideoChanged(bool state); - void screenShareHideCursorChanged(bool state); - void useStunServerChanged(bool state); - void onlyShareKeysWithVerifiedUsersChanged(bool state); - void shareKeysWithTrustedUsersChanged(bool state); - void useOnlineKeyBackupChanged(bool state); - void profileChanged(QString profile); - void userIdChanged(QString userId); - void accessTokenChanged(QString accessToken); - void deviceIdChanged(QString deviceId); - void homeserverChanged(QString homeserver); - void disableCertificateValidationChanged(bool disabled); - void useIdenticonChanged(bool state); + void groupViewStateChanged(bool state); + void roomSortingChanged(bool state); + void themeChanged(QString state); + void messageHoverHighlightChanged(bool state); + void enlargeEmojiOnlyMessagesChanged(bool state); + void trayChanged(bool state); + void startInTrayChanged(bool state); + void markdownChanged(bool state); + void animateImagesOnHoverChanged(bool state); + void typingNotificationsChanged(bool state); + void buttonInTimelineChanged(bool state); + void readReceiptsChanged(bool state); + void desktopNotificationsChanged(bool state); + void alertOnNotificationChanged(bool state); + void avatarCirclesChanged(bool state); + void decryptSidebarChanged(bool state); + void privacyScreenChanged(bool state); + void privacyScreenTimeoutChanged(int state); + void timelineMaxWidthChanged(int state); + void roomListWidthChanged(int state); + void communityListWidthChanged(int state); + void mobileModeChanged(bool mode); + void fontSizeChanged(double state); + void fontChanged(QString state); + void emojiFontChanged(QString state); + void presenceChanged(Presence state); + void ringtoneChanged(QString ringtone); + void microphoneChanged(QString microphone); + void cameraChanged(QString camera); + void cameraResolutionChanged(QString resolution); + void cameraFrameRateChanged(QString frameRate); + void screenShareFrameRateChanged(int frameRate); + void screenSharePiPChanged(bool state); + void screenShareRemoteVideoChanged(bool state); + void screenShareHideCursorChanged(bool state); + void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); + void shareKeysWithTrustedUsersChanged(bool state); + void useOnlineKeyBackupChanged(bool state); + void profileChanged(QString profile); + void userIdChanged(QString userId); + void accessTokenChanged(QString accessToken); + void deviceIdChanged(QString deviceId); + void homeserverChanged(QString homeserver); + void disableCertificateValidationChanged(bool disabled); + void useIdenticonChanged(bool state); private: - // Default to system theme if QT_QPA_PLATFORMTHEME var is set. - QString defaultTheme_ = - QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() - ? "light" - : "system"; - QString theme_; - bool messageHoverHighlight_; - bool enlargeEmojiOnlyMessages_; - bool tray_; - bool startInTray_; - bool groupView_; - bool markdown_; - bool animateImagesOnHover_; - bool typingNotifications_; - bool sortByImportance_; - bool buttonsInTimeline_; - bool readReceipts_; - bool hasDesktopNotifications_; - bool hasAlertOnNotification_; - bool avatarCircles_; - bool decryptSidebar_; - bool privacyScreen_; - int privacyScreenTimeout_; - bool shareKeysWithTrustedUsers_; - bool onlyShareKeysWithVerifiedUsers_; - bool useOnlineKeyBackup_; - bool mobileMode_; - int timelineMaxWidth_; - int roomListWidth_; - int communityListWidth_; - double baseFontSize_; - QString font_; - QString emojiFont_; - Presence presence_; - QString ringtone_; - QString microphone_; - QString camera_; - QString cameraResolution_; - QString cameraFrameRate_; - int screenShareFrameRate_; - bool screenSharePiP_; - bool screenShareRemoteVideo_; - bool screenShareHideCursor_; - bool useStunServer_; - bool disableCertificateValidation_ = false; - QString profile_; - QString userId_; - QString accessToken_; - QString deviceId_; - QString homeserver_; - QStringList hiddenTags_; - bool useIdenticon_; - - QSettings settings; - - static QSharedPointer<UserSettings> instance_; + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme_ = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; + QString theme_; + bool messageHoverHighlight_; + bool enlargeEmojiOnlyMessages_; + bool tray_; + bool startInTray_; + bool groupView_; + bool markdown_; + bool animateImagesOnHover_; + bool typingNotifications_; + bool sortByImportance_; + bool buttonsInTimeline_; + bool readReceipts_; + bool hasDesktopNotifications_; + bool hasAlertOnNotification_; + bool avatarCircles_; + bool decryptSidebar_; + bool privacyScreen_; + int privacyScreenTimeout_; + bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; + bool useOnlineKeyBackup_; + bool mobileMode_; + int timelineMaxWidth_; + int roomListWidth_; + int communityListWidth_; + double baseFontSize_; + QString font_; + QString emojiFont_; + Presence presence_; + QString ringtone_; + QString microphone_; + QString camera_; + QString cameraResolution_; + QString cameraFrameRate_; + int screenShareFrameRate_; + bool screenSharePiP_; + bool screenShareRemoteVideo_; + bool screenShareHideCursor_; + bool useStunServer_; + bool disableCertificateValidation_ = false; + QString profile_; + QString userId_; + QString accessToken_; + QString deviceId_; + QString homeserver_; + QStringList hiddenTags_; + bool useIdenticon_; + + QSettings settings; + + static QSharedPointer<UserSettings> instance_; }; class HorizontalLine : public QFrame { - Q_OBJECT + Q_OBJECT public: - HorizontalLine(QWidget *parent = nullptr); + HorizontalLine(QWidget *parent = nullptr); }; class UserSettingsPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr); + UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr); protected: - void showEvent(QShowEvent *event) override; - void paintEvent(QPaintEvent *event) override; + void showEvent(QShowEvent *event) override; + void paintEvent(QPaintEvent *event) override; signals: - void moveBack(); - void trayOptionChanged(bool value); - void themeChanged(); - void decryptSidebarChanged(); + void moveBack(); + void trayOptionChanged(bool value); + void themeChanged(); + void decryptSidebarChanged(); public slots: - void updateSecretStatus(); + void updateSecretStatus(); private slots: - void importSessionKeys(); - void exportSessionKeys(); + void importSessionKeys(); + void exportSessionKeys(); private: - // Layouts - QVBoxLayout *topLayout_; - QHBoxLayout *topBarLayout_; - QFormLayout *formLayout_; - - // Shared settings object. - QSharedPointer<UserSettings> settings_; - - Toggle *trayToggle_; - Toggle *startInTrayToggle_; - Toggle *groupViewToggle_; - Toggle *timelineButtonsToggle_; - Toggle *typingNotifications_; - Toggle *messageHoverHighlight_; - Toggle *enlargeEmojiOnlyMessages_; - Toggle *sortByImportance_; - Toggle *readReceipts_; - Toggle *markdown_; - Toggle *animateImagesOnHover_; - Toggle *desktopNotifications_; - Toggle *alertOnNotification_; - Toggle *avatarCircles_; - Toggle *useIdenticon_; - Toggle *useStunServer_; - Toggle *decryptSidebar_; - Toggle *privacyScreen_; - QSpinBox *privacyScreenTimeout_; - Toggle *shareKeysWithTrustedUsers_; - Toggle *onlyShareKeysWithVerifiedUsers_; - Toggle *useOnlineKeyBackup_; - Toggle *mobileMode_; - QLabel *deviceFingerprintValue_; - QLabel *deviceIdValue_; - QLabel *backupSecretCached; - QLabel *masterSecretCached; - QLabel *selfSigningSecretCached; - QLabel *userSigningSecretCached; - - QComboBox *themeCombo_; - QComboBox *scaleFactorCombo_; - QComboBox *fontSizeCombo_; - QFontComboBox *fontSelectionCombo_; - QComboBox *emojiFontSelectionCombo_; - QComboBox *ringtoneCombo_; - QComboBox *microphoneCombo_; - QComboBox *cameraCombo_; - QComboBox *cameraResolutionCombo_; - QComboBox *cameraFrameRateCombo_; - - QSpinBox *timelineMaxWidthSpin_; - - int sideMargin_ = 0; + // Layouts + QVBoxLayout *topLayout_; + QHBoxLayout *topBarLayout_; + QFormLayout *formLayout_; + + // Shared settings object. + QSharedPointer<UserSettings> settings_; + + Toggle *trayToggle_; + Toggle *startInTrayToggle_; + Toggle *groupViewToggle_; + Toggle *timelineButtonsToggle_; + Toggle *typingNotifications_; + Toggle *messageHoverHighlight_; + Toggle *enlargeEmojiOnlyMessages_; + Toggle *sortByImportance_; + Toggle *readReceipts_; + Toggle *markdown_; + Toggle *animateImagesOnHover_; + Toggle *desktopNotifications_; + Toggle *alertOnNotification_; + Toggle *avatarCircles_; + Toggle *useIdenticon_; + Toggle *useStunServer_; + Toggle *decryptSidebar_; + Toggle *privacyScreen_; + QSpinBox *privacyScreenTimeout_; + Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; + Toggle *useOnlineKeyBackup_; + Toggle *mobileMode_; + QLabel *deviceFingerprintValue_; + QLabel *deviceIdValue_; + QLabel *backupSecretCached; + QLabel *masterSecretCached; + QLabel *selfSigningSecretCached; + QLabel *userSigningSecretCached; + + QComboBox *themeCombo_; + QComboBox *scaleFactorCombo_; + QComboBox *fontSizeCombo_; + QFontComboBox *fontSelectionCombo_; + QComboBox *emojiFontSelectionCombo_; + QComboBox *ringtoneCombo_; + QComboBox *microphoneCombo_; + QComboBox *cameraCombo_; + QComboBox *cameraResolutionCombo_; + QComboBox *cameraFrameRateCombo_; + + QSpinBox *timelineMaxWidthSpin_; + + int sideMargin_ = 0; }; diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp index 13b05f0e..f82353cc 100644 --- a/src/UsersModel.cpp +++ b/src/UsersModel.cpp @@ -14,51 +14,51 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent) : QAbstractListModel(parent) , room_id(roomId) { - roomMembers_ = cache::roomMembers(roomId); - for (const auto &m : roomMembers_) { - displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); - userids.push_back(QString::fromStdString(m)); - } + roomMembers_ = cache::roomMembers(roomId); + for (const auto &m : roomMembers_) { + displayNames.push_back(QString::fromStdString(cache::displayName(room_id, m))); + userids.push_back(QString::fromStdString(m)); + } } QHash<int, QByteArray> UsersModel::roleNames() const { - return { - {CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::DisplayName, "displayName"}, - {Roles::AvatarUrl, "avatarUrl"}, - {Roles::UserID, "userid"}, - }; + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::UserID, "userid"}, + }; } QVariant UsersModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: - if (UserSettings::instance()->markdown()) - return QString("[%1](https://matrix.to/#/%2)") - .arg(displayNames[index.row()].toHtmlEscaped()) - .arg(QString(QUrl::toPercentEncoding(userids[index.row()]))); - else - return displayNames[index.row()]; - case CompletionModel::SearchRole: - return displayNames[index.row()]; - case Qt::DisplayRole: - case Roles::DisplayName: - return displayNames[index.row()].toHtmlEscaped(); - case CompletionModel::SearchRole2: - return userids[index.row()]; - case Roles::AvatarUrl: - return cache::avatarUrl(QString::fromStdString(room_id), - QString::fromStdString(roomMembers_[index.row()])); - case Roles::UserID: - return userids[index.row()].toHtmlEscaped(); - } + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + if (UserSettings::instance()->markdown()) + return QString("[%1](https://matrix.to/#/%2)") + .arg(displayNames[index.row()].toHtmlEscaped()) + .arg(QString(QUrl::toPercentEncoding(userids[index.row()]))); + else + return displayNames[index.row()]; + case CompletionModel::SearchRole: + return displayNames[index.row()]; + case Qt::DisplayRole: + case Roles::DisplayName: + return displayNames[index.row()].toHtmlEscaped(); + case CompletionModel::SearchRole2: + return userids[index.row()]; + case Roles::AvatarUrl: + return cache::avatarUrl(QString::fromStdString(room_id), + QString::fromStdString(roomMembers_[index.row()])); + case Roles::UserID: + return userids[index.row()].toHtmlEscaped(); } - return {}; + } + return {}; } diff --git a/src/UsersModel.h b/src/UsersModel.h index 5bc94b0f..e719a8bd 100644 --- a/src/UsersModel.h +++ b/src/UsersModel.h @@ -9,25 +9,25 @@ class UsersModel : public QAbstractListModel { public: - enum Roles - { - AvatarUrl = Qt::UserRole, - DisplayName, - UserID, - }; + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + UserID, + }; - UsersModel(const std::string &roomId, QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)roomMembers_.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + UsersModel(const std::string &roomId, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomMembers_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; private: - std::string room_id; - std::vector<std::string> roomMembers_; - std::vector<QString> displayNames; - std::vector<QString> userids; + std::string room_id; + std::vector<std::string> roomMembers_; + std::vector<QString> displayNames; + std::vector<QString> userids; }; diff --git a/src/Utils.cpp b/src/Utils.cpp index 3f524c6c..b0fb01b1 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -38,155 +38,154 @@ template<class T, class Event> static DescInfo createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName) { - const auto msg = std::get<T>(event); - const auto sender = QString::fromStdString(msg.sender); + const auto msg = std::get<T>(event); + const auto sender = QString::fromStdString(msg.sender); - const auto username = displayName; - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - auto body = utils::event_body(event).trimmed(); - if (mtx::accessors::relations(event).reply_to()) - body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString())); + const auto username = displayName; + const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + auto body = utils::event_body(event).trimmed(); + if (mtx::accessors::relations(event).reply_to()) + body = QString::fromStdString(utils::stripReplyFromBody(body.toStdString())); - return DescInfo{QString::fromStdString(msg.event_id), - sender, - utils::messageDescription<T>(username, body, sender == localUser), - utils::descriptiveTime(ts), - msg.origin_server_ts, - ts}; + return DescInfo{QString::fromStdString(msg.event_id), + sender, + utils::messageDescription<T>(username, body, sender == localUser), + utils::descriptiveTime(ts), + msg.origin_server_ts, + ts}; } std::string utils::stripReplyFromBody(const std::string &bodyi) { - QString body = QString::fromStdString(bodyi); - QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); - while (body.startsWith(">")) - body.remove(plainQuote); - if (body.startsWith("\n")) - body.remove(0, 1); + QString body = QString::fromStdString(bodyi); + QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); + while (body.startsWith(">")) + body.remove(plainQuote); + if (body.startsWith("\n")) + body.remove(0, 1); - body.replace("@room", QString::fromUtf8("@\u2060room")); - return body.toStdString(); + body.replace("@room", QString::fromUtf8("@\u2060room")); + return body.toStdString(); } std::string utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi) { - QString formatted_body = QString::fromStdString(formatted_bodyi); - formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>", - QRegularExpression::DotMatchesEverythingOption)); - formatted_body.replace("@room", QString::fromUtf8("@\u2060room")); - return formatted_body.toStdString(); + QString formatted_body = QString::fromStdString(formatted_bodyi); + formatted_body.remove(QRegularExpression("<mx-reply>.*</mx-reply>", + QRegularExpression::DotMatchesEverythingOption)); + formatted_body.replace("@room", QString::fromUtf8("@\u2060room")); + return formatted_body.toStdString(); } RelatedInfo utils::stripReplyFallbacks(const TimelineEvent &event, std::string id, QString room_id_) { - RelatedInfo related = {}; - related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); - related.related_event = std::move(id); - related.type = mtx::accessors::msg_type(event); + RelatedInfo related = {}; + related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); + related.related_event = std::move(id); + related.type = mtx::accessors::msg_type(event); - // get body, strip reply fallback, then transform the event to text, if it is a media event - // etc - related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); - related.quoted_body = - QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); - related.quoted_body = utils::getQuoteBody(related); + // get body, strip reply fallback, then transform the event to text, if it is a media event + // etc + related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); + related.quoted_body = + QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString())); + related.quoted_body = utils::getQuoteBody(related); - // get quoted body and strip reply fallback - related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); - related.quoted_formatted_body = QString::fromStdString( - stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); - related.room = room_id_; + // get quoted body and strip reply fallback + related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); + related.quoted_formatted_body = QString::fromStdString( + stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString())); + related.room = room_id_; - return related; + return related; } QString utils::localUser() { - return QString::fromStdString(http::client()->user_id().to_string()); + return QString::fromStdString(http::client()->user_id().to_string()); } bool utils::codepointIsEmoji(uint code) { - // TODO: Be more precise here. - return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || - (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; + // TODO: Be more precise here. + return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || + (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; } QString utils::replaceEmoji(const QString &body) { - QString fmtBody; - fmtBody.reserve(body.size()); - - QVector<uint> utf32_string = body.toUcs4(); - - bool insideFontBlock = false; - for (auto &code : utf32_string) { - if (utils::codepointIsEmoji(code)) { - if (!insideFontBlock) { - fmtBody += QStringLiteral("<font face=\"") % - UserSettings::instance()->emojiFont() % - QStringLiteral("\">"); - insideFontBlock = true; - } - - } else { - if (insideFontBlock) { - fmtBody += QStringLiteral("</font>"); - insideFontBlock = false; - } - } - if (QChar::requiresSurrogates(code)) { - QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)), - static_cast<ushort>(QChar::lowSurrogate(code))}; - fmtBody.append(emoji, 2); - } else { - fmtBody.append(QChar(static_cast<ushort>(code))); - } - } - if (insideFontBlock) { + QString fmtBody; + fmtBody.reserve(body.size()); + + QVector<uint> utf32_string = body.toUcs4(); + + bool insideFontBlock = false; + for (auto &code : utf32_string) { + if (utils::codepointIsEmoji(code)) { + if (!insideFontBlock) { + fmtBody += QStringLiteral("<font face=\"") % UserSettings::instance()->emojiFont() % + QStringLiteral("\">"); + insideFontBlock = true; + } + + } else { + if (insideFontBlock) { fmtBody += QStringLiteral("</font>"); + insideFontBlock = false; + } } + if (QChar::requiresSurrogates(code)) { + QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)), + static_cast<ushort>(QChar::lowSurrogate(code))}; + fmtBody.append(emoji, 2); + } else { + fmtBody.append(QChar(static_cast<ushort>(code))); + } + } + if (insideFontBlock) { + fmtBody += QStringLiteral("</font>"); + } - return fmtBody; + return fmtBody; } void utils::setScaleFactor(float factor) { - if (factor < 1 || factor > 3) - return; + if (factor < 1 || factor > 3) + return; - QSettings settings; - settings.setValue("settings/scale_factor", factor); + QSettings settings; + settings.setValue("settings/scale_factor", factor); } float utils::scaleFactor() { - QSettings settings; - return settings.value("settings/scale_factor", -1).toFloat(); + QSettings settings; + return settings.value("settings/scale_factor", -1).toFloat(); } QString utils::descriptiveTime(const QDateTime &then) { - const auto now = QDateTime::currentDateTime(); - const auto days = then.daysTo(now); + const auto now = QDateTime::currentDateTime(); + const auto days = then.daysTo(now); - if (days == 0) - return QLocale::system().toString(then.time(), QLocale::ShortFormat); - else if (days < 2) - return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); - else if (days < 7) - return then.toString("dddd"); + if (days == 0) + return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); + else if (days < 7) + return then.toString("dddd"); - return QLocale::system().toString(then.date(), QLocale::ShortFormat); + return QLocale::system().toString(then.date(), QLocale::ShortFormat); } DescInfo @@ -194,630 +193,622 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUser, const QString &displayName) { - using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; - using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; - using File = mtx::events::RoomEvent<mtx::events::msg::File>; - using Image = mtx::events::RoomEvent<mtx::events::msg::Image>; - using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>; - using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; - using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; - using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>; - using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>; - using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>; - using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - - if (std::holds_alternative<Audio>(event)) { - return createDescriptionInfo<Audio>(event, localUser, displayName); - } else if (std::holds_alternative<Emote>(event)) { - return createDescriptionInfo<Emote>(event, localUser, displayName); - } else if (std::holds_alternative<File>(event)) { - return createDescriptionInfo<File>(event, localUser, displayName); - } else if (std::holds_alternative<Image>(event)) { - return createDescriptionInfo<Image>(event, localUser, displayName); - } else if (std::holds_alternative<Notice>(event)) { - return createDescriptionInfo<Notice>(event, localUser, displayName); - } else if (std::holds_alternative<Text>(event)) { - return createDescriptionInfo<Text>(event, localUser, displayName); - } else if (std::holds_alternative<Video>(event)) { - return createDescriptionInfo<Video>(event, localUser, displayName); - } else if (std::holds_alternative<CallInvite>(event)) { - return createDescriptionInfo<CallInvite>(event, localUser, displayName); - } else if (std::holds_alternative<CallAnswer>(event)) { - return createDescriptionInfo<CallAnswer>(event, localUser, displayName); - } else if (std::holds_alternative<CallHangUp>(event)) { - return createDescriptionInfo<CallHangUp>(event, localUser, displayName); - } else if (std::holds_alternative<mtx::events::Sticker>(event)) { - return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName); - } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) { - const auto sender = QString::fromStdString(msg->sender); - - const auto username = displayName; - const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts); - - DescInfo info; - info.userid = sender; - info.body = QString(" %1").arg( - messageDescription<Encrypted>(username, "", sender == localUser)); - info.timestamp = msg->origin_server_ts; - info.descriptiveTime = utils::descriptiveTime(ts); - info.event_id = QString::fromStdString(msg->event_id); - info.datetime = ts; - - return info; - } + using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; + using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; + using File = mtx::events::RoomEvent<mtx::events::msg::File>; + using Image = mtx::events::RoomEvent<mtx::events::msg::Image>; + using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>; + using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; + using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; + using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>; + using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>; + using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>; + using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; + + if (std::holds_alternative<Audio>(event)) { + return createDescriptionInfo<Audio>(event, localUser, displayName); + } else if (std::holds_alternative<Emote>(event)) { + return createDescriptionInfo<Emote>(event, localUser, displayName); + } else if (std::holds_alternative<File>(event)) { + return createDescriptionInfo<File>(event, localUser, displayName); + } else if (std::holds_alternative<Image>(event)) { + return createDescriptionInfo<Image>(event, localUser, displayName); + } else if (std::holds_alternative<Notice>(event)) { + return createDescriptionInfo<Notice>(event, localUser, displayName); + } else if (std::holds_alternative<Text>(event)) { + return createDescriptionInfo<Text>(event, localUser, displayName); + } else if (std::holds_alternative<Video>(event)) { + return createDescriptionInfo<Video>(event, localUser, displayName); + } else if (std::holds_alternative<CallInvite>(event)) { + return createDescriptionInfo<CallInvite>(event, localUser, displayName); + } else if (std::holds_alternative<CallAnswer>(event)) { + return createDescriptionInfo<CallAnswer>(event, localUser, displayName); + } else if (std::holds_alternative<CallHangUp>(event)) { + return createDescriptionInfo<CallHangUp>(event, localUser, displayName); + } else if (std::holds_alternative<mtx::events::Sticker>(event)) { + return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName); + } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) { + const auto sender = QString::fromStdString(msg->sender); + + const auto username = displayName; + const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts); + + DescInfo info; + info.userid = sender; + info.body = + QString(" %1").arg(messageDescription<Encrypted>(username, "", sender == localUser)); + info.timestamp = msg->origin_server_ts; + info.descriptiveTime = utils::descriptiveTime(ts); + info.event_id = QString::fromStdString(msg->event_id); + info.datetime = ts; + + return info; + } - return DescInfo{}; + return DescInfo{}; } QString utils::firstChar(const QString &input) { - if (input.isEmpty()) - return input; + if (input.isEmpty()) + return input; - for (auto const &c : input.toUcs4()) { - if (QString::fromUcs4(&c, 1) != QString("#")) - return QString::fromUcs4(&c, 1).toUpper(); - } + for (auto const &c : input.toUcs4()) { + if (QString::fromUcs4(&c, 1) != QString("#")) + return QString::fromUcs4(&c, 1).toUpper(); + } - return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper(); + return QString::fromUcs4(&input.toUcs4().at(0), 1).toUpper(); } QString utils::humanReadableFileSize(uint64_t bytes) { - constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"}; - constexpr static const int length = sizeof(units) / sizeof(units[0]); + constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"}; + constexpr static const int length = sizeof(units) / sizeof(units[0]); - int u = 0; - double size = static_cast<double>(bytes); - while (size >= 1024.0 && u < length) { - ++u; - size /= 1024.0; - } + int u = 0; + double size = static_cast<double>(bytes); + while (size >= 1024.0 && u < length) { + ++u; + size /= 1024.0; + } - return QString::number(size, 'g', 4) + ' ' + units[u]; + return QString::number(size, 'g', 4) + ' ' + units[u]; } int utils::levenshtein_distance(const std::string &s1, const std::string &s2) { - const auto nlen = s1.size(); - const auto hlen = s2.size(); + const auto nlen = s1.size(); + const auto hlen = s2.size(); - if (hlen == 0) - return -1; - if (nlen == 1) - return (int)s2.find(s1); + if (hlen == 0) + return -1; + if (nlen == 1) + return (int)s2.find(s1); - std::vector<int> row1(hlen + 1, 0); + std::vector<int> row1(hlen + 1, 0); - for (size_t i = 0; i < nlen; ++i) { - std::vector<int> row2(1, (int)i + 1); - - for (size_t j = 0; j < hlen; ++j) { - const int cost = s1[i] != s2[j]; - row2.push_back( - std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost))); - } + for (size_t i = 0; i < nlen; ++i) { + std::vector<int> row2(1, (int)i + 1); - row1.swap(row2); + for (size_t j = 0; j < hlen; ++j) { + const int cost = s1[i] != s2[j]; + row2.push_back(std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost))); } - return *std::min_element(row1.begin(), row1.end()); + row1.swap(row2); + } + + return *std::min_element(row1.begin(), row1.end()); } QString utils::event_body(const mtx::events::collections::TimelineEvents &e) { - using namespace mtx::events; - if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr) - return QString::fromStdString(ev->content.body); - - return ""; + using namespace mtx::events; + if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + + return ""; } QPixmap utils::scaleImageToPixmap(const QImage &img, int size) { - if (img.isNull()) - return QPixmap(); + if (img.isNull()) + return QPixmap(); - // Deprecated in 5.13: const double sz = - // std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); - const double sz = - std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size); - return QPixmap::fromImage( - img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + // Deprecated in 5.13: const double sz = + // std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); + const double sz = + std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size); + return QPixmap::fromImage(img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } QPixmap utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source) { - if (source.isNull()) - return QPixmap(); + if (source.isNull()) + return QPixmap(); - const double widthRatio = (double)maxWidth / (double)source.width(); - const double heightRatio = (double)maxHeight / (double)source.height(); - const double minAspectRatio = std::min(widthRatio, heightRatio); + const double widthRatio = (double)maxWidth / (double)source.width(); + const double heightRatio = (double)maxHeight / (double)source.height(); + const double minAspectRatio = std::min(widthRatio, heightRatio); - // Size of the output image. - int w, h = 0; + // Size of the output image. + int w, h = 0; - if (minAspectRatio > 1) { - w = source.width(); - h = source.height(); - } else { - w = source.width() * minAspectRatio; - h = source.height() * minAspectRatio; - } + if (minAspectRatio > 1) { + w = source.width(); + h = source.height(); + } else { + w = source.width() * minAspectRatio; + h = source.height() * minAspectRatio; + } - return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } QString utils::mxcToHttp(const QUrl &url, const QString &server, int port) { - auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString()); + auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString()); - return QString("https://%1:%2/_matrix/media/r0/download/%3/%4") - .arg(server) - .arg(port) - .arg(QString::fromStdString(mxcParts.server)) - .arg(QString::fromStdString(mxcParts.media_id)); + return QString("https://%1:%2/_matrix/media/r0/download/%3/%4") + .arg(server) + .arg(port) + .arg(QString::fromStdString(mxcParts.server)) + .arg(QString::fromStdString(mxcParts.media_id)); } QString utils::humanReadableFingerprint(const std::string &ed25519) { - return humanReadableFingerprint(QString::fromStdString(ed25519)); + return humanReadableFingerprint(QString::fromStdString(ed25519)); } QString utils::humanReadableFingerprint(const QString &ed25519) { - QString fingerprint; - for (int i = 0; i < ed25519.length(); i = i + 4) { - fingerprint.append(ed25519.midRef(i, 4)); - if (i > 0 && i % 16 == 12) - fingerprint.append('\n'); - else if (i < ed25519.length()) - fingerprint.append(' '); - } - return fingerprint; + QString fingerprint; + for (int i = 0; i < ed25519.length(); i = i + 4) { + fingerprint.append(ed25519.midRef(i, 4)); + if (i > 0 && i % 16 == 12) + fingerprint.append('\n'); + else if (i < ed25519.length()) + fingerprint.append(' '); + } + return fingerprint; } QString utils::linkifyMessage(const QString &body) { - // Convert to valid XML. - auto doc = body; - doc.replace(conf::strings::url_regex, conf::strings::url_html); - doc.replace(QRegularExpression("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"), - conf::strings::url_html); + // Convert to valid XML. + auto doc = body; + doc.replace(conf::strings::url_regex, conf::strings::url_html); + doc.replace(QRegularExpression("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"), + conf::strings::url_html); - return doc; + return doc; } QString utils::escapeBlacklistedHtml(const QString &rawStr) { - static const std::array allowedTags = { - "font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2", - "h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6", - "blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul", - "ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li", - "b", "/b", "i", "/i", "u", "/u", "strong", "/strong", - "em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr", - "br", "br/", "div", "/div", "table", "/table", "thead", "/thead", - "tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td", - "caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img"}; - QByteArray data = rawStr.toUtf8(); - QByteArray buffer; - const int length = data.size(); - buffer.reserve(length); - bool escapingTag = false; - for (int pos = 0; pos != length; ++pos) { - switch (data.at(pos)) { - case '<': { - bool oneTagMatched = false; - const int endPos = - static_cast<int>(std::min(static_cast<size_t>(data.indexOf('>', pos)), - static_cast<size_t>(data.indexOf(' ', pos)))); - - auto mid = data.mid(pos + 1, endPos - pos - 1); - for (const auto &tag : allowedTags) { - // TODO: Check src and href attribute - if (mid.toLower() == tag) { - oneTagMatched = true; - } - } - if (oneTagMatched) - buffer.append('<'); - else { - escapingTag = true; - buffer.append("<"); - } - break; - } - case '>': - if (escapingTag) { - buffer.append(">"); - escapingTag = false; - } else - buffer.append('>'); - break; - default: - buffer.append(data.at(pos)); - break; + static const std::array allowedTags = { + "font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2", + "h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6", + "blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul", + "ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li", + "b", "/b", "i", "/i", "u", "/u", "strong", "/strong", + "em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr", + "br", "br/", "div", "/div", "table", "/table", "thead", "/thead", + "tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td", + "caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img"}; + QByteArray data = rawStr.toUtf8(); + QByteArray buffer; + const int length = data.size(); + buffer.reserve(length); + bool escapingTag = false; + for (int pos = 0; pos != length; ++pos) { + switch (data.at(pos)) { + case '<': { + bool oneTagMatched = false; + const int endPos = + static_cast<int>(std::min(static_cast<size_t>(data.indexOf('>', pos)), + static_cast<size_t>(data.indexOf(' ', pos)))); + + auto mid = data.mid(pos + 1, endPos - pos - 1); + for (const auto &tag : allowedTags) { + // TODO: Check src and href attribute + if (mid.toLower() == tag) { + oneTagMatched = true; } + } + if (oneTagMatched) + buffer.append('<'); + else { + escapingTag = true; + buffer.append("<"); + } + break; } - return QString::fromUtf8(buffer); + case '>': + if (escapingTag) { + buffer.append(">"); + escapingTag = false; + } else + buffer.append('>'); + break; + default: + buffer.append(data.at(pos)); + break; + } + } + return QString::fromUtf8(buffer); } QString utils::markdownToHtml(const QString &text, bool rainbowify) { - const auto str = text.toUtf8(); - cmark_node *const node = - cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE); - - if (rainbowify) { - // create iterator over node - cmark_iter *iter = cmark_iter_new(node); - - cmark_event_type ev_type; - - // First loop to get total text length - int textLen = 0; - while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { - cmark_node *cur = cmark_iter_get_node(iter); - // only text nodes (no code or semilar) - if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) - continue; - // count up by length of current node's text - QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, - QString(cmark_node_get_literal(cur))); - while (tbf.toNextBoundary() != -1) - textLen++; - } + const auto str = text.toUtf8(); + cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE); + + if (rainbowify) { + // create iterator over node + cmark_iter *iter = cmark_iter_new(node); + + cmark_event_type ev_type; + + // First loop to get total text length + int textLen = 0; + while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { + cmark_node *cur = cmark_iter_get_node(iter); + // only text nodes (no code or semilar) + if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) + continue; + // count up by length of current node's text + QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, + QString(cmark_node_get_literal(cur))); + while (tbf.toNextBoundary() != -1) + textLen++; + } - // create new iter to start over - cmark_iter_free(iter); - iter = cmark_iter_new(node); - - // Second loop to rainbowify - int charIdx = 0; - while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { - cmark_node *cur = cmark_iter_get_node(iter); - // only text nodes (no code or semilar) - if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) - continue; - - // get text in current node - QString nodeText(cmark_node_get_literal(cur)); - // create buffer to append rainbow text to - QString buf; - int boundaryStart = 0; - int boundaryEnd = 0; - // use QTextBoundaryFinder to iterate ofer graphemes - QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, - nodeText); - while ((boundaryEnd = tbf.toNextBoundary()) != -1) { - charIdx++; - // Split text to get current char - auto curChar = - nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart); - boundaryStart = boundaryEnd; - // Don't rainbowify whitespaces - if (curChar.trimmed().isEmpty() || - codepointIsEmoji(curChar.toUcs4().first())) { - buf.append(curChar); - continue; - } - - // get correct color for char index - // Use colors as described here: - // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html - auto color = - QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5); - // format color for HTML - auto colorString = color.name(QColor::NameFormat::HexRgb); - // create HTML element for current char - auto curCharColored = QString("<font color=\"%0\">%1</font>") - .arg(colorString) - .arg(curChar); - // append colored HTML element to buffer - buf.append(curCharColored); - } - - // create HTML_INLINE node to prevent HTML from being escaped - auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE); - // set content of HTML node to buffer contents - cmark_node_set_literal(htmlNode, buf.toUtf8().data()); - // replace current node with HTML node - cmark_node_replace(cur, htmlNode); - // free memory of old node - cmark_node_free(cur); + // create new iter to start over + cmark_iter_free(iter); + iter = cmark_iter_new(node); + + // Second loop to rainbowify + int charIdx = 0; + while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { + cmark_node *cur = cmark_iter_get_node(iter); + // only text nodes (no code or semilar) + if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) + continue; + + // get text in current node + QString nodeText(cmark_node_get_literal(cur)); + // create buffer to append rainbow text to + QString buf; + int boundaryStart = 0; + int boundaryEnd = 0; + // use QTextBoundaryFinder to iterate ofer graphemes + QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText); + while ((boundaryEnd = tbf.toNextBoundary()) != -1) { + charIdx++; + // Split text to get current char + auto curChar = nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart); + boundaryStart = boundaryEnd; + // Don't rainbowify whitespaces + if (curChar.trimmed().isEmpty() || codepointIsEmoji(curChar.toUcs4().first())) { + buf.append(curChar); + continue; } - cmark_iter_free(iter); + // get correct color for char index + // Use colors as described here: + // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html + auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5); + // format color for HTML + auto colorString = color.name(QColor::NameFormat::HexRgb); + // create HTML element for current char + auto curCharColored = + QString("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar); + // append colored HTML element to buffer + buf.append(curCharColored); + } + + // create HTML_INLINE node to prevent HTML from being escaped + auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE); + // set content of HTML node to buffer contents + cmark_node_set_literal(htmlNode, buf.toUtf8().data()); + // replace current node with HTML node + cmark_node_replace(cur, htmlNode); + // free memory of old node + cmark_node_free(cur); } - const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE); - // Copy the null terminated output buffer. - std::string html(tmp_buf); + cmark_iter_free(iter); + } - // The buffer is no longer needed. - free((char *)tmp_buf); + const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE); + // Copy the null terminated output buffer. + std::string html(tmp_buf); - auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed(); + // The buffer is no longer needed. + free((char *)tmp_buf); - if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) { - result = result.mid(3, result.size() - 3 - 4); - } + auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed(); - return result; -} + if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) { + result = result.mid(3, result.size() - 3 - 4); + } -QString -utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) -{ - auto getFormattedBody = [related]() -> QString { - using MsgType = mtx::events::MessageType; - - switch (related.type) { - case MsgType::File: { - return "sent a file."; - } - case MsgType::Image: { - return "sent an image."; - } - case MsgType::Audio: { - return "sent an audio file."; - } - case MsgType::Video: { - return "sent a video"; - } - default: { - return related.quoted_formatted_body; - } - } - }; - return QString("<mx-reply><blockquote><a " - "href=\"https://matrix.to/#/%1/%2\">In reply " - "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br" - "/>%5</blockquote></mx-reply>") - .arg(related.room, - QString::fromStdString(related.related_event), - related.quoted_user, - related.quoted_user, - getFormattedBody()) + - html; + return result; } QString -utils::getQuoteBody(const RelatedInfo &related) +utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) { + auto getFormattedBody = [related]() -> QString { using MsgType = mtx::events::MessageType; switch (related.type) { case MsgType::File: { - return "sent a file."; + return "sent a file."; } case MsgType::Image: { - return "sent an image."; + return "sent an image."; } case MsgType::Audio: { - return "sent an audio file."; + return "sent an audio file."; } case MsgType::Video: { - return "sent a video"; + return "sent a video"; } default: { - return related.quoted_body; + return related.quoted_formatted_body; } } + }; + return QString("<mx-reply><blockquote><a " + "href=\"https://matrix.to/#/%1/%2\">In reply " + "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br" + "/>%5</blockquote></mx-reply>") + .arg(related.room, + QString::fromStdString(related.related_event), + related.quoted_user, + related.quoted_user, + getFormattedBody()) + + html; +} + +QString +utils::getQuoteBody(const RelatedInfo &related) +{ + using MsgType = mtx::events::MessageType; + + switch (related.type) { + case MsgType::File: { + return "sent a file."; + } + case MsgType::Image: { + return "sent an image."; + } + case MsgType::Audio: { + return "sent an audio file."; + } + case MsgType::Video: { + return "sent a video"; + } + default: { + return related.quoted_body; + } + } } QString utils::linkColor() { - const auto theme = UserSettings::instance()->theme(); + const auto theme = UserSettings::instance()->theme(); - if (theme == "light") { - return "#0077b5"; - } else if (theme == "dark") { - return "#38A3D8"; - } else { - return QPalette().color(QPalette::Link).name(); - } + if (theme == "light") { + return "#0077b5"; + } else if (theme == "dark") { + return "#38A3D8"; + } else { + return QPalette().color(QPalette::Link).name(); + } } uint32_t utils::hashQString(const QString &input) { - uint32_t hash = 0; + uint32_t hash = 0; - for (int i = 0; i < input.length(); i++) { - hash = input.at(i).digitValue() + ((hash << 5) - hash); - } + for (int i = 0; i < input.length(); i++) { + hash = input.at(i).digitValue() + ((hash << 5) - hash); + } - return hash; + return hash; } QColor utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol) { - const qreal backgroundLum = luminance(backgroundCol); - - // Create a color for the input - auto hash = hashQString(input); - // create a hue value based on the hash of the input. - auto userHue = static_cast<int>(hash % 360); - // start with moderate saturation and lightness values. - auto sat = 220; - auto lightness = 125; - - // converting to a QColor makes the luminance calc easier. - QColor inputColor = QColor::fromHsl(userHue, sat, lightness); - - // calculate the initial luminance and contrast of the - // generated color. It's possible that no additional - // work will be necessary. - auto lum = luminance(inputColor); - auto contrast = computeContrast(lum, backgroundLum); - - // If the contrast doesn't meet our criteria, - // try again and again until they do by modifying first - // the lightness and then the saturation of the color. - int iterationCount = 9; - while (contrast < 5) { - // if our lightness is at it's bounds, try changing - // saturation instead. - if (lightness == 242 || lightness == 13) { - qreal newSat = qBound(26.0, sat * 1.25, 242.0); - - inputColor.setHsl(userHue, qFloor(newSat), lightness); - auto tmpLum = luminance(inputColor); - auto higherContrast = computeContrast(tmpLum, backgroundLum); - if (higherContrast > contrast) { - contrast = higherContrast; - sat = newSat; - } else { - newSat = qBound(26.0, sat / 1.25, 242.0); - inputColor.setHsl(userHue, qFloor(newSat), lightness); - tmpLum = luminance(inputColor); - auto lowerContrast = computeContrast(tmpLum, backgroundLum); - if (lowerContrast > contrast) { - contrast = lowerContrast; - sat = newSat; - } - } - } else { - qreal newLightness = qBound(13.0, lightness * 1.25, 242.0); - - inputColor.setHsl(userHue, sat, qFloor(newLightness)); - - auto tmpLum = luminance(inputColor); - auto higherContrast = computeContrast(tmpLum, backgroundLum); - - // Check to make sure we have actually improved contrast - if (higherContrast > contrast) { - contrast = higherContrast; - lightness = newLightness; - // otherwise, try going the other way instead. - } else { - newLightness = qBound(13.0, lightness / 1.25, 242.0); - inputColor.setHsl(userHue, sat, qFloor(newLightness)); - tmpLum = luminance(inputColor); - auto lowerContrast = computeContrast(tmpLum, backgroundLum); - if (lowerContrast > contrast) { - contrast = lowerContrast; - lightness = newLightness; - } - } + const qreal backgroundLum = luminance(backgroundCol); + + // Create a color for the input + auto hash = hashQString(input); + // create a hue value based on the hash of the input. + auto userHue = static_cast<int>(hash % 360); + // start with moderate saturation and lightness values. + auto sat = 220; + auto lightness = 125; + + // converting to a QColor makes the luminance calc easier. + QColor inputColor = QColor::fromHsl(userHue, sat, lightness); + + // calculate the initial luminance and contrast of the + // generated color. It's possible that no additional + // work will be necessary. + auto lum = luminance(inputColor); + auto contrast = computeContrast(lum, backgroundLum); + + // If the contrast doesn't meet our criteria, + // try again and again until they do by modifying first + // the lightness and then the saturation of the color. + int iterationCount = 9; + while (contrast < 5) { + // if our lightness is at it's bounds, try changing + // saturation instead. + if (lightness == 242 || lightness == 13) { + qreal newSat = qBound(26.0, sat * 1.25, 242.0); + + inputColor.setHsl(userHue, qFloor(newSat), lightness); + auto tmpLum = luminance(inputColor); + auto higherContrast = computeContrast(tmpLum, backgroundLum); + if (higherContrast > contrast) { + contrast = higherContrast; + sat = newSat; + } else { + newSat = qBound(26.0, sat / 1.25, 242.0); + inputColor.setHsl(userHue, qFloor(newSat), lightness); + tmpLum = luminance(inputColor); + auto lowerContrast = computeContrast(tmpLum, backgroundLum); + if (lowerContrast > contrast) { + contrast = lowerContrast; + sat = newSat; } - - // don't loop forever, just give up at some point! - // Someone smart may find a better solution - if (--iterationCount < 0) - break; + } + } else { + qreal newLightness = qBound(13.0, lightness * 1.25, 242.0); + + inputColor.setHsl(userHue, sat, qFloor(newLightness)); + + auto tmpLum = luminance(inputColor); + auto higherContrast = computeContrast(tmpLum, backgroundLum); + + // Check to make sure we have actually improved contrast + if (higherContrast > contrast) { + contrast = higherContrast; + lightness = newLightness; + // otherwise, try going the other way instead. + } else { + newLightness = qBound(13.0, lightness / 1.25, 242.0); + inputColor.setHsl(userHue, sat, qFloor(newLightness)); + tmpLum = luminance(inputColor); + auto lowerContrast = computeContrast(tmpLum, backgroundLum); + if (lowerContrast > contrast) { + contrast = lowerContrast; + lightness = newLightness; + } + } } - // get the hex value of the generated color. - auto colorHex = inputColor.name(); + // don't loop forever, just give up at some point! + // Someone smart may find a better solution + if (--iterationCount < 0) + break; + } - return colorHex; + // get the hex value of the generated color. + auto colorHex = inputColor.name(); + + return colorHex; } qreal utils::computeContrast(const qreal &one, const qreal &two) { - auto ratio = (one + 0.05) / (two + 0.05); + auto ratio = (one + 0.05) / (two + 0.05); - if (two > one) { - ratio = 1 / ratio; - } + if (two > one) { + ratio = 1 / ratio; + } - return ratio; + return ratio; } qreal utils::luminance(const QColor &col) { - int colRgb[3] = {col.red(), col.green(), col.blue()}; - qreal lumRgb[3]; + int colRgb[3] = {col.red(), col.green(), col.blue()}; + qreal lumRgb[3]; - for (int i = 0; i < 3; i++) { - qreal v = colRgb[i] / 255.0; - lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4); - } + for (int i = 0; i < 3; i++) { + qreal v = colRgb[i] / 255.0; + lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4); + } - auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722; + auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722; - return lum; + return lum; } void utils::centerWidget(QWidget *widget, QWidget *parent) { - auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint { - return QPoint(hostRect.center().x() - (childRect.width() * 0.5), - hostRect.center().y() - (childRect.height() * 0.5)); - }; + auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint { + return QPoint(hostRect.center().x() - (childRect.width() * 0.5), + hostRect.center().y() - (childRect.height() * 0.5)); + }; - if (parent) { - widget->move(parent->window()->frameGeometry().topLeft() + - parent->window()->rect().center() - widget->rect().center()); - return; - } + if (parent) { + widget->move(parent->window()->frameGeometry().topLeft() + + parent->window()->rect().center() - widget->rect().center()); + return; + } - // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry())); - widget->move(findCenter(QGuiApplication::primaryScreen()->geometry())); + // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry())); + widget->move(findCenter(QGuiApplication::primaryScreen()->geometry())); } void utils::restoreCombobox(QComboBox *combo, const QString &value) { - for (auto i = 0; i < combo->count(); ++i) { - if (value == combo->itemText(i)) { - combo->setCurrentIndex(i); - break; - } + for (auto i = 0; i < combo->count(); ++i) { + if (value == combo->itemText(i)) { + combo->setCurrentIndex(i); + break; } + } } QImage utils::readImageFromFile(const QString &filename) { - QImageReader reader(filename); - reader.setAutoTransform(true); - return reader.read(); + QImageReader reader(filename); + reader.setAutoTransform(true); + return reader.read(); } QImage utils::readImage(const QByteArray &data) { - QBuffer buf; - buf.setData(data); - QImageReader reader(&buf); - reader.setAutoTransform(true); - return reader.read(); + QBuffer buf; + buf.setData(data); + QImageReader reader(&buf); + reader.setAutoTransform(true); + return reader.read(); } bool utils::isReply(const mtx::events::collections::TimelineEvents &e) { - return mtx::accessors::relations(e).reply_to().has_value(); + return mtx::accessors::relations(e).reply_to().has_value(); } diff --git a/src/Utils.h b/src/Utils.h index 8f37a574..da82ec7c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -29,12 +29,12 @@ class QComboBox; // outgoing messages struct RelatedInfo { - using MsgType = mtx::events::MessageType; - MsgType type; - QString room; - QString quoted_body, quoted_formatted_body; - std::string related_event; - QString quoted_user; + using MsgType = mtx::events::MessageType; + MsgType type; + QString room; + QString quoted_body, quoted_formatted_body; + std::string related_event; + QString quoted_user; }; namespace utils { @@ -97,112 +97,96 @@ messageDescription(const QString &username = "", const QString &body = "", const bool isLocal = false) { - using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; - using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; - using File = mtx::events::RoomEvent<mtx::events::msg::File>; - using Image = mtx::events::RoomEvent<mtx::events::msg::Image>; - using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>; - using Sticker = mtx::events::Sticker; - using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; - using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; - using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>; - using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>; - using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>; - using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - - if (std::is_same<T, Audio>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent an audio clip"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent an audio clip") - .arg(username); - } else if (std::is_same<T, Image>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent an image"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent an image") - .arg(username); - } else if (std::is_same<T, File>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent a file"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent a file") - .arg(username); - } else if (std::is_same<T, Video>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent a video"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent a video") - .arg(username); - } else if (std::is_same<T, Sticker>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent a sticker"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent a sticker") - .arg(username); - } else if (std::is_same<T, Notice>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent a notification"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent a notification") - .arg(username); - } else if (std::is_same<T, Text>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", "You: %1") - .arg(body); - else - return QCoreApplication::translate("message-description sent:", "%1: %2") - .arg(username) - .arg(body); - } else if (std::is_same<T, Emote>::value) { - return QString("* %1 %2").arg(username).arg(body); - } else if (std::is_same<T, Encrypted>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You sent an encrypted message"); - else - return QCoreApplication::translate("message-description sent:", - "%1 sent an encrypted message") - .arg(username); - } else if (std::is_same<T, CallInvite>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You placed a call"); - else - return QCoreApplication::translate("message-description sent:", - "%1 placed a call") - .arg(username); - } else if (std::is_same<T, CallAnswer>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You answered a call"); - else - return QCoreApplication::translate("message-description sent:", - "%1 answered a call") - .arg(username); - } else if (std::is_same<T, CallHangUp>::value) { - if (isLocal) - return QCoreApplication::translate("message-description sent:", - "You ended a call"); - else - return QCoreApplication::translate("message-description sent:", - "%1 ended a call") - .arg(username); - } else { - return QCoreApplication::translate("utils", "Unknown Message Type"); - } + using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; + using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; + using File = mtx::events::RoomEvent<mtx::events::msg::File>; + using Image = mtx::events::RoomEvent<mtx::events::msg::Image>; + using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>; + using Sticker = mtx::events::Sticker; + using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; + using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; + using CallInvite = mtx::events::RoomEvent<mtx::events::msg::CallInvite>; + using CallAnswer = mtx::events::RoomEvent<mtx::events::msg::CallAnswer>; + using CallHangUp = mtx::events::RoomEvent<mtx::events::msg::CallHangUp>; + using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; + + if (std::is_same<T, Audio>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an audio clip"); + else + return QCoreApplication::translate("message-description sent:", "%1 sent an audio clip") + .arg(username); + } else if (std::is_same<T, Image>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You sent an image"); + else + return QCoreApplication::translate("message-description sent:", "%1 sent an image") + .arg(username); + } else if (std::is_same<T, File>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You sent a file"); + else + return QCoreApplication::translate("message-description sent:", "%1 sent a file") + .arg(username); + } else if (std::is_same<T, Video>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You sent a video"); + else + return QCoreApplication::translate("message-description sent:", "%1 sent a video") + .arg(username); + } else if (std::is_same<T, Sticker>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You sent a sticker"); + else + return QCoreApplication::translate("message-description sent:", "%1 sent a sticker") + .arg(username); + } else if (std::is_same<T, Notice>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a notification"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a notification") + .arg(username); + } else if (std::is_same<T, Text>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You: %1").arg(body); + else + return QCoreApplication::translate("message-description sent:", "%1: %2") + .arg(username) + .arg(body); + } else if (std::is_same<T, Emote>::value) { + return QString("* %1 %2").arg(username).arg(body); + } else if (std::is_same<T, Encrypted>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an encrypted message"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an encrypted message") + .arg(username); + } else if (std::is_same<T, CallInvite>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You placed a call"); + else + return QCoreApplication::translate("message-description sent:", "%1 placed a call") + .arg(username); + } else if (std::is_same<T, CallAnswer>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You answered a call"); + else + return QCoreApplication::translate("message-description sent:", "%1 answered a call") + .arg(username); + } else if (std::is_same<T, CallHangUp>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You ended a call"); + else + return QCoreApplication::translate("message-description sent:", "%1 ended a call") + .arg(username); + } else { + return QCoreApplication::translate("utils", "Unknown Message Type"); + } } //! Scale down an image to fit to the given width & height limitations. @@ -214,19 +198,19 @@ template<typename ContainerT, typename PredicateT> void erase_if(ContainerT &items, const PredicateT &predicate) { - for (auto it = items.begin(); it != items.end();) { - if (predicate(*it)) - it = items.erase(it); - else - ++it; - } + for (auto it = items.begin(); it != items.end();) { + if (predicate(*it)) + it = items.erase(it); + else + ++it; + } } template<class T> QString message_body(const mtx::events::collections::TimelineEvents &event) { - return QString::fromStdString(std::get<T>(event).content.body); + return QString::fromStdString(std::get<T>(event).content.body); } //! Calculate the Levenshtein distance between two strings with character skipping. @@ -253,13 +237,13 @@ template<typename RoomMessageT> QString getMessageBody(const RoomMessageT &event) { - if (event.content.format.empty()) - return QString::fromStdString(event.content.body).toHtmlEscaped(); + if (event.content.format.empty()) + return QString::fromStdString(event.content.body).toHtmlEscaped(); - if (event.content.format != mtx::common::FORMAT_MSG_TYPE) - return QString::fromStdString(event.content.body).toHtmlEscaped(); + if (event.content.format != mtx::common::FORMAT_MSG_TYPE) + return QString::fromStdString(event.content.body).toHtmlEscaped(); - return QString::fromStdString(event.content.formatted_body); + return QString::fromStdString(event.content.formatted_body); } //! Replace raw URLs in text with HTML link tags. diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index 72330435..801a365c 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -43,47 +43,47 @@ using webrtc::State; WebRTCSession::WebRTCSession() : devices_(CallDevices::instance()) { - qRegisterMetaType<webrtc::CallType>(); - qmlRegisterUncreatableMetaObject( - webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum"); + qRegisterMetaType<webrtc::CallType>(); + qmlRegisterUncreatableMetaObject( + webrtc::staticMetaObject, "im.nheko", 1, 0, "CallType", "Can't instantiate enum"); - qRegisterMetaType<webrtc::State>(); - qmlRegisterUncreatableMetaObject( - webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum"); + qRegisterMetaType<webrtc::State>(); + qmlRegisterUncreatableMetaObject( + webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum"); - connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); - init(); + connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); + init(); } bool WebRTCSession::init(std::string *errorMessage) { #ifdef GSTREAMER_AVAILABLE - if (initialised_) - return true; + if (initialised_) + return true; - GError *error = nullptr; - if (!gst_init_check(nullptr, nullptr, &error)) { - std::string strError("WebRTC: failed to initialise GStreamer: "); - if (error) { - strError += error->message; - g_error_free(error); - } - nhlog::ui()->error(strError); - if (errorMessage) - *errorMessage = strError; - return false; + GError *error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + std::string strError("WebRTC: failed to initialise GStreamer: "); + if (error) { + strError += error->message; + g_error_free(error); } - - initialised_ = true; - gchar *version = gst_version_string(); - nhlog::ui()->info("WebRTC: initialised {}", version); - g_free(version); - devices_.init(); - return true; -#else - (void)errorMessage; + nhlog::ui()->error(strError); + if (errorMessage) + *errorMessage = strError; return false; + } + + initialised_ = true; + gchar *version = gst_version_string(); + nhlog::ui()->info("WebRTC: initialised {}", version); + g_free(version); + devices_.init(); + return true; +#else + (void)errorMessage; + return false; #endif } @@ -100,81 +100,77 @@ GstPad *remotePiPSinkPad_ = nullptr; gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data) { - WebRTCSession *session = static_cast<WebRTCSession *>(user_data); - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_EOS: - nhlog::ui()->error("WebRTC: end of stream"); - session->end(); - break; - case GST_MESSAGE_ERROR: - GError *error; - gchar *debug; - gst_message_parse_error(msg, &error, &debug); - nhlog::ui()->error( - "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message); - g_clear_error(&error); - g_free(debug); - session->end(); - break; - default: - break; - } - return TRUE; + WebRTCSession *session = static_cast<WebRTCSession *>(user_data); + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + nhlog::ui()->error("WebRTC: end of stream"); + session->end(); + break; + case GST_MESSAGE_ERROR: + GError *error; + gchar *debug; + gst_message_parse_error(msg, &error, &debug); + nhlog::ui()->error( + "WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message); + g_clear_error(&error); + g_free(debug); + session->end(); + break; + default: + break; + } + return TRUE; } GstWebRTCSessionDescription * parseSDP(const std::string &sdp, GstWebRTCSDPType type) { - GstSDPMessage *msg; - gst_sdp_message_new(&msg); - if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) { - return gst_webrtc_session_description_new(type, msg); - } else { - nhlog::ui()->error("WebRTC: failed to parse remote session description"); - gst_sdp_message_free(msg); - return nullptr; - } + GstSDPMessage *msg; + gst_sdp_message_new(&msg); + if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) { + return gst_webrtc_session_description_new(type, msg); + } else { + nhlog::ui()->error("WebRTC: failed to parse remote session description"); + gst_sdp_message_free(msg); + return nullptr; + } } void setLocalDescription(GstPromise *promise, gpointer webrtc) { - const GstStructure *reply = gst_promise_get_reply(promise); - gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer")); - GstWebRTCSessionDescription *gstsdp = nullptr; - gst_structure_get(reply, - isAnswer ? "answer" : "offer", - GST_TYPE_WEBRTC_SESSION_DESCRIPTION, - &gstsdp, - nullptr); - gst_promise_unref(promise); - g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); - - gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); - localsdp_ = std::string(sdp); - g_free(sdp); - gst_webrtc_session_description_free(gstsdp); - - nhlog::ui()->debug( - "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_); + const GstStructure *reply = gst_promise_get_reply(promise); + gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer")); + GstWebRTCSessionDescription *gstsdp = nullptr; + gst_structure_get( + reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr); + gst_promise_unref(promise); + g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); + + gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); + localsdp_ = std::string(sdp); + g_free(sdp); + gst_webrtc_session_description_free(gstsdp); + + nhlog::ui()->debug( + "WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_); } void createOffer(GstElement *webrtc) { - // create-offer first, then set-local-description - GstPromise *promise = - gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); - g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise); + // create-offer first, then set-local-description + GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); + g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise); } void createAnswer(GstPromise *promise, gpointer webrtc) { - // create-answer first, then set-local-description - gst_promise_unref(promise); - promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); - g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise); + // create-answer first, then set-local-description + gst_promise_unref(promise); + promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr); + g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise); } void @@ -182,18 +178,18 @@ iceGatheringStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { - GstWebRTCICEGatheringState newState; - g_object_get(webrtc, "ice-gathering-state", &newState, nullptr); - if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) { - nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete"); - if (WebRTCSession::instance().isOffering()) { - emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(State::OFFERSENT); - } else { - emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); - } + GstWebRTCICEGatheringState newState; + g_object_get(webrtc, "ice-gathering-state", &newState, nullptr); + if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) { + nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete"); + if (WebRTCSession::instance().isOffering()) { + emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); + emit WebRTCSession::instance().stateChanged(State::OFFERSENT); + } else { + emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); + emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); } + } } void @@ -202,8 +198,8 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, gchar *candidate, gpointer G_GNUC_UNUSED) { - nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); - localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate}); + nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); + localcandidates_.push_back({std::string() /*max-bundle*/, (uint16_t)mlineIndex, candidate}); } void @@ -211,337 +207,327 @@ iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { - GstWebRTCICEConnectionState newState; - g_object_get(webrtc, "ice-connection-state", &newState, nullptr); - switch (newState) { - case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: - nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); - emit WebRTCSession::instance().stateChanged(State::CONNECTING); - break; - case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: - nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed"); - emit WebRTCSession::instance().stateChanged(State::ICEFAILED); - break; - default: - break; - } + GstWebRTCICEConnectionState newState; + g_object_get(webrtc, "ice-connection-state", &newState, nullptr); + switch (newState) { + case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: + nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); + emit WebRTCSession::instance().stateChanged(State::CONNECTING); + break; + case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: + nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed"); + emit WebRTCSession::instance().stateChanged(State::ICEFAILED); + break; + default: + break; + } } // https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1164 struct KeyFrameRequestData { - GstElement *pipe = nullptr; - GstElement *decodebin = nullptr; - gint packetsLost = 0; - guint timerid = 0; - std::string statsField; + GstElement *pipe = nullptr; + GstElement *decodebin = nullptr; + gint packetsLost = 0; + guint timerid = 0; + std::string statsField; } keyFrameRequestData_; void sendKeyFrameRequest() { - GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink"); - if (!gst_pad_push_event(sinkpad, - gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, - gst_structure_new_empty("GstForceKeyUnit")))) - nhlog::ui()->error("WebRTC: key frame request failed"); - else - nhlog::ui()->debug("WebRTC: sent key frame request"); - - gst_object_unref(sinkpad); + GstPad *sinkpad = gst_element_get_static_pad(keyFrameRequestData_.decodebin, "sink"); + if (!gst_pad_push_event(sinkpad, + gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, + gst_structure_new_empty("GstForceKeyUnit")))) + nhlog::ui()->error("WebRTC: key frame request failed"); + else + nhlog::ui()->debug("WebRTC: sent key frame request"); + + gst_object_unref(sinkpad); } void testPacketLoss_(GstPromise *promise, gpointer G_GNUC_UNUSED) { - const GstStructure *reply = gst_promise_get_reply(promise); - gint packetsLost = 0; - GstStructure *rtpStats; - if (!gst_structure_get(reply, - keyFrameRequestData_.statsField.c_str(), - GST_TYPE_STRUCTURE, - &rtpStats, - nullptr)) { - nhlog::ui()->error("WebRTC: get-stats: no field: {}", - keyFrameRequestData_.statsField); - gst_promise_unref(promise); - return; - } - gst_structure_get_int(rtpStats, "packets-lost", &packetsLost); - gst_structure_free(rtpStats); + const GstStructure *reply = gst_promise_get_reply(promise); + gint packetsLost = 0; + GstStructure *rtpStats; + if (!gst_structure_get( + reply, keyFrameRequestData_.statsField.c_str(), GST_TYPE_STRUCTURE, &rtpStats, nullptr)) { + nhlog::ui()->error("WebRTC: get-stats: no field: {}", keyFrameRequestData_.statsField); gst_promise_unref(promise); - if (packetsLost > keyFrameRequestData_.packetsLost) { - nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost); - keyFrameRequestData_.packetsLost = packetsLost; - sendKeyFrameRequest(); - } + return; + } + gst_structure_get_int(rtpStats, "packets-lost", &packetsLost); + gst_structure_free(rtpStats); + gst_promise_unref(promise); + if (packetsLost > keyFrameRequestData_.packetsLost) { + nhlog::ui()->debug("WebRTC: inbound video lost packet count: {}", packetsLost); + keyFrameRequestData_.packetsLost = packetsLost; + sendKeyFrameRequest(); + } } gboolean testPacketLoss(gpointer G_GNUC_UNUSED) { - if (keyFrameRequestData_.pipe) { - GstElement *webrtc = - gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin"); - GstPromise *promise = - gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr); - g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise); - gst_object_unref(webrtc); - return TRUE; - } - return FALSE; + if (keyFrameRequestData_.pipe) { + GstElement *webrtc = gst_bin_get_by_name(GST_BIN(keyFrameRequestData_.pipe), "webrtcbin"); + GstPromise *promise = gst_promise_new_with_change_func(testPacketLoss_, nullptr, nullptr); + g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise); + gst_object_unref(webrtc); + return TRUE; + } + return FALSE; } void setWaitForKeyFrame(GstBin *decodebin G_GNUC_UNUSED, GstElement *element, gpointer G_GNUC_UNUSED) { - if (!std::strcmp( - gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), - "rtpvp8depay")) - g_object_set(element, "wait-for-keyframe", TRUE, nullptr); + if (!std::strcmp( + gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), + "rtpvp8depay")) + g_object_set(element, "wait-for-keyframe", TRUE, nullptr); } GstElement * newAudioSinkChain(GstElement *pipe) { - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *convert = gst_element_factory_make("audioconvert", nullptr); - GstElement *resample = gst_element_factory_make("audioresample", nullptr); - GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr); - gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr); - gst_element_link_many(queue, convert, resample, sink, nullptr); - gst_element_sync_state_with_parent(queue); - gst_element_sync_state_with_parent(convert); - gst_element_sync_state_with_parent(resample); - gst_element_sync_state_with_parent(sink); - return queue; + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *convert = gst_element_factory_make("audioconvert", nullptr); + GstElement *resample = gst_element_factory_make("audioresample", nullptr); + GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr); + gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr); + gst_element_link_many(queue, convert, resample, sink, nullptr); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(convert); + gst_element_sync_state_with_parent(resample); + gst_element_sync_state_with_parent(sink); + return queue; } GstElement * newVideoSinkChain(GstElement *pipe) { - // use compositor for now; acceleration needs investigation - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *compositor = gst_element_factory_make("compositor", "compositor"); - GstElement *glupload = gst_element_factory_make("glupload", nullptr); - GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr); - GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); - GstElement *glsinkbin = gst_element_factory_make("glsinkbin", nullptr); - g_object_set(compositor, "background", 1, nullptr); - g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr); - g_object_set(glsinkbin, "sink", qmlglsink, nullptr); - gst_bin_add_many( - GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); - gst_element_sync_state_with_parent(queue); - gst_element_sync_state_with_parent(compositor); - gst_element_sync_state_with_parent(glupload); - gst_element_sync_state_with_parent(glcolorconvert); - gst_element_sync_state_with_parent(glsinkbin); - return queue; + // use compositor for now; acceleration needs investigation + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *compositor = gst_element_factory_make("compositor", "compositor"); + GstElement *glupload = gst_element_factory_make("glupload", nullptr); + GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr); + GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); + GstElement *glsinkbin = gst_element_factory_make("glsinkbin", nullptr); + g_object_set(compositor, "background", 1, nullptr); + g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr); + g_object_set(glsinkbin, "sink", qmlglsink, nullptr); + gst_bin_add_many( + GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); + gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(compositor); + gst_element_sync_state_with_parent(glupload); + gst_element_sync_state_with_parent(glcolorconvert); + gst_element_sync_state_with_parent(glsinkbin); + return queue; } std::pair<int, int> getResolution(GstPad *pad) { - std::pair<int, int> ret; - GstCaps *caps = gst_pad_get_current_caps(pad); - const GstStructure *s = gst_caps_get_structure(caps, 0); - gst_structure_get_int(s, "width", &ret.first); - gst_structure_get_int(s, "height", &ret.second); - gst_caps_unref(caps); - return ret; + std::pair<int, int> ret; + GstCaps *caps = gst_pad_get_current_caps(pad); + const GstStructure *s = gst_caps_get_structure(caps, 0); + gst_structure_get_int(s, "width", &ret.first); + gst_structure_get_int(s, "height", &ret.second); + gst_caps_unref(caps); + return ret; } std::pair<int, int> getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName) { - GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName); - GstPad *pad = gst_element_get_static_pad(element, padName); - auto ret = getResolution(pad); - gst_object_unref(pad); - gst_object_unref(element); - return ret; + GstElement *element = gst_bin_get_by_name(GST_BIN(pipe), elementName); + GstPad *pad = gst_element_get_static_pad(element, padName); + auto ret = getResolution(pad); + gst_object_unref(pad); + gst_object_unref(element); + return ret; } std::pair<int, int> getPiPDimensions(const std::pair<int, int> &resolution, int fullWidth, double scaleFactor) { - int pipWidth = fullWidth * scaleFactor; - int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth; - return {pipWidth, pipHeight}; + int pipWidth = fullWidth * scaleFactor; + int pipHeight = static_cast<double>(resolution.second) / resolution.first * pipWidth; + return {pipWidth, pipHeight}; } void addLocalPiP(GstElement *pipe, const std::pair<int, int> &videoCallSize) { - // embed localUser's camera into received video (CallType::VIDEO) - // OR embed screen share into received video (CallType::SCREEN) - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); - if (!tee) - return; - - GstElement *queue = gst_element_factory_make("queue", nullptr); - gst_bin_add(GST_BIN(pipe), queue); - gst_element_link(tee, queue); - gst_element_sync_state_with_parent(queue); - gst_object_unref(tee); - - GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor"); - localPiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); - g_object_set(localPiPSinkPad_, "zorder", 2, nullptr); - - bool isVideo = WebRTCSession::instance().callType() == CallType::VIDEO; - const gchar *element = isVideo ? "camerafilter" : "screenshare"; - const gchar *pad = isVideo ? "sink" : "src"; - auto resolution = getResolution(pipe, element, pad); - auto pipSize = getPiPDimensions(resolution, videoCallSize.first, 0.25); - nhlog::ui()->debug( - "WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second); - g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); - gint offset = videoCallSize.first / 80; - g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr); - - GstPad *srcpad = gst_element_get_static_pad(queue, "src"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_))) - nhlog::ui()->error("WebRTC: failed to link local PiP elements"); - gst_object_unref(srcpad); - gst_object_unref(compositor); + // embed localUser's camera into received video (CallType::VIDEO) + // OR embed screen share into received video (CallType::SCREEN) + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); + if (!tee) + return; + + GstElement *queue = gst_element_factory_make("queue", nullptr); + gst_bin_add(GST_BIN(pipe), queue); + gst_element_link(tee, queue); + gst_element_sync_state_with_parent(queue); + gst_object_unref(tee); + + GstElement *compositor = gst_bin_get_by_name(GST_BIN(pipe), "compositor"); + localPiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); + g_object_set(localPiPSinkPad_, "zorder", 2, nullptr); + + bool isVideo = WebRTCSession::instance().callType() == CallType::VIDEO; + const gchar *element = isVideo ? "camerafilter" : "screenshare"; + const gchar *pad = isVideo ? "sink" : "src"; + auto resolution = getResolution(pipe, element, pad); + auto pipSize = getPiPDimensions(resolution, videoCallSize.first, 0.25); + nhlog::ui()->debug("WebRTC: local picture-in-picture: {}x{}", pipSize.first, pipSize.second); + g_object_set(localPiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); + gint offset = videoCallSize.first / 80; + g_object_set(localPiPSinkPad_, "xpos", offset, "ypos", offset, nullptr); + + GstPad *srcpad = gst_element_get_static_pad(queue, "src"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, localPiPSinkPad_))) + nhlog::ui()->error("WebRTC: failed to link local PiP elements"); + gst_object_unref(srcpad); + gst_object_unref(compositor); } void addRemotePiP(GstElement *pipe) { - // embed localUser's camera into screen image being shared - if (remotePiPSinkPad_) { - auto camRes = getResolution(pipe, "camerafilter", "sink"); - auto shareRes = getResolution(pipe, "screenshare", "src"); - auto pipSize = getPiPDimensions(camRes, shareRes.first, 0.2); - nhlog::ui()->debug( - "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second); - - gint offset = shareRes.first / 100; - g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr); - g_object_set( - remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); - g_object_set(remotePiPSinkPad_, - "xpos", - shareRes.first - pipSize.first - offset, - "ypos", - shareRes.second - pipSize.second - offset, - nullptr); - } + // embed localUser's camera into screen image being shared + if (remotePiPSinkPad_) { + auto camRes = getResolution(pipe, "camerafilter", "sink"); + auto shareRes = getResolution(pipe, "screenshare", "src"); + auto pipSize = getPiPDimensions(camRes, shareRes.first, 0.2); + nhlog::ui()->debug( + "WebRTC: screen share picture-in-picture: {}x{}", pipSize.first, pipSize.second); + + gint offset = shareRes.first / 100; + g_object_set(remotePiPSinkPad_, "zorder", 2, nullptr); + g_object_set(remotePiPSinkPad_, "width", pipSize.first, "height", pipSize.second, nullptr); + g_object_set(remotePiPSinkPad_, + "xpos", + shareRes.first - pipSize.first - offset, + "ypos", + shareRes.second - pipSize.second - offset, + nullptr); + } } void addLocalVideo(GstElement *pipe) { - GstElement *queue = newVideoSinkChain(pipe); - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); - GstPad *srcpad = gst_element_get_request_pad(tee, "src_%u"); - GstPad *sinkpad = gst_element_get_static_pad(queue, "sink"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad))) - nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain"); - gst_object_unref(srcpad); + GstElement *queue = newVideoSinkChain(pipe); + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee"); + GstPad *srcpad = gst_element_get_request_pad(tee, "src_%u"); + GstPad *sinkpad = gst_element_get_static_pad(queue, "sink"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, sinkpad))) + nhlog::ui()->error("WebRTC: failed to link videosrctee -> video sink chain"); + gst_object_unref(srcpad); } void linkNewPad(GstElement *decodebin, GstPad *newpad, GstElement *pipe) { - GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); - GstCaps *sinkcaps = gst_pad_get_current_caps(sinkpad); - const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0); - - gchar *mediaType = nullptr; - guint ssrc = 0; - gst_structure_get( - structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr); - gst_caps_unref(sinkcaps); - gst_object_unref(sinkpad); - - WebRTCSession *session = &WebRTCSession::instance(); - GstElement *queue = nullptr; - if (!std::strcmp(mediaType, "audio")) { - nhlog::ui()->debug("WebRTC: received incoming audio stream"); - haveAudioStream_ = true; - queue = newAudioSinkChain(pipe); - } else if (!std::strcmp(mediaType, "video")) { - nhlog::ui()->debug("WebRTC: received incoming video stream"); - if (!session->getVideoItem()) { - g_free(mediaType); - nhlog::ui()->error("WebRTC: video call item not set"); - return; - } - haveVideoStream_ = true; - keyFrameRequestData_.statsField = - std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc); - queue = newVideoSinkChain(pipe); - auto videoCallSize = getResolution(newpad); - nhlog::ui()->info("WebRTC: incoming video resolution: {}x{}", - videoCallSize.first, - videoCallSize.second); - addLocalPiP(pipe, videoCallSize); - } else { - g_free(mediaType); - nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad)); - return; + GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); + GstCaps *sinkcaps = gst_pad_get_current_caps(sinkpad); + const GstStructure *structure = gst_caps_get_structure(sinkcaps, 0); + + gchar *mediaType = nullptr; + guint ssrc = 0; + gst_structure_get( + structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr); + gst_caps_unref(sinkcaps); + gst_object_unref(sinkpad); + + WebRTCSession *session = &WebRTCSession::instance(); + GstElement *queue = nullptr; + if (!std::strcmp(mediaType, "audio")) { + nhlog::ui()->debug("WebRTC: received incoming audio stream"); + haveAudioStream_ = true; + queue = newAudioSinkChain(pipe); + } else if (!std::strcmp(mediaType, "video")) { + nhlog::ui()->debug("WebRTC: received incoming video stream"); + if (!session->getVideoItem()) { + g_free(mediaType); + nhlog::ui()->error("WebRTC: video call item not set"); + return; } - - GstPad *queuepad = gst_element_get_static_pad(queue, "sink"); - if (queuepad) { - if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) - nhlog::ui()->error("WebRTC: unable to link new pad"); - else { - if (session->callType() == CallType::VOICE || - (haveAudioStream_ && - (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) { - emit session->stateChanged(State::CONNECTED); - if (haveVideoStream_) { - keyFrameRequestData_.pipe = pipe; - keyFrameRequestData_.decodebin = decodebin; - keyFrameRequestData_.timerid = - g_timeout_add_seconds(3, testPacketLoss, nullptr); - } - addRemotePiP(pipe); - if (session->isRemoteVideoRecvOnly()) - addLocalVideo(pipe); - } + haveVideoStream_ = true; + keyFrameRequestData_.statsField = + std::string("rtp-inbound-stream-stats_") + std::to_string(ssrc); + queue = newVideoSinkChain(pipe); + auto videoCallSize = getResolution(newpad); + nhlog::ui()->info( + "WebRTC: incoming video resolution: {}x{}", videoCallSize.first, videoCallSize.second); + addLocalPiP(pipe, videoCallSize); + } else { + g_free(mediaType); + nhlog::ui()->error("WebRTC: unknown pad type: {}", GST_PAD_NAME(newpad)); + return; + } + + GstPad *queuepad = gst_element_get_static_pad(queue, "sink"); + if (queuepad) { + if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) + nhlog::ui()->error("WebRTC: unable to link new pad"); + else { + if (session->callType() == CallType::VOICE || + (haveAudioStream_ && (haveVideoStream_ || session->isRemoteVideoRecvOnly()))) { + emit session->stateChanged(State::CONNECTED); + if (haveVideoStream_) { + keyFrameRequestData_.pipe = pipe; + keyFrameRequestData_.decodebin = decodebin; + keyFrameRequestData_.timerid = + g_timeout_add_seconds(3, testPacketLoss, nullptr); } - gst_object_unref(queuepad); + addRemotePiP(pipe); + if (session->isRemoteVideoRecvOnly()) + addLocalVideo(pipe); + } } - g_free(mediaType); + gst_object_unref(queuepad); + } + g_free(mediaType); } void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe) { - if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) - return; - - nhlog::ui()->debug("WebRTC: received incoming stream"); - GstElement *decodebin = gst_element_factory_make("decodebin", nullptr); - // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed - g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr); - g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe); - g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr); - gst_bin_add(GST_BIN(pipe), decodebin); - gst_element_sync_state_with_parent(decodebin); - GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); - if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) - nhlog::ui()->error("WebRTC: unable to link decodebin"); - gst_object_unref(sinkpad); + if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) + return; + + nhlog::ui()->debug("WebRTC: received incoming stream"); + GstElement *decodebin = gst_element_factory_make("decodebin", nullptr); + // hardware decoding needs investigation; eg rendering fails if vaapi plugin installed + g_object_set(decodebin, "force-sw-decoders", TRUE, nullptr); + g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe); + g_signal_connect(decodebin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr); + gst_bin_add(GST_BIN(pipe), decodebin); + gst_element_sync_state_with_parent(decodebin); + GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink"); + if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) + nhlog::ui()->error("WebRTC: unable to link decodebin"); + gst_object_unref(sinkpad); } bool contains(std::string_view str1, std::string_view str2) { - return std::search(str1.cbegin(), - str1.cend(), - str2.cbegin(), - str2.cend(), - [](unsigned char c1, unsigned char c2) { - return std::tolower(c1) == std::tolower(c2); - }) != str1.cend(); + return std::search(str1.cbegin(), + str1.cend(), + str2.cbegin(), + str2.cend(), + [](unsigned char c1, unsigned char c2) { + return std::tolower(c1) == std::tolower(c2); + }) != str1.cend(); } bool @@ -552,582 +538,566 @@ getMediaAttributes(const GstSDPMessage *sdp, bool &recvOnly, bool &sendOnly) { - payloadType = -1; - recvOnly = false; - sendOnly = false; - for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) { - const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex); - if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) { - recvOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr; - sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr; - const gchar *rtpval = nullptr; - for (guint n = 0; n == 0 || rtpval; ++n) { - rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n); - if (rtpval && contains(rtpval, encoding)) { - payloadType = std::atoi(rtpval); - break; - } - } - return true; + payloadType = -1; + recvOnly = false; + sendOnly = false; + for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); ++mlineIndex) { + const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex); + if (!std::strcmp(gst_sdp_media_get_media(media), mediaType)) { + recvOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr; + sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr; + const gchar *rtpval = nullptr; + for (guint n = 0; n == 0 || rtpval; ++n) { + rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n); + if (rtpval && contains(rtpval, encoding)) { + payloadType = std::atoi(rtpval); + break; } + } + return true; } - return false; + } + return false; } } bool WebRTCSession::havePlugins(bool isVideo, std::string *errorMessage) { - if (!initialised_ && !init(errorMessage)) - return false; - if (!isVideo && haveVoicePlugins_) - return true; - if (isVideo && haveVideoPlugins_) - return true; - - const gchar *voicePlugins[] = {"audioconvert", - "audioresample", - "autodetect", - "dtls", - "nice", - "opus", - "playback", - "rtpmanager", - "srtp", - "volume", - "webrtc", - nullptr}; - - const gchar *videoPlugins[] = { - "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; - - std::string strError("Missing GStreamer plugins: "); - const gchar **needed = isVideo ? videoPlugins : voicePlugins; - bool &havePlugins = isVideo ? haveVideoPlugins_ : haveVoicePlugins_; - havePlugins = true; - GstRegistry *registry = gst_registry_get(); - for (guint i = 0; i < g_strv_length((gchar **)needed); i++) { - GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); - if (!plugin) { - havePlugins = false; - strError += std::string(needed[i]) + " "; - continue; - } - gst_object_unref(plugin); - } - if (!havePlugins) { - nhlog::ui()->error(strError); - if (errorMessage) - *errorMessage = strError; - return false; - } + if (!initialised_ && !init(errorMessage)) + return false; + if (!isVideo && haveVoicePlugins_) + return true; + if (isVideo && haveVideoPlugins_) + return true; - if (isVideo) { - // load qmlglsink to register GStreamer's GstGLVideoItem QML type - GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); - gst_object_unref(qmlglsink); + const gchar *voicePlugins[] = {"audioconvert", + "audioresample", + "autodetect", + "dtls", + "nice", + "opus", + "playback", + "rtpmanager", + "srtp", + "volume", + "webrtc", + nullptr}; + + const gchar *videoPlugins[] = { + "compositor", "opengl", "qmlgl", "rtp", "videoconvert", "vpx", nullptr}; + + std::string strError("Missing GStreamer plugins: "); + const gchar **needed = isVideo ? videoPlugins : voicePlugins; + bool &havePlugins = isVideo ? haveVideoPlugins_ : haveVoicePlugins_; + havePlugins = true; + GstRegistry *registry = gst_registry_get(); + for (guint i = 0; i < g_strv_length((gchar **)needed); i++) { + GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]); + if (!plugin) { + havePlugins = false; + strError += std::string(needed[i]) + " "; + continue; } - return true; + gst_object_unref(plugin); + } + if (!havePlugins) { + nhlog::ui()->error(strError); + if (errorMessage) + *errorMessage = strError; + return false; + } + + if (isVideo) { + // load qmlglsink to register GStreamer's GstGLVideoItem QML type + GstElement *qmlglsink = gst_element_factory_make("qmlglsink", nullptr); + gst_object_unref(qmlglsink); + } + return true; } bool WebRTCSession::createOffer(CallType callType, uint32_t shareWindowId) { - clear(); - isOffering_ = true; - callType_ = callType; - shareWindowId_ = shareWindowId; - - // opus and vp8 rtp payload types must be defined dynamically - // therefore from the range [96-127] - // see for example https://tools.ietf.org/html/rfc7587 - constexpr int opusPayloadType = 111; - constexpr int vp8PayloadType = 96; - return startPipeline(opusPayloadType, vp8PayloadType); + clear(); + isOffering_ = true; + callType_ = callType; + shareWindowId_ = shareWindowId; + + // opus and vp8 rtp payload types must be defined dynamically + // therefore from the range [96-127] + // see for example https://tools.ietf.org/html/rfc7587 + constexpr int opusPayloadType = 111; + constexpr int vp8PayloadType = 96; + return startPipeline(opusPayloadType, vp8PayloadType); } bool WebRTCSession::acceptOffer(const std::string &sdp) { - nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp); - if (state_ != State::DISCONNECTED) - return false; + nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp); + if (state_ != State::DISCONNECTED) + return false; - clear(); - GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); - if (!offer) - return false; + clear(); + GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER); + if (!offer) + return false; - int opusPayloadType; - bool recvOnly; - bool sendOnly; - if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) { - if (opusPayloadType == -1) { - nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding"); - gst_webrtc_session_description_free(offer); - return false; - } - } else { - nhlog::ui()->error("WebRTC: remote offer - no audio media"); - gst_webrtc_session_description_free(offer); - return false; + int opusPayloadType; + bool recvOnly; + bool sendOnly; + if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, recvOnly, sendOnly)) { + if (opusPayloadType == -1) { + nhlog::ui()->error("WebRTC: remote audio offer - no opus encoding"); + gst_webrtc_session_description_free(offer); + return false; } + } else { + nhlog::ui()->error("WebRTC: remote offer - no audio media"); + gst_webrtc_session_description_free(offer); + return false; + } - int vp8PayloadType; - bool isVideo = getMediaAttributes(offer->sdp, - "video", - "vp8", - vp8PayloadType, - isRemoteVideoRecvOnly_, - isRemoteVideoSendOnly_); - if (isVideo && vp8PayloadType == -1) { - nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding"); - gst_webrtc_session_description_free(offer); - return false; - } - callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; + int vp8PayloadType; + bool isVideo = getMediaAttributes( + offer->sdp, "video", "vp8", vp8PayloadType, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_); + if (isVideo && vp8PayloadType == -1) { + nhlog::ui()->error("WebRTC: remote video offer - no vp8 encoding"); + gst_webrtc_session_description_free(offer); + return false; + } + callType_ = isVideo ? CallType::VIDEO : CallType::VOICE; - if (!startPipeline(opusPayloadType, vp8PayloadType)) { - gst_webrtc_session_description_free(offer); - return false; - } + if (!startPipeline(opusPayloadType, vp8PayloadType)) { + gst_webrtc_session_description_free(offer); + return false; + } - // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc - // lines) - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + // avoid a race that sometimes leaves the generated answer without media tracks (a=ssrc + // lines) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); - // set-remote-description first, then create-answer - GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr); - g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise); - gst_webrtc_session_description_free(offer); - return true; + // set-remote-description first, then create-answer + GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr); + g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise); + gst_webrtc_session_description_free(offer); + return true; } bool WebRTCSession::acceptAnswer(const std::string &sdp) { - nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); - if (state_ != State::OFFERSENT) - return false; - - GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); - if (!answer) { - end(); - return false; - } - - if (callType_ != CallType::VOICE) { - int unused; - if (!getMediaAttributes(answer->sdp, - "video", - "vp8", - unused, - isRemoteVideoRecvOnly_, - isRemoteVideoSendOnly_)) - isRemoteVideoRecvOnly_ = true; - } + nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp); + if (state_ != State::OFFERSENT) + return false; - g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr); - gst_webrtc_session_description_free(answer); - return true; + GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER); + if (!answer) { + end(); + return false; + } + + if (callType_ != CallType::VOICE) { + int unused; + if (!getMediaAttributes( + answer->sdp, "video", "vp8", unused, isRemoteVideoRecvOnly_, isRemoteVideoSendOnly_)) + isRemoteVideoRecvOnly_ = true; + } + + g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr); + gst_webrtc_session_description_free(answer); + return true; } void WebRTCSession::acceptICECandidates( const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates) { - if (state_ >= State::INITIATED) { - for (const auto &c : candidates) { - nhlog::ui()->debug( - "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); - if (!c.candidate.empty()) { - g_signal_emit_by_name(webrtc_, - "add-ice-candidate", - c.sdpMLineIndex, - c.candidate.c_str()); - } - } + if (state_ >= State::INITIATED) { + for (const auto &c : candidates) { + nhlog::ui()->debug( + "WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); + if (!c.candidate.empty()) { + g_signal_emit_by_name( + webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str()); + } } + } } bool WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType) { - if (state_ != State::DISCONNECTED) - return false; - - emit stateChanged(State::INITIATING); - - if (!createPipeline(opusPayloadType, vp8PayloadType)) { - end(); - return false; - } - - webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); - - if (ChatPage::instance()->userSettings()->useStunServer()) { - nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER); - g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr); - } - - for (const auto &uri : turnServers_) { - nhlog::ui()->info("WebRTC: setting TURN server: {}", uri); - gboolean udata; - g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata)); - } - if (turnServers_.empty()) - nhlog::ui()->warn("WebRTC: no TURN server provided"); - - // generate the offer when the pipeline goes to PLAYING - if (isOffering_) - g_signal_connect( - webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr); - - // on-ice-candidate is emitted when a local ICE candidate has been gathered - g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr); - - // capture ICE failure - g_signal_connect( - webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr); - - // incoming streams trigger pad-added - gst_element_set_state(pipe_, GST_STATE_READY); - g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_); - - // capture ICE gathering completion - g_signal_connect( - webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr); + if (state_ != State::DISCONNECTED) + return false; - // webrtcbin lifetime is the same as that of the pipeline - gst_object_unref(webrtc_); + emit stateChanged(State::INITIATING); - // start the pipeline - GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) { - nhlog::ui()->error("WebRTC: unable to start pipeline"); - end(); - return false; - } + if (!createPipeline(opusPayloadType, vp8PayloadType)) { + end(); + return false; + } + + webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); + + if (ChatPage::instance()->userSettings()->useStunServer()) { + nhlog::ui()->info("WebRTC: setting STUN server: {}", STUN_SERVER); + g_object_set(webrtc_, "stun-server", STUN_SERVER, nullptr); + } + + for (const auto &uri : turnServers_) { + nhlog::ui()->info("WebRTC: setting TURN server: {}", uri); + gboolean udata; + g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata)); + } + if (turnServers_.empty()) + nhlog::ui()->warn("WebRTC: no TURN server provided"); + + // generate the offer when the pipeline goes to PLAYING + if (isOffering_) + g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr); + + // on-ice-candidate is emitted when a local ICE candidate has been gathered + g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr); + + // capture ICE failure + g_signal_connect( + webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr); + + // incoming streams trigger pad-added + gst_element_set_state(pipe_, GST_STATE_READY); + g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_); + + // capture ICE gathering completion + g_signal_connect( + webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr); + + // webrtcbin lifetime is the same as that of the pipeline + gst_object_unref(webrtc_); + + // start the pipeline + GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING); + if (ret == GST_STATE_CHANGE_FAILURE) { + nhlog::ui()->error("WebRTC: unable to start pipeline"); + end(); + return false; + } - GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); - busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this); - gst_object_unref(bus); - emit stateChanged(State::INITIATED); - return true; + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_)); + busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this); + gst_object_unref(bus); + emit stateChanged(State::INITIATED); + return true; } bool WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType) { - GstDevice *device = devices_.audioDevice(); - if (!device) - return false; - - GstElement *source = gst_device_create_element(device, nullptr); - GstElement *volume = gst_element_factory_make("volume", "srclevel"); - GstElement *convert = gst_element_factory_make("audioconvert", nullptr); - GstElement *resample = gst_element_factory_make("audioresample", nullptr); - GstElement *queue1 = gst_element_factory_make("queue", nullptr); - GstElement *opusenc = gst_element_factory_make("opusenc", nullptr); - GstElement *rtp = gst_element_factory_make("rtpopuspay", nullptr); - GstElement *queue2 = gst_element_factory_make("queue", nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + GstDevice *device = devices_.audioDevice(); + if (!device) + return false; - GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", - "media", - G_TYPE_STRING, - "audio", - "encoding-name", - G_TYPE_STRING, - "OPUS", - "payload", - G_TYPE_INT, - opusPayloadType, - nullptr); - g_object_set(capsfilter, "caps", rtpcaps, nullptr); - gst_caps_unref(rtpcaps); - - GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin"); - g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr); - - pipe_ = gst_pipeline_new(nullptr); - gst_bin_add_many(GST_BIN(pipe_), - source, - volume, - convert, - resample, - queue1, - opusenc, - rtp, - queue2, - capsfilter, - webrtcbin, - nullptr); - - if (!gst_element_link_many(source, - volume, - convert, - resample, - queue1, - opusenc, - rtp, - queue2, - capsfilter, - webrtcbin, - nullptr)) { - nhlog::ui()->error("WebRTC: failed to link audio pipeline elements"); - return false; - } + GstElement *source = gst_device_create_element(device, nullptr); + GstElement *volume = gst_element_factory_make("volume", "srclevel"); + GstElement *convert = gst_element_factory_make("audioconvert", nullptr); + GstElement *resample = gst_element_factory_make("audioresample", nullptr); + GstElement *queue1 = gst_element_factory_make("queue", nullptr); + GstElement *opusenc = gst_element_factory_make("opusenc", nullptr); + GstElement *rtp = gst_element_factory_make("rtpopuspay", nullptr); + GstElement *queue2 = gst_element_factory_make("queue", nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + + GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", + "media", + G_TYPE_STRING, + "audio", + "encoding-name", + G_TYPE_STRING, + "OPUS", + "payload", + G_TYPE_INT, + opusPayloadType, + nullptr); + g_object_set(capsfilter, "caps", rtpcaps, nullptr); + gst_caps_unref(rtpcaps); + + GstElement *webrtcbin = gst_element_factory_make("webrtcbin", "webrtcbin"); + g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr); + + pipe_ = gst_pipeline_new(nullptr); + gst_bin_add_many(GST_BIN(pipe_), + source, + volume, + convert, + resample, + queue1, + opusenc, + rtp, + queue2, + capsfilter, + webrtcbin, + nullptr); + + if (!gst_element_link_many(source, + volume, + convert, + resample, + queue1, + opusenc, + rtp, + queue2, + capsfilter, + webrtcbin, + nullptr)) { + nhlog::ui()->error("WebRTC: failed to link audio pipeline elements"); + return false; + } - return callType_ == CallType::VOICE || isRemoteVideoSendOnly_ - ? true - : addVideoPipeline(vp8PayloadType); + return callType_ == CallType::VOICE || isRemoteVideoSendOnly_ + ? true + : addVideoPipeline(vp8PayloadType); } bool WebRTCSession::addVideoPipeline(int vp8PayloadType) { - // allow incoming video calls despite localUser having no webcam - if (callType_ == CallType::VIDEO && !devices_.haveCamera()) - return !isOffering_; - - auto settings = ChatPage::instance()->userSettings(); - GstElement *camerafilter = nullptr; - GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); - GstElement *tee = gst_element_factory_make("tee", "videosrctee"); - gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr); - if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) { - std::pair<int, int> resolution; - std::pair<int, int> frameRate; - GstDevice *device = devices_.videoDevice(resolution, frameRate); - if (!device) - return false; - - GstElement *camera = gst_device_create_element(device, nullptr); - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "width", - G_TYPE_INT, - resolution.first, - "height", - G_TYPE_INT, - resolution.second, - "framerate", - GST_TYPE_FRACTION, - frameRate.first, - frameRate.second, - nullptr); - camerafilter = gst_element_factory_make("capsfilter", "camerafilter"); - g_object_set(camerafilter, "caps", caps, nullptr); - gst_caps_unref(caps); - - gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr); - if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link camera elements"); - return false; - } - if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) { - nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee"); - return false; - } - } + // allow incoming video calls despite localUser having no webcam + if (callType_ == CallType::VIDEO && !devices_.haveCamera()) + return !isOffering_; + + auto settings = ChatPage::instance()->userSettings(); + GstElement *camerafilter = nullptr; + GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); + GstElement *tee = gst_element_factory_make("tee", "videosrctee"); + gst_bin_add_many(GST_BIN(pipe_), videoconvert, tee, nullptr); + if (callType_ == CallType::VIDEO || (settings->screenSharePiP() && devices_.haveCamera())) { + std::pair<int, int> resolution; + std::pair<int, int> frameRate; + GstDevice *device = devices_.videoDevice(resolution, frameRate); + if (!device) + return false; + + GstElement *camera = gst_device_create_element(device, nullptr); + GstCaps *caps = gst_caps_new_simple("video/x-raw", + "width", + G_TYPE_INT, + resolution.first, + "height", + G_TYPE_INT, + resolution.second, + "framerate", + GST_TYPE_FRACTION, + frameRate.first, + frameRate.second, + nullptr); + camerafilter = gst_element_factory_make("capsfilter", "camerafilter"); + g_object_set(camerafilter, "caps", caps, nullptr); + gst_caps_unref(caps); - if (callType_ == CallType::SCREEN) { - nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps", - settings->screenShareFrameRate()); - nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}", - settings->screenSharePiP()); - nhlog::ui()->debug("WebRTC: screen share request remote camera: {}", - settings->screenShareRemoteVideo()); - nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}", - settings->screenShareHideCursor()); - - GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare"); - if (!ximagesrc) { - nhlog::ui()->error("WebRTC: failed to create ximagesrc"); - return false; - } - g_object_set(ximagesrc, "use-damage", FALSE, nullptr); - g_object_set(ximagesrc, "xid", shareWindowId_, nullptr); - g_object_set( - ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr); - - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "framerate", - GST_TYPE_FRACTION, - settings->screenShareFrameRate(), - 1, - nullptr); - GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); - g_object_set(capsfilter, "caps", caps, nullptr); - gst_caps_unref(caps); - gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr); - - if (settings->screenSharePiP() && devices_.haveCamera()) { - GstElement *compositor = gst_element_factory_make("compositor", nullptr); - g_object_set(compositor, "background", 1, nullptr); - gst_bin_add(GST_BIN(pipe_), compositor); - if (!gst_element_link_many( - ximagesrc, compositor, capsfilter, tee, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link screen share elements"); - return false; - } - - GstPad *srcpad = gst_element_get_static_pad(camerafilter, "src"); - remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); - if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) { - nhlog::ui()->error( - "WebRTC: failed to link camerafilter -> compositor"); - gst_object_unref(srcpad); - return false; - } - gst_object_unref(srcpad); - } else if (!gst_element_link_many( - ximagesrc, videoconvert, capsfilter, tee, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link screen share elements"); - return false; - } + gst_bin_add_many(GST_BIN(pipe_), camera, camerafilter, nullptr); + if (!gst_element_link_many(camera, videoconvert, camerafilter, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link camera elements"); + return false; } - - GstElement *queue = gst_element_factory_make("queue", nullptr); - GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr); - g_object_set(vp8enc, "deadline", 1, nullptr); - g_object_set(vp8enc, "error-resilient", 1, nullptr); - GstElement *rtpvp8pay = gst_element_factory_make("rtpvp8pay", nullptr); - GstElement *rtpqueue = gst_element_factory_make("queue", nullptr); - GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr); - GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", - "media", - G_TYPE_STRING, - "video", - "encoding-name", - G_TYPE_STRING, - "VP8", - "payload", - G_TYPE_INT, - vp8PayloadType, - nullptr); - g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr); - gst_caps_unref(rtpcaps); - - gst_bin_add_many( - GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr); - - GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); - if (!gst_element_link_many( - tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) { - nhlog::ui()->error("WebRTC: failed to link rtp video elements"); - gst_object_unref(webrtcbin); - return false; + if (callType_ == CallType::VIDEO && !gst_element_link(camerafilter, tee)) { + nhlog::ui()->error("WebRTC: failed to link camerafilter -> tee"); + return false; } - - if (callType_ == CallType::SCREEN && - !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) { - GArray *transceivers; - g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers); - GstWebRTCRTPTransceiver *transceiver = - g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1); - transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; - g_array_unref(transceivers); + } + + if (callType_ == CallType::SCREEN) { + nhlog::ui()->debug("WebRTC: screen share frame rate: {} fps", + settings->screenShareFrameRate()); + nhlog::ui()->debug("WebRTC: screen share picture-in-picture: {}", + settings->screenSharePiP()); + nhlog::ui()->debug("WebRTC: screen share request remote camera: {}", + settings->screenShareRemoteVideo()); + nhlog::ui()->debug("WebRTC: screen share hide mouse cursor: {}", + settings->screenShareHideCursor()); + + GstElement *ximagesrc = gst_element_factory_make("ximagesrc", "screenshare"); + if (!ximagesrc) { + nhlog::ui()->error("WebRTC: failed to create ximagesrc"); + return false; } + g_object_set(ximagesrc, "use-damage", FALSE, nullptr); + g_object_set(ximagesrc, "xid", shareWindowId_, nullptr); + g_object_set(ximagesrc, "show-pointer", !settings->screenShareHideCursor(), nullptr); + + GstCaps *caps = gst_caps_new_simple("video/x-raw", + "framerate", + GST_TYPE_FRACTION, + settings->screenShareFrameRate(), + 1, + nullptr); + GstElement *capsfilter = gst_element_factory_make("capsfilter", nullptr); + g_object_set(capsfilter, "caps", caps, nullptr); + gst_caps_unref(caps); + gst_bin_add_many(GST_BIN(pipe_), ximagesrc, capsfilter, nullptr); + + if (settings->screenSharePiP() && devices_.haveCamera()) { + GstElement *compositor = gst_element_factory_make("compositor", nullptr); + g_object_set(compositor, "background", 1, nullptr); + gst_bin_add(GST_BIN(pipe_), compositor); + if (!gst_element_link_many(ximagesrc, compositor, capsfilter, tee, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link screen share elements"); + return false; + } + GstPad *srcpad = gst_element_get_static_pad(camerafilter, "src"); + remotePiPSinkPad_ = gst_element_get_request_pad(compositor, "sink_%u"); + if (GST_PAD_LINK_FAILED(gst_pad_link(srcpad, remotePiPSinkPad_))) { + nhlog::ui()->error("WebRTC: failed to link camerafilter -> compositor"); + gst_object_unref(srcpad); + return false; + } + gst_object_unref(srcpad); + } else if (!gst_element_link_many(ximagesrc, videoconvert, capsfilter, tee, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link screen share elements"); + return false; + } + } + + GstElement *queue = gst_element_factory_make("queue", nullptr); + GstElement *vp8enc = gst_element_factory_make("vp8enc", nullptr); + g_object_set(vp8enc, "deadline", 1, nullptr); + g_object_set(vp8enc, "error-resilient", 1, nullptr); + GstElement *rtpvp8pay = gst_element_factory_make("rtpvp8pay", nullptr); + GstElement *rtpqueue = gst_element_factory_make("queue", nullptr); + GstElement *rtpcapsfilter = gst_element_factory_make("capsfilter", nullptr); + GstCaps *rtpcaps = gst_caps_new_simple("application/x-rtp", + "media", + G_TYPE_STRING, + "video", + "encoding-name", + G_TYPE_STRING, + "VP8", + "payload", + G_TYPE_INT, + vp8PayloadType, + nullptr); + g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr); + gst_caps_unref(rtpcaps); + + gst_bin_add_many(GST_BIN(pipe_), queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, nullptr); + + GstElement *webrtcbin = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin"); + if (!gst_element_link_many( + tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) { + nhlog::ui()->error("WebRTC: failed to link rtp video elements"); gst_object_unref(webrtcbin); - return true; + return false; + } + + if (callType_ == CallType::SCREEN && + !ChatPage::instance()->userSettings()->screenShareRemoteVideo()) { + GArray *transceivers; + g_signal_emit_by_name(webrtcbin, "get-transceivers", &transceivers); + GstWebRTCRTPTransceiver *transceiver = + g_array_index(transceivers, GstWebRTCRTPTransceiver *, 1); + transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY; + g_array_unref(transceivers); + } + + gst_object_unref(webrtcbin); + return true; } bool WebRTCSession::haveLocalPiP() const { - if (state_ >= State::INITIATED) { - if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_) - return false; - else if (callType_ == CallType::SCREEN) - return true; - else { - GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee"); - if (tee) { - gst_object_unref(tee); - return true; - } - } + if (state_ >= State::INITIATED) { + if (callType_ == CallType::VOICE || isRemoteVideoRecvOnly_) + return false; + else if (callType_ == CallType::SCREEN) + return true; + else { + GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee"); + if (tee) { + gst_object_unref(tee); + return true; + } } - return false; + } + return false; } bool WebRTCSession::isMicMuted() const { - if (state_ < State::INITIATED) - return false; + if (state_ < State::INITIATED) + return false; - GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); - gboolean muted; - g_object_get(srclevel, "mute", &muted, nullptr); - gst_object_unref(srclevel); - return muted; + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + gst_object_unref(srclevel); + return muted; } bool WebRTCSession::toggleMicMute() { - if (state_ < State::INITIATED) - return false; + if (state_ < State::INITIATED) + return false; - GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); - gboolean muted; - g_object_get(srclevel, "mute", &muted, nullptr); - g_object_set(srclevel, "mute", !muted, nullptr); - gst_object_unref(srclevel); - return !muted; + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + g_object_set(srclevel, "mute", !muted, nullptr); + gst_object_unref(srclevel); + return !muted; } void WebRTCSession::toggleLocalPiP() { - if (localPiPSinkPad_) { - guint zorder; - g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr); - g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr); - } + if (localPiPSinkPad_) { + guint zorder; + g_object_get(localPiPSinkPad_, "zorder", &zorder, nullptr); + g_object_set(localPiPSinkPad_, "zorder", zorder ? 0 : 2, nullptr); + } } void WebRTCSession::clear() { - callType_ = webrtc::CallType::VOICE; - isOffering_ = false; - isRemoteVideoRecvOnly_ = false; - isRemoteVideoSendOnly_ = false; - videoItem_ = nullptr; - pipe_ = nullptr; - webrtc_ = nullptr; - busWatchId_ = 0; - shareWindowId_ = 0; - haveAudioStream_ = false; - haveVideoStream_ = false; - localPiPSinkPad_ = nullptr; - remotePiPSinkPad_ = nullptr; - localsdp_.clear(); - localcandidates_.clear(); + callType_ = webrtc::CallType::VOICE; + isOffering_ = false; + isRemoteVideoRecvOnly_ = false; + isRemoteVideoSendOnly_ = false; + videoItem_ = nullptr; + pipe_ = nullptr; + webrtc_ = nullptr; + busWatchId_ = 0; + shareWindowId_ = 0; + haveAudioStream_ = false; + haveVideoStream_ = false; + localPiPSinkPad_ = nullptr; + remotePiPSinkPad_ = nullptr; + localsdp_.clear(); + localcandidates_.clear(); } void WebRTCSession::end() { - nhlog::ui()->debug("WebRTC: ending session"); - keyFrameRequestData_ = KeyFrameRequestData{}; - if (pipe_) { - gst_element_set_state(pipe_, GST_STATE_NULL); - gst_object_unref(pipe_); - pipe_ = nullptr; - if (busWatchId_) { - g_source_remove(busWatchId_); - busWatchId_ = 0; - } + nhlog::ui()->debug("WebRTC: ending session"); + keyFrameRequestData_ = KeyFrameRequestData{}; + if (pipe_) { + gst_element_set_state(pipe_, GST_STATE_NULL); + gst_object_unref(pipe_); + pipe_ = nullptr; + if (busWatchId_) { + g_source_remove(busWatchId_); + busWatchId_ = 0; } + } - clear(); - if (state_ != State::DISCONNECTED) - emit stateChanged(State::DISCONNECTED); + clear(); + if (state_ != State::DISCONNECTED) + emit stateChanged(State::DISCONNECTED); } #else @@ -1135,13 +1105,13 @@ WebRTCSession::end() bool WebRTCSession::havePlugins(bool, std::string *) { - return false; + return false; } bool WebRTCSession::haveLocalPiP() const { - return false; + return false; } bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; } @@ -1149,13 +1119,13 @@ bool WebRTCSession::createOffer(webrtc::CallType, uint32_t) { return false; } bool WebRTCSession::acceptOffer(const std::string &) { - return false; + return false; } bool WebRTCSession::acceptAnswer(const std::string &) { - return false; + return false; } void @@ -1165,13 +1135,13 @@ WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandi bool WebRTCSession::isMicMuted() const { - return false; + return false; } bool WebRTCSession::toggleMicMute() { - return false; + return false; } void diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index 97487c5c..56c0a295 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -20,22 +20,22 @@ Q_NAMESPACE enum class CallType { - VOICE, - VIDEO, - SCREEN // localUser is sharing screen + VOICE, + VIDEO, + SCREEN // localUser is sharing screen }; Q_ENUM_NS(CallType) enum class State { - DISCONNECTED, - ICEFAILED, - INITIATING, - INITIATED, - OFFERSENT, - ANSWERSENT, - CONNECTING, - CONNECTED + DISCONNECTED, + ICEFAILED, + INITIATING, + INITIATED, + OFFERSENT, + ANSWERSENT, + CONNECTING, + CONNECTED }; Q_ENUM_NS(State) @@ -43,75 +43,75 @@ Q_ENUM_NS(State) class WebRTCSession : public QObject { - Q_OBJECT + Q_OBJECT public: - static WebRTCSession &instance() - { - static WebRTCSession instance; - return instance; - } - - bool havePlugins(bool isVideo, std::string *errorMessage = nullptr); - webrtc::CallType callType() const { return callType_; } - webrtc::State state() const { return state_; } - bool haveLocalPiP() const; - bool isOffering() const { return isOffering_; } - bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; } - bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; } - - bool createOffer(webrtc::CallType, uint32_t shareWindowId); - bool acceptOffer(const std::string &sdp); - bool acceptAnswer(const std::string &sdp); - void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &); - - bool isMicMuted() const; - bool toggleMicMute(); - void toggleLocalPiP(); - void end(); - - void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; } - - void setVideoItem(QQuickItem *item) { videoItem_ = item; } - QQuickItem *getVideoItem() const { return videoItem_; } + static WebRTCSession &instance() + { + static WebRTCSession instance; + return instance; + } + + bool havePlugins(bool isVideo, std::string *errorMessage = nullptr); + webrtc::CallType callType() const { return callType_; } + webrtc::State state() const { return state_; } + bool haveLocalPiP() const; + bool isOffering() const { return isOffering_; } + bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; } + bool isRemoteVideoSendOnly() const { return isRemoteVideoSendOnly_; } + + bool createOffer(webrtc::CallType, uint32_t shareWindowId); + bool acceptOffer(const std::string &sdp); + bool acceptAnswer(const std::string &sdp); + void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &); + + bool isMicMuted() const; + bool toggleMicMute(); + void toggleLocalPiP(); + void end(); + + void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; } + + void setVideoItem(QQuickItem *item) { videoItem_ = item; } + QQuickItem *getVideoItem() const { return videoItem_; } signals: - void offerCreated(const std::string &sdp, - const std::vector<mtx::events::msg::CallCandidates::Candidate> &); - void answerCreated(const std::string &sdp, - const std::vector<mtx::events::msg::CallCandidates::Candidate> &); - void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &); - void stateChanged(webrtc::State); + void offerCreated(const std::string &sdp, + const std::vector<mtx::events::msg::CallCandidates::Candidate> &); + void answerCreated(const std::string &sdp, + const std::vector<mtx::events::msg::CallCandidates::Candidate> &); + void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &); + void stateChanged(webrtc::State); private slots: - void setState(webrtc::State state) { state_ = state; } + void setState(webrtc::State state) { state_ = state; } private: - WebRTCSession(); - - CallDevices &devices_; - bool initialised_ = false; - bool haveVoicePlugins_ = false; - bool haveVideoPlugins_ = false; - webrtc::CallType callType_ = webrtc::CallType::VOICE; - webrtc::State state_ = webrtc::State::DISCONNECTED; - bool isOffering_ = false; - bool isRemoteVideoRecvOnly_ = false; - bool isRemoteVideoSendOnly_ = false; - QQuickItem *videoItem_ = nullptr; - GstElement *pipe_ = nullptr; - GstElement *webrtc_ = nullptr; - unsigned int busWatchId_ = 0; - std::vector<std::string> turnServers_; - uint32_t shareWindowId_ = 0; - - bool init(std::string *errorMessage = nullptr); - bool startPipeline(int opusPayloadType, int vp8PayloadType); - bool createPipeline(int opusPayloadType, int vp8PayloadType); - bool addVideoPipeline(int vp8PayloadType); - void clear(); + WebRTCSession(); + + CallDevices &devices_; + bool initialised_ = false; + bool haveVoicePlugins_ = false; + bool haveVideoPlugins_ = false; + webrtc::CallType callType_ = webrtc::CallType::VOICE; + webrtc::State state_ = webrtc::State::DISCONNECTED; + bool isOffering_ = false; + bool isRemoteVideoRecvOnly_ = false; + bool isRemoteVideoSendOnly_ = false; + QQuickItem *videoItem_ = nullptr; + GstElement *pipe_ = nullptr; + GstElement *webrtc_ = nullptr; + unsigned int busWatchId_ = 0; + std::vector<std::string> turnServers_; + uint32_t shareWindowId_ = 0; + + bool init(std::string *errorMessage = nullptr); + bool startPipeline(int opusPayloadType, int vp8PayloadType); + bool createPipeline(int opusPayloadType, int vp8PayloadType); + bool addVideoPipeline(int vp8PayloadType); + void clear(); public: - WebRTCSession(WebRTCSession const &) = delete; - void operator=(WebRTCSession const &) = delete; + WebRTCSession(WebRTCSession const &) = delete; + void operator=(WebRTCSession const &) = delete; }; diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp index 2cce7b8d..c7168789 100644 --- a/src/WelcomePage.cpp +++ b/src/WelcomePage.cpp @@ -16,72 +16,72 @@ WelcomePage::WelcomePage(QWidget *parent) : QWidget(parent) { - auto topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(20); - topLayout_->setAlignment(Qt::AlignCenter); + auto topLayout_ = new QVBoxLayout(this); + topLayout_->setSpacing(20); + topLayout_->setAlignment(Qt::AlignCenter); - QFont headingFont; - headingFont.setPointSizeF(headingFont.pointSizeF() * 2); - QFont subTitleFont; - subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5); + QFont headingFont; + headingFont.setPointSizeF(headingFont.pointSizeF() * 2); + QFont subTitleFont; + subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5); - QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})}; + QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})}; - auto logo_ = new QLabel(this); - logo_->setPixmap(icon.pixmap(256)); - logo_->setAlignment(Qt::AlignCenter); + auto logo_ = new QLabel(this); + logo_->setPixmap(icon.pixmap(256)); + logo_->setAlignment(Qt::AlignCenter); - QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol.")); - QString main(tr("Enjoy your stay!")); + QString heading(tr("Welcome to nheko! The desktop client for the Matrix protocol.")); + QString main(tr("Enjoy your stay!")); - auto intoTxt_ = new TextLabel(heading, this); - intoTxt_->setFont(headingFont); - intoTxt_->setAlignment(Qt::AlignCenter); + auto intoTxt_ = new TextLabel(heading, this); + intoTxt_->setFont(headingFont); + intoTxt_->setAlignment(Qt::AlignCenter); - auto subTitle = new TextLabel(main, this); - subTitle->setFont(subTitleFont); - subTitle->setAlignment(Qt::AlignCenter); + auto subTitle = new TextLabel(main, this); + subTitle->setFont(subTitleFont); + subTitle->setAlignment(Qt::AlignCenter); - topLayout_->addStretch(1); - topLayout_->addWidget(logo_); - topLayout_->addWidget(intoTxt_); - topLayout_->addWidget(subTitle); + topLayout_->addStretch(1); + topLayout_->addWidget(logo_); + topLayout_->addWidget(intoTxt_); + topLayout_->addWidget(subTitle); - auto btnLayout_ = new QHBoxLayout(); - btnLayout_->setSpacing(20); - btnLayout_->setContentsMargins(0, 20, 0, 20); + auto btnLayout_ = new QHBoxLayout(); + btnLayout_->setSpacing(20); + btnLayout_->setContentsMargins(0, 20, 0, 20); - const int fontHeight = QFontMetrics{subTitleFont}.height(); - const int buttonHeight = fontHeight * 2.5; - const int buttonWidth = fontHeight * 8; + const int fontHeight = QFontMetrics{subTitleFont}.height(); + const int buttonHeight = fontHeight * 2.5; + const int buttonWidth = fontHeight * 8; - auto registerBtn = new RaisedButton(tr("REGISTER"), this); - registerBtn->setMinimumSize(buttonWidth, buttonHeight); - registerBtn->setFontSize(subTitleFont.pointSizeF()); - registerBtn->setCornerRadius(conf::btn::cornerRadius); + auto registerBtn = new RaisedButton(tr("REGISTER"), this); + registerBtn->setMinimumSize(buttonWidth, buttonHeight); + registerBtn->setFontSize(subTitleFont.pointSizeF()); + registerBtn->setCornerRadius(conf::btn::cornerRadius); - auto loginBtn = new RaisedButton(tr("LOGIN"), this); - loginBtn->setMinimumSize(buttonWidth, buttonHeight); - loginBtn->setFontSize(subTitleFont.pointSizeF()); - loginBtn->setCornerRadius(conf::btn::cornerRadius); + auto loginBtn = new RaisedButton(tr("LOGIN"), this); + loginBtn->setMinimumSize(buttonWidth, buttonHeight); + loginBtn->setFontSize(subTitleFont.pointSizeF()); + loginBtn->setCornerRadius(conf::btn::cornerRadius); - btnLayout_->addStretch(1); - btnLayout_->addWidget(registerBtn); - btnLayout_->addWidget(loginBtn); - btnLayout_->addStretch(1); + btnLayout_->addStretch(1); + btnLayout_->addWidget(registerBtn); + btnLayout_->addWidget(loginBtn); + btnLayout_->addStretch(1); - topLayout_->addLayout(btnLayout_); - topLayout_->addStretch(1); + topLayout_->addLayout(btnLayout_); + topLayout_->addStretch(1); - connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister); - connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin); + connect(registerBtn, &QPushButton::clicked, this, &WelcomePage::userRegister); + connect(loginBtn, &QPushButton::clicked, this, &WelcomePage::userLogin); } void WelcomePage::paintEvent(QPaintEvent *) { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/WelcomePage.h b/src/WelcomePage.h index d2dcc0c9..aa531a03 100644 --- a/src/WelcomePage.h +++ b/src/WelcomePage.h @@ -8,18 +8,18 @@ class WelcomePage : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit WelcomePage(QWidget *parent = nullptr); + explicit WelcomePage(QWidget *parent = nullptr); protected: - void paintEvent(QPaintEvent *) override; + void paintEvent(QPaintEvent *) override; signals: - // Notify that the user wants to login in. - void userLogin(); + // Notify that the user wants to login in. + void userLogin(); - // Notify that the user wants to register. - void userRegister(); + // Notify that the user wants to register. + void userRegister(); }; diff --git a/src/dialogs/CreateRoom.cpp b/src/dialogs/CreateRoom.cpp index ba385436..30dbf83d 100644 --- a/src/dialogs/CreateRoom.cpp +++ b/src/dialogs/CreateRoom.cpp @@ -18,142 +18,142 @@ using namespace dialogs; CreateRoom::CreateRoom(QWidget *parent) : QFrame(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(conf::modals::MIN_WIDGET_HEIGHT); - setMinimumWidth(conf::window::minModalWidth); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - - confirmBtn_ = new QPushButton(tr("Create room"), this); - confirmBtn_->setDefault(true); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - - buttonLayout->addStretch(1); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); - - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.3); - - nameInput_ = new TextField(this); - nameInput_->setLabel(tr("Name")); - - topicInput_ = new TextField(this); - topicInput_->setLabel(tr("Topic")); - - aliasInput_ = new TextField(this); - aliasInput_->setLabel(tr("Alias")); - - auto visibilityLayout = new QHBoxLayout; - visibilityLayout->setContentsMargins(0, 10, 0, 10); - - auto presetLayout = new QHBoxLayout; - presetLayout->setContentsMargins(0, 10, 0, 10); - - auto visibilityLabel = new QLabel(tr("Room Visibility"), this); - visibilityCombo_ = new QComboBox(this); - visibilityCombo_->addItem("Private"); - visibilityCombo_->addItem("Public"); - - visibilityLayout->addWidget(visibilityLabel); - visibilityLayout->addWidget(visibilityCombo_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto presetLabel = new QLabel(tr("Room Preset"), this); - presetCombo_ = new QComboBox(this); - presetCombo_->addItem("Private Chat"); - presetCombo_->addItem("Public Chat"); - presetCombo_->addItem("Trusted Private Chat"); - - presetLayout->addWidget(presetLabel); - presetLayout->addWidget(presetCombo_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto directLabel_ = new QLabel(tr("Direct Chat"), this); - directToggle_ = new Toggle(this); - directToggle_->setActiveColor(QColor("#38A3D8")); - directToggle_->setInactiveColor(QColor("gray")); - directToggle_->setState(false); - - auto directLayout = new QHBoxLayout; - directLayout->setContentsMargins(0, 10, 0, 10); - directLayout->addWidget(directLabel_); - directLayout->addWidget(directToggle_, 0, Qt::AlignBottom | Qt::AlignRight); - - layout->addWidget(nameInput_); - layout->addWidget(topicInput_); - layout->addWidget(aliasInput_); - layout->addLayout(visibilityLayout); - layout->addLayout(presetLayout); - layout->addLayout(directLayout); - layout->addLayout(buttonLayout); - - connect(confirmBtn_, &QPushButton::clicked, this, [this]() { - request_.name = nameInput_->text().toStdString(); - request_.topic = topicInput_->text().toStdString(); - request_.room_alias_name = aliasInput_->text().toStdString(); - - emit createRoom(request_); - - clearFields(); - emit close(); - }); - - connect(cancelBtn_, &QPushButton::clicked, this, [this]() { - clearFields(); - emit close(); - }); - - connect(visibilityCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &text) { - if (text == "Private") { - request_.visibility = mtx::common::RoomVisibility::Private; - } else { - request_.visibility = mtx::common::RoomVisibility::Public; - } - }); - - connect(presetCombo_, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), - [this](const QString &text) { - if (text == "Private Chat") { - request_.preset = mtx::requests::Preset::PrivateChat; - } else if (text == "Public Chat") { - request_.preset = mtx::requests::Preset::PublicChat; - } else { - request_.preset = mtx::requests::Preset::TrustedPrivateChat; - } - }); - - connect(directToggle_, &Toggle::toggled, this, [this](bool isEnabled) { - request_.is_direct = isEnabled; - }); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + QFont largeFont; + largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); + + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + setMinimumHeight(conf::modals::MIN_WIDGET_HEIGHT); + setMinimumWidth(conf::window::minModalWidth); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(15); + + confirmBtn_ = new QPushButton(tr("Create room"), this); + confirmBtn_->setDefault(true); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); + + QFont font; + font.setPointSizeF(font.pointSizeF() * 1.3); + + nameInput_ = new TextField(this); + nameInput_->setLabel(tr("Name")); + + topicInput_ = new TextField(this); + topicInput_->setLabel(tr("Topic")); + + aliasInput_ = new TextField(this); + aliasInput_->setLabel(tr("Alias")); + + auto visibilityLayout = new QHBoxLayout; + visibilityLayout->setContentsMargins(0, 10, 0, 10); + + auto presetLayout = new QHBoxLayout; + presetLayout->setContentsMargins(0, 10, 0, 10); + + auto visibilityLabel = new QLabel(tr("Room Visibility"), this); + visibilityCombo_ = new QComboBox(this); + visibilityCombo_->addItem("Private"); + visibilityCombo_->addItem("Public"); + + visibilityLayout->addWidget(visibilityLabel); + visibilityLayout->addWidget(visibilityCombo_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto presetLabel = new QLabel(tr("Room Preset"), this); + presetCombo_ = new QComboBox(this); + presetCombo_->addItem("Private Chat"); + presetCombo_->addItem("Public Chat"); + presetCombo_->addItem("Trusted Private Chat"); + + presetLayout->addWidget(presetLabel); + presetLayout->addWidget(presetCombo_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto directLabel_ = new QLabel(tr("Direct Chat"), this); + directToggle_ = new Toggle(this); + directToggle_->setActiveColor(QColor("#38A3D8")); + directToggle_->setInactiveColor(QColor("gray")); + directToggle_->setState(false); + + auto directLayout = new QHBoxLayout; + directLayout->setContentsMargins(0, 10, 0, 10); + directLayout->addWidget(directLabel_); + directLayout->addWidget(directToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + layout->addWidget(nameInput_); + layout->addWidget(topicInput_); + layout->addWidget(aliasInput_); + layout->addLayout(visibilityLayout); + layout->addLayout(presetLayout); + layout->addLayout(directLayout); + layout->addLayout(buttonLayout); + + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + request_.name = nameInput_->text().toStdString(); + request_.topic = topicInput_->text().toStdString(); + request_.room_alias_name = aliasInput_->text().toStdString(); + + emit createRoom(request_); + + clearFields(); + emit close(); + }); + + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + clearFields(); + emit close(); + }); + + connect(visibilityCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &text) { + if (text == "Private") { + request_.visibility = mtx::common::RoomVisibility::Private; + } else { + request_.visibility = mtx::common::RoomVisibility::Public; + } + }); + + connect(presetCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), + [this](const QString &text) { + if (text == "Private Chat") { + request_.preset = mtx::requests::Preset::PrivateChat; + } else if (text == "Public Chat") { + request_.preset = mtx::requests::Preset::PublicChat; + } else { + request_.preset = mtx::requests::Preset::TrustedPrivateChat; + } + }); + + connect(directToggle_, &Toggle::toggled, this, [this](bool isEnabled) { + request_.is_direct = isEnabled; + }); } void CreateRoom::clearFields() { - nameInput_->clear(); - topicInput_->clear(); - aliasInput_->clear(); + nameInput_->clear(); + topicInput_->clear(); + aliasInput_->clear(); } void CreateRoom::showEvent(QShowEvent *event) { - nameInput_->setFocus(); + nameInput_->setFocus(); - QFrame::showEvent(event); + QFrame::showEvent(event); } diff --git a/src/dialogs/CreateRoom.h b/src/dialogs/CreateRoom.h index d4c6474d..d9d90a10 100644 --- a/src/dialogs/CreateRoom.h +++ b/src/dialogs/CreateRoom.h @@ -17,32 +17,32 @@ namespace dialogs { class CreateRoom : public QFrame { - Q_OBJECT + Q_OBJECT public: - CreateRoom(QWidget *parent = nullptr); + CreateRoom(QWidget *parent = nullptr); signals: - void createRoom(const mtx::requests::CreateRoom &request); + void createRoom(const mtx::requests::CreateRoom &request); protected: - void showEvent(QShowEvent *event) override; + void showEvent(QShowEvent *event) override; private: - void clearFields(); + void clearFields(); - QComboBox *visibilityCombo_; - QComboBox *presetCombo_; + QComboBox *visibilityCombo_; + QComboBox *presetCombo_; - Toggle *directToggle_; + Toggle *directToggle_; - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; - TextField *nameInput_; - TextField *topicInput_; - TextField *aliasInput_; + TextField *nameInput_; + TextField *topicInput_; + TextField *aliasInput_; - mtx::requests::CreateRoom request_; + mtx::requests::CreateRoom request_; }; } // dialogs diff --git a/src/dialogs/FallbackAuth.cpp b/src/dialogs/FallbackAuth.cpp index c7b179f4..2b8dfed9 100644 --- a/src/dialogs/FallbackAuth.cpp +++ b/src/dialogs/FallbackAuth.cpp @@ -18,56 +18,56 @@ using namespace dialogs; FallbackAuth::FallbackAuth(const QString &authType, const QString &session, QWidget *parent) : QWidget(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(8); - buttonLayout->setMargin(0); + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(8); + buttonLayout->setMargin(0); - openBtn_ = new QPushButton(tr("Open Fallback in Browser"), this); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - confirmBtn_ = new QPushButton(tr("Confirm"), this); - confirmBtn_->setDefault(true); + openBtn_ = new QPushButton(tr("Open Fallback in Browser"), this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + confirmBtn_ = new QPushButton(tr("Confirm"), this); + confirmBtn_->setDefault(true); - buttonLayout->addStretch(1); - buttonLayout->addWidget(openBtn_); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); + buttonLayout->addStretch(1); + buttonLayout->addWidget(openBtn_); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); + QFont font; + font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - auto label = new QLabel( - tr("Open the fallback, follow the steps and confirm after completing them."), this); - label->setFont(font); + auto label = new QLabel( + tr("Open the fallback, follow the steps and confirm after completing them."), this); + label->setFont(font); - layout->addWidget(label); - layout->addLayout(buttonLayout); + layout->addWidget(label); + layout->addLayout(buttonLayout); - connect(openBtn_, &QPushButton::clicked, [session, authType]() { - const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/" - "fallback/web?session=%3") - .arg(QString::fromStdString(http::client()->server())) - .arg(http::client()->port()) - .arg(session) - .arg(authType); + connect(openBtn_, &QPushButton::clicked, [session, authType]() { + const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/" + "fallback/web?session=%3") + .arg(QString::fromStdString(http::client()->server())) + .arg(http::client()->port()) + .arg(session) + .arg(authType); - QDesktopServices::openUrl(url); - }); + QDesktopServices::openUrl(url); + }); - connect(confirmBtn_, &QPushButton::clicked, this, [this]() { - emit confirmation(); - emit close(); - }); - connect(cancelBtn_, &QPushButton::clicked, this, [this]() { - emit cancel(); - emit close(); - }); + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit confirmation(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); } diff --git a/src/dialogs/FallbackAuth.h b/src/dialogs/FallbackAuth.h index 8e4e28ea..6bfd59f7 100644 --- a/src/dialogs/FallbackAuth.h +++ b/src/dialogs/FallbackAuth.h @@ -13,18 +13,18 @@ namespace dialogs { class FallbackAuth : public QWidget { - Q_OBJECT + Q_OBJECT public: - FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr); + FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr); signals: - void confirmation(); - void cancel(); + void confirmation(); + void cancel(); private: - QPushButton *openBtn_; - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *openBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; }; } // dialogs diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index 12813d57..8c90a744 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -19,84 +19,83 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) : QWidget{parent} , originalImage_{image} { - setMouseTracking(true); - setParent(nullptr); + setMouseTracking(true); + setParent(nullptr); - setWindowFlags(windowFlags() | Qt::FramelessWindowHint); + setWindowFlags(windowFlags() | Qt::FramelessWindowHint); - setAttribute(Qt::WA_NoSystemBackground, true); - setAttribute(Qt::WA_TranslucentBackground, true); - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowState(Qt::WindowFullScreen); - close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_TranslucentBackground, true); + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowState(Qt::WindowFullScreen); + close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this); - connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing); - connect(this, &ImageOverlay::closing, this, &ImageOverlay::close); + connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing); + connect(this, &ImageOverlay::closing, this, &ImageOverlay::close); - raise(); + raise(); } void ImageOverlay::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - // Full screen overlay. - painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170)); + // Full screen overlay. + painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170)); - // Left and Right margins - int outer_margin = width() * 0.12; - int buttonSize = 36; - int margin = outer_margin * 0.1; + // Left and Right margins + int outer_margin = width() * 0.12; + int buttonSize = 36; + int margin = outer_margin * 0.1; - int max_width = width() - 2 * outer_margin; - int max_height = height(); + int max_width = width() - 2 * outer_margin; + int max_height = height(); - image_ = utils::scaleDown(max_width, max_height, originalImage_); + image_ = utils::scaleDown(max_width, max_height, originalImage_); - int diff_x = max_width - image_.width(); - int diff_y = max_height - image_.height(); + int diff_x = max_width - image_.width(); + int diff_y = max_height - image_.height(); - content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); - close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize); - save_button_ = - QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); + content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); + close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize); + save_button_ = QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); - // Draw main content_. - painter.drawPixmap(content_, image_); + // Draw main content_. + painter.drawPixmap(content_, image_); - // Draw top right corner X. - QPen pen; - pen.setCapStyle(Qt::RoundCap); - pen.setWidthF(5); - pen.setColor("gray"); + // Draw top right corner X. + QPen pen; + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(5); + pen.setColor("gray"); - auto center = close_button_.center(); + auto center = close_button_.center(); - painter.setPen(pen); - painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15)); - painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15)); + painter.setPen(pen); + painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15)); + painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15)); - // Draw download button - center = save_button_.center(); - painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15)); - painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15)); - painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0)); + // Draw download button + center = save_button_.center(); + painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15)); + painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15)); + painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0)); } void ImageOverlay::mousePressEvent(QMouseEvent *event) { - if (event->button() != Qt::LeftButton) - return; - - if (close_button_.contains(event->pos())) - emit closing(); - else if (save_button_.contains(event->pos())) - emit saving(); - else if (!content_.contains(event->pos())) - emit closing(); + if (event->button() != Qt::LeftButton) + return; + + if (close_button_.contains(event->pos())) + emit closing(); + else if (save_button_.contains(event->pos())) + emit saving(); + else if (!content_.contains(event->pos())) + emit closing(); } diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h index 9d4187bf..2174279f 100644 --- a/src/dialogs/ImageOverlay.h +++ b/src/dialogs/ImageOverlay.h @@ -14,25 +14,25 @@ namespace dialogs { class ImageOverlay : public QWidget { - Q_OBJECT + Q_OBJECT public: - ImageOverlay(QPixmap image, QWidget *parent = nullptr); + ImageOverlay(QPixmap image, QWidget *parent = nullptr); protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; signals: - void closing(); - void saving(); + void closing(); + void saving(); private: - QPixmap originalImage_; - QPixmap image_; + QPixmap originalImage_; + QPixmap image_; - QRect content_; - QRect close_button_; - QRect save_button_; - QShortcut *close_shortcut_; + QRect content_; + QRect close_button_; + QRect save_button_; + QShortcut *close_shortcut_; }; } // dialogs diff --git a/src/dialogs/JoinRoom.cpp b/src/dialogs/JoinRoom.cpp index dc2e4804..76baf857 100644 --- a/src/dialogs/JoinRoom.cpp +++ b/src/dialogs/JoinRoom.cpp @@ -16,58 +16,58 @@ using namespace dialogs; JoinRoom::JoinRoom(QWidget *parent) : QFrame(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); - setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(15); - confirmBtn_ = new QPushButton(tr("Join"), this); - confirmBtn_->setDefault(true); - cancelBtn_ = new QPushButton(tr("Cancel"), this); + confirmBtn_ = new QPushButton(tr("Join"), this); + confirmBtn_->setDefault(true); + cancelBtn_ = new QPushButton(tr("Cancel"), this); - buttonLayout->addStretch(1); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); + buttonLayout->addStretch(1); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); - roomInput_ = new TextField(this); - roomInput_->setLabel(tr("Room ID or alias")); + roomInput_ = new TextField(this); + roomInput_->setLabel(tr("Room ID or alias")); - layout->addWidget(roomInput_); - layout->addLayout(buttonLayout); - layout->addStretch(1); + layout->addWidget(roomInput_); + layout->addLayout(buttonLayout); + layout->addStretch(1); - connect(roomInput_, &QLineEdit::returnPressed, this, &JoinRoom::handleInput); - connect(confirmBtn_, &QPushButton::clicked, this, &JoinRoom::handleInput); - connect(cancelBtn_, &QPushButton::clicked, this, &JoinRoom::close); + connect(roomInput_, &QLineEdit::returnPressed, this, &JoinRoom::handleInput); + connect(confirmBtn_, &QPushButton::clicked, this, &JoinRoom::handleInput); + connect(cancelBtn_, &QPushButton::clicked, this, &JoinRoom::close); } void JoinRoom::handleInput() { - if (roomInput_->text().isEmpty()) - return; + if (roomInput_->text().isEmpty()) + return; - // TODO: input validation with error messages. - emit joinRoom(roomInput_->text()); - roomInput_->clear(); + // TODO: input validation with error messages. + emit joinRoom(roomInput_->text()); + roomInput_->clear(); - emit close(); + emit close(); } void JoinRoom::showEvent(QShowEvent *event) { - roomInput_->setFocus(); + roomInput_->setFocus(); - QFrame::showEvent(event); + QFrame::showEvent(event); } diff --git a/src/dialogs/JoinRoom.h b/src/dialogs/JoinRoom.h index f399f1fb..11c54d7c 100644 --- a/src/dialogs/JoinRoom.h +++ b/src/dialogs/JoinRoom.h @@ -13,24 +13,24 @@ namespace dialogs { class JoinRoom : public QFrame { - Q_OBJECT + Q_OBJECT public: - JoinRoom(QWidget *parent = nullptr); + JoinRoom(QWidget *parent = nullptr); signals: - void joinRoom(const QString &room); + void joinRoom(const QString &room); protected: - void showEvent(QShowEvent *event) override; + void showEvent(QShowEvent *event) override; private slots: - void handleInput(); + void handleInput(); private: - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; - TextField *roomInput_; + TextField *roomInput_; }; } // dialogs diff --git a/src/dialogs/LeaveRoom.cpp b/src/dialogs/LeaveRoom.cpp index 5246d693..9eb431da 100644 --- a/src/dialogs/LeaveRoom.cpp +++ b/src/dialogs/LeaveRoom.cpp @@ -15,39 +15,39 @@ using namespace dialogs; LeaveRoom::LeaveRoom(QWidget *parent) : QFrame(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); - setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(0); - buttonLayout->setMargin(0); + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(0); + buttonLayout->setMargin(0); - confirmBtn_ = new QPushButton("Leave", this); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - cancelBtn_->setDefault(true); + confirmBtn_ = new QPushButton("Leave", this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + cancelBtn_->setDefault(true); - buttonLayout->addStretch(1); - buttonLayout->setSpacing(15); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); + buttonLayout->addStretch(1); + buttonLayout->setSpacing(15); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); - auto label = new QLabel(tr("Are you sure you want to leave?"), this); + auto label = new QLabel(tr("Are you sure you want to leave?"), this); - layout->addWidget(label); - layout->addLayout(buttonLayout); + layout->addWidget(label); + layout->addLayout(buttonLayout); - connect(confirmBtn_, &QPushButton::clicked, this, [this]() { - emit leaving(); - emit close(); - }); - connect(cancelBtn_, &QPushButton::clicked, this, &LeaveRoom::close); + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit leaving(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, &LeaveRoom::close); } diff --git a/src/dialogs/LeaveRoom.h b/src/dialogs/LeaveRoom.h index e9465579..edf88282 100644 --- a/src/dialogs/LeaveRoom.h +++ b/src/dialogs/LeaveRoom.h @@ -12,15 +12,15 @@ namespace dialogs { class LeaveRoom : public QFrame { - Q_OBJECT + Q_OBJECT public: - explicit LeaveRoom(QWidget *parent = nullptr); + explicit LeaveRoom(QWidget *parent = nullptr); signals: - void leaving(); + void leaving(); private: - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; }; } // dialogs diff --git a/src/dialogs/Logout.cpp b/src/dialogs/Logout.cpp index fdfc3338..d10e4cdf 100644 --- a/src/dialogs/Logout.cpp +++ b/src/dialogs/Logout.cpp @@ -15,40 +15,40 @@ using namespace dialogs; Logout::Logout(QWidget *parent) : QFrame(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(0); - buttonLayout->setMargin(0); - - confirmBtn_ = new QPushButton("Logout", this); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - cancelBtn_->setDefault(true); - - buttonLayout->addStretch(1); - buttonLayout->setSpacing(15); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); - - auto label = new QLabel(tr("Logout. Are you sure?"), this); - - layout->addWidget(label); - layout->addLayout(buttonLayout); - layout->addStretch(1); - - connect(confirmBtn_, &QPushButton::clicked, this, [this]() { - emit loggingOut(); - emit close(); - }); - connect(cancelBtn_, &QPushButton::clicked, this, &Logout::close); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(0); + buttonLayout->setMargin(0); + + confirmBtn_ = new QPushButton("Logout", this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + cancelBtn_->setDefault(true); + + buttonLayout->addStretch(1); + buttonLayout->setSpacing(15); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); + + auto label = new QLabel(tr("Logout. Are you sure?"), this); + + layout->addWidget(label); + layout->addLayout(buttonLayout); + layout->addStretch(1); + + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit loggingOut(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, &Logout::close); } diff --git a/src/dialogs/Logout.h b/src/dialogs/Logout.h index 9d8d0f4b..7783c68f 100644 --- a/src/dialogs/Logout.h +++ b/src/dialogs/Logout.h @@ -13,15 +13,15 @@ namespace dialogs { class Logout : public QFrame { - Q_OBJECT + Q_OBJECT public: - explicit Logout(QWidget *parent = nullptr); + explicit Logout(QWidget *parent = nullptr); signals: - void loggingOut(); + void loggingOut(); private: - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; }; } // dialogs diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp index 66fa1b37..e850c03b 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp @@ -29,188 +29,185 @@ PreviewUploadOverlay::PreviewUploadOverlay(QWidget *parent) , upload_{tr("Upload"), this} , cancel_{tr("Cancel"), this} { - auto hlayout = new QHBoxLayout; - hlayout->addStretch(1); - hlayout->addWidget(&cancel_); - hlayout->addWidget(&upload_); - hlayout->setMargin(0); - - auto vlayout = new QVBoxLayout{this}; - vlayout->addWidget(&titleLabel_); - vlayout->addWidget(&infoLabel_); - vlayout->addWidget(&fileName_); - vlayout->addLayout(hlayout); - vlayout->setSpacing(conf::modals::WIDGET_SPACING); - vlayout->setMargin(conf::modals::WIDGET_MARGIN); - - upload_.setDefault(true); - connect(&upload_, &QPushButton::clicked, [this]() { - emit confirmUpload(data_, mediaType_, fileName_.text()); - close(); - }); - - connect(&fileName_, &QLineEdit::returnPressed, this, [this]() { - emit confirmUpload(data_, mediaType_, fileName_.text()); - close(); - }); - - connect(&cancel_, &QPushButton::clicked, this, [this]() { - emit aborted(); - close(); - }); + auto hlayout = new QHBoxLayout; + hlayout->addStretch(1); + hlayout->addWidget(&cancel_); + hlayout->addWidget(&upload_); + hlayout->setMargin(0); + + auto vlayout = new QVBoxLayout{this}; + vlayout->addWidget(&titleLabel_); + vlayout->addWidget(&infoLabel_); + vlayout->addWidget(&fileName_); + vlayout->addLayout(hlayout); + vlayout->setSpacing(conf::modals::WIDGET_SPACING); + vlayout->setMargin(conf::modals::WIDGET_MARGIN); + + upload_.setDefault(true); + connect(&upload_, &QPushButton::clicked, [this]() { + emit confirmUpload(data_, mediaType_, fileName_.text()); + close(); + }); + + connect(&fileName_, &QLineEdit::returnPressed, this, [this]() { + emit confirmUpload(data_, mediaType_, fileName_.text()); + close(); + }); + + connect(&cancel_, &QPushButton::clicked, this, [this]() { + emit aborted(); + close(); + }); } void PreviewUploadOverlay::init() { - QSize winsize; - QPoint center; - - auto window = MainWindow::instance(); - if (window) { - winsize = window->frameGeometry().size(); - center = window->frameGeometry().center(); - } else { - nhlog::ui()->warn("unable to retrieve MainWindow's size"); - } - - fileName_.setText(QFileInfo{filePath_}.fileName()); - - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - titleLabel_.setFont(font); - titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - titleLabel_.setAlignment(Qt::AlignCenter); - infoLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - fileName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - fileName_.setAlignment(Qt::AlignCenter); - upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - if (isImage_) { - infoLabel_.setAlignment(Qt::AlignCenter); - - const auto maxWidth = winsize.width() * 0.8; - const auto maxHeight = winsize.height() * 0.8; - - // Scale image preview to fit into the application window. - infoLabel_.setPixmap(utils::scaleDown(maxWidth, maxHeight, image_)); - move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); - } else { - infoLabel_.setAlignment(Qt::AlignLeft); - } - infoLabel_.setScaledContents(false); - - show(); + QSize winsize; + QPoint center; + + auto window = MainWindow::instance(); + if (window) { + winsize = window->frameGeometry().size(); + center = window->frameGeometry().center(); + } else { + nhlog::ui()->warn("unable to retrieve MainWindow's size"); + } + + fileName_.setText(QFileInfo{filePath_}.fileName()); + + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + QFont font; + font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); + + titleLabel_.setFont(font); + titleLabel_.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + titleLabel_.setAlignment(Qt::AlignCenter); + infoLabel_.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + fileName_.setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + fileName_.setAlignment(Qt::AlignCenter); + upload_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + cancel_.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + if (isImage_) { + infoLabel_.setAlignment(Qt::AlignCenter); + + const auto maxWidth = winsize.width() * 0.8; + const auto maxHeight = winsize.height() * 0.8; + + // Scale image preview to fit into the application window. + infoLabel_.setPixmap(utils::scaleDown(maxWidth, maxHeight, image_)); + move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); + } else { + infoLabel_.setAlignment(Qt::AlignLeft); + } + infoLabel_.setScaledContents(false); + + show(); } void PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64_t upload_size) { - if (mediaType_.split('/')[0] == "image") { - if (!image_.loadFromData(data_)) { - titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); - } else { - titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_)); - } - isImage_ = true; + if (mediaType_.split('/')[0] == "image") { + if (!image_.loadFromData(data_)) { + titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); } else { - auto const info = QString{tr("Media type: %1\n" - "Media size: %2\n")} - .arg(mime) - .arg(utils::humanReadableFileSize(upload_size)); - - titleLabel_.setText(QString{tr(DEFAULT)}.arg("file")); - infoLabel_.setText(info); + titleLabel_.setText(QString{tr(DEFAULT)}.arg(mediaType_)); } + isImage_ = true; + } else { + auto const info = QString{tr("Media type: %1\n" + "Media size: %2\n")} + .arg(mime) + .arg(utils::humanReadableFileSize(upload_size)); + + titleLabel_.setText(QString{tr(DEFAULT)}.arg("file")); + infoLabel_.setText(info); + } } void PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime) { - nhlog::ui()->info("Pasting image with size: {}x{}, format: {}", - src.height(), - src.width(), - mime.toStdString()); - - auto const &split = mime.split('/'); - auto const &type = split[1]; - - QBuffer buffer(&data_); - buffer.open(QIODevice::WriteOnly); - if (src.save(&buffer, type.toStdString().c_str())) - titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); - else - titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); - - mediaType_ = mime; - filePath_ = "clipboard." + type; - image_.convertFromImage(src); - isImage_ = true; + nhlog::ui()->info( + "Pasting image with size: {}x{}, format: {}", src.height(), src.width(), mime.toStdString()); + + auto const &split = mime.split('/'); + auto const &type = split[1]; + QBuffer buffer(&data_); + buffer.open(QIODevice::WriteOnly); + if (src.save(&buffer, type.toStdString().c_str())) titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); - init(); + else + titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); + + mediaType_ = mime; + filePath_ = "clipboard." + type; + image_.convertFromImage(src); + isImage_ = true; + + titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); + init(); } void PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime) { - auto const &split = mime.split('/'); - auto const &type = split[1]; + auto const &split = mime.split('/'); + auto const &type = split[1]; - data_ = data; - mediaType_ = mime; - filePath_ = "clipboard." + type; - isImage_ = false; + data_ = data; + mediaType_ = mime; + filePath_ = "clipboard." + type; + isImage_ = false; - setLabels(type, mime, data_.size()); - init(); + setLabels(type, mime, data_.size()); + init(); } void PreviewUploadOverlay::setPreview(const QString &path) { - QFile file{path}; - - if (!file.open(QIODevice::ReadOnly)) { - nhlog::ui()->warn("Failed to open file ({}): {}", - path.toStdString(), - file.errorString().toStdString()); - close(); - return; - } + QFile file{path}; - QMimeDatabase db; - auto mime = db.mimeTypeForFileNameAndData(path, &file); + if (!file.open(QIODevice::ReadOnly)) { + nhlog::ui()->warn( + "Failed to open file ({}): {}", path.toStdString(), file.errorString().toStdString()); + close(); + return; + } - if ((data_ = file.readAll()).isEmpty()) { - nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString()); - close(); - return; - } + QMimeDatabase db; + auto mime = db.mimeTypeForFileNameAndData(path, &file); - auto const &split = mime.name().split('/'); + if ((data_ = file.readAll()).isEmpty()) { + nhlog::ui()->warn("Failed to read media: {}", file.errorString().toStdString()); + close(); + return; + } - mediaType_ = mime.name(); - filePath_ = file.fileName(); - isImage_ = false; + auto const &split = mime.name().split('/'); - setLabels(split[1], mime.name(), data_.size()); - init(); + mediaType_ = mime.name(); + filePath_ = file.fileName(); + isImage_ = false; + + setLabels(split[1], mime.name(), data_.size()); + init(); } void PreviewUploadOverlay::keyPressEvent(QKeyEvent *event) { - if (event->matches(QKeySequence::Cancel)) { - emit aborted(); - close(); - } else { - QWidget::keyPressEvent(event); - } + if (event->matches(QKeySequence::Cancel)) { + emit aborted(); + close(); + } else { + QWidget::keyPressEvent(event); + } } \ No newline at end of file diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h index d23ea0ae..e9078069 100644 --- a/src/dialogs/PreviewUploadOverlay.h +++ b/src/dialogs/PreviewUploadOverlay.h @@ -18,35 +18,35 @@ namespace dialogs { class PreviewUploadOverlay : public QWidget { - Q_OBJECT + Q_OBJECT public: - PreviewUploadOverlay(QWidget *parent = nullptr); + PreviewUploadOverlay(QWidget *parent = nullptr); - void setPreview(const QImage &src, const QString &mime); - void setPreview(const QByteArray data, const QString &mime); - void setPreview(const QString &path); - void keyPressEvent(QKeyEvent *event); + void setPreview(const QImage &src, const QString &mime); + void setPreview(const QByteArray data, const QString &mime); + void setPreview(const QString &path); + void keyPressEvent(QKeyEvent *event); signals: - void confirmUpload(const QByteArray data, const QString &media, const QString &filename); - void aborted(); + void confirmUpload(const QByteArray data, const QString &media, const QString &filename); + void aborted(); private: - void init(); - void setLabels(const QString &type, const QString &mime, uint64_t upload_size); + void init(); + void setLabels(const QString &type, const QString &mime, uint64_t upload_size); - bool isImage_; - QPixmap image_; + bool isImage_; + QPixmap image_; - QByteArray data_; - QString filePath_; - QString mediaType_; + QByteArray data_; + QString filePath_; + QString mediaType_; - QLabel titleLabel_; - QLabel infoLabel_; - QLineEdit fileName_; + QLabel titleLabel_; + QLabel infoLabel_; + QLineEdit fileName_; - QPushButton upload_; - QPushButton cancel_; + QPushButton upload_; + QPushButton cancel_; }; } // dialogs diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp index c7b95f1a..0ae46bba 100644 --- a/src/dialogs/ReCaptcha.cpp +++ b/src/dialogs/ReCaptcha.cpp @@ -18,54 +18,54 @@ using namespace dialogs; ReCaptcha::ReCaptcha(const QString &session, QWidget *parent) : QWidget(parent) { - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(8); - buttonLayout->setMargin(0); + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(8); + buttonLayout->setMargin(0); - openCaptchaBtn_ = new QPushButton("Open reCAPTCHA", this); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - confirmBtn_ = new QPushButton(tr("Confirm"), this); - confirmBtn_->setDefault(true); + openCaptchaBtn_ = new QPushButton("Open reCAPTCHA", this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + confirmBtn_ = new QPushButton(tr("Confirm"), this); + confirmBtn_->setDefault(true); - buttonLayout->addStretch(1); - buttonLayout->addWidget(openCaptchaBtn_); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); + buttonLayout->addStretch(1); + buttonLayout->addWidget(openCaptchaBtn_); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); + QFont font; + font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - auto label = new QLabel(tr("Solve the reCAPTCHA and press the confirm button"), this); - label->setFont(font); + auto label = new QLabel(tr("Solve the reCAPTCHA and press the confirm button"), this); + label->setFont(font); - layout->addWidget(label); - layout->addLayout(buttonLayout); + layout->addWidget(label); + layout->addLayout(buttonLayout); - connect(openCaptchaBtn_, &QPushButton::clicked, [session]() { - const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/" - "fallback/web?session=%3") - .arg(QString::fromStdString(http::client()->server())) - .arg(http::client()->port()) - .arg(session); + connect(openCaptchaBtn_, &QPushButton::clicked, [session]() { + const auto url = QString("https://%1:%2/_matrix/client/r0/auth/m.login.recaptcha/" + "fallback/web?session=%3") + .arg(QString::fromStdString(http::client()->server())) + .arg(http::client()->port()) + .arg(session); - QDesktopServices::openUrl(url); - }); + QDesktopServices::openUrl(url); + }); - connect(confirmBtn_, &QPushButton::clicked, this, [this]() { - emit confirmation(); - emit close(); - }); - connect(cancelBtn_, &QPushButton::clicked, this, [this]() { - emit cancel(); - emit close(); - }); + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit confirmation(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); } diff --git a/src/dialogs/ReCaptcha.h b/src/dialogs/ReCaptcha.h index 0c9f7539..1e69de66 100644 --- a/src/dialogs/ReCaptcha.h +++ b/src/dialogs/ReCaptcha.h @@ -12,18 +12,18 @@ namespace dialogs { class ReCaptcha : public QWidget { - Q_OBJECT + Q_OBJECT public: - ReCaptcha(const QString &session, QWidget *parent = nullptr); + ReCaptcha(const QString &session, QWidget *parent = nullptr); signals: - void confirmation(); - void cancel(); + void confirmation(); + void cancel(); private: - QPushButton *openCaptchaBtn_; - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; + QPushButton *openCaptchaBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; }; } // dialogs diff --git a/src/emoji/EmojiModel.cpp b/src/emoji/EmojiModel.cpp index 66e7aeda..07e6fdbd 100644 --- a/src/emoji/EmojiModel.cpp +++ b/src/emoji/EmojiModel.cpp @@ -14,63 +14,60 @@ using namespace emoji; int EmojiModel::categoryToIndex(int category) { - auto dist = std::distance(Provider::emoji.begin(), - std::lower_bound(Provider::emoji.begin(), - Provider::emoji.end(), - static_cast<Emoji::Category>(category), - [](const struct Emoji &e, Emoji::Category c) { - return e.category < c; - })); + auto dist = std::distance( + Provider::emoji.begin(), + std::lower_bound(Provider::emoji.begin(), + Provider::emoji.end(), + static_cast<Emoji::Category>(category), + [](const struct Emoji &e, Emoji::Category c) { return e.category < c; })); - return static_cast<int>(dist); + return static_cast<int>(dist); } QHash<int, QByteArray> EmojiModel::roleNames() const { - static QHash<int, QByteArray> roles; + static QHash<int, QByteArray> roles; - if (roles.isEmpty()) { - roles = QAbstractListModel::roleNames(); - roles[static_cast<int>(EmojiModel::Roles::Unicode)] = QByteArrayLiteral("unicode"); - roles[static_cast<int>(EmojiModel::Roles::ShortName)] = - QByteArrayLiteral("shortName"); - roles[static_cast<int>(EmojiModel::Roles::Category)] = - QByteArrayLiteral("category"); - roles[static_cast<int>(EmojiModel::Roles::Emoji)] = QByteArrayLiteral("emoji"); - } + if (roles.isEmpty()) { + roles = QAbstractListModel::roleNames(); + roles[static_cast<int>(EmojiModel::Roles::Unicode)] = QByteArrayLiteral("unicode"); + roles[static_cast<int>(EmojiModel::Roles::ShortName)] = QByteArrayLiteral("shortName"); + roles[static_cast<int>(EmojiModel::Roles::Category)] = QByteArrayLiteral("category"); + roles[static_cast<int>(EmojiModel::Roles::Emoji)] = QByteArrayLiteral("emoji"); + } - return roles; + return roles; } int EmojiModel::rowCount(const QModelIndex &parent) const { - return parent == QModelIndex() ? Provider::emoji.count() : 0; + return parent == QModelIndex() ? Provider::emoji.count() : 0; } QVariant EmojiModel::data(const QModelIndex &index, int role) const { - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case Qt::DisplayRole: - case CompletionModel::CompletionRole: - case static_cast<int>(EmojiModel::Roles::Unicode): - return Provider::emoji[index.row()].unicode; + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case Qt::DisplayRole: + case CompletionModel::CompletionRole: + case static_cast<int>(EmojiModel::Roles::Unicode): + return Provider::emoji[index.row()].unicode; - case Qt::ToolTipRole: - case CompletionModel::SearchRole: - case static_cast<int>(EmojiModel::Roles::ShortName): - return Provider::emoji[index.row()].shortName; + case Qt::ToolTipRole: + case CompletionModel::SearchRole: + case static_cast<int>(EmojiModel::Roles::ShortName): + return Provider::emoji[index.row()].shortName; - case static_cast<int>(EmojiModel::Roles::Category): - return QVariant::fromValue(Provider::emoji[index.row()].category); + case static_cast<int>(EmojiModel::Roles::Category): + return QVariant::fromValue(Provider::emoji[index.row()].category); - case static_cast<int>(EmojiModel::Roles::Emoji): - return QVariant::fromValue(Provider::emoji[index.row()]); - } + case static_cast<int>(EmojiModel::Roles::Emoji): + return QVariant::fromValue(Provider::emoji[index.row()]); } + } - return {}; + return {}; } diff --git a/src/emoji/EmojiModel.h b/src/emoji/EmojiModel.h index 679563f1..882d3eb8 100644 --- a/src/emoji/EmojiModel.h +++ b/src/emoji/EmojiModel.h @@ -18,22 +18,22 @@ namespace emoji { */ class EmojiModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT public: - enum Roles - { - Unicode = Qt::UserRole, // unicode of emoji - Category, // category of emoji - ShortName, // shortext of the emoji - Emoji, // Contains everything from the Emoji - }; + enum Roles + { + Unicode = Qt::UserRole, // unicode of emoji + Category, // category of emoji + ShortName, // shortext of the emoji + Emoji, // Contains everything from the Emoji + }; - using QAbstractListModel::QAbstractListModel; + using QAbstractListModel::QAbstractListModel; - Q_INVOKABLE int categoryToIndex(int category); + Q_INVOKABLE int categoryToIndex(int category); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; }; } diff --git a/src/emoji/MacHelper.h b/src/emoji/MacHelper.h index b3e2e631..ff49f9ba 100644 --- a/src/emoji/MacHelper.h +++ b/src/emoji/MacHelper.h @@ -9,6 +9,6 @@ class MacHelper { public: - static void showEmojiWindow(); - static void initializeMenus(); + static void showEmojiWindow(); + static void initializeMenus(); }; diff --git a/src/emoji/Provider.h b/src/emoji/Provider.h index 43c880a2..965329f9 100644 --- a/src/emoji/Provider.h +++ b/src/emoji/Provider.h @@ -16,37 +16,37 @@ Q_NAMESPACE struct Emoji { - Q_GADGET + Q_GADGET public: - enum class Category - { - People, - Nature, - Food, - Activity, - Travel, - Objects, - Symbols, - Flags, - Search - }; - Q_ENUM(Category) - - Q_PROPERTY(const QString &unicode MEMBER unicode) - Q_PROPERTY(const QString &shortName MEMBER shortName) - Q_PROPERTY(emoji::Emoji::Category category MEMBER category) + enum class Category + { + People, + Nature, + Food, + Activity, + Travel, + Objects, + Symbols, + Flags, + Search + }; + Q_ENUM(Category) + + Q_PROPERTY(const QString &unicode MEMBER unicode) + Q_PROPERTY(const QString &shortName MEMBER shortName) + Q_PROPERTY(emoji::Emoji::Category category MEMBER category) public: - QString unicode; - QString shortName; - Category category; + QString unicode; + QString shortName; + Category category; }; class Provider { public: - // all emoji for QML purposes - static const QVector<Emoji> emoji; + // all emoji for QML purposes + static const QVector<Emoji> emoji; }; } // namespace emoji diff --git a/src/main.cpp b/src/main.cpp index 09168e0c..cf7e29e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,47 +48,47 @@ QQmlDebuggingEnabler enabler; void stacktraceHandler(int signum) { - std::signal(signum, SIG_DFL); + std::signal(signum, SIG_DFL); - // boost::stacktrace::safe_dump_to("./nheko-backtrace.dump"); + // boost::stacktrace::safe_dump_to("./nheko-backtrace.dump"); - // see - // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes/77336#77336 - void *array[50]; - size_t size; + // see + // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes/77336#77336 + void *array[50]; + size_t size; - // get void*'s for all entries on the stack - size = backtrace(array, 50); + // get void*'s for all entries on the stack + size = backtrace(array, 50); - // print out all the frames to stderr - fprintf(stderr, "Error: signal %d:\n", signum); - backtrace_symbols_fd(array, size, STDERR_FILENO); + // print out all the frames to stderr + fprintf(stderr, "Error: signal %d:\n", signum); + backtrace_symbols_fd(array, size, STDERR_FILENO); - int file = ::open("/tmp/nheko-crash.dump", - O_CREAT | O_WRONLY | O_TRUNC + int file = ::open("/tmp/nheko-crash.dump", + O_CREAT | O_WRONLY | O_TRUNC #if defined(S_IWUSR) && defined(S_IRUSR) - , - S_IWUSR | S_IRUSR + , + S_IWUSR | S_IRUSR #elif defined(S_IWRITE) && defined(S_IREAD) - , - S_IWRITE | S_IREAD + , + S_IWRITE | S_IREAD #endif - ); - if (file != -1) { - constexpr char header[] = "Error: signal\n"; - [[maybe_unused]] auto ret = write(file, header, std::size(header) - 1); - backtrace_symbols_fd(array, size, file); - close(file); - } - - std::raise(SIGABRT); + ); + if (file != -1) { + constexpr char header[] = "Error: signal\n"; + [[maybe_unused]] auto ret = write(file, header, std::size(header) - 1); + backtrace_symbols_fd(array, size, file); + close(file); + } + + std::raise(SIGABRT); } void registerSignalHandlers() { - std::signal(SIGSEGV, &stacktraceHandler); - std::signal(SIGABRT, &stacktraceHandler); + std::signal(SIGSEGV, &stacktraceHandler); + std::signal(SIGABRT, &stacktraceHandler); } #else @@ -103,203 +103,200 @@ registerSignalHandlers() QPoint screenCenter(int width, int height) { - // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry(); - QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); + // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry(); + QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); - int x = (screenGeometry.width() - width) / 2; - int y = (screenGeometry.height() - height) / 2; + int x = (screenGeometry.width() - width) / 2; + int y = (screenGeometry.height() - height) / 2; - return QPoint(x, y); + return QPoint(x, y); } void createStandardDirectory(QStandardPaths::StandardLocation path) { - auto dir = QStandardPaths::writableLocation(path); + auto dir = QStandardPaths::writableLocation(path); - if (!QDir().mkpath(dir)) { - throw std::runtime_error( - ("Unable to create state directory:" + dir).toStdString().c_str()); - } + if (!QDir().mkpath(dir)) { + throw std::runtime_error(("Unable to create state directory:" + dir).toStdString().c_str()); + } } int main(int argc, char *argv[]) { - QCoreApplication::setApplicationName("nheko"); - QCoreApplication::setApplicationVersion(nheko::version); - QCoreApplication::setOrganizationName("nheko"); - QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - - // this needs to be after setting the application name. Or how would we find our settings - // file then? + QCoreApplication::setApplicationName("nheko"); + QCoreApplication::setApplicationVersion(nheko::version); + QCoreApplication::setOrganizationName("nheko"); + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + // this needs to be after setting the application name. Or how would we find our settings + // file then? #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) - if (qgetenv("QT_SCALE_FACTOR").size() == 0) { - float factor = utils::scaleFactor(); + if (qgetenv("QT_SCALE_FACTOR").size() == 0) { + float factor = utils::scaleFactor(); - if (factor != -1) - qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8()); - } + if (factor != -1) + qputenv("QT_SCALE_FACTOR", QString::number(factor).toUtf8()); + } #endif - // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name - // parsed before the SingleApplication userdata is set. - QString userdata{""}; - QString matrixUri; - for (int i = 1; i < argc; ++i) { - QString arg{argv[i]}; - if (arg.startsWith("--profile=")) { - arg.remove("--profile="); - userdata = arg; - } else if (arg.startsWith("--p=")) { - arg.remove("-p="); - userdata = arg; - } else if (arg == "--profile" || arg == "-p") { - if (i < argc - 1) // if i is less than argc - 1, we still have a parameter - // left to process as the name - { - ++i; // the next arg is the name, so increment - userdata = QString{argv[i]}; - } - } else if (arg.startsWith("matrix:")) { - matrixUri = arg; - } - } - - SingleApplication app(argc, - argv, - true, - SingleApplication::Mode::User | - SingleApplication::Mode::ExcludeAppPath | - SingleApplication::Mode::ExcludeAppVersion | - SingleApplication::Mode::SecondaryNotification, - 100, - userdata); - - if (app.isSecondary()) { - // open uri in main instance - app.sendMessage(matrixUri.toUtf8()); - return 0; - } - - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); - QCommandLineOption debugOption("debug", "Enable debug output"); - parser.addOption(debugOption); - - // This option is not actually parsed via Qt due to the need to parse it before the app - // name is set. It only exists to keep Qt from complaining about the --profile/-p - // option and thereby crashing the app. - QCommandLineOption configName( - QStringList() << "p" - << "profile", - QCoreApplication::tr("Create a unique profile, which allows you to log into several " - "accounts at the same time and start multiple instances of nheko."), - QCoreApplication::tr("profile"), - QCoreApplication::tr("profile name")); - parser.addOption(configName); - - parser.process(app); - - app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"})); - - http::init(); - - createStandardDirectory(QStandardPaths::CacheLocation); - createStandardDirectory(QStandardPaths::AppDataLocation); - - registerSignalHandlers(); - - if (parser.isSet(debugOption)) - nhlog::enable_debug_log_from_commandline = true; - - try { - nhlog::init(QString("%1/nheko.log") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .toStdString()); - } catch (const spdlog::spdlog_ex &ex) { - std::cout << "Log initialization failed: " << ex.what() << std::endl; - std::exit(1); - } - - if (parser.isSet(configName)) - UserSettings::initialize(parser.value(configName)); - else - UserSettings::initialize(std::nullopt); - - auto settings = UserSettings::instance().toWeakRef(); - - QFont font; - QString userFontFamily = settings.lock()->font(); - if (!userFontFamily.isEmpty() && userFontFamily != "default") { - font.setFamily(userFontFamily); + // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name + // parsed before the SingleApplication userdata is set. + QString userdata{""}; + QString matrixUri; + for (int i = 1; i < argc; ++i) { + QString arg{argv[i]}; + if (arg.startsWith("--profile=")) { + arg.remove("--profile="); + userdata = arg; + } else if (arg.startsWith("--p=")) { + arg.remove("-p="); + userdata = arg; + } else if (arg == "--profile" || arg == "-p") { + if (i < argc - 1) // if i is less than argc - 1, we still have a parameter + // left to process as the name + { + ++i; // the next arg is the name, so increment + userdata = QString{argv[i]}; + } + } else if (arg.startsWith("matrix:")) { + matrixUri = arg; } - font.setPointSizeF(settings.lock()->fontSize()); - - app.setFont(font); - - QString lang = QLocale::system().name(); - - QTranslator qtTranslator; - qtTranslator.load( - QLocale(), "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - app.installTranslator(&qtTranslator); - - QTranslator appTranslator; - appTranslator.load(QLocale(), "nheko", "_", ":/translations"); - app.installTranslator(&appTranslator); - - MainWindow w; - - // Move the MainWindow to the center - w.move(screenCenter(w.width(), w.height())); - - if (!(settings.lock()->startInTray() && settings.lock()->tray())) - w.show(); - - QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() { - w.saveCurrentWindowSize(); - if (http::client() != nullptr) { - nhlog::net()->debug("shutting down all I/O threads & open connections"); - http::client()->close(true); - nhlog::net()->debug("bye"); - } - }); - QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() { - w.show(); - w.raise(); - w.activateWindow(); - }); - - QObject::connect( - &app, - &SingleApplication::receivedMessage, - ChatPage::instance(), - [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); }); - - QMetaObject::Connection uriConnection; - if (app.isPrimary() && !matrixUri.isEmpty()) { - uriConnection = QObject::connect(ChatPage::instance(), - &ChatPage::contentLoaded, - ChatPage::instance(), - [&uriConnection, matrixUri]() { - ChatPage::instance()->handleMatrixUri( - matrixUri.toUtf8()); - QObject::disconnect(uriConnection); - }); + } + + SingleApplication app(argc, + argv, + true, + SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppPath | + SingleApplication::Mode::ExcludeAppVersion | + SingleApplication::Mode::SecondaryNotification, + 100, + userdata); + + if (app.isSecondary()) { + // open uri in main instance + app.sendMessage(matrixUri.toUtf8()); + return 0; + } + + QCommandLineParser parser; + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption debugOption("debug", "Enable debug output"); + parser.addOption(debugOption); + + // This option is not actually parsed via Qt due to the need to parse it before the app + // name is set. It only exists to keep Qt from complaining about the --profile/-p + // option and thereby crashing the app. + QCommandLineOption configName( + QStringList() << "p" + << "profile", + QCoreApplication::tr("Create a unique profile, which allows you to log into several " + "accounts at the same time and start multiple instances of nheko."), + QCoreApplication::tr("profile"), + QCoreApplication::tr("profile name")); + parser.addOption(configName); + + parser.process(app); + + app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"})); + + http::init(); + + createStandardDirectory(QStandardPaths::CacheLocation); + createStandardDirectory(QStandardPaths::AppDataLocation); + + registerSignalHandlers(); + + if (parser.isSet(debugOption)) + nhlog::enable_debug_log_from_commandline = true; + + try { + nhlog::init(QString("%1/nheko.log") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .toStdString()); + } catch (const spdlog::spdlog_ex &ex) { + std::cout << "Log initialization failed: " << ex.what() << std::endl; + std::exit(1); + } + + if (parser.isSet(configName)) + UserSettings::initialize(parser.value(configName)); + else + UserSettings::initialize(std::nullopt); + + auto settings = UserSettings::instance().toWeakRef(); + + QFont font; + QString userFontFamily = settings.lock()->font(); + if (!userFontFamily.isEmpty() && userFontFamily != "default") { + font.setFamily(userFontFamily); + } + font.setPointSizeF(settings.lock()->fontSize()); + + app.setFont(font); + + QString lang = QLocale::system().name(); + + QTranslator qtTranslator; + qtTranslator.load(QLocale(), "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + app.installTranslator(&qtTranslator); + + QTranslator appTranslator; + appTranslator.load(QLocale(), "nheko", "_", ":/translations"); + app.installTranslator(&appTranslator); + + MainWindow w; + + // Move the MainWindow to the center + w.move(screenCenter(w.width(), w.height())); + + if (!(settings.lock()->startInTray() && settings.lock()->tray())) + w.show(); + + QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() { + w.saveCurrentWindowSize(); + if (http::client() != nullptr) { + nhlog::net()->debug("shutting down all I/O threads & open connections"); + http::client()->close(true); + nhlog::net()->debug("bye"); } - QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri"); + }); + QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() { + w.show(); + w.raise(); + w.activateWindow(); + }); + + QObject::connect( + &app, + &SingleApplication::receivedMessage, + ChatPage::instance(), + [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); }); + + QMetaObject::Connection uriConnection; + if (app.isPrimary() && !matrixUri.isEmpty()) { + uriConnection = + QObject::connect(ChatPage::instance(), + &ChatPage::contentLoaded, + ChatPage::instance(), + [&uriConnection, matrixUri]() { + ChatPage::instance()->handleMatrixUri(matrixUri.toUtf8()); + QObject::disconnect(uriConnection); + }); + } + QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri"); #if defined(Q_OS_MAC) - // Temporary solution for the emoji picker until - // nheko has a proper menu bar with more functionality. - MacHelper::initializeMenus(); + // Temporary solution for the emoji picker until + // nheko has a proper menu bar with more functionality. + MacHelper::initializeMenus(); #endif - nhlog::ui()->info("starting nheko {}", nheko::version); + nhlog::ui()->info("starting nheko {}", nheko::version); - return app.exec(); + return app.exec(); } diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp index be580b08..f24a48ff 100644 --- a/src/notifications/Manager.cpp +++ b/src/notifications/Manager.cpp @@ -11,30 +11,30 @@ QString NotificationsManager::getMessageTemplate(const mtx::responses::Notification ¬ification) { - const auto sender = - cache::displayName(QString::fromStdString(notification.room_id), - QString::fromStdString(mtx::accessors::sender(notification.event))); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); - // TODO: decrypt this message if the decryption setting is on in the UserSettings - if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - ¬ification.event); - msg != nullptr) { - return tr("%1 sent an encrypted message").arg(sender); - } + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (auto msg = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + ¬ification.event); + msg != nullptr) { + return tr("%1 sent an encrypted message").arg(sender); + } - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) { - return tr("* %1 %2", - "Format an emote message in a notification, %1 is the sender, %2 the " - "message") - .arg(sender); - } else if (utils::isReply(notification.event)) { - return tr("%1 replied: %2", - "Format a reply in a notification. %1 is the sender, %2 the message") - .arg(sender); - } else { - return tr("%1: %2", - "Format a normal message in a notification. %1 is the sender, %2 the " - "message") - .arg(sender); - } + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote) { + return tr("* %1 %2", + "Format an emote message in a notification, %1 is the sender, %2 the " + "message") + .arg(sender); + } else if (utils::isReply(notification.event)) { + return tr("%1 replied: %2", + "Format a reply in a notification. %1 is the sender, %2 the message") + .arg(sender); + } else { + return tr("%1: %2", + "Format a normal message in a notification. %1 is the sender, %2 the " + "message") + .arg(sender); + } } diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index 416530e0..4e24dd1b 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -22,83 +22,83 @@ struct roomEventId { - QString roomId; - QString eventId; + QString roomId; + QString eventId; }; inline bool operator==(const roomEventId &a, const roomEventId &b) { - return a.roomId == b.roomId && a.eventId == b.eventId; + return a.roomId == b.roomId && a.eventId == b.eventId; } class NotificationsManager : public QObject { - Q_OBJECT + Q_OBJECT public: - NotificationsManager(QObject *parent = nullptr); + NotificationsManager(QObject *parent = nullptr); - void postNotification(const mtx::responses::Notification ¬ification, const QImage &icon); + void postNotification(const mtx::responses::Notification ¬ification, const QImage &icon); signals: - void notificationClicked(const QString roomId, const QString eventId); - void sendNotificationReply(const QString roomId, const QString eventId, const QString body); - void systemPostNotificationCb(const QString &room_id, - const QString &event_id, - const QString &roomName, - const QString &text, - const QImage &icon); + void notificationClicked(const QString roomId, const QString eventId); + void sendNotificationReply(const QString roomId, const QString eventId, const QString body); + void systemPostNotificationCb(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); public slots: - void removeNotification(const QString &roomId, const QString &eventId); + void removeNotification(const QString &roomId, const QString &eventId); #if defined(NHEKO_DBUS_SYS) public: - void closeNotifications(QString roomId); + void closeNotifications(QString roomId); private: - QDBusInterface dbus; + QDBusInterface dbus; - void systemPostNotification(const QString &room_id, - const QString &event_id, - const QString &roomName, - const QString &text, - const QImage &icon); - void closeNotification(uint id); + void systemPostNotification(const QString &room_id, + const QString &event_id, + const QString &roomName, + const QString &text, + const QImage &icon); + void closeNotification(uint id); - // notification ID to (room ID, event ID) - QMap<uint, roomEventId> notificationIds; + // notification ID to (room ID, event ID) + QMap<uint, roomEventId> notificationIds; - const bool hasMarkup_; - const bool hasImages_; + const bool hasMarkup_; + const bool hasImages_; #endif #if defined(Q_OS_MACOS) private: - // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification - // posting is split out - void objCxxPostNotification(const QString &title, - const QString &subtitle, - const QString &informativeText, - const QImage &bodyImage); + // Objective-C(++) doesn't like to do lots of regular C++, so the actual notification + // posting is split out + void objCxxPostNotification(const QString &title, + const QString &subtitle, + const QString &informativeText, + const QImage &bodyImage); #endif #if defined(Q_OS_WINDOWS) private: - void systemPostNotification(const QString &line1, - const QString &line2, - const QString &iconPath); + void systemPostNotification(const QString &line1, + const QString &line2, + const QString &iconPath); #endif - // these slots are platform specific (D-Bus only) - // but Qt slot declarations can not be inside an ifdef! + // these slots are platform specific (D-Bus only) + // but Qt slot declarations can not be inside an ifdef! private slots: - void actionInvoked(uint id, QString action); - void notificationClosed(uint id, uint reason); - void notificationReplied(uint id, QString reply); + void actionInvoked(uint id, QString action); + void notificationClosed(uint id, uint reason); + void notificationReplied(uint id, QString reply); private: - QString getMessageTemplate(const mtx::responses::Notification ¬ification); + QString getMessageTemplate(const mtx::responses::Notification ¬ification); }; #if defined(NHEKO_DBUS_SYS) diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index 2809de87..758cb615 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -34,109 +34,100 @@ NotificationsManager::NotificationsManager(QObject *parent) QDBusConnection::sessionBus(), this) , hasMarkup_{std::invoke([this]() -> bool { - for (auto x : dbus.call("GetCapabilities").arguments()) - if (x.toStringList().contains("body-markup")) - return true; - return false; + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-markup")) + return true; + return false; })} , hasImages_{std::invoke([this]() -> bool { - for (auto x : dbus.call("GetCapabilities").arguments()) - if (x.toStringList().contains("body-images")) - return true; - return false; + for (auto x : dbus.call("GetCapabilities").arguments()) + if (x.toStringList().contains("body-images")) + return true; + return false; })} { - qDBusRegisterMetaType<QImage>(); + qDBusRegisterMetaType<QImage>(); - QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications", - "ActionInvoked", - this, - SLOT(actionInvoked(uint, QString))); - QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications", - "NotificationClosed", - this, - SLOT(notificationClosed(uint, uint))); - QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - "org.freedesktop.Notifications", - "NotificationReplied", - this, - SLOT(notificationReplied(uint, QString))); + QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "ActionInvoked", + this, + SLOT(actionInvoked(uint, QString))); + QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "NotificationClosed", + this, + SLOT(notificationClosed(uint, uint))); + QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "NotificationReplied", + this, + SLOT(notificationReplied(uint, QString))); - connect(this, - &NotificationsManager::systemPostNotificationCb, - this, - &NotificationsManager::systemPostNotification, - Qt::QueuedConnection); + connect(this, + &NotificationsManager::systemPostNotificationCb, + this, + &NotificationsManager::systemPostNotification, + Qt::QueuedConnection); } void NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, const QImage &icon) { - const auto room_id = QString::fromStdString(notification.room_id); - const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); - const auto room_name = - QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto room_id = QString::fromStdString(notification.room_id); + const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event)); + const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); - auto postNotif = [this, room_id, event_id, room_name, icon](QString text) { - emit systemPostNotificationCb(room_id, event_id, room_name, text, icon); - }; - - QString template_ = getMessageTemplate(notification); - // TODO: decrypt this message if the decryption setting is on in the UserSettings - if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - notification.event)) { - postNotif(template_); - return; - } + auto postNotif = [this, room_id, event_id, room_name, icon](QString text) { + emit systemPostNotificationCb(room_id, event_id, room_name, text, icon); + }; - if (hasMarkup_) { - if (hasImages_ && mtx::accessors::msg_type(notification.event) == - mtx::events::MessageType::Image) { - MxcImageProvider::download( - QString::fromStdString(mtx::accessors::url(notification.event)) - .remove("mxc://"), - QSize(200, 80), - [postNotif, notification, template_]( - QString, QSize, QImage, QString imgPath) { - if (imgPath.isEmpty()) - postNotif(template_ - .arg(utils::stripReplyFallbacks( - notification.event, {}, {}) - .quoted_formatted_body) - .replace("<em>", "<i>") - .replace("</em>", "</i>") - .replace("<strong>", "<b>") - .replace("</strong>", "</b>")); - else - postNotif(template_.arg( - QStringLiteral("<br><img src=\"file:///") % imgPath % - "\" alt=\"" % - mtx::accessors::formattedBodyWithFallback( - notification.event) % - "\">")); - }); - return; - } + QString template_ = getMessageTemplate(notification); + // TODO: decrypt this message if the decryption setting is on in the UserSettings + if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + notification.event)) { + postNotif(template_); + return; + } - postNotif( - template_ - .arg( - utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body) - .replace("<em>", "<i>") - .replace("</em>", "</i>") - .replace("<strong>", "<b>") - .replace("</strong>", "</b>")); - return; + if (hasMarkup_) { + if (hasImages_ && + mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) { + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"), + QSize(200, 80), + [postNotif, notification, template_](QString, QSize, QImage, QString imgPath) { + if (imgPath.isEmpty()) + postNotif(template_ + .arg(utils::stripReplyFallbacks(notification.event, {}, {}) + .quoted_formatted_body) + .replace("<em>", "<i>") + .replace("</em>", "</i>") + .replace("<strong>", "<b>") + .replace("</strong>", "</b>")); + else + postNotif(template_.arg( + QStringLiteral("<br><img src=\"file:///") % imgPath % "\" alt=\"" % + mtx::accessors::formattedBodyWithFallback(notification.event) % "\">")); + }); + return; } postNotif( - template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body)); + template_ + .arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_formatted_body) + .replace("<em>", "<i>") + .replace("</em>", "</i>") + .replace("<strong>", "<b>") + .replace("</strong>", "</b>")); + return; + } + + postNotif(template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body)); } /** @@ -152,99 +143,99 @@ NotificationsManager::systemPostNotification(const QString &room_id, const QString &text, const QImage &icon) { - QVariantMap hints; - hints["image-data"] = icon; - hints["sound-name"] = "message-new-instant"; - QList<QVariant> argumentList; - argumentList << "nheko"; // app_name - argumentList << (uint)0; // replace_id - argumentList << ""; // app_icon - argumentList << roomName; // summary - argumentList << text; // body + QVariantMap hints; + hints["image-data"] = icon; + hints["sound-name"] = "message-new-instant"; + QList<QVariant> argumentList; + argumentList << "nheko"; // app_name + argumentList << (uint)0; // replace_id + argumentList << ""; // app_icon + argumentList << roomName; // summary + argumentList << text; // body - // The list of actions has always the action name and then a localized version of that - // action. Currently we just use an empty string for that. - // TODO(Nico): Look into what to actually put there. - argumentList << (QStringList("default") << "" - << "inline-reply" - << ""); // actions - argumentList << hints; // hints - argumentList << (int)-1; // timeout in ms + // The list of actions has always the action name and then a localized version of that + // action. Currently we just use an empty string for that. + // TODO(Nico): Look into what to actually put there. + argumentList << (QStringList("default") << "" + << "inline-reply" + << ""); // actions + argumentList << hints; // hints + argumentList << (int)-1; // timeout in ms - QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList); - auto watcher = new QDBusPendingCallWatcher{call, this}; - connect( - watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() { - if (watcher->reply().type() == QDBusMessage::ErrorMessage) - qDebug() << "D-Bus Error:" << watcher->reply().errorMessage(); - else - notificationIds[watcher->reply().arguments().first().toUInt()] = - roomEventId{room_id, event_id}; - watcher->deleteLater(); - }); + QDBusPendingCall call = dbus.asyncCallWithArgumentList("Notify", argumentList); + auto watcher = new QDBusPendingCallWatcher{call, this}; + connect( + watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() { + if (watcher->reply().type() == QDBusMessage::ErrorMessage) + qDebug() << "D-Bus Error:" << watcher->reply().errorMessage(); + else + notificationIds[watcher->reply().arguments().first().toUInt()] = + roomEventId{room_id, event_id}; + watcher->deleteLater(); + }); } void NotificationsManager::closeNotification(uint id) { - auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id - auto watcher = new QDBusPendingCallWatcher{call, this}; - connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() { - if (watcher->reply().type() == QDBusMessage::ErrorMessage) { - qDebug() << "D-Bus Error:" << watcher->reply().errorMessage(); - }; - watcher->deleteLater(); - }); + auto call = dbus.asyncCall("CloseNotification", (uint)id); // replace_id + auto watcher = new QDBusPendingCallWatcher{call, this}; + connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() { + if (watcher->reply().type() == QDBusMessage::ErrorMessage) { + qDebug() << "D-Bus Error:" << watcher->reply().errorMessage(); + }; + watcher->deleteLater(); + }); } void NotificationsManager::removeNotification(const QString &roomId, const QString &eventId) { - roomEventId reId = {roomId, eventId}; - for (auto elem = notificationIds.begin(); elem != notificationIds.end(); ++elem) { - if (elem.value().roomId != roomId) - continue; + roomEventId reId = {roomId, eventId}; + for (auto elem = notificationIds.begin(); elem != notificationIds.end(); ++elem) { + if (elem.value().roomId != roomId) + continue; - // close all notifications matching the eventId or having a lower - // notificationId - // This relies on the notificationId not wrapping around. This allows for - // approximately 2,147,483,647 notifications, so it is a bit unlikely. - // Otherwise we would need to store a 64bit counter instead. - closeNotification(elem.key()); + // close all notifications matching the eventId or having a lower + // notificationId + // This relies on the notificationId not wrapping around. This allows for + // approximately 2,147,483,647 notifications, so it is a bit unlikely. + // Otherwise we would need to store a 64bit counter instead. + closeNotification(elem.key()); - // FIXME: compare index of event id of the read receipt and the notification instead - // of just the id to prevent read receipts of events without notification clearing - // all notifications in that room! - if (elem.value() == reId) - break; - } + // FIXME: compare index of event id of the read receipt and the notification instead + // of just the id to prevent read receipts of events without notification clearing + // all notifications in that room! + if (elem.value() == reId) + break; + } } void NotificationsManager::actionInvoked(uint id, QString action) { - if (notificationIds.contains(id)) { - roomEventId idEntry = notificationIds[id]; - if (action == "default") { - emit notificationClicked(idEntry.roomId, idEntry.eventId); - } + if (notificationIds.contains(id)) { + roomEventId idEntry = notificationIds[id]; + if (action == "default") { + emit notificationClicked(idEntry.roomId, idEntry.eventId); } + } } void NotificationsManager::notificationReplied(uint id, QString reply) { - if (notificationIds.contains(id)) { - roomEventId idEntry = notificationIds[id]; - emit sendNotificationReply(idEntry.roomId, idEntry.eventId, reply); - } + if (notificationIds.contains(id)) { + roomEventId idEntry = notificationIds[id]; + emit sendNotificationReply(idEntry.roomId, idEntry.eventId, reply); + } } void NotificationsManager::notificationClosed(uint id, uint reason) { - Q_UNUSED(reason); - notificationIds.remove(id); + Q_UNUSED(reason); + notificationIds.remove(id); } /** @@ -259,52 +250,52 @@ NotificationsManager::notificationClosed(uint id, uint reason) QDBusArgument & operator<<(QDBusArgument &arg, const QImage &image) { - if (image.isNull()) { - arg.beginStructure(); - arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray(); - arg.endStructure(); - return arg; - } + if (image.isNull()) { + arg.beginStructure(); + arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray(); + arg.endStructure(); + return arg; + } - QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation); - scaled = scaled.convertToFormat(QImage::Format_ARGB32); + QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation); + scaled = scaled.convertToFormat(QImage::Format_ARGB32); #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN - // ABGR -> ARGB - QImage i = scaled.rgbSwapped(); + // ABGR -> ARGB + QImage i = scaled.rgbSwapped(); #else - // ABGR -> GBAR - QImage i(scaled.size(), scaled.format()); - for (int y = 0; y < i.height(); ++y) { - QRgb *p = (QRgb *)scaled.scanLine(y); - QRgb *q = (QRgb *)i.scanLine(y); - QRgb *end = p + scaled.width(); - while (p < end) { - *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p)); - p++; - q++; - } + // ABGR -> GBAR + QImage i(scaled.size(), scaled.format()); + for (int y = 0; y < i.height(); ++y) { + QRgb *p = (QRgb *)scaled.scanLine(y); + QRgb *q = (QRgb *)i.scanLine(y); + QRgb *end = p + scaled.width(); + while (p < end) { + *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p)); + p++; + q++; } + } #endif - arg.beginStructure(); - arg << i.width(); - arg << i.height(); - arg << i.bytesPerLine(); - arg << i.hasAlphaChannel(); - int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); - arg << i.depth() / channels; - arg << channels; - arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes()); - arg.endStructure(); + arg.beginStructure(); + arg << i.width(); + arg << i.height(); + arg << i.bytesPerLine(); + arg << i.hasAlphaChannel(); + int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); + arg << i.depth() / channels; + arg << channels; + arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes()); + arg.endStructure(); - return arg; + return arg; } const QDBusArgument & operator>>(const QDBusArgument &arg, QImage &) { - // This is needed to link but shouldn't be called. - Q_ASSERT(0); - return arg; + // This is needed to link but shouldn't be called. + Q_ASSERT(0); + return arg; } diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp index 8e36985c..f69cec2c 100644 --- a/src/notifications/ManagerMac.cpp +++ b/src/notifications/ManagerMac.cpp @@ -19,48 +19,42 @@ static QString formatNotification(const mtx::responses::Notification ¬ification) { - return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body; + return utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body; } void NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, const QImage &icon) { - Q_UNUSED(icon) - - const auto room_name = - QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); - const auto sender = - cache::displayName(QString::fromStdString(notification.room_id), - QString::fromStdString(mtx::accessors::sender(notification.event))); - - const auto isEncrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - ¬ification.event) != nullptr; - const auto isReply = utils::isReply(notification.event); - if (isEncrypted) { - // TODO: decrypt this message if the decryption setting is on in the UserSettings - const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message") - : tr("%1 sent an encrypted message")) - .arg(sender); - objCxxPostNotification(room_name, messageInfo, "", QImage()); - } else { - const QString messageInfo = - (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) - MxcImageProvider::download( - QString::fromStdString(mtx::accessors::url(notification.event)) - .remove("mxc://"), - QSize(200, 80), - [this, notification, room_name, messageInfo]( - QString, QSize, QImage image, QString) { - objCxxPostNotification(room_name, - messageInfo, - formatNotification(notification), - image); - }); - else - objCxxPostNotification( - room_name, messageInfo, formatNotification(notification), QImage()); - } + Q_UNUSED(icon) + + const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + ¬ification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + if (isEncrypted) { + // TODO: decrypt this message if the decryption setting is on in the UserSettings + const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + .arg(sender); + objCxxPostNotification(room_name, messageInfo, "", QImage()); + } else { + const QString messageInfo = + (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender); + if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + MxcImageProvider::download( + QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"), + QSize(200, 80), + [this, notification, room_name, messageInfo](QString, QSize, QImage image, QString) { + objCxxPostNotification( + room_name, messageInfo, formatNotification(notification), image); + }); + else + objCxxPostNotification( + room_name, messageInfo, formatNotification(notification), QImage()); + } } diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp index fe7830a7..4376e4d8 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp @@ -20,10 +20,10 @@ using namespace WinToastLib; class CustomHandler : public IWinToastHandler { public: - void toastActivated() const {} - void toastActivated(int) const {} - void toastFailed() const { std::wcout << L"Error showing current toast" << std::endl; } - void toastDismissed(WinToastDismissalReason) const {} + void toastActivated() const {} + void toastActivated(int) const {} + void toastFailed() const { std::wcout << L"Error showing current toast" << std::endl; } + void toastDismissed(WinToastDismissalReason) const {} }; namespace { @@ -32,12 +32,12 @@ bool isInitialized = false; void init() { - isInitialized = true; + isInitialized = true; - WinToast::instance()->setAppName(L"Nheko"); - WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"nheko", L"nheko")); - if (!WinToast::instance()->initialize()) - std::wcout << "Your system is not compatible with toast notifications\n"; + WinToast::instance()->setAppName(L"Nheko"); + WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"nheko", L"nheko")); + if (!WinToast::instance()->initialize()) + std::wcout << "Your system is not compatible with toast notifications\n"; } } @@ -49,41 +49,37 @@ void NotificationsManager::postNotification(const mtx::responses::Notification ¬ification, const QImage &icon) { - const auto room_name = - QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); - const auto sender = - cache::displayName(QString::fromStdString(notification.room_id), - QString::fromStdString(mtx::accessors::sender(notification.event))); - - const auto isEncrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - ¬ification.event) != nullptr; - const auto isReply = utils::isReply(notification.event); - - auto formatNotification = [this, notification, sender] { - const auto template_ = getMessageTemplate(notification); - if (std::holds_alternative< - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - notification.event)) { - return template_; - } - - return template_.arg( - utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body); - }; - - const auto line1 = - (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name); - const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message") - : tr("%1 sent an encrypted message")) - : formatNotification()); - - auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + - room_name + "-room-avatar.png"; - if (!icon.save(iconPath)) - iconPath.clear(); - - systemPostNotification(line1, line2, iconPath); + const auto room_name = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name); + const auto sender = + cache::displayName(QString::fromStdString(notification.room_id), + QString::fromStdString(mtx::accessors::sender(notification.event))); + + const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + ¬ification.event) != nullptr; + const auto isReply = utils::isReply(notification.event); + + auto formatNotification = [this, notification, sender] { + const auto template_ = getMessageTemplate(notification); + if (std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + notification.event)) { + return template_; + } + + return template_.arg(utils::stripReplyFallbacks(notification.event, {}, {}).quoted_body); + }; + + const auto line1 = + (room_name == sender) ? sender : QString("%1 - %2").arg(sender).arg(room_name); + const auto line2 = (isEncrypted ? (isReply ? tr("%1 replied with an encrypted message") + : tr("%1 sent an encrypted message")) + : formatNotification()); + + auto iconPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + room_name + + "-room-avatar.png"; + if (!icon.save(iconPath)) + iconPath.clear(); + + systemPostNotification(line1, line2, iconPath); } void @@ -91,17 +87,17 @@ NotificationsManager::systemPostNotification(const QString &line1, const QString &line2, const QString &iconPath) { - if (!isInitialized) - init(); + if (!isInitialized) + init(); - auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); - templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine); - templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine); + auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); + templ.setTextField(line1.toStdWString(), WinToastTemplate::FirstLine); + templ.setTextField(line2.toStdWString(), WinToastTemplate::SecondLine); - if (!iconPath.isNull()) - templ.setImagePath(iconPath.toStdWString()); + if (!iconPath.isNull()) + templ.setImagePath(iconPath.toStdWString()); - WinToast::instance()->showToast(templ, new CustomHandler()); + WinToast::instance()->showToast(templ, new CustomHandler()); } void NotificationsManager::actionInvoked(uint, QString) {} diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp index 97bfa76d..77bed387 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp @@ -16,231 +16,230 @@ CommunitiesModel::CommunitiesModel(QObject *parent) QHash<int, QByteArray> CommunitiesModel::roleNames() const { - return { - {AvatarUrl, "avatarUrl"}, - {DisplayName, "displayName"}, - {Tooltip, "tooltip"}, - {ChildrenHidden, "childrenHidden"}, - {Hidden, "hidden"}, - {Id, "id"}, - }; + return { + {AvatarUrl, "avatarUrl"}, + {DisplayName, "displayName"}, + {Tooltip, "tooltip"}, + {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, + }; } QVariant CommunitiesModel::data(const QModelIndex &index, int role) const { - if (index.row() == 0) { - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/world.png"); - case CommunitiesModel::Roles::DisplayName: - return tr("All rooms"); - case CommunitiesModel::Roles::Tooltip: - return tr("Shows all rooms without filtering."); - case CommunitiesModel::Roles::ChildrenHidden: - return false; - case CommunitiesModel::Roles::Hidden: - return false; - case CommunitiesModel::Roles::Id: - return ""; - } - } else if (index.row() - 1 < spaceOrder_.size()) { - auto id = spaceOrder_.at(index.row() - 1); - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString::fromStdString(spaces_.at(id).avatar_url); - case CommunitiesModel::Roles::DisplayName: - case CommunitiesModel::Roles::Tooltip: - return QString::fromStdString(spaces_.at(id).name); - case CommunitiesModel::Roles::ChildrenHidden: - return true; - case CommunitiesModel::Roles::Hidden: - return hiddentTagIds_.contains("space:" + id); - case CommunitiesModel::Roles::Id: - return "space:" + id; - } - } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) { - auto tag = tags_.at(index.row() - 1 - spaceOrder_.size()); - if (tag == "m.favourite") { - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/star.png"); - case CommunitiesModel::Roles::DisplayName: - return tr("Favourites"); - case CommunitiesModel::Roles::Tooltip: - return tr("Rooms you have favourited."); - } - } else if (tag == "m.lowpriority") { - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/lowprio.png"); - case CommunitiesModel::Roles::DisplayName: - return tr("Low Priority"); - case CommunitiesModel::Roles::Tooltip: - return tr("Rooms with low priority."); - } - } else if (tag == "m.server_notice") { - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/tag.png"); - case CommunitiesModel::Roles::DisplayName: - return tr("Server Notices"); - case CommunitiesModel::Roles::Tooltip: - return tr("Messages from your server or administrator."); - } - } else { - switch (role) { - case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/tag.png"); - case CommunitiesModel::Roles::DisplayName: - case CommunitiesModel::Roles::Tooltip: - return tag.mid(2); - } - } + if (index.row() == 0) { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/world.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("All rooms"); + case CommunitiesModel::Roles::Tooltip: + return tr("Shows all rooms without filtering."); + case CommunitiesModel::Roles::ChildrenHidden: + return false; + case CommunitiesModel::Roles::Hidden: + return false; + case CommunitiesModel::Roles::Id: + return ""; + } + } else if (index.row() - 1 < spaceOrder_.size()) { + auto id = spaceOrder_.at(index.row() - 1); + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString::fromStdString(spaces_.at(id).avatar_url); + case CommunitiesModel::Roles::DisplayName: + case CommunitiesModel::Roles::Tooltip: + return QString::fromStdString(spaces_.at(id).name); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("space:" + id); + case CommunitiesModel::Roles::Id: + return "space:" + id; + } + } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) { + auto tag = tags_.at(index.row() - 1 - spaceOrder_.size()); + if (tag == "m.favourite") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Favourites"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms you have favourited."); + } + } else if (tag == "m.lowpriority") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/lowprio.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Low Priority"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms with low priority."); + } + } else if (tag == "m.server_notice") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Server Notices"); + case CommunitiesModel::Roles::Tooltip: + return tr("Messages from your server or administrator."); + } + } else { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + case CommunitiesModel::Roles::Tooltip: + return tag.mid(2); + } + } - switch (role) { - case CommunitiesModel::Roles::Hidden: - return hiddentTagIds_.contains("tag:" + tag); - case CommunitiesModel::Roles::ChildrenHidden: - return true; - case CommunitiesModel::Roles::Id: - return "tag:" + tag; - } + switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Id: + return "tag:" + tag; } - return QVariant(); + } + return QVariant(); } void CommunitiesModel::initializeSidebar() { - beginResetModel(); - tags_.clear(); - spaceOrder_.clear(); - spaces_.clear(); - - std::set<std::string> ts; - std::vector<RoomInfo> tempSpaces; - auto infos = cache::roomInfo(); - for (auto it = infos.begin(); it != infos.end(); it++) { - if (it.value().is_space) { - spaceOrder_.push_back(it.key()); - spaces_[it.key()] = it.value(); - } else { - for (const auto &t : it.value().tags) { - if (t.find("u.") == 0 || t.find("m." == 0)) { - ts.insert(t); - } - } + beginResetModel(); + tags_.clear(); + spaceOrder_.clear(); + spaces_.clear(); + + std::set<std::string> ts; + std::vector<RoomInfo> tempSpaces; + auto infos = cache::roomInfo(); + for (auto it = infos.begin(); it != infos.end(); it++) { + if (it.value().is_space) { + spaceOrder_.push_back(it.key()); + spaces_[it.key()] = it.value(); + } else { + for (const auto &t : it.value().tags) { + if (t.find("u.") == 0 || t.find("m." == 0)) { + ts.insert(t); } + } } + } - for (const auto &t : ts) - tags_.push_back(QString::fromStdString(t)); + for (const auto &t : ts) + tags_.push_back(QString::fromStdString(t)); - hiddentTagIds_ = UserSettings::instance()->hiddenTags(); - endResetModel(); + hiddentTagIds_ = UserSettings::instance()->hiddenTags(); + endResetModel(); - emit tagsChanged(); - emit hiddenTagsChanged(); + emit tagsChanged(); + emit hiddenTagsChanged(); } void CommunitiesModel::clear() { - beginResetModel(); - tags_.clear(); - endResetModel(); - resetCurrentTagId(); + beginResetModel(); + tags_.clear(); + endResetModel(); + resetCurrentTagId(); - emit tagsChanged(); + emit tagsChanged(); } void CommunitiesModel::sync(const mtx::responses::Rooms &rooms) { - bool tagsUpdated = false; - - for (const auto &[roomid, room] : rooms.join) { - (void)roomid; - for (const auto &e : room.account_data.events) - if (std::holds_alternative< - mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { - tagsUpdated = true; - } - for (const auto &e : room.state.events) - if (std::holds_alternative< - mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || - std::holds_alternative< - mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { - tagsUpdated = true; - } - for (const auto &e : room.timeline.events) - if (std::holds_alternative< - mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || - std::holds_alternative< - mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { - tagsUpdated = true; - } - } - for (const auto &[roomid, room] : rooms.leave) { - (void)room; - if (spaceOrder_.contains(QString::fromStdString(roomid))) - tagsUpdated = true; - } - - if (tagsUpdated) - initializeSidebar(); + bool tagsUpdated = false; + + for (const auto &[roomid, room] : rooms.join) { + (void)roomid; + for (const auto &e : room.account_data.events) + if (std::holds_alternative< + mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { + tagsUpdated = true; + } + for (const auto &e : room.state.events) + if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>( + e) || + std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>( + e)) { + tagsUpdated = true; + } + for (const auto &e : room.timeline.events) + if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>( + e) || + std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>( + e)) { + tagsUpdated = true; + } + } + for (const auto &[roomid, room] : rooms.leave) { + (void)room; + if (spaceOrder_.contains(QString::fromStdString(roomid))) + tagsUpdated = true; + } + + if (tagsUpdated) + initializeSidebar(); } void CommunitiesModel::setCurrentTagId(QString tagId) { - if (tagId.startsWith("tag:")) { - auto tag = tagId.mid(4); - for (const auto &t : tags_) { - if (t == tag) { - this->currentTagId_ = tagId; - emit currentTagIdChanged(currentTagId_); - return; - } - } - } else if (tagId.startsWith("space:")) { - auto tag = tagId.mid(6); - for (const auto &t : spaceOrder_) { - if (t == tag) { - this->currentTagId_ = tagId; - emit currentTagIdChanged(currentTagId_); - return; - } - } + if (tagId.startsWith("tag:")) { + auto tag = tagId.mid(4); + for (const auto &t : tags_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } + } + } else if (tagId.startsWith("space:")) { + auto tag = tagId.mid(6); + for (const auto &t : spaceOrder_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } } + } - this->currentTagId_ = ""; - emit currentTagIdChanged(currentTagId_); + this->currentTagId_ = ""; + emit currentTagIdChanged(currentTagId_); } void CommunitiesModel::toggleTagId(QString tagId) { - if (hiddentTagIds_.contains(tagId)) { - hiddentTagIds_.removeOne(tagId); - UserSettings::instance()->setHiddenTags(hiddentTagIds_); - } else { - hiddentTagIds_.push_back(tagId); - UserSettings::instance()->setHiddenTags(hiddentTagIds_); - } - - if (tagId.startsWith("tag:")) { - auto idx = tags_.indexOf(tagId.mid(4)); - if (idx != -1) - emit dataChanged(index(idx + 1 + spaceOrder_.size()), - index(idx + 1 + spaceOrder_.size()), - {Hidden}); - } else if (tagId.startsWith("space:")) { - auto idx = spaceOrder_.indexOf(tagId.mid(6)); - if (idx != -1) - emit dataChanged(index(idx + 1), index(idx + 1), {Hidden}); - } - - emit hiddenTagsChanged(); + if (hiddentTagIds_.contains(tagId)) { + hiddentTagIds_.removeOne(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } else { + hiddentTagIds_.push_back(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } + + if (tagId.startsWith("tag:")) { + auto idx = tags_.indexOf(tagId.mid(4)); + if (idx != -1) + emit dataChanged( + index(idx + 1 + spaceOrder_.size()), index(idx + 1 + spaceOrder_.size()), {Hidden}); + } else if (tagId.startsWith("space:")) { + auto idx = spaceOrder_.indexOf(tagId.mid(6)); + if (idx != -1) + emit dataChanged(index(idx + 1), index(idx + 1), {Hidden}); + } + + emit hiddenTagsChanged(); } diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h index 677581dc..0440d17f 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h @@ -15,64 +15,64 @@ class CommunitiesModel : public QAbstractListModel { - Q_OBJECT - Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY - currentTagIdChanged RESET resetCurrentTagId) - Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) - Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged) + Q_OBJECT + Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY + currentTagIdChanged RESET resetCurrentTagId) + Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) + Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged) public: - enum Roles - { - AvatarUrl = Qt::UserRole, - DisplayName, - Tooltip, - ChildrenHidden, - Hidden, - Id, - }; + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + Tooltip, + ChildrenHidden, + Hidden, + Id, + }; - CommunitiesModel(QObject *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return 1 + tags_.size() + spaceOrder_.size(); - } - QVariant data(const QModelIndex &index, int role) const override; + CommunitiesModel(QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return 1 + tags_.size() + spaceOrder_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; public slots: - void initializeSidebar(); - void sync(const mtx::responses::Rooms &rooms); - void clear(); - QString currentTagId() const { return currentTagId_; } - void setCurrentTagId(QString tagId); - void resetCurrentTagId() - { - currentTagId_.clear(); - emit currentTagIdChanged(currentTagId_); - } - QStringList tags() const { return tags_; } - QStringList tagsWithDefault() const - { - QStringList tagsWD = tags_; - tagsWD.prepend("m.lowpriority"); - tagsWD.prepend("m.favourite"); - tagsWD.removeOne("m.server_notice"); - tagsWD.removeDuplicates(); - return tagsWD; - } - void toggleTagId(QString tagId); + void initializeSidebar(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + QString currentTagId() const { return currentTagId_; } + void setCurrentTagId(QString tagId); + void resetCurrentTagId() + { + currentTagId_.clear(); + emit currentTagIdChanged(currentTagId_); + } + QStringList tags() const { return tags_; } + QStringList tagsWithDefault() const + { + QStringList tagsWD = tags_; + tagsWD.prepend("m.lowpriority"); + tagsWD.prepend("m.favourite"); + tagsWD.removeOne("m.server_notice"); + tagsWD.removeDuplicates(); + return tagsWD; + } + void toggleTagId(QString tagId); signals: - void currentTagIdChanged(QString tagId); - void hiddenTagsChanged(); - void tagsChanged(); + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); + void tagsChanged(); private: - QStringList tags_; - QString currentTagId_; - QStringList hiddentTagIds_; - QStringList spaceOrder_; - std::map<QString, RoomInfo> spaces_; + QStringList tags_; + QString currentTagId_; + QStringList hiddentTagIds_; + QStringList spaceOrder_; + std::map<QString, RoomInfo> spaces_; }; diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp index 39c8fa17..682077ae 100644 --- a/src/timeline/DelegateChooser.cpp +++ b/src/timeline/DelegateChooser.cpp @@ -13,127 +13,126 @@ QQmlComponent * DelegateChoice::delegate() const { - return delegate_; + return delegate_; } void DelegateChoice::setDelegate(QQmlComponent *delegate) { - if (delegate != delegate_) { - delegate_ = delegate; - emit delegateChanged(); - emit changed(); - } + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } } QVariant DelegateChoice::roleValue() const { - return roleValue_; + return roleValue_; } void DelegateChoice::setRoleValue(const QVariant &value) { - if (value != roleValue_) { - roleValue_ = value; - emit roleValueChanged(); - emit changed(); - } + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } } QVariant DelegateChooser::roleValue() const { - return roleValue_; + return roleValue_; } void DelegateChooser::setRoleValue(const QVariant &value) { - if (value != roleValue_) { - roleValue_ = value; - recalcChild(); - emit roleValueChanged(); - } + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } } QQmlListProperty<DelegateChoice> DelegateChooser::choices() { - return QQmlListProperty<DelegateChoice>(this, - this, - &DelegateChooser::appendChoice, - &DelegateChooser::choiceCount, - &DelegateChooser::choice, - &DelegateChooser::clearChoices); + return QQmlListProperty<DelegateChoice>(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); } void DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c) { - DelegateChooser *dc = static_cast<DelegateChooser *>(p->object); - dc->choices_.append(c); + DelegateChooser *dc = static_cast<DelegateChooser *>(p->object); + dc->choices_.append(c); } int DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p) { - return static_cast<DelegateChooser *>(p->object)->choices_.count(); + return static_cast<DelegateChooser *>(p->object)->choices_.count(); } DelegateChoice * DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index) { - return static_cast<DelegateChooser *>(p->object)->choices_.at(index); + return static_cast<DelegateChooser *>(p->object)->choices_.at(index); } void DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p) { - static_cast<DelegateChooser *>(p->object)->choices_.clear(); + static_cast<DelegateChooser *>(p->object)->choices_.clear(); } void DelegateChooser::recalcChild() { - for (const auto choice : qAsConst(choices_)) { - auto choiceValue = choice->roleValue(); - if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { - if (child_) { - child_->setParentItem(nullptr); - child_ = nullptr; - } - - choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); - return; - } + for (const auto choice : qAsConst(choices_)) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child_) { + child_->setParentItem(nullptr); + child_ = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; } + } } void DelegateChooser::componentComplete() { - QQuickItem::componentComplete(); - recalcChild(); + QQuickItem::componentComplete(); + recalcChild(); } void DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) { - if (status == QQmlIncubator::Ready) { - chooser.child_ = dynamic_cast<QQuickItem *>(object()); - if (chooser.child_ == nullptr) { - nhlog::ui()->error("Delegate has to be derived of Item!"); - return; - } - - chooser.child_->setParentItem(&chooser); - QQmlEngine::setObjectOwnership(chooser.child_, - QQmlEngine::ObjectOwnership::JavaScriptOwnership); - emit chooser.childChanged(); - - } else if (status == QQmlIncubator::Error) { - for (const auto &e : errors()) - nhlog::ui()->error("Error instantiating delegate: {}", - e.toString().toStdString()); + if (status == QQmlIncubator::Ready) { + chooser.child_ = dynamic_cast<QQuickItem *>(object()); + if (chooser.child_ == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; } + + chooser.child_->setParentItem(&chooser); + QQmlEngine::setObjectOwnership(chooser.child_, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + emit chooser.childChanged(); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); + } } diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h index 22e423a2..3e4b16d7 100644 --- a/src/timeline/DelegateChooser.h +++ b/src/timeline/DelegateChooser.h @@ -18,73 +18,73 @@ class QQmlAdaptorModel; class DelegateChoice : public QObject { - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "delegate") + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") public: - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) - QQmlComponent *delegate() const; - void setDelegate(QQmlComponent *delegate); + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); - QVariant roleValue() const; - void setRoleValue(const QVariant &value); + QVariant roleValue() const; + void setRoleValue(const QVariant &value); signals: - void delegateChanged(); - void roleValueChanged(); - void changed(); + void delegateChanged(); + void roleValueChanged(); + void changed(); private: - QVariant roleValue_; - QQmlComponent *delegate_ = nullptr; + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; }; class DelegateChooser : public QQuickItem { - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "choices") + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") public: - Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT) - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - Q_PROPERTY(QQuickItem *child READ child NOTIFY childChanged) + Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQuickItem *child READ child NOTIFY childChanged) - QQmlListProperty<DelegateChoice> choices(); + QQmlListProperty<DelegateChoice> choices(); - QVariant roleValue() const; - void setRoleValue(const QVariant &value); + QVariant roleValue() const; + void setRoleValue(const QVariant &value); - QQuickItem *child() const { return child_; } + QQuickItem *child() const { return child_; } - void recalcChild(); - void componentComplete() override; + void recalcChild(); + void componentComplete() override; signals: - void roleChanged(); - void roleValueChanged(); - void childChanged(); + void roleChanged(); + void roleValueChanged(); + void childChanged(); private: - struct DelegateIncubator : public QQmlIncubator - { - DelegateIncubator(DelegateChooser &parent) - : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) - , chooser(parent) - {} - void statusChanged(QQmlIncubator::Status status) override; - - DelegateChooser &chooser; - }; - - QVariant roleValue_; - QList<DelegateChoice *> choices_; - QQuickItem *child_ = nullptr; - DelegateIncubator incubator{*this}; - - static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *); - static int choiceCount(QQmlListProperty<DelegateChoice> *); - static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index); - static void clearChoices(QQmlListProperty<DelegateChoice> *); + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList<DelegateChoice *> choices_; + QQuickItem *child_ = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *); + static int choiceCount(QQmlListProperty<DelegateChoice> *); + static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index); + static void clearChoices(QQmlListProperty<DelegateChoice> *); }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 881fd5bb..7144424a 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -28,393 +28,373 @@ QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore:: EventStore::EventStore(std::string room_id, QObject *) : room_id_(std::move(room_id)) { - static auto reactionType = qRegisterMetaType<Reaction>(); - (void)reactionType; - - auto range = cache::client()->getTimelineRange(room_id_); - - if (range) { - this->first = range->first; - this->last = range->last; - } - - connect( - this, - &EventStore::eventFetched, - this, - [this](std::string id, - std::string relatedTo, - mtx::events::collections::TimelineEvents timeline) { - cache::client()->storeEvent(room_id_, id, {timeline}); - - if (!relatedTo.empty()) { - auto idx = idToIndex(relatedTo); - if (idx) - emit dataChanged(*idx, *idx); - } - }, - Qt::QueuedConnection); - - connect( - this, - &EventStore::oldMessagesRetrieved, - this, - [this](const mtx::responses::Messages &res) { - if (res.end.empty() || cache::client()->previousBatchToken(room_id_) == res.end) { - noMoreMessages = true; - emit fetchedMore(); - return; + static auto reactionType = qRegisterMetaType<Reaction>(); + (void)reactionType; + + auto range = cache::client()->getTimelineRange(room_id_); + + if (range) { + this->first = range->first; + this->last = range->last; + } + + connect( + this, + &EventStore::eventFetched, + this, + [this]( + std::string id, std::string relatedTo, mtx::events::collections::TimelineEvents timeline) { + cache::client()->storeEvent(room_id_, id, {timeline}); + + if (!relatedTo.empty()) { + auto idx = idToIndex(relatedTo); + if (idx) + emit dataChanged(*idx, *idx); + } + }, + Qt::QueuedConnection); + + connect( + this, + &EventStore::oldMessagesRetrieved, + this, + [this](const mtx::responses::Messages &res) { + if (res.end.empty() || cache::client()->previousBatchToken(room_id_) == res.end) { + noMoreMessages = true; + emit fetchedMore(); + return; + } + + uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); + if (newFirst == first) + fetchMore(); + else { + if (this->last != std::numeric_limits<uint64_t>::max()) { + auto oldFirst = this->first; + emit beginInsertRows(toExternalIdx(newFirst), toExternalIdx(this->first - 1)); + this->first = newFirst; + emit endInsertRows(); + emit fetchedMore(); + emit dataChanged(toExternalIdx(oldFirst), toExternalIdx(oldFirst)); + } else { + auto range = cache::client()->getTimelineRange(room_id_); + + if (range && range->last - range->first != 0) { + emit beginInsertRows(0, int(range->last - range->first)); + this->first = range->first; + this->last = range->last; + emit endInsertRows(); + emit fetchedMore(); + } else { + fetchMore(); } + } + } + }, + Qt::QueuedConnection); + + connect(this, &EventStore::processPending, this, [this]() { + if (!current_txn.empty()) { + nhlog::ui()->debug("Already processing {}", current_txn); + return; + } - uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); - if (newFirst == first) - fetchMore(); - else { - if (this->last != std::numeric_limits<uint64_t>::max()) { - auto oldFirst = this->first; - emit beginInsertRows(toExternalIdx(newFirst), - toExternalIdx(this->first - 1)); - this->first = newFirst; - emit endInsertRows(); - emit fetchedMore(); - emit dataChanged(toExternalIdx(oldFirst), - toExternalIdx(oldFirst)); - } else { - auto range = cache::client()->getTimelineRange(room_id_); - - if (range && range->last - range->first != 0) { - emit beginInsertRows(0, int(range->last - range->first)); - this->first = range->first; - this->last = range->last; - emit endInsertRows(); - emit fetchedMore(); - } else { - fetchMore(); - } - } - } - }, - Qt::QueuedConnection); + auto event = cache::client()->firstPendingMessage(room_id_); - connect(this, &EventStore::processPending, this, [this]() { - if (!current_txn.empty()) { - nhlog::ui()->debug("Already processing {}", current_txn); - return; - } + if (!event) { + nhlog::ui()->debug("No event to send"); + return; + } - auto event = cache::client()->firstPendingMessage(room_id_); + std::visit( + [this](auto e) { + auto txn_id = e.event_id; + this->current_txn = txn_id; - if (!event) { - nhlog::ui()->debug("No event to send"); - return; - } + if (txn_id.empty() || txn_id[0] != 'm') { + nhlog::ui()->debug("Invalid txn id '{}'", txn_id); + cache::client()->removePendingStatus(room_id_, txn_id); + return; + } + + if constexpr (mtx::events::message_content_to_type<decltype(e.content)> != + mtx::events::EventType::Unsupported) + http::client()->send_room_message( + room_id_, + txn_id, + e.content, + [this, txn_id, e](const mtx::responses::EventId &event_id, + mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id); + return; + } - std::visit( - [this](auto e) { - auto txn_id = e.event_id; - this->current_txn = txn_id; - - if (txn_id.empty() || txn_id[0] != 'm') { - nhlog::ui()->debug("Invalid txn id '{}'", txn_id); - cache::client()->removePendingStatus(room_id_, txn_id); - return; - } - - if constexpr (mtx::events::message_content_to_type<decltype(e.content)> != - mtx::events::EventType::Unsupported) - http::client()->send_room_message( - room_id_, - txn_id, - e.content, - [this, txn_id, e](const mtx::responses::EventId &event_id, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast<int>(err->status_code); - nhlog::net()->warn( - "[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, event_id.event_id.to_string()); - if constexpr (std::is_same_v< - decltype(e.content), - mtx::events::msg::Encrypted>) { - auto event = - decryptEvent({room_id_, e.event_id}, e); - 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()); - } - } - } - }); - }, - event->data); - }); - - connect( - this, - &EventStore::messageFailed, - this, - [this](std::string txn_id) { - if (current_txn == txn_id) { - current_txn_error_count++; - if (current_txn_error_count > 10) { - nhlog::ui()->debug("failing txn id '{}'", txn_id); - cache::client()->removePendingStatus(room_id_, txn_id); - current_txn_error_count = 0; - } - } - QTimer::singleShot(1000, this, [this]() { - nhlog::ui()->debug("timeout"); - this->current_txn = ""; - emit processPending(); - }); + emit messageSent(txn_id, event_id.event_id.to_string()); + if constexpr (std::is_same_v<decltype(e.content), + mtx::events::msg::Encrypted>) { + auto event = decryptEvent({room_id_, e.event_id}, e); + 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()); + } + } + } + }); }, - Qt::QueuedConnection); - - connect( - this, - &EventStore::messageSent, - this, - [this](std::string txn_id, std::string event_id) { - nhlog::ui()->debug("sent {}", txn_id); - - // Replace the event_id in pending edits/replies/redactions with the actual - // event_id of this event. This allows one to edit and reply to events that are - // currently pending. - - // FIXME (introduced by balsoft): this doesn't work for encrypted events, but - // allegedly it's hard to fix so I'll leave my first contribution at that - for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) { - if (cache::client()->getEvent(room_id_, related_event_id)) { - auto related_event = - cache::client()->getEvent(room_id_, related_event_id).value(); - auto relations = mtx::accessors::relations(related_event.data); - - // Replace the blockquote in fallback reply - auto related_text = - std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>( - &related_event.data); - if (related_text && relations.reply_to() == txn_id) { - size_t index = - related_text->content.formatted_body.find(txn_id); - if (index != std::string::npos) { - related_text->content.formatted_body.replace( - index, event_id.length(), event_id); - } - } + event->data); + }); + + connect( + this, + &EventStore::messageFailed, + this, + [this](std::string txn_id) { + if (current_txn == txn_id) { + current_txn_error_count++; + if (current_txn_error_count > 10) { + nhlog::ui()->debug("failing txn id '{}'", txn_id); + cache::client()->removePendingStatus(room_id_, txn_id); + current_txn_error_count = 0; + } + } + QTimer::singleShot(1000, this, [this]() { + nhlog::ui()->debug("timeout"); + this->current_txn = ""; + emit processPending(); + }); + }, + Qt::QueuedConnection); + + connect( + this, + &EventStore::messageSent, + this, + [this](std::string txn_id, std::string event_id) { + nhlog::ui()->debug("sent {}", txn_id); + + // Replace the event_id in pending edits/replies/redactions with the actual + // event_id of this event. This allows one to edit and reply to events that are + // currently pending. + + // FIXME (introduced by balsoft): this doesn't work for encrypted events, but + // allegedly it's hard to fix so I'll leave my first contribution at that + for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) { + if (cache::client()->getEvent(room_id_, related_event_id)) { + auto related_event = + cache::client()->getEvent(room_id_, related_event_id).value(); + auto relations = mtx::accessors::relations(related_event.data); + + // Replace the blockquote in fallback reply + auto related_text = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>( + &related_event.data); + if (related_text && relations.reply_to() == txn_id) { + size_t index = related_text->content.formatted_body.find(txn_id); + if (index != std::string::npos) { + related_text->content.formatted_body.replace( + index, event_id.length(), event_id); + } + } - for (mtx::common::Relation &rel : relations.relations) { - if (rel.event_id == txn_id) - rel.event_id = event_id; - } + for (mtx::common::Relation &rel : relations.relations) { + if (rel.event_id == txn_id) + rel.event_id = event_id; + } - mtx::accessors::set_relations(related_event.data, relations); + mtx::accessors::set_relations(related_event.data, relations); - cache::client()->replaceEvent( - room_id_, related_event_id, related_event); + cache::client()->replaceEvent(room_id_, related_event_id, related_event); - auto idx = idToIndex(related_event_id); + auto idx = idToIndex(related_event_id); - events_by_id_.remove({room_id_, related_event_id}); - events_.remove({room_id_, toInternalIdx(*idx)}); - } - } + events_by_id_.remove({room_id_, related_event_id}); + events_.remove({room_id_, toInternalIdx(*idx)}); + } + } - http::client()->read_event( - room_id_, event_id, [this, event_id](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", room_id_, event_id); - } - }); + http::client()->read_event( + room_id_, event_id, [this, event_id](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", room_id_, event_id); + } + }); - auto idx = idToIndex(event_id); + auto idx = idToIndex(event_id); - if (idx) - emit dataChanged(*idx, *idx); + if (idx) + emit dataChanged(*idx, *idx); - cache::client()->removePendingStatus(room_id_, txn_id); - this->current_txn = ""; - this->current_txn_error_count = 0; - emit processPending(); - }, - Qt::QueuedConnection); + cache::client()->removePendingStatus(room_id_, txn_id); + this->current_txn = ""; + this->current_txn_error_count = 0; + emit processPending(); + }, + Qt::QueuedConnection); } void EventStore::addPending(mtx::events::collections::TimelineEvents event) { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); - cache::client()->savePendingMessage(this->room_id_, {event}); - mtx::responses::Timeline events; - events.limited = false; - events.events.emplace_back(event); - handleSync(events); + cache::client()->savePendingMessage(this->room_id_, {event}); + mtx::responses::Timeline events; + events.limited = false; + events.events.emplace_back(event); + handleSync(events); - emit processPending(); + emit processPending(); } void EventStore::clearTimeline() { - emit beginResetModel(); - - cache::client()->clearTimeline(room_id_); - auto range = cache::client()->getTimelineRange(room_id_); - if (range) { - nhlog::db()->info("Range {} {}", range->last, range->first); - this->last = range->last; - this->first = range->first; - } else { - this->first = std::numeric_limits<uint64_t>::max(); - this->last = std::numeric_limits<uint64_t>::max(); - } - nhlog::ui()->info("Range {} {}", this->last, this->first); - - decryptedEvents_.clear(); - events_.clear(); - - emit endResetModel(); + emit beginResetModel(); + + cache::client()->clearTimeline(room_id_); + auto range = cache::client()->getTimelineRange(room_id_); + if (range) { + nhlog::db()->info("Range {} {}", range->last, range->first); + this->last = range->last; + this->first = range->first; + } else { + this->first = std::numeric_limits<uint64_t>::max(); + this->last = std::numeric_limits<uint64_t>::max(); + } + nhlog::ui()->info("Range {} {}", this->last, this->first); + + decryptedEvents_.clear(); + events_.clear(); + + emit endResetModel(); } void EventStore::receivedSessionKey(const std::string &session_id) { - if (!pending_key_requests.count(session_id)) - return; + if (!pending_key_requests.count(session_id)) + return; - auto request = pending_key_requests.at(session_id); + auto request = pending_key_requests.at(session_id); - // Don't request keys again until Nheko is restarted (for now) - pending_key_requests[session_id].events.clear(); + // 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); + 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); - if (idx) { - decryptedEvents_.remove({room_id_, e.event_id}); - events_by_id_.remove({room_id_, e.event_id}); - events_.remove({room_id_, toInternalIdx(*idx)}); - emit dataChanged(*idx, *idx); - } + for (const auto &e : request.events) { + auto idx = idToIndex(e.event_id); + if (idx) { + decryptedEvents_.remove({room_id_, e.event_id}); + events_by_id_.remove({room_id_, e.event_id}); + events_.remove({room_id_, toInternalIdx(*idx)}); + emit dataChanged(*idx, *idx); } + } } void EventStore::handleSync(const mtx::responses::Timeline &events) { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); - - auto range = cache::client()->getTimelineRange(room_id_); - if (!range) { - 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; - } + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); - if (events.limited) { - 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)); - this->last = range->last; - emit endInsertRows(); - } + auto range = cache::client()->getTimelineRange(room_id_); + if (!range) { + emit beginResetModel(); + this->first = std::numeric_limits<uint64_t>::max(); + this->last = std::numeric_limits<uint64_t>::max(); - for (const auto &event : events.events) { - std::set<std::string> relates_to; - if (auto redaction = - std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>( - &event)) { - // fixup reactions - auto redacted = events_by_id_.object({room_id_, redaction->redacts}); - if (redacted) { - auto id = mtx::accessors::relations(*redacted); - if (id.annotates()) { - auto idx = idToIndex(id.annotates()->event_id); - if (idx) { - events_by_id_.remove( - {room_id_, redaction->redacts}); - events_.remove({room_id_, toInternalIdx(*idx)}); - emit dataChanged(*idx, *idx); - } - } - } + decryptedEvents_.clear(); + events_.clear(); + emit endResetModel(); + return; + } - relates_to.insert(redaction->redacts); - } else { - for (const auto &r : mtx::accessors::relations(event).relations) - relates_to.insert(r.event_id); - } + if (events.limited) { + emit beginResetModel(); + this->last = range->last; + this->first = range->first; - for (const auto &relates_to_id : relates_to) { - auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id); - if (idx) { - events_by_id_.remove({room_id_, relates_to_id}); - decryptedEvents_.remove({room_id_, relates_to_id}); - events_.remove({room_id_, *idx}); - emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); - } + decryptedEvents_.clear(); + events_.clear(); + emit endResetModel(); + } else if (range->last > this->last) { + emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last)); + this->last = range->last; + emit endInsertRows(); + } + + for (const auto &event : events.events) { + std::set<std::string> relates_to; + if (auto redaction = + std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&event)) { + // fixup reactions + auto redacted = events_by_id_.object({room_id_, redaction->redacts}); + if (redacted) { + auto id = mtx::accessors::relations(*redacted); + if (id.annotates()) { + auto idx = idToIndex(id.annotates()->event_id); + if (idx) { + events_by_id_.remove({room_id_, redaction->redacts}); + events_.remove({room_id_, toInternalIdx(*idx)}); + emit dataChanged(*idx, *idx); + } } + } - if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) { - auto idx = cache::client()->getTimelineIndex( - room_id_, mtx::accessors::event_id(event)); - if (idx) { - Index index{room_id_, *idx}; - events_.remove(index); - emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); - } - } + relates_to.insert(redaction->redacts); + } else { + for (const auto &r : mtx::accessors::relations(event).relations) + relates_to.insert(r.event_id); + } - // decrypting and checking some encrypted messages - if (auto encrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - &event)) { - 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->event)) { - handle_room_verification(*d_event->event); - } - } + for (const auto &relates_to_id : relates_to) { + auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id); + if (idx) { + events_by_id_.remove({room_id_, relates_to_id}); + decryptedEvents_.remove({room_id_, relates_to_id}); + events_.remove({room_id_, *idx}); + emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); + } + } + + if (auto txn_id = mtx::accessors::transaction_id(event); !txn_id.empty()) { + auto idx = cache::client()->getTimelineIndex(room_id_, mtx::accessors::event_id(event)); + if (idx) { + Index index{room_id_, *idx}; + events_.remove(index); + emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); + } } + + // decrypting and checking some encrypted messages + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + 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->event)) { + handle_room_verification(*d_event->event); + } + } + } } namespace { template<class... Ts> struct overloaded : Ts... { - using Ts::operator()...; + using Ts::operator()...; }; template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; @@ -423,462 +403,451 @@ overloaded(Ts...) -> overloaded<Ts...>; void EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event) { - std::visit( - overloaded{ - [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) { - nhlog::db()->debug("handle_room_verification: Request"); - emit startDMVerification(msg); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) { - nhlog::db()->debug("handle_room_verification: Cancel"); - ChatPage::instance()->receivedDeviceVerificationCancel(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) { - nhlog::db()->debug("handle_room_verification: Accept"); - ChatPage::instance()->receivedDeviceVerificationAccept(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) { - nhlog::db()->debug("handle_room_verification: Key"); - ChatPage::instance()->receivedDeviceVerificationKey(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) { - nhlog::db()->debug("handle_room_verification: Mac"); - ChatPage::instance()->receivedDeviceVerificationMac(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) { - nhlog::db()->debug("handle_room_verification: Ready"); - ChatPage::instance()->receivedDeviceVerificationReady(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) { - nhlog::db()->debug("handle_room_verification: Done"); - ChatPage::instance()->receivedDeviceVerificationDone(msg.content); - }, - [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) { - nhlog::db()->debug("handle_room_verification: Start"); - ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender); - }, - [](const auto &) {}, - }, - event); + std::visit( + overloaded{ + [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) { + nhlog::db()->debug("handle_room_verification: Request"); + emit startDMVerification(msg); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) { + nhlog::db()->debug("handle_room_verification: Cancel"); + ChatPage::instance()->receivedDeviceVerificationCancel(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) { + nhlog::db()->debug("handle_room_verification: Accept"); + ChatPage::instance()->receivedDeviceVerificationAccept(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) { + nhlog::db()->debug("handle_room_verification: Key"); + ChatPage::instance()->receivedDeviceVerificationKey(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) { + nhlog::db()->debug("handle_room_verification: Mac"); + ChatPage::instance()->receivedDeviceVerificationMac(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) { + nhlog::db()->debug("handle_room_verification: Ready"); + ChatPage::instance()->receivedDeviceVerificationReady(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) { + nhlog::db()->debug("handle_room_verification: Done"); + ChatPage::instance()->receivedDeviceVerificationDone(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) { + nhlog::db()->debug("handle_room_verification: Start"); + ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender); + }, + [](const auto &) {}, + }, + event); } std::vector<mtx::events::collections::TimelineEvents> 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) - return {}; - - auto original_sender = mtx::accessors::sender(*original_event); - auto original_relations = mtx::accessors::relations(*original_event); - - std::vector<mtx::events::collections::TimelineEvents> edits; - for (const auto &id : event_ids) { - auto related_event = get(id, event_id, false, false); - if (!related_event) - continue; - - auto related_ev = *related_event; - - auto edit_rel = mtx::accessors::relations(related_ev); - if (edit_rel.replaces() == event_id && - original_sender == mtx::accessors::sender(related_ev)) { - if (edit_rel.synthesized && original_relations.reply_to() && - !edit_rel.reply_to()) { - edit_rel.relations.push_back( - {mtx::common::RelationType::InReplyTo, - original_relations.reply_to().value()}); - mtx::accessors::set_relations(related_ev, std::move(edit_rel)); - } - edits.push_back(std::move(related_ev)); - } + auto event_ids = cache::client()->relatedEvents(room_id_, event_id); + + auto original_event = get(event_id, "", false, false); + if (!original_event) + return {}; + + auto original_sender = mtx::accessors::sender(*original_event); + auto original_relations = mtx::accessors::relations(*original_event); + + std::vector<mtx::events::collections::TimelineEvents> edits; + for (const auto &id : event_ids) { + auto related_event = get(id, event_id, false, false); + if (!related_event) + continue; + + auto related_ev = *related_event; + + auto edit_rel = mtx::accessors::relations(related_ev); + if (edit_rel.replaces() == event_id && + original_sender == mtx::accessors::sender(related_ev)) { + if (edit_rel.synthesized && original_relations.reply_to() && !edit_rel.reply_to()) { + edit_rel.relations.push_back( + {mtx::common::RelationType::InReplyTo, original_relations.reply_to().value()}); + mtx::accessors::set_relations(related_ev, std::move(edit_rel)); + } + edits.push_back(std::move(related_ev)); } - - auto c = cache::client(); - std::sort(edits.begin(), - edits.end(), - [this, c](const mtx::events::collections::TimelineEvents &a, - const mtx::events::collections::TimelineEvents &b) { - return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) < - c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b)); - }); - - return edits; + } + + auto c = cache::client(); + std::sort(edits.begin(), + edits.end(), + [this, c](const mtx::events::collections::TimelineEvents &a, + const mtx::events::collections::TimelineEvents &b) { + return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) < + c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b)); + }); + + return edits; } QVariantList EventStore::reactions(const std::string &event_id) { - auto event_ids = cache::client()->relatedEvents(room_id_, event_id); - - struct TempReaction - { - int count = 0; - std::vector<std::string> users; - std::string reactedBySelf; - }; - std::map<std::string, TempReaction> aggregation; - std::vector<Reaction> reactions; - - auto self = http::client()->user_id().to_string(); - for (const auto &id : event_ids) { - auto related_event = get(id, event_id); - if (!related_event) - continue; - - if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>( - related_event); - reaction && reaction->content.relations.annotates() && - reaction->content.relations.annotates()->key) { - auto key = reaction->content.relations.annotates()->key.value(); - auto &agg = aggregation[key]; - - if (agg.count == 0) { - Reaction temp{}; - temp.key_ = QString::fromStdString(key); - reactions.push_back(temp); - } - - agg.count++; - agg.users.push_back(cache::displayName(room_id_, reaction->sender)); - if (reaction->sender == self) - agg.reactedBySelf = reaction->event_id; - } + auto event_ids = cache::client()->relatedEvents(room_id_, event_id); + + struct TempReaction + { + int count = 0; + std::vector<std::string> users; + std::string reactedBySelf; + }; + std::map<std::string, TempReaction> aggregation; + std::vector<Reaction> reactions; + + auto self = http::client()->user_id().to_string(); + for (const auto &id : event_ids) { + auto related_event = get(id, event_id); + if (!related_event) + continue; + + if (auto reaction = + std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(related_event); + reaction && reaction->content.relations.annotates() && + reaction->content.relations.annotates()->key) { + auto key = reaction->content.relations.annotates()->key.value(); + auto &agg = aggregation[key]; + + if (agg.count == 0) { + Reaction temp{}; + temp.key_ = QString::fromStdString(key); + reactions.push_back(temp); + } + + agg.count++; + agg.users.push_back(cache::displayName(room_id_, reaction->sender)); + if (reaction->sender == self) + agg.reactedBySelf = reaction->event_id; } - - QVariantList temp; - for (auto &reaction : reactions) { - const auto &agg = aggregation[reaction.key_.toStdString()]; - reaction.count_ = agg.count; - reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf); - - bool firstReaction = true; - for (const auto &user : agg.users) { - if (firstReaction) - firstReaction = false; - else - reaction.users_ += ", "; - - reaction.users_ += QString::fromStdString(user); - } - - nhlog::db()->debug("key: {}, count: {}, users: {}", - reaction.key_.toStdString(), - reaction.count_, - reaction.users_.toStdString()); - temp.append(QVariant::fromValue(reaction)); + } + + QVariantList temp; + for (auto &reaction : reactions) { + const auto &agg = aggregation[reaction.key_.toStdString()]; + reaction.count_ = agg.count; + reaction.selfReactedEvent_ = QString::fromStdString(agg.reactedBySelf); + + bool firstReaction = true; + for (const auto &user : agg.users) { + if (firstReaction) + firstReaction = false; + else + reaction.users_ += ", "; + + reaction.users_ += QString::fromStdString(user); } - return temp; + nhlog::db()->debug("key: {}, count: {}, users: {}", + reaction.key_.toStdString(), + reaction.count_, + reaction.users_.toStdString()); + temp.append(QVariant::fromValue(reaction)); + } + + return temp; } mtx::events::collections::TimelineEvents * EventStore::get(int idx, bool decrypt) { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); - - Index index{room_id_, toInternalIdx(idx)}; - if (index.idx > last || index.idx < first) - return nullptr; - - auto event_ptr = events_.object(index); - if (!event_ptr) { - auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx); - if (!event_id) - return nullptr; - - std::optional<mtx::events::collections::TimelineEvent> event; - auto edits_ = edits(*event_id); - if (edits_.empty()) - event = cache::client()->getEvent(room_id_, *event_id); - else - event = {edits_.back()}; - - if (!event) - return nullptr; - else - event_ptr = - new mtx::events::collections::TimelineEvents(std::move(event->data)); - events_.insert(index, event_ptr); - } + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); + + Index index{room_id_, toInternalIdx(idx)}; + if (index.idx > last || index.idx < first) + return nullptr; + + auto event_ptr = events_.object(index); + if (!event_ptr) { + auto event_id = cache::client()->getTimelineEventId(room_id_, index.idx); + if (!event_id) + return nullptr; + + std::optional<mtx::events::collections::TimelineEvent> event; + auto edits_ = edits(*event_id); + if (edits_.empty()) + event = cache::client()->getEvent(room_id_, *event_id); + else + event = {edits_.back()}; - if (decrypt) { - if (auto encrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - event_ptr)) { - auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted); - if (decrypted->event) - return &*decrypted->event; - } + if (!event) + return nullptr; + else + event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); + events_.insert(index, event_ptr); + } + + if (decrypt) { + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) { + auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (decrypted->event) + return &*decrypted->event; } + } - return event_ptr; + return event_ptr; } std::optional<int> EventStore::idToIndex(std::string_view id) const { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); - - auto idx = cache::client()->getTimelineIndex(room_id_, id); - if (idx) - return toExternalIdx(*idx); - else - return std::nullopt; + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); + + auto idx = cache::client()->getTimelineIndex(room_id_, id); + if (idx) + return toExternalIdx(*idx); + else + return std::nullopt; } std::optional<std::string> EventStore::indexToId(int idx) const { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); - return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); + return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx)); } olm::DecryptionResult * EventStore::decryptEvent(const IdIndex &idx, const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) { - if (auto cachedEvent = decryptedEvents_.object(idx)) - return cachedEvent; - - MegolmSessionIndex index(room_id_, e.content); - - 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); - - if (decryptionResult.error) { - switch (decryptionResult.error) { - case olm::DecryptionErrorCode::MissingSession: - case olm::DecryptionErrorCode::MissingSessionIndex: { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - - requestSession(e, false); - break; - } - case olm::DecryptionErrorCode::DbError: - nhlog::db()->critical( - "failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - decryptionResult.error_message.value_or("")); - break; - case olm::DecryptionErrorCode::DecryptionFailed: - nhlog::crypto()->critical( - "failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - decryptionResult.error_message.value_or("")); - break; - case olm::DecryptionErrorCode::ParsingFailed: - break; - case olm::DecryptionErrorCode::ReplayAttack: - nhlog::crypto()->critical( - "Reply attack while decryptiong event {} in room {} from {}!", - e.event_id, - room_id_, - index.sender_key); - break; - case olm::DecryptionErrorCode::NoError: - // unreachable - break; - } - return asCacheEntry(std::move(decryptionResult)); - } + if (auto cachedEvent = decryptedEvents_.object(idx)) + return cachedEvent; + + MegolmSessionIndex index(room_id_, e.content); + + auto asCacheEntry = [&idx](olm::DecryptionResult &&event) { + auto event_ptr = new olm::DecryptionResult(std::move(event)); + decryptedEvents_.insert(idx, event_ptr); + return event_ptr; + }; - auto encInfo = mtx::accessors::file(decryptionResult.event.value()); - if (encInfo) - emit newEncryptedImage(encInfo.value()); + auto decryptionResult = olm::decryptEvent(index, e); + if (decryptionResult.error) { + switch (decryptionResult.error) { + case olm::DecryptionErrorCode::MissingSession: + case olm::DecryptionErrorCode::MissingSessionIndex: { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + + requestSession(e, false); + break; + } + case olm::DecryptionErrorCode::DbError: + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + decryptionResult.error_message.value_or("")); + break; + case olm::DecryptionErrorCode::DecryptionFailed: + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + decryptionResult.error_message.value_or("")); + break; + case olm::DecryptionErrorCode::ParsingFailed: + break; + case olm::DecryptionErrorCode::ReplayAttack: + nhlog::crypto()->critical("Reply attack while decryptiong event {} in room {} from {}!", + e.event_id, + room_id_, + index.sender_key); + break; + case olm::DecryptionErrorCode::NoError: + // unreachable + break; + } return asCacheEntry(std::move(decryptionResult)); + } + + auto encInfo = mtx::accessors::file(decryptionResult.event.value()); + if (encInfo) + emit newEncryptedImage(encInfo.value()); + + return asCacheEntry(std::move(decryptionResult)); } void EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &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::lookup_keybackup(room_id_, ev.content.session_id); - 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::lookup_keybackup(room_id_, ev.content.session_id); - olm::send_key_request_for(copy, request.request_id); - pending_key_requests[ev.content.session_id] = request; + // 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::lookup_keybackup(room_id_, ev.content.session_id); + 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::lookup_keybackup(room_id_, ev.content.session_id); + olm::send_key_request_for(copy, request.request_id); + pending_key_requests[ev.content.session_id] = request; + } } void EventStore::enableKeyRequests(bool suppressKeyRequests_) { - if (!suppressKeyRequests_) { - for (const auto &key : decryptedEvents_.keys()) - if (key.room == this->room_id_) - decryptedEvents_.remove(key); - suppressKeyRequests = false; - } else - suppressKeyRequests = true; + if (!suppressKeyRequests_) { + for (const auto &key : decryptedEvents_.keys()) + if (key.room == this->room_id_) + decryptedEvents_.remove(key); + suppressKeyRequests = false; + } else + suppressKeyRequests = true; } mtx::events::collections::TimelineEvents * EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { - if (this->thread() != QThread::currentThread()) - nhlog::db()->warn("{} called from a different thread!", __func__); - - if (id.empty()) - return nullptr; - - IdIndex index{room_id_, std::move(id)}; - if (resolve_edits) { - 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); - } - } + if (this->thread() != QThread::currentThread()) + nhlog::db()->warn("{} called from a different thread!", __func__); - auto event_ptr = events_by_id_.object(index); - if (!event_ptr) { - auto event = cache::client()->getEvent(room_id_, index.id); - if (!event) { - http::client()->get_event( - room_id_, - index.id, - [this, relatedTo = std::string(related_to), id = index.id]( - const mtx::events::collections::TimelineEvents &timeline, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error( - "Failed to retrieve event with id {}, which was " - "requested to show the replyTo for event {}", - relatedTo, - id); - return; - } - emit eventFetched(id, relatedTo, timeline); - }); - return nullptr; - } - event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); - events_by_id_.insert(index, event_ptr); + if (id.empty()) + return nullptr; + + IdIndex index{room_id_, std::move(id)}; + if (resolve_edits) { + 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) { + http::client()->get_event(room_id_, + index.id, + [this, relatedTo = std::string(related_to), id = index.id]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "Failed to retrieve event with id {}, which was " + "requested to show the replyTo for event {}", + relatedTo, + id); + return; + } + emit eventFetched(id, relatedTo, timeline); + }); + return nullptr; + } + event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data)); + events_by_id_.insert(index, event_ptr); + } - if (decrypt) { - if (auto encrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - event_ptr)) { - auto decrypted = decryptEvent(index, *encrypted); - if (decrypted->event) - return &*decrypted->event; - } + if (decrypt) { + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + if (decrypted->event) + return &*decrypted->event; } + } - return event_ptr; + 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 (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); - } + if (id.empty()) + return olm::DecryptionErrorCode::NoError; - 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); + 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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) { - auto decrypted = decryptEvent(index, *encrypted); - return decrypted->error; - } + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) { + auto decrypted = decryptEvent(index, *encrypted); + return decrypted->error; + } - return olm::DecryptionErrorCode::NoError; + return olm::DecryptionErrorCode::NoError; } void EventStore::fetchMore() { - if (noMoreMessages) - return; - - mtx::http::MessagesOpts opts; - opts.room_id = room_id_; - opts.from = cache::client()->previousBatchToken(room_id_); - - nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (cache::client()->previousBatchToken(room_id_) != opts.from) { - nhlog::net()->warn("Cache cleared while fetching more messages, dropping " - "/messages response"); - if (!opts.to.empty()) - emit fetchedMore(); - return; - } - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error, - err->parse_error); - emit fetchedMore(); - return; - } - - emit oldMessagesRetrieved(std::move(res)); - }); + if (noMoreMessages) + return; + + mtx::http::MessagesOpts opts; + opts.room_id = room_id_; + opts.from = cache::client()->previousBatchToken(room_id_); + + nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (cache::client()->previousBatchToken(room_id_) != opts.from) { + nhlog::net()->warn("Cache cleared while fetching more messages, dropping " + "/messages response"); + if (!opts.to.empty()) + emit fetchedMore(); + return; + } + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error, + err->parse_error); + emit fetchedMore(); + return; + } + + emit oldMessagesRetrieved(std::move(res)); + }); } diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index 59c1c7c0..53dbaff4 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -20,133 +20,131 @@ class EventStore : public QObject { - Q_OBJECT + Q_OBJECT public: - EventStore(std::string room_id, QObject *parent); - - // taken from QtPrivate::QHashCombine - static uint hashCombine(uint hash, uint seed) - { - return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2)); - }; - struct Index + EventStore(std::string room_id, QObject *parent); + + // taken from QtPrivate::QHashCombine + static uint hashCombine(uint hash, uint seed) + { + return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2)); + }; + struct Index + { + std::string room; + uint64_t idx; + + friend uint qHash(const Index &i, uint seed = 0) noexcept { - std::string room; - uint64_t idx; - - friend uint qHash(const Index &i, uint seed = 0) noexcept - { - seed = - hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed); - seed = hashCombine(qHash(i.idx, seed), seed); - return seed; - } - - friend bool operator==(const Index &a, const Index &b) noexcept - { - return a.idx == b.idx && a.room == b.room; - } - }; - struct IdIndex + seed = hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed); + seed = hashCombine(qHash(i.idx, seed), seed); + return seed; + } + + friend bool operator==(const Index &a, const Index &b) noexcept { - std::string room, id; - - friend uint qHash(const IdIndex &i, uint seed = 0) noexcept - { - seed = - hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed); - seed = hashCombine(qHashBits(i.id.data(), (int)i.id.size(), seed), seed); - return seed; - } - - friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept - { - return a.id == b.id && a.room == b.room; - } - }; - - void fetchMore(); - void handleSync(const mtx::responses::Timeline &events); - - // optionally returns the event or nullptr and fetches it, after which it emits a - // relatedFetched event - mtx::events::collections::TimelineEvents *get(std::string id, - std::string_view related_to, - bool decrypt = true, - bool resolve_edits = true); - // always returns a proper event as long as the idx is valid - 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<mtx::events::msg::Encrypted> &ev, - bool manual); - - int size() const + return a.idx == b.idx && a.room == b.room; + } + }; + struct IdIndex + { + std::string room, id; + + friend uint qHash(const IdIndex &i, uint seed = 0) noexcept { - return (last != std::numeric_limits<uint64_t>::max() && last >= first) - ? static_cast<int>(last - first) + 1 - : 0; + seed = hashCombine(qHashBits(i.room.data(), (int)i.room.size(), seed), seed); + seed = hashCombine(qHashBits(i.id.data(), (int)i.id.size(), seed), seed); + return seed; } - int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); } - uint64_t toInternalIdx(int idx) const { return first + idx; } - std::optional<int> idToIndex(std::string_view id) const; - std::optional<std::string> indexToId(int idx) const; + friend bool operator==(const IdIndex &a, const IdIndex &b) noexcept + { + return a.id == b.id && a.room == b.room; + } + }; + + void fetchMore(); + void handleSync(const mtx::responses::Timeline &events); + + // optionally returns the event or nullptr and fetches it, after which it emits a + // relatedFetched event + mtx::events::collections::TimelineEvents *get(std::string id, + std::string_view related_to, + bool decrypt = true, + bool resolve_edits = true); + // always returns a proper event as long as the idx is valid + 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<mtx::events::msg::Encrypted> &ev, + bool manual); + + int size() const + { + return (last != std::numeric_limits<uint64_t>::max() && last >= first) + ? static_cast<int>(last - first) + 1 + : 0; + } + int toExternalIdx(uint64_t idx) const { return static_cast<int>(idx - first); } + uint64_t toInternalIdx(int idx) const { return first + idx; } + + std::optional<int> idToIndex(std::string_view id) const; + std::optional<std::string> indexToId(int idx) const; signals: - void beginInsertRows(int from, int to); - void endInsertRows(); - void beginResetModel(); - void endResetModel(); - void dataChanged(int from, int to); - void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); - void eventFetched(std::string id, - std::string relatedTo, - mtx::events::collections::TimelineEvents timeline); - void oldMessagesRetrieved(const mtx::responses::Messages &); - void fetchedMore(); - - void processPending(); - void messageSent(std::string txn_id, std::string event_id); - void messageFailed(std::string txn_id); - void startDMVerification( - const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg); - void updateFlowEventId(std::string event_id); + void beginInsertRows(int from, int to); + void endInsertRows(); + void beginResetModel(); + void endResetModel(); + void dataChanged(int from, int to); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); + void eventFetched(std::string id, + std::string relatedTo, + mtx::events::collections::TimelineEvents timeline); + void oldMessagesRetrieved(const mtx::responses::Messages &); + void fetchedMore(); + + void processPending(); + void messageSent(std::string txn_id, std::string event_id); + void messageFailed(std::string txn_id); + void startDMVerification( + const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg); + void updateFlowEventId(std::string event_id); public slots: - void addPending(mtx::events::collections::TimelineEvents event); - void receivedSessionKey(const std::string &session_id); - void clearTimeline(); - void enableKeyRequests(bool suppressKeyRequests_); + void addPending(mtx::events::collections::TimelineEvents event); + void receivedSessionKey(const std::string &session_id); + void clearTimeline(); + 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); - void handle_room_verification(mtx::events::collections::TimelineEvents event); - - std::string room_id_; - - uint64_t first = std::numeric_limits<uint64_t>::max(), - last = std::numeric_limits<uint64_t>::max(); - - static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_; - static QCache<Index, mtx::events::collections::TimelineEvents> events_; - static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_; - - struct PendingKeyRequests - { - std::string request_id; - std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events; - qint64 requested_at; - }; - std::map<std::string, PendingKeyRequests> pending_key_requests; - - std::string current_txn; - int current_txn_error_count = 0; - bool noMoreMessages = false; - bool suppressKeyRequests = true; + 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); + void handle_room_verification(mtx::events::collections::TimelineEvents event); + + std::string room_id_; + + uint64_t first = std::numeric_limits<uint64_t>::max(), + last = std::numeric_limits<uint64_t>::max(); + + static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_; + static QCache<Index, mtx::events::collections::TimelineEvents> events_; + static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_; + + struct PendingKeyRequests + { + std::string request_id; + std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events; + qint64 requested_at; + }; + std::map<std::string, PendingKeyRequests> pending_key_requests; + + std::string current_txn; + int current_txn_error_count = 0; + bool noMoreMessages = false; + bool suppressKeyRequests = true; }; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index a6fbab78..f0c38c84 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -43,374 +43,370 @@ static constexpr size_t INPUT_HISTORY_SIZE = 10; void InputBar::paste(bool fromMouse) { - const QMimeData *md = nullptr; + const QMimeData *md = nullptr; - if (fromMouse) { - if (QGuiApplication::clipboard()->supportsSelection()) { - md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection); - } - } else { - md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard); + if (fromMouse) { + if (QGuiApplication::clipboard()->supportsSelection()) { + md = QGuiApplication::clipboard()->mimeData(QClipboard::Selection); } + } else { + md = QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard); + } - if (md) - insertMimeData(md); + if (md) + insertMimeData(md); } void InputBar::insertMimeData(const QMimeData *md) { - if (!md) - return; - - 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 (!md) + return; + + 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; - } + 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; - } - } + 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()); - } - } else if (md->hasText()) { - emit insertText(md->text()); + if (!path.isEmpty()) { + showPreview(*md, path, formats); } else { - nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString()); + nhlog::ui()->warn("Clipboard does not contain any valid file paths: {}", + data.join(", ").toStdString()); } + } else if (md->hasText()) { + emit insertText(md->text()); + } else { + nhlog::ui()->debug("formats: {}", md->formats().join(", ").toStdString()); + } } void InputBar::updateAtRoom(const QString &t) { - bool roomMention = false; - - if (t.size() > 4) { - QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, t); - - finder.toStart(); - do { - auto start = finder.position(); - finder.toNextBoundary(); - auto end = finder.position(); - if (start > 0 && end - start >= 4 && - t.midRef(start, end - start) == "room" && - t.at(start - 1) == QChar('@')) { - roomMention = true; - break; - } - } while (finder.position() < t.size()); - } - - if (roomMention != this->containsAtRoom_) { - this->containsAtRoom_ = roomMention; - emit containsAtRoomChanged(); - } + bool roomMention = false; + + if (t.size() > 4) { + QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, t); + + finder.toStart(); + do { + auto start = finder.position(); + finder.toNextBoundary(); + auto end = finder.position(); + if (start > 0 && end - start >= 4 && t.midRef(start, end - start) == "room" && + t.at(start - 1) == QChar('@')) { + roomMention = true; + break; + } + } while (finder.position() < t.size()); + } + + if (roomMention != this->containsAtRoom_) { + this->containsAtRoom_ = roomMention; + emit containsAtRoomChanged(); + } } void InputBar::setText(QString newText) { - if (history_.empty()) - history_.push_front(newText); - else - history_.front() = newText; - history_index_ = 0; + if (history_.empty()) + history_.push_front(newText); + else + history_.front() = newText; + history_index_ = 0; - if (history_.size() == INPUT_HISTORY_SIZE) - history_.pop_back(); + if (history_.size() == INPUT_HISTORY_SIZE) + history_.pop_back(); - emit textChanged(newText); + emit textChanged(newText); } void InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_) { - if (text_.isEmpty()) - stopTyping(); - else - startTyping(); + if (text_.isEmpty()) + stopTyping(); + else + startTyping(); - if (text_ != text()) { - if (history_.empty()) - history_.push_front(text_); - else - history_.front() = text_; - history_index_ = 0; + if (text_ != text()) { + if (history_.empty()) + history_.push_front(text_); + else + history_.front() = text_; + history_index_ = 0; - updateAtRoom(text_); - } + updateAtRoom(text_); + } - selectionStart = selectionStart_; - selectionEnd = selectionEnd_; - cursorPosition = cursorPosition_; + selectionStart = selectionStart_; + selectionEnd = selectionEnd_; + cursorPosition = cursorPosition_; } QString InputBar::text() const { - if (history_index_ < history_.size()) - return history_.at(history_index_); + if (history_index_ < history_.size()) + return history_.at(history_index_); - return ""; + return ""; } QString InputBar::previousText() { - history_index_++; - if (history_index_ >= INPUT_HISTORY_SIZE) - history_index_ = INPUT_HISTORY_SIZE; - else if (text().isEmpty()) - history_index_--; - - updateAtRoom(text()); - return text(); + history_index_++; + if (history_index_ >= INPUT_HISTORY_SIZE) + history_index_ = INPUT_HISTORY_SIZE; + else if (text().isEmpty()) + history_index_--; + + updateAtRoom(text()); + return text(); } QString InputBar::nextText() { - history_index_--; - if (history_index_ >= INPUT_HISTORY_SIZE) - history_index_ = 0; + history_index_--; + if (history_index_ >= INPUT_HISTORY_SIZE) + history_index_ = 0; - updateAtRoom(text()); - return text(); + updateAtRoom(text()); + return text(); } void InputBar::send() { - if (text().trimmed().isEmpty()) - return; - - nhlog::ui()->debug("Send: {}", text().toStdString()); - - auto wasEdit = !room->edit().isEmpty(); - - if (text().startsWith('/')) { - int command_end = text().indexOf(QRegularExpression("\\s")); - if (command_end == -1) - command_end = text().size(); - auto name = text().mid(1, command_end - 1); - auto args = text().mid(command_end + 1); - if (name.isEmpty() || name == "/") { - message(args); - } else { - command(name, args); - } - } else { - message(text()); - } + if (text().trimmed().isEmpty()) + return; - if (!wasEdit) { - history_.push_front(""); - setText(""); + nhlog::ui()->debug("Send: {}", text().toStdString()); + + auto wasEdit = !room->edit().isEmpty(); + + if (text().startsWith('/')) { + int command_end = text().indexOf(QRegularExpression("\\s")); + if (command_end == -1) + command_end = text().size(); + auto name = text().mid(1, command_end - 1); + auto args = text().mid(command_end + 1); + if (name.isEmpty() || name == "/") { + message(args); + } else { + command(name, args); } + } else { + message(text()); + } + + if (!wasEdit) { + history_.push_front(""); + setText(""); + } } 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 (*)")); + 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; + if (fileName.isEmpty()) + return; - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - QFile file{fileName}; + QFile file{fileName}; - if (!file.open(QIODevice::ReadOnly)) { - emit ChatPage::instance()->showNotification( - QString("Error while reading media: %1").arg(file.errorString())); - return; - } + if (!file.open(QIODevice::ReadOnly)) { + emit ChatPage::instance()->showNotification( + QString("Error while reading media: %1").arg(file.errorString())); + return; + } - setUploading(true); + setUploading(true); - auto bin = file.readAll(); + auto bin = file.readAll(); - QMimeData data; - data.setData(mime.name(), bin); + QMimeData data; + data.setData(mime.name(), bin); - showPreview(data, fileName, QStringList{mime.name()}); + showPreview(data, fileName, QStringList{mime.name()}); } void InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify) { - mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + + if ((ChatPage::instance()->userSettings()->markdown() && + useMarkdown == MarkdownOverride::NOT_SPECIFIED) || + useMarkdown == MarkdownOverride::ON) { + text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString(); + // Remove markdown links by completer + text.body = msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString(); + + // 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"; + } - if ((ChatPage::instance()->userSettings()->markdown() && - useMarkdown == MarkdownOverride::NOT_SPECIFIED) || - useMarkdown == MarkdownOverride::ON) { - text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString(); - // Remove markdown links by completer - text.body = - msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString(); - - // 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"; + if (!room->edit().isEmpty()) { + if (!room->reply().isEmpty()) { + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); } - 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()) { - 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").arg(line); - } - } + text.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + + } else if (!room->reply().isEmpty()) { + 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").arg(line); + } + } - text.body = QString("%1\n%2").arg(body).arg(msg).toStdString(); + 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"; - if ((ChatPage::instance()->userSettings()->markdown() && - useMarkdown == MarkdownOverride::NOT_SPECIFIED) || - useMarkdown == MarkdownOverride::ON) - text.formatted_body = utils::getFormattedQuoteBody( - related, utils::markdownToHtml(msg, rainbowify)) - .toStdString(); - else - text.formatted_body = - utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); + // NOTE(Nico): rich replies always need a formatted_body! + text.format = "org.matrix.custom.html"; + if ((ChatPage::instance()->userSettings()->markdown() && + useMarkdown == MarkdownOverride::NOT_SPECIFIED) || + useMarkdown == MarkdownOverride::ON) + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg, rainbowify)) + .toStdString(); + else + text.formatted_body = + utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); - text.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, related.related_event}); - } + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, related.related_event}); + } - room->sendMessageEvent(text, mtx::events::EventType::RoomMessage); + room->sendMessageEvent(text, mtx::events::EventType::RoomMessage); } void InputBar::emote(QString msg, bool rainbowify) { - auto html = utils::markdownToHtml(msg, rainbowify); - - 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"; - // Remove markdown links by completer - emote.body = - msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString(); - } - - if (!room->reply().isEmpty()) { - emote.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - emote.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); + auto html = utils::markdownToHtml(msg, rainbowify); + + 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"; + // Remove markdown links by completer + emote.body = + msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString(); + } + + if (!room->reply().isEmpty()) { + emote.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + emote.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); } 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); + 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); } void @@ -422,29 +418,29 @@ InputBar::image(const QString &filename, 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()) { - image.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - image.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); + 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()) { + image.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + image.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); } void @@ -454,26 +450,26 @@ InputBar::file(const QString &filename, 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()) { - file.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - file.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); + 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()) { + file.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + file.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); } void @@ -483,27 +479,27 @@ InputBar::audio(const QString &filename, 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()) { - audio.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - audio.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); + 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()) { + audio.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + audio.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); } void @@ -513,320 +509,310 @@ InputBar::video(const QString &filename, 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()) { - video.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - video.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); + 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()) { + video.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + video.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); } void InputBar::sticker(CombinedImagePackModel *model, int row) { - if (!model || row < 0) - return; - - auto img = model->imageAt(row); - - mtx::events::msg::StickerImage sticker{}; - sticker.info = img.info.value_or(mtx::common::ImageInfo{}); - sticker.url = img.url; - sticker.body = img.body; - - // workaround for https://github.com/vector-im/element-ios/issues/2353 - sticker.info.thumbnail_url = sticker.url; - sticker.info.thumbnail_info.mimetype = sticker.info.mimetype; - sticker.info.thumbnail_info.size = sticker.info.size; - sticker.info.thumbnail_info.h = sticker.info.h; - sticker.info.thumbnail_info.w = sticker.info.w; - - if (!room->reply().isEmpty()) { - sticker.relations.relations.push_back( - {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); - } - if (!room->edit().isEmpty()) { - sticker.relations.relations.push_back( - {mtx::common::RelationType::Replace, room->edit().toStdString()}); - } - - room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); + if (!model || row < 0) + return; + + auto img = model->imageAt(row); + + mtx::events::msg::StickerImage sticker{}; + sticker.info = img.info.value_or(mtx::common::ImageInfo{}); + sticker.url = img.url; + sticker.body = img.body; + + // workaround for https://github.com/vector-im/element-ios/issues/2353 + sticker.info.thumbnail_url = sticker.url; + sticker.info.thumbnail_info.mimetype = sticker.info.mimetype; + sticker.info.thumbnail_info.size = sticker.info.size; + sticker.info.thumbnail_info.h = sticker.info.h; + sticker.info.thumbnail_info.w = sticker.info.w; + + if (!room->reply().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + + room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); } void InputBar::command(QString command, QString args) { - if (command == "me") { - emote(args, false); - } else if (command == "react") { - auto eventId = room->reply(); - if (!eventId.isEmpty()) - reaction(eventId, args.trimmed()); - } else if (command == "join") { - ChatPage::instance()->joinRoom(args); - } else if (command == "part" || command == "leave") { - MainWindow::instance()->openLeaveRoomDialog(room->roomId()); - } 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()); - } else if (command == "md") { - message(args, MarkdownOverride::ON); - } else if (command == "plain") { - message(args, MarkdownOverride::OFF); - } else if (command == "rainbow") { - message(args, MarkdownOverride::ON, true); - } else if (command == "rainbowme") { - emote(args, true); - } else if (command == "notice") { - notice(args, false); - } else if (command == "rainbownotice") { - notice(args, true); - } else if (command == "goto") { - // Goto has three different modes: - // 1 - Going directly to a given event ID - if (args[0] == '$') { - room->showEvent(args); - return; - } - // 2 - Going directly to a given message index - if (args[0] >= '0' && args[0] <= '9') { - room->showEvent(args); - return; - } - // 3 - Matrix URI handler, as if you clicked the URI - if (ChatPage::instance()->handleMatrixUri(args)) { - return; - } - nhlog::net()->error("Could not resolve goto: {}", args.toStdString()); + if (command == "me") { + emote(args, false); + } else if (command == "react") { + auto eventId = room->reply(); + if (!eventId.isEmpty()) + reaction(eventId, args.trimmed()); + } else if (command == "join") { + ChatPage::instance()->joinRoom(args); + } else if (command == "part" || command == "leave") { + MainWindow::instance()->openLeaveRoomDialog(room->roomId()); + } 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()); + } else if (command == "md") { + message(args, MarkdownOverride::ON); + } else if (command == "plain") { + message(args, MarkdownOverride::OFF); + } else if (command == "rainbow") { + message(args, MarkdownOverride::ON, true); + } else if (command == "rainbowme") { + emote(args, true); + } else if (command == "notice") { + notice(args, false); + } else if (command == "rainbownotice") { + notice(args, true); + } else if (command == "goto") { + // Goto has three different modes: + // 1 - Going directly to a given event ID + if (args[0] == '$') { + room->showEvent(args); + return; + } + // 2 - Going directly to a given message index + if (args[0] >= '0' && args[0] <= '9') { + room->showEvent(args); + return; + } + // 3 - Matrix URI handler, as if you clicked the URI + if (ChatPage::instance()->handleMatrixUri(args)) { + return; } + nhlog::net()->error("Could not resolve goto: {}", args.toStdString()); + } } 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); - - setText(""); - - 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); + 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); + + setText(""); + + 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]; + nhlog::ui()->debug("Mime: {}", mime.toStdString()); + if (mimeClass == "image") { + QImage img = utils::readImage(data); + + dimensions = img.size(); + if (img.height() > 200 && img.width() > 360) + img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); + std::vector<unsigned char> data_; + for (int y = 0; y < img.height(); y++) { + for (int x = 0; x < img.width(); x++) { + auto p = img.pixel(x, y); + 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))); } + } + blurhash = QString::fromStdString( + blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); + } + + 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; + } - QSize dimensions; - QString blurhash; - auto mimeClass = mime.split("/")[0]; - nhlog::ui()->debug("Mime: {}", mime.toStdString()); - if (mimeClass == "image") { - QImage img = utils::readImage(data); - - dimensions = img.size(); - if (img.height() > 200 && img.width() > 360) - img = img.scaled(360, 200, Qt::KeepAspectRatioByExpanding); - std::vector<unsigned char> data_; - for (int y = 0; y < img.height(); y++) { - for (int x = 0; x < img.width(); x++) { - auto p = img.pixel(x, y); - 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))); - } - } - blurhash = QString::fromStdString( - blurhash::encode(data_.data(), img.width(), img.height(), 4, 3)); - } + 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); - 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); - }); - }); + setUploading(false); + }); + }); } 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); - } - }); - } + 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(); + } + typingTimeout_.start(); } void InputBar::stopTyping() { - typingRefresh_.stop(); - typingTimeout_.stop(); + typingRefresh_.stop(); + typingTimeout_.stop(); - if (!ChatPage::instance()->userSettings()->typingNotifications()) - return; + 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); - } - }); + 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); + } + }); } 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; - } - } + auto reactions = room->reactions(reactedEvent.toStdString()); - 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); + 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); + } } diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index cdc66a06..4a0f4401 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -19,105 +19,105 @@ class QStringList; enum class MarkdownOverride { - NOT_SPECIFIED, // no override set - ON, - OFF, + NOT_SPECIFIED, // no override set + ON, + OFF, }; class InputBar : public QObject { - Q_OBJECT - Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) - Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged) - Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_OBJECT + Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged) + Q_PROPERTY(bool containsAtRoom READ containsAtRoom NOTIFY containsAtRoomChanged) + Q_PROPERTY(QString text READ text NOTIFY textChanged) public: - InputBar(TimelineModel *parent) - : QObject() - , room(parent) - { - typingRefresh_.setInterval(10'000); - typingRefresh_.setSingleShot(true); - typingTimeout_.setInterval(5'000); - typingTimeout_.setSingleShot(true); - connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping); - connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); - } + InputBar(TimelineModel *parent) + : QObject() + , room(parent) + { + typingRefresh_.setInterval(10'000); + typingRefresh_.setSingleShot(true); + typingTimeout_.setInterval(5'000); + typingTimeout_.setSingleShot(true); + connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping); + connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping); + } public slots: - QString text() const; - QString previousText(); - QString nextText(); - void setText(QString newText); - - bool containsAtRoom() const { return containsAtRoom_; } - - void send(); - void paste(bool fromMouse); - void insertMimeData(const QMimeData *data); - void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); - void openFileSelection(); - bool uploading() const { return uploading_; } - void message(QString body, - MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, - bool rainbowify = false); - void reaction(const QString &reactedEvent, const QString &reactionKey); - void sticker(CombinedImagePackModel *model, int row); + QString text() const; + QString previousText(); + QString nextText(); + void setText(QString newText); + + bool containsAtRoom() const { return containsAtRoom_; } + + void send(); + void paste(bool fromMouse); + void insertMimeData(const QMimeData *data); + void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); + void openFileSelection(); + bool uploading() const { return uploading_; } + void message(QString body, + MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, + bool rainbowify = false); + void reaction(const QString &reactedEvent, const QString &reactionKey); + void sticker(CombinedImagePackModel *model, int row); private slots: - void startTyping(); - void stopTyping(); + void startTyping(); + void stopTyping(); signals: - void insertText(QString text); - void textChanged(QString newText); - void uploadingChanged(bool value); - void containsAtRoomChanged(); + void insertText(QString text); + void textChanged(QString newText); + void uploadingChanged(bool value); + void containsAtRoomChanged(); private: - void emote(QString body, bool rainbowify); - void notice(QString body, bool rainbowify); - void command(QString name, QString args); - void 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); - void file(const QString &filename, - const std::optional<mtx::crypto::EncryptedFile> &encryptedFile, - const QString &url, - const QString &mime, - uint64_t dsize); - void audio(const QString &filename, - const std::optional<mtx::crypto::EncryptedFile> &file, - const QString &url, - const QString &mime, - uint64_t dsize); - void video(const QString &filename, - const std::optional<mtx::crypto::EncryptedFile> &file, - const QString &url, - const QString &mime, - uint64_t dsize); - - void showPreview(const QMimeData &source, QString path, const QStringList &formats); - void setUploading(bool value) - { - if (value != uploading_) { - uploading_ = value; - emit uploadingChanged(value); - } + void emote(QString body, bool rainbowify); + void notice(QString body, bool rainbowify); + void command(QString name, QString args); + void 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); + void file(const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize); + void audio(const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize); + void video(const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize); + + void showPreview(const QMimeData &source, QString path, const QStringList &formats); + void setUploading(bool value) + { + if (value != uploading_) { + uploading_ = value; + emit uploadingChanged(value); } - - void updateAtRoom(const QString &t); - - QTimer typingRefresh_; - QTimer typingTimeout_; - TimelineModel *room; - std::deque<QString> history_; - std::size_t history_index_ = 0; - int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; - bool uploading_ = false; - bool containsAtRoom_ = false; + } + + void updateAtRoom(const QString &t); + + QTimer typingRefresh_; + QTimer typingTimeout_; + TimelineModel *room; + std::deque<QString> history_; + std::size_t history_index_ = 0; + int selectionStart = 0, selectionEnd = 0, cursorPosition = 0; + bool uploading_ = false; + bool containsAtRoom_ = false; }; diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp index 5dafc325..4e45f2e2 100644 --- a/src/timeline/Permissions.cpp +++ b/src/timeline/Permissions.cpp @@ -12,59 +12,59 @@ Permissions::Permissions(QString roomId, QObject *parent) : QObject(parent) , roomId_(roomId) { - invalidate(); + invalidate(); } void Permissions::invalidate() { - pl = cache::client() - ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString()) - .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) - .content; + pl = cache::client() + ->getStateEvent<mtx::events::state::PowerLevels>(roomId_.toStdString()) + .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) + .content; } bool Permissions::canInvite() { - return pl.user_level(http::client()->user_id().to_string()) >= pl.invite; + return pl.user_level(http::client()->user_id().to_string()) >= pl.invite; } bool Permissions::canBan() { - return pl.user_level(http::client()->user_id().to_string()) >= pl.ban; + return pl.user_level(http::client()->user_id().to_string()) >= pl.ban; } bool Permissions::canKick() { - return pl.user_level(http::client()->user_id().to_string()) >= pl.kick; + return pl.user_level(http::client()->user_id().to_string()) >= pl.kick; } bool Permissions::canRedact() { - return pl.user_level(http::client()->user_id().to_string()) >= pl.redact; + return pl.user_level(http::client()->user_id().to_string()) >= pl.redact; } bool Permissions::canChange(int eventType) { - return pl.user_level(http::client()->user_id().to_string()) >= - pl.state_level(to_string(qml_mtx_events::fromRoomEventType( - static_cast<qml_mtx_events::EventType>(eventType)))); + return pl.user_level(http::client()->user_id().to_string()) >= + pl.state_level(to_string( + qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType)))); } bool Permissions::canSend(int eventType) { - return pl.user_level(http::client()->user_id().to_string()) >= - pl.event_level(to_string(qml_mtx_events::fromRoomEventType( - static_cast<qml_mtx_events::EventType>(eventType)))); + return pl.user_level(http::client()->user_id().to_string()) >= + pl.event_level(to_string( + qml_mtx_events::fromRoomEventType(static_cast<qml_mtx_events::EventType>(eventType)))); } bool Permissions::canPingRoom() { - return pl.user_level(http::client()->user_id().to_string()) >= - pl.notification_level(mtx::events::state::notification_keys::room); + return pl.user_level(http::client()->user_id().to_string()) >= + pl.notification_level(mtx::events::state::notification_keys::room); } diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index 349520d5..b80a66aa 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -12,24 +12,24 @@ class TimelineModel; class Permissions : public QObject { - Q_OBJECT + Q_OBJECT public: - Permissions(QString roomId, QObject *parent = nullptr); + Permissions(QString roomId, QObject *parent = nullptr); - Q_INVOKABLE bool canInvite(); - Q_INVOKABLE bool canBan(); - Q_INVOKABLE bool canKick(); + Q_INVOKABLE bool canInvite(); + Q_INVOKABLE bool canBan(); + Q_INVOKABLE bool canKick(); - Q_INVOKABLE bool canRedact(); - Q_INVOKABLE bool canChange(int eventType); - Q_INVOKABLE bool canSend(int eventType); + Q_INVOKABLE bool canRedact(); + Q_INVOKABLE bool canChange(int eventType); + Q_INVOKABLE bool canSend(int eventType); - Q_INVOKABLE bool canPingRoom(); + Q_INVOKABLE bool canPingRoom(); - void invalidate(); + void invalidate(); private: - QString roomId_; - mtx::events::state::PowerLevels pl; + QString roomId_; + mtx::events::state::PowerLevels pl; }; diff --git a/src/timeline/Reaction.h b/src/timeline/Reaction.h index 788e9ced..fcdd61a4 100644 --- a/src/timeline/Reaction.h +++ b/src/timeline/Reaction.h @@ -9,20 +9,20 @@ struct Reaction { - Q_GADGET - Q_PROPERTY(QString key READ key) - Q_PROPERTY(QString users READ users) - Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent) - Q_PROPERTY(int count READ count) + Q_GADGET + Q_PROPERTY(QString key READ key) + Q_PROPERTY(QString users READ users) + Q_PROPERTY(QString selfReactedEvent READ selfReactedEvent) + Q_PROPERTY(int count READ count) public: - QString key() const { return key_.toHtmlEscaped(); } - QString users() const { return users_.toHtmlEscaped(); } - QString selfReactedEvent() const { return selfReactedEvent_; } - int count() const { return count_; } + QString key() const { return key_.toHtmlEscaped(); } + QString users() const { return users_.toHtmlEscaped(); } + QString selfReactedEvent() const { return selfReactedEvent_; } + int count() const { return count_; } - QString key_; - QString users_; - QString selfReactedEvent_; - int count_; + QString key_; + QString users_; + QString selfReactedEvent_; + int count_; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 2d1dd49d..2d60dcb3 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -17,978 +17,947 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) : QAbstractListModel(parent) , manager(parent) { - [[maybe_unused]] static auto id = qRegisterMetaType<RoomPreview>(); - - connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { - auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); - QHash<QString, QSharedPointer<TimelineModel>>::iterator i; - for (i = models.begin(); i != models.end(); ++i) { - auto ptr = i.value(); - - if (!ptr.isNull()) { - ptr->setDecryptDescription(decrypt); - ptr->updateLastMessage(); - } - } - }); - - connect(this, - &RoomlistModel::totalUnreadMessageCountUpdated, - ChatPage::instance(), - &ChatPage::unreadMessages); - - connect( - this, - &RoomlistModel::fetchedPreview, - this, - [this](QString roomid, RoomInfo info) { - if (this->previewedRooms.contains(roomid)) { - this->previewedRooms.insert(roomid, std::move(info)); - auto idx = this->roomidToIndex(roomid); - emit dataChanged(index(idx), - index(idx), - { - Roles::RoomName, - Roles::AvatarUrl, - Roles::IsSpace, - Roles::IsPreviewFetched, - Qt::DisplayRole, - }); - } - }, - Qt::QueuedConnection); + [[maybe_unused]] static auto id = qRegisterMetaType<RoomPreview>(); + + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); + QHash<QString, QSharedPointer<TimelineModel>>::iterator i; + for (i = models.begin(); i != models.end(); ++i) { + auto ptr = i.value(); + + if (!ptr.isNull()) { + ptr->setDecryptDescription(decrypt); + ptr->updateLastMessage(); + } + } + }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); + + connect( + this, + &RoomlistModel::fetchedPreview, + this, + [this](QString roomid, RoomInfo info) { + if (this->previewedRooms.contains(roomid)) { + this->previewedRooms.insert(roomid, std::move(info)); + auto idx = this->roomidToIndex(roomid); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + Roles::AvatarUrl, + Roles::IsSpace, + Roles::IsPreviewFetched, + Qt::DisplayRole, + }); + } + }, + Qt::QueuedConnection); } QHash<int, QByteArray> RoomlistModel::roleNames() const { - return { - {AvatarUrl, "avatarUrl"}, - {RoomName, "roomName"}, - {RoomId, "roomId"}, - {LastMessage, "lastMessage"}, - {Time, "time"}, - {Timestamp, "timestamp"}, - {HasUnreadMessages, "hasUnreadMessages"}, - {HasLoudNotification, "hasLoudNotification"}, - {NotificationCount, "notificationCount"}, - {IsInvite, "isInvite"}, - {IsSpace, "isSpace"}, - {Tags, "tags"}, - {ParentSpaces, "parentSpaces"}, - {IsDirect, "isDirect"}, - {DirectChatOtherUserId, "directChatOtherUserId"}, - }; + return { + {AvatarUrl, "avatarUrl"}, + {RoomName, "roomName"}, + {RoomId, "roomId"}, + {LastMessage, "lastMessage"}, + {Time, "time"}, + {Timestamp, "timestamp"}, + {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, + {NotificationCount, "notificationCount"}, + {IsInvite, "isInvite"}, + {IsSpace, "isSpace"}, + {Tags, "tags"}, + {ParentSpaces, "parentSpaces"}, + {IsDirect, "isDirect"}, + {DirectChatOtherUserId, "directChatOtherUserId"}, + }; } QVariant RoomlistModel::data(const QModelIndex &index, int role) const { - if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { - auto roomid = roomids.at(index.row()); - - if (role == Roles::ParentSpaces) { - auto parents = cache::client()->getParentRoomIds(roomid.toStdString()); - QStringList list; - for (const auto &t : parents) - list.push_back(QString::fromStdString(t)); - return list; - } else if (role == Roles::RoomId) { - return roomid; - } + if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { + auto roomid = roomids.at(index.row()); + + if (role == Roles::ParentSpaces) { + auto parents = cache::client()->getParentRoomIds(roomid.toStdString()); + QStringList list; + for (const auto &t : parents) + list.push_back(QString::fromStdString(t)); + return list; + } else if (role == Roles::RoomId) { + return roomid; + } - if (models.contains(roomid)) { - auto room = models.value(roomid); - switch (role) { - case Roles::AvatarUrl: - return room->roomAvatarUrl(); - case Roles::RoomName: - return room->plainRoomName(); - case Roles::LastMessage: - return room->lastMessage().body; - case Roles::Time: - return room->lastMessage().descriptiveTime; - case Roles::Timestamp: - return QVariant( - static_cast<quint64>(room->lastMessage().timestamp)); - case Roles::HasUnreadMessages: - return this->roomReadStatus.count(roomid) && - this->roomReadStatus.at(roomid); - case Roles::HasLoudNotification: - return room->hasMentions(); - case Roles::NotificationCount: - return room->notificationCount(); - case Roles::IsInvite: - return false; - case Roles::IsSpace: - return room->isSpace(); - case Roles::IsPreview: - return false; - case Roles::Tags: { - auto info = cache::singleRoomInfo(roomid.toStdString()); - QStringList list; - for (const auto &t : info.tags) - list.push_back(QString::fromStdString(t)); - return list; - } - case Roles::IsDirect: - return room->isDirect(); - case Roles::DirectChatOtherUserId: - return room->directChatOtherUserId(); - default: - return {}; - } - } else if (invites.contains(roomid)) { - auto room = invites.value(roomid); - switch (role) { - case Roles::AvatarUrl: - return QString::fromStdString(room.avatar_url); - case Roles::RoomName: - return QString::fromStdString(room.name); - case Roles::LastMessage: - return tr("Pending invite."); - case Roles::Time: - return QString(); - case Roles::Timestamp: - return QVariant(static_cast<quint64>(0)); - case Roles::HasUnreadMessages: - case Roles::HasLoudNotification: - return false; - case Roles::NotificationCount: - return 0; - case Roles::IsInvite: - return true; - case Roles::IsSpace: - return false; - case Roles::IsPreview: - return false; - case Roles::Tags: - return QStringList(); - case Roles::IsDirect: - // The list of users from the room doesn't contain the invited - // users, so we won't factor the invite into the count - return room.member_count == 1; - case Roles::DirectChatOtherUserId: - return cache::getMembersFromInvite(roomid.toStdString(), 0, 1) - .front() - .user_id; - default: - return {}; - } - } else if (previewedRooms.contains(roomid) && - previewedRooms.value(roomid).has_value()) { - auto room = previewedRooms.value(roomid).value(); - switch (role) { - case Roles::AvatarUrl: - return QString::fromStdString(room.avatar_url); - case Roles::RoomName: - return QString::fromStdString(room.name); - case Roles::LastMessage: - return tr("Previewing this room"); - case Roles::Time: - return QString(); - case Roles::Timestamp: - return QVariant(static_cast<quint64>(0)); - case Roles::HasUnreadMessages: - case Roles::HasLoudNotification: - return false; - case Roles::NotificationCount: - return 0; - case Roles::IsInvite: - return false; - case Roles::IsSpace: - return room.is_space; - case Roles::IsPreview: - return true; - case Roles::IsPreviewFetched: - return true; - case Roles::Tags: - return QStringList(); - case Roles::IsDirect: - return false; - case Roles::DirectChatOtherUserId: - return QString{}; // should never be reached - default: - return {}; - } - } else { - if (role == Roles::IsPreview) - return true; - else if (role == Roles::IsPreviewFetched) - return false; - - fetchPreview(roomid); - switch (role) { - case Roles::AvatarUrl: - return QString(); - case Roles::RoomName: - return tr("No preview available"); - case Roles::LastMessage: - return QString(); - case Roles::Time: - return QString(); - case Roles::Timestamp: - return QVariant(static_cast<quint64>(0)); - case Roles::HasUnreadMessages: - case Roles::HasLoudNotification: - return false; - case Roles::NotificationCount: - return 0; - case Roles::IsInvite: - return false; - case Roles::IsSpace: - return false; - case Roles::Tags: - return QStringList(); - default: - return {}; - } - } + if (models.contains(roomid)) { + auto room = models.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->plainRoomName(); + case Roles::LastMessage: + return room->lastMessage().body; + case Roles::Time: + return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast<quint64>(room->lastMessage().timestamp)); + case Roles::HasUnreadMessages: + return this->roomReadStatus.count(roomid) && this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); + case Roles::NotificationCount: + return room->notificationCount(); + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return room->isSpace(); + case Roles::IsPreview: + return false; + case Roles::Tags: { + auto info = cache::singleRoomInfo(roomid.toStdString()); + QStringList list; + for (const auto &t : info.tags) + list.push_back(QString::fromStdString(t)); + return list; + } + case Roles::IsDirect: + return room->isDirect(); + case Roles::DirectChatOtherUserId: + return room->directChatOtherUserId(); + default: + return {}; + } + } else if (invites.contains(roomid)) { + auto room = invites.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::LastMessage: + return tr("Pending invite."); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return true; + case Roles::IsSpace: + return false; + case Roles::IsPreview: + return false; + case Roles::Tags: + return QStringList(); + case Roles::IsDirect: + // The list of users from the room doesn't contain the invited + // users, so we won't factor the invite into the count + return room.member_count == 1; + case Roles::DirectChatOtherUserId: + return cache::getMembersFromInvite(roomid.toStdString(), 0, 1).front().user_id; + default: + return {}; + } + } else if (previewedRooms.contains(roomid) && previewedRooms.value(roomid).has_value()) { + auto room = previewedRooms.value(roomid).value(); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::LastMessage: + return tr("Previewing this room"); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return room.is_space; + case Roles::IsPreview: + return true; + case Roles::IsPreviewFetched: + return true; + case Roles::Tags: + return QStringList(); + case Roles::IsDirect: + return false; + case Roles::DirectChatOtherUserId: + return QString{}; // should never be reached + default: + return {}; + } } else { + if (role == Roles::IsPreview) + return true; + else if (role == Roles::IsPreviewFetched) + return false; + + fetchPreview(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString(); + case Roles::RoomName: + return tr("No preview available"); + case Roles::LastMessage: + return QString(); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: return {}; + } } + } else { + return {}; + } } void RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_) { - std::vector<int> roomsToUpdate; - roomsToUpdate.resize(roomReadStatus_.size()); - for (const auto &[roomid, roomUnread] : roomReadStatus_) { - if (roomUnread != roomReadStatus[roomid]) { - roomsToUpdate.push_back(this->roomidToIndex(roomid)); - } - - this->roomReadStatus[roomid] = roomUnread; + std::vector<int> roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); } - for (auto idx : roomsToUpdate) { - emit dataChanged(index(idx), - index(idx), - { - Roles::HasUnreadMessages, - }); - } + this->roomReadStatus[roomid] = roomUnread; + } + + for (auto idx : roomsToUpdate) { + emit dataChanged(index(idx), + index(idx), + { + Roles::HasUnreadMessages, + }); + } } void RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) { - if (!models.contains(room_id)) { - // ensure we get read status updates and are only connected once - connect(cache::client(), - &Cache::roomReadStatus, - this, - &RoomlistModel::updateReadStatus, - Qt::UniqueConnection); - - QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); - newRoom->setDecryptDescription( - ChatPage::instance()->userSettings()->decryptSidebar()); - - connect(newRoom.data(), - &TimelineModel::newEncryptedImage, - manager->imageProvider(), - &MxcImageProvider::addEncryptionInfo); - connect(newRoom.data(), - &TimelineModel::forwardToRoom, - manager, - &TimelineViewManager::forwardMessageToRoom); - connect( - newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { - auto idx = this->roomidToIndex(room_id); - emit dataChanged(index(idx), - index(idx), - { - Roles::HasLoudNotification, - Roles::LastMessage, - Roles::Timestamp, - Roles::NotificationCount, - Qt::DisplayRole, - }); - }); - connect( - newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { - auto idx = this->roomidToIndex(room_id); - emit dataChanged(index(idx), - index(idx), - { - Roles::AvatarUrl, - }); - }); - connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { - auto idx = this->roomidToIndex(room_id); - emit dataChanged(index(idx), - index(idx), - { - Roles::RoomName, - }); - }); - connect( - newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { - auto idx = this->roomidToIndex(room_id); - emit dataChanged(index(idx), - index(idx), - { - Roles::HasLoudNotification, - Roles::NotificationCount, - Qt::DisplayRole, - }); - - int total_unread_msgs = 0; - - for (const auto &room : models) { - if (!room.isNull()) - total_unread_msgs += room->notificationCount(); - } - - emit totalUnreadMessageCountUpdated(total_unread_msgs); - }); - - newRoom->updateLastMessage(); - - std::vector<QString> previewsToAdd; - if (newRoom->isSpace()) { - auto childs = cache::client()->getChildRoomIds(room_id.toStdString()); - for (const auto &c : childs) { - auto id = QString::fromStdString(c); - if (!(models.contains(id) || invites.contains(id) || - previewedRooms.contains(id))) { - previewsToAdd.push_back(std::move(id)); - } - } - } + if (!models.contains(room_id)) { + // ensure we get read status updates and are only connected once + connect(cache::client(), + &Cache::roomReadStatus, + this, + &RoomlistModel::updateReadStatus, + Qt::UniqueConnection); + + QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); + newRoom->setDecryptDescription(ChatPage::instance()->userSettings()->decryptSidebar()); + + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + manager->imageProvider(), + &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + manager, + &TimelineViewManager::forwardMessageToRoom); + connect(newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::LastMessage, + Roles::Timestamp, + Roles::NotificationCount, + Qt::DisplayRole, + }); + }); + connect(newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::AvatarUrl, + }); + }); + connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + }); + }); + connect(newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::NotificationCount, + Qt::DisplayRole, + }); + + int total_unread_msgs = 0; + + for (const auto &room : models) { + if (!room.isNull()) + total_unread_msgs += room->notificationCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); + }); - bool wasInvite = invites.contains(room_id); - bool wasPreview = previewedRooms.contains(room_id); - if (!suppressInsertNotification && - ((!wasInvite && !wasPreview) || !previewedRooms.empty())) - // if the old room was already in the list, don't add it. Also add all - // previews at the same time. - beginInsertRows(QModelIndex(), - (int)roomids.size(), - (int)(roomids.size() + previewsToAdd.size() - - ((wasInvite || wasPreview) ? 1 : 0))); - - models.insert(room_id, std::move(newRoom)); - if (wasInvite) { - auto idx = roomidToIndex(room_id); - invites.remove(room_id); - emit dataChanged(index(idx), index(idx)); - } else if (wasPreview) { - auto idx = roomidToIndex(room_id); - previewedRooms.remove(room_id); - emit dataChanged(index(idx), index(idx)); - } else { - roomids.push_back(room_id); - } + newRoom->updateLastMessage(); - if ((wasInvite || wasPreview) && currentRoomPreview_ && - currentRoomPreview_->roomid() == room_id) { - currentRoom_ = models.value(room_id); - currentRoomPreview_.reset(); - emit currentRoomChanged(); + std::vector<QString> previewsToAdd; + if (newRoom->isSpace()) { + auto childs = cache::client()->getChildRoomIds(room_id.toStdString()); + for (const auto &c : childs) { + auto id = QString::fromStdString(c); + if (!(models.contains(id) || invites.contains(id) || previewedRooms.contains(id))) { + previewsToAdd.push_back(std::move(id)); } + } + } - for (auto p : previewsToAdd) { - previewedRooms.insert(p, std::nullopt); - roomids.push_back(std::move(p)); - } + bool wasInvite = invites.contains(room_id); + bool wasPreview = previewedRooms.contains(room_id); + if (!suppressInsertNotification && ((!wasInvite && !wasPreview) || !previewedRooms.empty())) + // if the old room was already in the list, don't add it. Also add all + // previews at the same time. + beginInsertRows( + QModelIndex(), + (int)roomids.size(), + (int)(roomids.size() + previewsToAdd.size() - ((wasInvite || wasPreview) ? 1 : 0))); + + models.insert(room_id, std::move(newRoom)); + if (wasInvite) { + auto idx = roomidToIndex(room_id); + invites.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else if (wasPreview) { + auto idx = roomidToIndex(room_id); + previewedRooms.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else { + roomids.push_back(room_id); + } - if (!suppressInsertNotification && - ((!wasInvite && !wasPreview) || !previewedRooms.empty())) - endInsertRows(); + if ((wasInvite || wasPreview) && currentRoomPreview_ && + currentRoomPreview_->roomid() == room_id) { + currentRoom_ = models.value(room_id); + currentRoomPreview_.reset(); + emit currentRoomChanged(); + } - emit ChatPage::instance()->newRoom(room_id); + for (auto p : previewsToAdd) { + previewedRooms.insert(p, std::nullopt); + roomids.push_back(std::move(p)); } + + if (!suppressInsertNotification && ((!wasInvite && !wasPreview) || !previewedRooms.empty())) + endInsertRows(); + + emit ChatPage::instance()->newRoom(room_id); + } } void RoomlistModel::fetchPreview(QString roomid_) const { - std::string roomid = roomid_.toStdString(); - http::client()->get_state_event<mtx::events::state::Create>( - roomid, - "", - [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) { - bool is_space = false; - if (!err) { - is_space = c.type == mtx::events::state::room_type::space; - } - - http::client()->get_state_event<mtx::events::state::Avatar>( - roomid, - "", - [this, roomid, is_space](const mtx::events::state::Avatar &a, - mtx::http::RequestErr) { - auto avatar_url = a.url; - - http::client()->get_state_event<mtx::events::state::Topic>( - roomid, - "", - [this, roomid, avatar_url, is_space]( - const mtx::events::state::Topic &t, mtx::http::RequestErr) { - auto topic = t.topic; - http::client()->get_state_event<mtx::events::state::Name>( - roomid, - "", - [this, roomid, topic, avatar_url, is_space]( - const mtx::events::state::Name &n, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "Failed to fetch name event to " - "create preview for {}", - roomid); - } - - // don't even add a preview, if we got not a single - // response - if (n.name.empty() && avatar_url.empty() && - topic.empty()) - return; - - RoomInfo info{}; - info.name = n.name; - info.is_space = is_space; - info.avatar_url = avatar_url; - info.topic = topic; - - const_cast<RoomlistModel *>(this)->fetchedPreview( - QString::fromStdString(roomid), info); - }); - }); - }); - }); + std::string roomid = roomid_.toStdString(); + http::client()->get_state_event<mtx::events::state::Create>( + roomid, "", [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) { + bool is_space = false; + if (!err) { + is_space = c.type == mtx::events::state::room_type::space; + } + + http::client()->get_state_event<mtx::events::state::Avatar>( + roomid, + "", + [this, roomid, is_space](const mtx::events::state::Avatar &a, mtx::http::RequestErr) { + auto avatar_url = a.url; + + http::client()->get_state_event<mtx::events::state::Topic>( + roomid, + "", + [this, roomid, avatar_url, is_space](const mtx::events::state::Topic &t, + mtx::http::RequestErr) { + auto topic = t.topic; + http::client()->get_state_event<mtx::events::state::Name>( + roomid, + "", + [this, roomid, topic, avatar_url, is_space]( + const mtx::events::state::Name &n, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("Failed to fetch name event to " + "create preview for {}", + roomid); + } + + // don't even add a preview, if we got not a single + // response + if (n.name.empty() && avatar_url.empty() && topic.empty()) + return; + + RoomInfo info{}; + info.name = n.name; + info.is_space = is_space; + info.avatar_url = avatar_url; + info.topic = topic; + + const_cast<RoomlistModel *>(this)->fetchedPreview( + QString::fromStdString(roomid), info); + }); + }); + }); + }); } void RoomlistModel::sync(const mtx::responses::Rooms &rooms) { - for (const auto &[room_id, room] : rooms.join) { - auto qroomid = QString::fromStdString(room_id); - - // addRoom will only add the room, if it doesn't exist - addRoom(qroomid); - const auto &room_model = models.value(qroomid); - room_model->sync(room); - // room_model->addEvents(room.timeline); - connect(room_model.data(), - &TimelineModel::newCallEvent, - manager->callManager(), - &CallManager::syncEvent, - Qt::UniqueConnection); - - if (ChatPage::instance()->userSettings()->typingNotifications()) { - for (const auto &ev : room.ephemeral.events) { - if (auto t = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( - &ev)) { - std::vector<QString> typing; - typing.reserve(t->content.user_ids.size()); - for (const auto &user : t->content.user_ids) { - if (user != http::client()->user_id().to_string()) - typing.push_back( - QString::fromStdString(user)); - } - room_model->updateTypingUsers(typing); - } - } + for (const auto &[room_id, room] : rooms.join) { + auto qroomid = QString::fromStdString(room_id); + + // addRoom will only add the room, if it doesn't exist + addRoom(qroomid); + const auto &room_model = models.value(qroomid); + room_model->sync(room); + // room_model->addEvents(room.timeline); + connect(room_model.data(), + &TimelineModel::newCallEvent, + manager->callManager(), + &CallManager::syncEvent, + Qt::UniqueConnection); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + for (const auto &ev : room.ephemeral.events) { + if (auto t = + std::get_if<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( + &ev)) { + std::vector<QString> typing; + typing.reserve(t->content.user_ids.size()); + for (const auto &user : t->content.user_ids) { + if (user != http::client()->user_id().to_string()) + typing.push_back(QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); } + } } - - for (const auto &[room_id, room] : rooms.leave) { - (void)room; - auto qroomid = QString::fromStdString(room_id); - - if ((currentRoom_ && currentRoom_->roomId() == qroomid) || - (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid)) - resetCurrentRoom(); - - auto idx = this->roomidToIndex(qroomid); - if (idx != -1) { - beginRemoveRows(QModelIndex(), idx, idx); - roomids.erase(roomids.begin() + idx); - if (models.contains(qroomid)) - models.remove(qroomid); - else if (invites.contains(qroomid)) - invites.remove(qroomid); - endRemoveRows(); - } + } + + for (const auto &[room_id, room] : rooms.leave) { + (void)room; + auto qroomid = QString::fromStdString(room_id); + + if ((currentRoom_ && currentRoom_->roomId() == qroomid) || + (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid)) + resetCurrentRoom(); + + auto idx = this->roomidToIndex(qroomid); + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + if (models.contains(qroomid)) + models.remove(qroomid); + else if (invites.contains(qroomid)) + invites.remove(qroomid); + endRemoveRows(); } + } - for (const auto &[room_id, room] : rooms.invite) { - (void)room; - auto qroomid = QString::fromStdString(room_id); - - auto invite = cache::client()->invite(room_id); - if (!invite) - continue; - - if (invites.contains(qroomid)) { - invites[qroomid] = *invite; - auto idx = roomidToIndex(qroomid); - emit dataChanged(index(idx), index(idx)); - } else { - beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); - invites.insert(qroomid, *invite); - roomids.push_back(std::move(qroomid)); - endInsertRows(); - } + for (const auto &[room_id, room] : rooms.invite) { + (void)room; + auto qroomid = QString::fromStdString(room_id); + + auto invite = cache::client()->invite(room_id); + if (!invite) + continue; + + if (invites.contains(qroomid)) { + invites[qroomid] = *invite; + auto idx = roomidToIndex(qroomid); + emit dataChanged(index(idx), index(idx)); + } else { + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + invites.insert(qroomid, *invite); + roomids.push_back(std::move(qroomid)); + endInsertRows(); } + } } void RoomlistModel::initializeRooms() { - beginResetModel(); - models.clear(); - roomids.clear(); - invites.clear(); - currentRoom_ = nullptr; + beginResetModel(); + models.clear(); + roomids.clear(); + invites.clear(); + currentRoom_ = nullptr; - invites = cache::client()->invites(); - for (const auto &id : invites.keys()) - roomids.push_back(id); + invites = cache::client()->invites(); + for (const auto &id : invites.keys()) + roomids.push_back(id); - for (const auto &id : cache::client()->roomIds()) - addRoom(id, true); + for (const auto &id : cache::client()->roomIds()) + addRoom(id, true); - nhlog::db()->info("Restored {} rooms from cache", rowCount()); + nhlog::db()->info("Restored {} rooms from cache", rowCount()); - endResetModel(); + endResetModel(); } void RoomlistModel::clear() { - beginResetModel(); - models.clear(); - invites.clear(); - roomids.clear(); - currentRoom_ = nullptr; - emit currentRoomChanged(); - endResetModel(); + beginResetModel(); + models.clear(); + invites.clear(); + roomids.clear(); + currentRoom_ = nullptr; + emit currentRoomChanged(); + endResetModel(); } void RoomlistModel::joinPreview(QString roomid, QString parentSpace) { - if (previewedRooms.contains(roomid)) { - auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>( - parentSpace.toStdString(), roomid.toStdString()); - ChatPage::instance()->joinRoomVia(roomid.toStdString(), - (child && child->content.via) - ? child->content.via.value() - : std::vector<std::string>{}, - false); - } + if (previewedRooms.contains(roomid)) { + auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>( + parentSpace.toStdString(), roomid.toStdString()); + ChatPage::instance()->joinRoomVia( + roomid.toStdString(), + (child && child->content.via) ? child->content.via.value() : std::vector<std::string>{}, + false); + } } void RoomlistModel::acceptInvite(QString roomid) { - if (invites.contains(roomid)) { - // Don't remove invite yet, so that we can switch to it - ChatPage::instance()->joinRoom(roomid); - } + if (invites.contains(roomid)) { + // Don't remove invite yet, so that we can switch to it + ChatPage::instance()->joinRoom(roomid); + } } void RoomlistModel::declineInvite(QString roomid) { - if (invites.contains(roomid)) { - auto idx = roomidToIndex(roomid); - - if (idx != -1) { - beginRemoveRows(QModelIndex(), idx, idx); - roomids.erase(roomids.begin() + idx); - invites.remove(roomid); - endRemoveRows(); - ChatPage::instance()->leaveRoom(roomid); - } + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); } + } } void RoomlistModel::leave(QString roomid) { - if (models.contains(roomid)) { - auto idx = roomidToIndex(roomid); - - if (idx != -1) { - beginRemoveRows(QModelIndex(), idx, idx); - roomids.erase(roomids.begin() + idx); - models.remove(roomid); - endRemoveRows(); - ChatPage::instance()->leaveRoom(roomid); - } + if (models.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + models.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); } + } } void RoomlistModel::setCurrentRoom(QString roomid) { - if ((currentRoom_ && currentRoom_->roomId() == roomid) || - (currentRoomPreview_ && currentRoomPreview_->roomid() == roomid)) - return; + if ((currentRoom_ && currentRoom_->roomId() == roomid) || + (currentRoomPreview_ && currentRoomPreview_->roomid() == roomid)) + return; + + nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); + if (models.contains(roomid)) { + currentRoom_ = models.value(roomid); + currentRoomPreview_.reset(); + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) { + currentRoom_ = nullptr; + std::optional<RoomInfo> i; - nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); - if (models.contains(roomid)) { - currentRoom_ = models.value(roomid); - currentRoomPreview_.reset(); - emit currentRoomChanged(); - nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); - } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) { - currentRoom_ = nullptr; - std::optional<RoomInfo> i; - - RoomPreview p; - - if (invites.contains(roomid)) { - i = invites.value(roomid); - p.isInvite_ = true; - } else { - i = previewedRooms.value(roomid); - p.isInvite_ = false; - } + RoomPreview p; - if (i) { - p.roomid_ = roomid; - p.roomName_ = QString::fromStdString(i->name); - p.roomTopic_ = QString::fromStdString(i->topic); - p.roomAvatarUrl_ = QString::fromStdString(i->avatar_url); - currentRoomPreview_ = std::move(p); - } + if (invites.contains(roomid)) { + i = invites.value(roomid); + p.isInvite_ = true; + } else { + i = previewedRooms.value(roomid); + p.isInvite_ = false; + } - emit currentRoomChanged(); - nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + if (i) { + p.roomid_ = roomid; + p.roomName_ = QString::fromStdString(i->name); + p.roomTopic_ = QString::fromStdString(i->topic); + p.roomAvatarUrl_ = QString::fromStdString(i->avatar_url); + currentRoomPreview_ = std::move(p); } + + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } } namespace { enum NotificationImportance : short { - ImportanceDisabled = -3, - NoPreview = -2, - Preview = -1, - AllEventsRead = 0, - NewMessage = 1, - NewMentions = 2, - Invite = 3, - SubSpace = 4, - CurrentSpace = 5, + ImportanceDisabled = -3, + NoPreview = -2, + Preview = -1, + AllEventsRead = 0, + NewMessage = 1, + NewMentions = 2, + Invite = 3, + SubSpace = 4, + CurrentSpace = 5, }; } short int FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const { - // Returns the degree of importance of the unread messages in the room. - // If sorting by importance is disabled in settings, this only ever - // returns ImportanceDisabled or Invite - if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) { - if (filterType == FilterBy::Space && - filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString()) - return CurrentSpace; - else - return SubSpace; - } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) { - if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool()) - return Preview; - else - return NoPreview; - } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { - return Invite; - } else if (!this->sortByImportance) { - return ImportanceDisabled; - } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { - return NewMentions; - } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { - return NewMessage; - } else { - return AllEventsRead; - } + // Returns the degree of importance of the unread messages in the room. + // If sorting by importance is disabled in settings, this only ever + // returns ImportanceDisabled or Invite + if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) { + if (filterType == FilterBy::Space && + filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString()) + return CurrentSpace; + else + return SubSpace; + } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) { + if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool()) + return Preview; + else + return NoPreview; + } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + return Invite; + } else if (!this->sortByImportance) { + return ImportanceDisabled; + } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { + return NewMentions; + } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { + return NewMessage; + } else { + return AllEventsRead; + } } bool FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { - QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); - QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); - - // Sort by "importance" (i.e. invites before mentions before - // notifs before new events before old events), then secondly - // by recency. - - // Checking importance first - const auto a_importance = calculateImportance(left_idx); - const auto b_importance = calculateImportance(right_idx); - if (a_importance != b_importance) { - return a_importance > b_importance; - } - - // Now sort by recency - // Zero if empty, otherwise the time that the event occured - uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); - uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); - - if (a_recency != b_recency) - return a_recency > b_recency; - else - return left.row() < right.row(); + QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); + QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); + + // Sort by "importance" (i.e. invites before mentions before + // notifs before new events before old events), then secondly + // by recency. + + // Checking importance first + const auto a_importance = calculateImportance(left_idx); + const auto b_importance = calculateImportance(right_idx); + if (a_importance != b_importance) { + return a_importance > b_importance; + } + + // Now sort by recency + // Zero if empty, otherwise the time that the event occured + uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); + uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); + + if (a_recency != b_recency) + return a_recency > b_recency; + else + return left.row() < right.row(); } FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent) : QSortFilterProxyModel(parent) , roomlistmodel(model) { - this->sortByImportance = UserSettings::instance()->sortByImportance(); - setSourceModel(model); - setDynamicSortFilter(true); - - QObject::connect(UserSettings::instance().get(), - &UserSettings::roomSortingChanged, - this, - [this](bool sortByImportance_) { - this->sortByImportance = sortByImportance_; - invalidate(); - }); - - connect(roomlistmodel, - &RoomlistModel::currentRoomChanged, - this, - &FilteredRoomlistModel::currentRoomChanged); - - sort(0); + this->sortByImportance = UserSettings::instance()->sortByImportance(); + setSourceModel(model); + setDynamicSortFilter(true); + + QObject::connect(UserSettings::instance().get(), + &UserSettings::roomSortingChanged, + this, + [this](bool sortByImportance_) { + this->sortByImportance = sortByImportance_; + invalidate(); + }); + + connect(roomlistmodel, + &RoomlistModel::currentRoomChanged, + this, + &FilteredRoomlistModel::currentRoomChanged); + + sort(0); } void FilteredRoomlistModel::updateHiddenTagsAndSpaces() { - hiddenTags.clear(); - hiddenSpaces.clear(); - for (const auto &t : UserSettings::instance()->hiddenTags()) { - if (t.startsWith("tag:")) - hiddenTags.push_back(t.mid(4)); - else if (t.startsWith("space:")) - hiddenSpaces.push_back(t.mid(6)); - } - - invalidateFilter(); + hiddenTags.clear(); + hiddenSpaces.clear(); + for (const auto &t : UserSettings::instance()->hiddenTags()) { + if (t.startsWith("tag:")) + hiddenTags.push_back(t.mid(4)); + else if (t.startsWith("space:")) + hiddenSpaces.push_back(t.mid(6)); + } + + invalidateFilter(); } bool FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const { - if (filterType == FilterBy::Nothing) { - if (sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) - .toBool()) { - return false; - } + if (filterType == FilterBy::Nothing) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) + .toBool()) { + return false; + } - if (sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) - .toBool()) { - return false; - } + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } - if (!hiddenTags.empty()) { - auto tags = - sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) - .toStringList(); + if (!hiddenTags.empty()) { + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); - for (const auto &t : tags) - if (hiddenTags.contains(t)) - return false; - } + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } - if (!hiddenSpaces.empty()) { - auto parents = - sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) - .toStringList(); - for (const auto &t : parents) - if (hiddenSpaces.contains(t)) - return false; - } + if (!hiddenSpaces.empty()) { + auto parents = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } - return true; - } else if (filterType == FilterBy::Tag) { - if (sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) - .toBool()) { - return false; - } + return true; + } else if (filterType == FilterBy::Tag) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) + .toBool()) { + return false; + } - if (sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) - .toBool()) { - return false; - } + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } - auto tags = sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) - .toStringList(); + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); - if (!tags.contains(filterStr)) - return false; + if (!tags.contains(filterStr)) + return false; - if (!hiddenTags.empty()) { - for (const auto &t : tags) - if (t != filterStr && hiddenTags.contains(t)) - return false; - } + if (!hiddenTags.empty()) { + for (const auto &t : tags) + if (t != filterStr && hiddenTags.contains(t)) + return false; + } - if (!hiddenSpaces.empty()) { - auto parents = - sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) - .toStringList(); - for (const auto &t : parents) - if (hiddenSpaces.contains(t)) - return false; - } + if (!hiddenSpaces.empty()) { + auto parents = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } - return true; - } else if (filterType == FilterBy::Space) { - if (filterStr == sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId) - .toString()) - return true; - - auto parents = - sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) - .toStringList(); - - if (!parents.contains(filterStr)) - return false; - - if (!hiddenTags.empty()) { - auto tags = - sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) - .toStringList(); - - for (const auto &t : tags) - if (hiddenTags.contains(t)) - return false; - } + return true; + } else if (filterType == FilterBy::Space) { + if (filterStr == sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId) + .toString()) + return true; - if (!hiddenSpaces.empty()) { - for (const auto &t : parents) - if (hiddenSpaces.contains(t)) - return false; - } + auto parents = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); - if (sourceModel() - ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) - .toBool() && - !parents.contains(filterStr)) { - return false; - } + if (!parents.contains(filterStr)) + return false; - return true; - } else { - return true; + if (!hiddenTags.empty()) { + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } + + if (!hiddenSpaces.empty()) { + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool() && + !parents.contains(filterStr)) { + return false; } + + return true; + } else { + return true; + } } void FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) { - if (on) { - http::client()->put_tag( - roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error("Failed to add tag: {}, {}", - tag.toStdString(), - err->matrix_error.error); - } - }); - } else { - http::client()->delete_tag( - roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error("Failed to delete tag: {}, {}", - tag.toStdString(), - err->matrix_error.error); - } - }); - } + if (on) { + http::client()->put_tag( + roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error( + "Failed to add tag: {}, {}", tag.toStdString(), err->matrix_error.error); + } + }); + } else { + http::client()->delete_tag( + roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error( + "Failed to delete tag: {}, {}", tag.toStdString(), err->matrix_error.error); + } + }); + } } void FilteredRoomlistModel::nextRoomWithActivity() { - int roomWithMention = -1; - int roomWithNotification = -1; - int roomWithUnreadMessage = -1; - auto r = currentRoom(); - int currentRoomIdx = r ? roomidToIndex(r->roomId()) : -1; - // first look for mentions - for (int i = 0; i < (int)roomlistmodel->roomids.size(); i++) { - if (i == currentRoomIdx) - continue; - if (this->data(index(i, 0), RoomlistModel::HasLoudNotification).toBool()) { - roomWithMention = i; - break; - } - if (roomWithNotification == -1 && - this->data(index(i, 0), RoomlistModel::NotificationCount).toInt() > 0) { - roomWithNotification = i; - // don't break, we must continue looking for rooms with mentions - } - if (roomWithNotification == -1 && roomWithUnreadMessage == -1 && - this->data(index(i, 0), RoomlistModel::HasUnreadMessages).toBool()) { - roomWithUnreadMessage = i; - // don't break, we must continue looking for rooms with mentions - } + int roomWithMention = -1; + int roomWithNotification = -1; + int roomWithUnreadMessage = -1; + auto r = currentRoom(); + int currentRoomIdx = r ? roomidToIndex(r->roomId()) : -1; + // first look for mentions + for (int i = 0; i < (int)roomlistmodel->roomids.size(); i++) { + if (i == currentRoomIdx) + continue; + if (this->data(index(i, 0), RoomlistModel::HasLoudNotification).toBool()) { + roomWithMention = i; + break; } - QString targetRoomId = nullptr; - if (roomWithMention != -1) { - targetRoomId = - this->data(index(roomWithMention, 0), RoomlistModel::RoomId).toString(); - nhlog::ui()->debug("choosing {} for mentions", targetRoomId.toStdString()); - } else if (roomWithNotification != -1) { - targetRoomId = - this->data(index(roomWithNotification, 0), RoomlistModel::RoomId).toString(); - nhlog::ui()->debug("choosing {} for notifications", targetRoomId.toStdString()); - } else if (roomWithUnreadMessage != -1) { - targetRoomId = - this->data(index(roomWithUnreadMessage, 0), RoomlistModel::RoomId).toString(); - nhlog::ui()->debug("choosing {} for unread messages", targetRoomId.toStdString()); + if (roomWithNotification == -1 && + this->data(index(i, 0), RoomlistModel::NotificationCount).toInt() > 0) { + roomWithNotification = i; + // don't break, we must continue looking for rooms with mentions } - if (targetRoomId != nullptr) { - setCurrentRoom(targetRoomId); + if (roomWithNotification == -1 && roomWithUnreadMessage == -1 && + this->data(index(i, 0), RoomlistModel::HasUnreadMessages).toBool()) { + roomWithUnreadMessage = i; + // don't break, we must continue looking for rooms with mentions } + } + QString targetRoomId = nullptr; + if (roomWithMention != -1) { + targetRoomId = this->data(index(roomWithMention, 0), RoomlistModel::RoomId).toString(); + nhlog::ui()->debug("choosing {} for mentions", targetRoomId.toStdString()); + } else if (roomWithNotification != -1) { + targetRoomId = this->data(index(roomWithNotification, 0), RoomlistModel::RoomId).toString(); + nhlog::ui()->debug("choosing {} for notifications", targetRoomId.toStdString()); + } else if (roomWithUnreadMessage != -1) { + targetRoomId = + this->data(index(roomWithUnreadMessage, 0), RoomlistModel::RoomId).toString(); + nhlog::ui()->debug("choosing {} for unread messages", targetRoomId.toStdString()); + } + if (targetRoomId != nullptr) { + setCurrentRoom(targetRoomId); + } } void FilteredRoomlistModel::nextRoom() { - auto r = currentRoom(); - - if (r) { - int idx = roomidToIndex(r->roomId()); - idx++; - if (idx < rowCount()) { - setCurrentRoom( - data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); - } + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx++; + if (idx < rowCount()) { + setCurrentRoom(data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); } + } } void FilteredRoomlistModel::previousRoom() { - auto r = currentRoom(); - - if (r) { - int idx = roomidToIndex(r->roomId()); - idx--; - if (idx >= 0) { - setCurrentRoom( - data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); - } + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx--; + if (idx >= 0) { + setCurrentRoom(data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); } + } } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 27c14bec..458e0fe7 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -20,195 +20,191 @@ class TimelineViewManager; class RoomPreview { - Q_GADGET - Q_PROPERTY(QString roomid READ roomid CONSTANT) - Q_PROPERTY(QString roomName READ roomName CONSTANT) - Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT) - Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT) - Q_PROPERTY(bool isInvite READ isInvite CONSTANT) + Q_GADGET + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString roomName READ roomName CONSTANT) + Q_PROPERTY(QString roomTopic READ roomTopic CONSTANT) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl CONSTANT) + Q_PROPERTY(bool isInvite READ isInvite CONSTANT) public: - RoomPreview() {} + RoomPreview() {} - QString roomid() const { return roomid_; } - QString roomName() const { return roomName_; } - QString roomTopic() const { return roomTopic_; } - QString roomAvatarUrl() const { return roomAvatarUrl_; } - bool isInvite() const { return isInvite_; } + QString roomid() const { return roomid_; } + QString roomName() const { return roomName_; } + QString roomTopic() const { return roomTopic_; } + QString roomAvatarUrl() const { return roomAvatarUrl_; } + bool isInvite() const { return isInvite_; } - QString roomid_, roomName_, roomAvatarUrl_, roomTopic_; - bool isInvite_ = false; + QString roomid_, roomName_, roomAvatarUrl_, roomTopic_; + bool isInvite_ = false; }; class RoomlistModel : public QAbstractListModel { - Q_OBJECT - Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET - resetCurrentRoom) - Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged - RESET resetCurrentRoom) + Q_OBJECT + Q_PROPERTY( + TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom) + Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged + RESET resetCurrentRoom) public: - enum Roles - { - AvatarUrl = Qt::UserRole, - RoomName, - RoomId, - LastMessage, - Time, - Timestamp, - HasUnreadMessages, - HasLoudNotification, - NotificationCount, - IsInvite, - IsSpace, - IsPreview, - IsPreviewFetched, - Tags, - ParentSpaces, - IsDirect, - DirectChatOtherUserId, - }; - - RoomlistModel(TimelineViewManager *parent = nullptr); - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)roomids.size(); - } - QVariant data(const QModelIndex &index, int role) const override; - QSharedPointer<TimelineModel> getRoomById(QString id) const - { - if (models.contains(id)) - return models.value(id); - else - return {}; - } + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomName, + RoomId, + LastMessage, + Time, + Timestamp, + HasUnreadMessages, + HasLoudNotification, + NotificationCount, + IsInvite, + IsSpace, + IsPreview, + IsPreviewFetched, + Tags, + ParentSpaces, + IsDirect, + DirectChatOtherUserId, + }; + + RoomlistModel(TimelineViewManager *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + QSharedPointer<TimelineModel> getRoomById(QString id) const + { + if (models.contains(id)) + return models.value(id); + else + return {}; + } public slots: - void initializeRooms(); - void sync(const mtx::responses::Rooms &rooms); - void clear(); - int roomidToIndex(QString roomid) - { - for (int i = 0; i < (int)roomids.size(); i++) { - if (roomids[i] == roomid) - return i; - } - - return -1; - } - void joinPreview(QString roomid, QString parentSpace); - void acceptInvite(QString roomid); - void declineInvite(QString roomid); - void leave(QString roomid); - TimelineModel *currentRoom() const { return currentRoom_.get(); } - RoomPreview currentRoomPreview() const - { - return currentRoomPreview_.value_or(RoomPreview{}); - } - void setCurrentRoom(QString roomid); - void resetCurrentRoom() - { - currentRoom_ = nullptr; - currentRoomPreview_.reset(); - emit currentRoomChanged(); + void initializeRooms(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + int roomidToIndex(QString roomid) + { + for (int i = 0; i < (int)roomids.size(); i++) { + if (roomids[i] == roomid) + return i; } + return -1; + } + void joinPreview(QString roomid, QString parentSpace); + void acceptInvite(QString roomid); + void declineInvite(QString roomid); + void leave(QString roomid); + TimelineModel *currentRoom() const { return currentRoom_.get(); } + RoomPreview currentRoomPreview() const { return currentRoomPreview_.value_or(RoomPreview{}); } + void setCurrentRoom(QString roomid); + void resetCurrentRoom() + { + currentRoom_ = nullptr; + currentRoomPreview_.reset(); + emit currentRoomChanged(); + } + private slots: - void updateReadStatus(const std::map<QString, bool> roomReadStatus_); + void updateReadStatus(const std::map<QString, bool> roomReadStatus_); signals: - void totalUnreadMessageCountUpdated(int unreadMessages); - void currentRoomChanged(); - void fetchedPreview(QString roomid, RoomInfo info); + void totalUnreadMessageCountUpdated(int unreadMessages); + void currentRoomChanged(); + void fetchedPreview(QString roomid, RoomInfo info); private: - void addRoom(const QString &room_id, bool suppressInsertNotification = false); - void fetchPreview(QString roomid) const; + void addRoom(const QString &room_id, bool suppressInsertNotification = false); + void fetchPreview(QString roomid) const; - TimelineViewManager *manager = nullptr; - std::vector<QString> roomids; - QHash<QString, RoomInfo> invites; - QHash<QString, QSharedPointer<TimelineModel>> models; - std::map<QString, bool> roomReadStatus; - QHash<QString, std::optional<RoomInfo>> previewedRooms; + TimelineViewManager *manager = nullptr; + std::vector<QString> roomids; + QHash<QString, RoomInfo> invites; + QHash<QString, QSharedPointer<TimelineModel>> models; + std::map<QString, bool> roomReadStatus; + QHash<QString, std::optional<RoomInfo>> previewedRooms; - QSharedPointer<TimelineModel> currentRoom_; - std::optional<RoomPreview> currentRoomPreview_; + QSharedPointer<TimelineModel> currentRoom_; + std::optional<RoomPreview> currentRoomPreview_; - friend class FilteredRoomlistModel; + friend class FilteredRoomlistModel; }; class FilteredRoomlistModel : public QSortFilterProxyModel { - Q_OBJECT - Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET - resetCurrentRoom) - Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged - RESET resetCurrentRoom) + Q_OBJECT + Q_PROPERTY( + TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET resetCurrentRoom) + Q_PROPERTY(RoomPreview currentRoomPreview READ currentRoomPreview NOTIFY currentRoomChanged + RESET resetCurrentRoom) public: - FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; + FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; public slots: - int roomidToIndex(QString roomid) - { - return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) - .row(); - } - void joinPreview(QString roomid) - { - roomlistmodel->joinPreview(roomid, filterType == FilterBy::Space ? filterStr : ""); - } - void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } - void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } - void leave(QString roomid) { roomlistmodel->leave(roomid); } - void toggleTag(QString roomid, QString tag, bool on); - - TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } - RoomPreview currentRoomPreview() const { return roomlistmodel->currentRoomPreview(); } - void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } - void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); } - - void nextRoomWithActivity(); - void nextRoom(); - void previousRoom(); - - void updateFilterTag(QString tagId) - { - if (tagId.startsWith("tag:")) { - filterType = FilterBy::Tag; - filterStr = tagId.mid(4); - } else if (tagId.startsWith("space:")) { - filterType = FilterBy::Space; - filterStr = tagId.mid(6); - } else { - filterType = FilterBy::Nothing; - filterStr.clear(); - } - - invalidateFilter(); + int roomidToIndex(QString roomid) + { + return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))).row(); + } + void joinPreview(QString roomid) + { + roomlistmodel->joinPreview(roomid, filterType == FilterBy::Space ? filterStr : ""); + } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } + void leave(QString roomid) { roomlistmodel->leave(roomid); } + void toggleTag(QString roomid, QString tag, bool on); + + TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } + RoomPreview currentRoomPreview() const { return roomlistmodel->currentRoomPreview(); } + void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } + void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); } + + void nextRoomWithActivity(); + void nextRoom(); + void previousRoom(); + + void updateFilterTag(QString tagId) + { + if (tagId.startsWith("tag:")) { + filterType = FilterBy::Tag; + filterStr = tagId.mid(4); + } else if (tagId.startsWith("space:")) { + filterType = FilterBy::Space; + filterStr = tagId.mid(6); + } else { + filterType = FilterBy::Nothing; + filterStr.clear(); } - void updateHiddenTagsAndSpaces(); + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); signals: - void currentRoomChanged(); + void currentRoomChanged(); private: - short int calculateImportance(const QModelIndex &idx) const; - RoomlistModel *roomlistmodel; - bool sortByImportance = true; - - enum class FilterBy - { - Tag, - Space, - Nothing, - }; - QString filterStr = ""; - FilterBy filterType = FilterBy::Nothing; - QStringList hiddenTags, hiddenSpaces; + short int calculateImportance(const QModelIndex &idx) const; + RoomlistModel *roomlistmodel; + bool sortByImportance = true; + + enum class FilterBy + { + Tag, + Space, + Nothing, + }; + QString filterStr = ""; + FilterBy filterType = FilterBy::Nothing; + QStringList hiddenTags, hiddenSpaces; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 00f6d9df..720a78fe 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -38,288 +38,285 @@ namespace std { inline uint qHash(const std::string &key, uint seed = 0) { - return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed); + return qHash(QByteArray::fromRawData(key.data(), (int)key.length()), seed); } } namespace { struct RoomEventType { - template<class T> - qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) - { - using mtx::events::EventType; - switch (e.type) { - case EventType::RoomKeyRequest: - return qml_mtx_events::EventType::KeyRequest; - case EventType::Reaction: - return qml_mtx_events::EventType::Reaction; - case EventType::RoomAliases: - return qml_mtx_events::EventType::Aliases; - case EventType::RoomAvatar: - return qml_mtx_events::EventType::Avatar; - case EventType::RoomCanonicalAlias: - return qml_mtx_events::EventType::CanonicalAlias; - case EventType::RoomCreate: - return qml_mtx_events::EventType::RoomCreate; - case EventType::RoomEncrypted: - return qml_mtx_events::EventType::Encrypted; - case EventType::RoomEncryption: - return qml_mtx_events::EventType::Encryption; - case EventType::RoomGuestAccess: - return qml_mtx_events::EventType::RoomGuestAccess; - case EventType::RoomHistoryVisibility: - return qml_mtx_events::EventType::RoomHistoryVisibility; - case EventType::RoomJoinRules: - return qml_mtx_events::EventType::RoomJoinRules; - case EventType::RoomMember: - return qml_mtx_events::EventType::Member; - case EventType::RoomMessage: - return qml_mtx_events::EventType::UnknownMessage; - case EventType::RoomName: - return qml_mtx_events::EventType::Name; - case EventType::RoomPowerLevels: - return qml_mtx_events::EventType::PowerLevels; - case EventType::RoomTopic: - return qml_mtx_events::EventType::Topic; - case EventType::RoomTombstone: - return qml_mtx_events::EventType::Tombstone; - case EventType::RoomRedaction: - return qml_mtx_events::EventType::Redaction; - case EventType::RoomPinnedEvents: - return qml_mtx_events::EventType::PinnedEvents; - case EventType::Sticker: - return qml_mtx_events::EventType::Sticker; - case EventType::Tag: - return qml_mtx_events::EventType::Tag; - case EventType::Unsupported: - return qml_mtx_events::EventType::Unsupported; - default: - return qml_mtx_events::EventType::UnknownMessage; - } - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) - { - return qml_mtx_events::EventType::AudioMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) - { - return qml_mtx_events::EventType::EmoteMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) - { - return qml_mtx_events::EventType::FileMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) - { - return qml_mtx_events::EventType::ImageMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) - { - return qml_mtx_events::EventType::NoticeMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) - { - return qml_mtx_events::EventType::TextMessage; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) - { - return qml_mtx_events::EventType::VideoMessage; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &) - { - return qml_mtx_events::EventType::KeyVerificationRequest; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &) - { - return qml_mtx_events::EventType::KeyVerificationStart; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &) - { - return qml_mtx_events::EventType::KeyVerificationMac; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &) - { - return qml_mtx_events::EventType::KeyVerificationAccept; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &) - { - return qml_mtx_events::EventType::KeyVerificationReady; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &) - { - return qml_mtx_events::EventType::KeyVerificationCancel; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &) - { - return qml_mtx_events::EventType::KeyVerificationKey; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &) - { - return qml_mtx_events::EventType::KeyVerificationDone; - } - qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) - { - return qml_mtx_events::EventType::Redacted; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallInvite> &) - { - return qml_mtx_events::EventType::CallInvite; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallAnswer> &) - { - return qml_mtx_events::EventType::CallAnswer; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallHangUp> &) - { - return qml_mtx_events::EventType::CallHangUp; - } - qml_mtx_events::EventType operator()( - const mtx::events::Event<mtx::events::msg::CallCandidates> &) - { - return qml_mtx_events::EventType::CallCandidates; - } - // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return - // ::EventType::LocationMessage; } + template<class T> + qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) + { + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::Reaction: + return qml_mtx_events::EventType::Reaction; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::RoomCreate; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::RoomGuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::RoomHistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::RoomJoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + return qml_mtx_events::EventType::Unsupported; + default: + return qml_mtx_events::EventType::UnknownMessage; + } + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) + { + return qml_mtx_events::EventType::AudioMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) + { + return qml_mtx_events::EventType::EmoteMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) + { + return qml_mtx_events::EventType::FileMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) + { + return qml_mtx_events::EventType::ImageMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) + { + return qml_mtx_events::EventType::NoticeMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) + { + return qml_mtx_events::EventType::TextMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) + { + return qml_mtx_events::EventType::VideoMessage; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &) + { + return qml_mtx_events::EventType::KeyVerificationRequest; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &) + { + return qml_mtx_events::EventType::KeyVerificationStart; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &) + { + return qml_mtx_events::EventType::KeyVerificationMac; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &) + { + return qml_mtx_events::EventType::KeyVerificationAccept; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &) + { + return qml_mtx_events::EventType::KeyVerificationReady; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &) + { + return qml_mtx_events::EventType::KeyVerificationCancel; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &) + { + return qml_mtx_events::EventType::KeyVerificationKey; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &) + { + return qml_mtx_events::EventType::KeyVerificationDone; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) + { + return qml_mtx_events::EventType::Redacted; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallInvite> &) + { + return qml_mtx_events::EventType::CallInvite; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallAnswer> &) + { + return qml_mtx_events::EventType::CallAnswer; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::CallHangUp> &) + { + return qml_mtx_events::EventType::CallHangUp; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::CallCandidates> &) + { + return qml_mtx_events::EventType::CallCandidates; + } + // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return + // ::EventType::LocationMessage; } }; } qml_mtx_events::EventType toRoomEventType(const mtx::events::collections::TimelineEvents &event) { - return std::visit(RoomEventType{}, event); + return std::visit(RoomEventType{}, event); } QString toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) { - return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, - event); + return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, + event); } mtx::events::EventType qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) { - switch (t) { - // Unsupported event - case qml_mtx_events::Unsupported: - return mtx::events::EventType::Unsupported; - - /// m.room_key_request - case qml_mtx_events::KeyRequest: - return mtx::events::EventType::RoomKeyRequest; - /// m.reaction: - case qml_mtx_events::Reaction: - return mtx::events::EventType::Reaction; - /// m.room.aliases - case qml_mtx_events::Aliases: - return mtx::events::EventType::RoomAliases; - /// m.room.avatar - case qml_mtx_events::Avatar: - return mtx::events::EventType::RoomAvatar; - /// m.call.invite - case qml_mtx_events::CallInvite: - return mtx::events::EventType::CallInvite; - /// m.call.answer - case qml_mtx_events::CallAnswer: - return mtx::events::EventType::CallAnswer; - /// m.call.hangup - case qml_mtx_events::CallHangUp: - return mtx::events::EventType::CallHangUp; - /// m.call.candidates - case qml_mtx_events::CallCandidates: - return mtx::events::EventType::CallCandidates; - /// m.room.canonical_alias - case qml_mtx_events::CanonicalAlias: - return mtx::events::EventType::RoomCanonicalAlias; - /// m.room.create - case qml_mtx_events::RoomCreate: - return mtx::events::EventType::RoomCreate; - /// m.room.encrypted. - case qml_mtx_events::Encrypted: - return mtx::events::EventType::RoomEncrypted; - /// m.room.encryption. - case qml_mtx_events::Encryption: - return mtx::events::EventType::RoomEncryption; - /// m.room.guest_access - case qml_mtx_events::RoomGuestAccess: - return mtx::events::EventType::RoomGuestAccess; - /// m.room.history_visibility - case qml_mtx_events::RoomHistoryVisibility: - return mtx::events::EventType::RoomHistoryVisibility; - /// m.room.join_rules - case qml_mtx_events::RoomJoinRules: - return mtx::events::EventType::RoomJoinRules; - /// m.room.member - case qml_mtx_events::Member: - return mtx::events::EventType::RoomMember; - /// m.room.name - case qml_mtx_events::Name: - return mtx::events::EventType::RoomName; - /// m.room.power_levels - case qml_mtx_events::PowerLevels: - return mtx::events::EventType::RoomPowerLevels; - /// m.room.tombstone - case qml_mtx_events::Tombstone: - return mtx::events::EventType::RoomTombstone; - /// m.room.topic - case qml_mtx_events::Topic: - return mtx::events::EventType::RoomTopic; - /// m.room.redaction - case qml_mtx_events::Redaction: - return mtx::events::EventType::RoomRedaction; - /// m.room.pinned_events - case qml_mtx_events::PinnedEvents: - return mtx::events::EventType::RoomPinnedEvents; - // m.sticker - case qml_mtx_events::Sticker: - return mtx::events::EventType::Sticker; - // m.tag - case qml_mtx_events::Tag: - return mtx::events::EventType::Tag; - /// m.room.message - case qml_mtx_events::AudioMessage: - case qml_mtx_events::EmoteMessage: - case qml_mtx_events::FileMessage: - case qml_mtx_events::ImageMessage: - case qml_mtx_events::LocationMessage: - case qml_mtx_events::NoticeMessage: - case qml_mtx_events::TextMessage: - case qml_mtx_events::VideoMessage: - case qml_mtx_events::Redacted: - case qml_mtx_events::UnknownMessage: - case qml_mtx_events::KeyVerificationRequest: - case qml_mtx_events::KeyVerificationStart: - case qml_mtx_events::KeyVerificationMac: - case qml_mtx_events::KeyVerificationAccept: - case qml_mtx_events::KeyVerificationCancel: - case qml_mtx_events::KeyVerificationKey: - case qml_mtx_events::KeyVerificationDone: - case qml_mtx_events::KeyVerificationReady: - return mtx::events::EventType::RoomMessage; - //! m.image_pack, currently im.ponies.room_emotes - case qml_mtx_events::ImagePackInRoom: - return mtx::events::EventType::ImagePackInRoom; - //! m.image_pack, currently im.ponies.user_emotes - case qml_mtx_events::ImagePackInAccountData: - return mtx::events::EventType::ImagePackInAccountData; - //! m.image_pack.rooms, currently im.ponies.emote_rooms - case qml_mtx_events::ImagePackRooms: - return mtx::events::EventType::ImagePackRooms; - default: - return mtx::events::EventType::Unsupported; - }; + switch (t) { + // Unsupported event + case qml_mtx_events::Unsupported: + return mtx::events::EventType::Unsupported; + + /// m.room_key_request + case qml_mtx_events::KeyRequest: + return mtx::events::EventType::RoomKeyRequest; + /// m.reaction: + case qml_mtx_events::Reaction: + return mtx::events::EventType::Reaction; + /// m.room.aliases + case qml_mtx_events::Aliases: + return mtx::events::EventType::RoomAliases; + /// m.room.avatar + case qml_mtx_events::Avatar: + return mtx::events::EventType::RoomAvatar; + /// m.call.invite + case qml_mtx_events::CallInvite: + return mtx::events::EventType::CallInvite; + /// m.call.answer + case qml_mtx_events::CallAnswer: + return mtx::events::EventType::CallAnswer; + /// m.call.hangup + case qml_mtx_events::CallHangUp: + return mtx::events::EventType::CallHangUp; + /// m.call.candidates + case qml_mtx_events::CallCandidates: + return mtx::events::EventType::CallCandidates; + /// m.room.canonical_alias + case qml_mtx_events::CanonicalAlias: + return mtx::events::EventType::RoomCanonicalAlias; + /// m.room.create + case qml_mtx_events::RoomCreate: + return mtx::events::EventType::RoomCreate; + /// m.room.encrypted. + case qml_mtx_events::Encrypted: + return mtx::events::EventType::RoomEncrypted; + /// m.room.encryption. + case qml_mtx_events::Encryption: + return mtx::events::EventType::RoomEncryption; + /// m.room.guest_access + case qml_mtx_events::RoomGuestAccess: + return mtx::events::EventType::RoomGuestAccess; + /// m.room.history_visibility + case qml_mtx_events::RoomHistoryVisibility: + return mtx::events::EventType::RoomHistoryVisibility; + /// m.room.join_rules + case qml_mtx_events::RoomJoinRules: + return mtx::events::EventType::RoomJoinRules; + /// m.room.member + case qml_mtx_events::Member: + return mtx::events::EventType::RoomMember; + /// m.room.name + case qml_mtx_events::Name: + return mtx::events::EventType::RoomName; + /// m.room.power_levels + case qml_mtx_events::PowerLevels: + return mtx::events::EventType::RoomPowerLevels; + /// m.room.tombstone + case qml_mtx_events::Tombstone: + return mtx::events::EventType::RoomTombstone; + /// m.room.topic + case qml_mtx_events::Topic: + return mtx::events::EventType::RoomTopic; + /// m.room.redaction + case qml_mtx_events::Redaction: + return mtx::events::EventType::RoomRedaction; + /// m.room.pinned_events + case qml_mtx_events::PinnedEvents: + return mtx::events::EventType::RoomPinnedEvents; + // m.sticker + case qml_mtx_events::Sticker: + return mtx::events::EventType::Sticker; + // m.tag + case qml_mtx_events::Tag: + return mtx::events::EventType::Tag; + /// m.room.message + case qml_mtx_events::AudioMessage: + case qml_mtx_events::EmoteMessage: + case qml_mtx_events::FileMessage: + case qml_mtx_events::ImageMessage: + case qml_mtx_events::LocationMessage: + case qml_mtx_events::NoticeMessage: + case qml_mtx_events::TextMessage: + case qml_mtx_events::VideoMessage: + case qml_mtx_events::Redacted: + case qml_mtx_events::UnknownMessage: + case qml_mtx_events::KeyVerificationRequest: + case qml_mtx_events::KeyVerificationStart: + case qml_mtx_events::KeyVerificationMac: + case qml_mtx_events::KeyVerificationAccept: + case qml_mtx_events::KeyVerificationCancel: + case qml_mtx_events::KeyVerificationKey: + case qml_mtx_events::KeyVerificationDone: + case qml_mtx_events::KeyVerificationReady: + return mtx::events::EventType::RoomMessage; + //! m.image_pack, currently im.ponies.room_emotes + case qml_mtx_events::ImagePackInRoom: + return mtx::events::EventType::ImagePackInRoom; + //! m.image_pack, currently im.ponies.user_emotes + case qml_mtx_events::ImagePackInAccountData: + return mtx::events::EventType::ImagePackInAccountData; + //! m.image_pack.rooms, currently im.ponies.emote_rooms + case qml_mtx_events::ImagePackRooms: + return mtx::events::EventType::ImagePackRooms; + default: + return mtx::events::EventType::Unsupported; + }; } TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) @@ -329,566 +326,549 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , manager_(manager) , permissions_{room_id} { - lastMessage_.timestamp = 0; - - if (auto create = - cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) - this->isSpace_ = create->content.type == mtx::events::state::room_type::space; - this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - - // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it - // needs to be - connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged); - - connect( - this, - &TimelineModel::redactionFailed, - this, - [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }, - Qt::QueuedConnection); - - connect(this, - &TimelineModel::newMessageToSend, - this, - &TimelineModel::addPendingMessage, - Qt::QueuedConnection); - connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending); - - connect(&events, &EventStore::dataChanged, this, [this](int from, int to) { - relatedEventCacheBuster++; - nhlog::ui()->debug( - "data changed {} to {}", events.size() - to - 1, events.size() - from - 1); - emit dataChanged(index(events.size() - to - 1, 0), - index(events.size() - from - 1, 0)); - }); - - connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) { - int first = events.size() - to; - int last = events.size() - from; - if (from >= events.size()) { - int batch_size = to - from; - first += batch_size; - last += batch_size; - } else { - first -= 1; - last -= 1; - } - nhlog::ui()->debug("begin insert from {} to {}", first, last); - beginInsertRows(QModelIndex(), first, last); - }); - connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); }); - connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); }); - connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); }); - connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); - connect( - &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); - connect(&events, - &EventStore::startDMVerification, - this, - [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) { - ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this); - }); - connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { - this->updateFlowEventId(event_id); - }); - - // When a message is sent, check if the current edit/reply relates to that message, - // and update the event_id so that it points to the sent message and not the pending one. - connect(&events, - &EventStore::messageSent, - this, - [this](std::string txn_id, std::string event_id) { - if (edit_.toStdString() == txn_id) { - edit_ = QString::fromStdString(event_id); - emit editChanged(edit_); - } - if (reply_.toStdString() == txn_id) { - reply_ = QString::fromStdString(event_id); - emit replyChanged(reply_); - } - }); - - connect(manager_, - &TimelineViewManager::initialSyncChanged, - &events, - &EventStore::enableKeyRequests); - - connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged); - connect( - this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged); - connect(cache::client(), - &Cache::verificationStatusChanged, - this, - &TimelineModel::trustlevelChanged); - - showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); + lastMessage_.timestamp = 0; + + if (auto create = + cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) + this->isSpace_ = create->content.type == mtx::events::state::room_type::space; + this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + + // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it + // needs to be + connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged); + + connect( + this, + &TimelineModel::redactionFailed, + this, + [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }, + Qt::QueuedConnection); + + connect(this, + &TimelineModel::newMessageToSend, + this, + &TimelineModel::addPendingMessage, + Qt::QueuedConnection); + connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending); + + connect(&events, &EventStore::dataChanged, this, [this](int from, int to) { + relatedEventCacheBuster++; + nhlog::ui()->debug( + "data changed {} to {}", events.size() - to - 1, events.size() - from - 1); + emit dataChanged(index(events.size() - to - 1, 0), index(events.size() - from - 1, 0)); + }); + + connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) { + int first = events.size() - to; + int last = events.size() - from; + if (from >= events.size()) { + int batch_size = to - from; + first += batch_size; + last += batch_size; + } else { + first -= 1; + last -= 1; + } + nhlog::ui()->debug("begin insert from {} to {}", first, last); + beginInsertRows(QModelIndex(), first, last); + }); + connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); }); + connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); }); + connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); }); + connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); + connect(&events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); + connect(&events, + &EventStore::startDMVerification, + this, + [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) { + ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this); + }); + connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { + this->updateFlowEventId(event_id); + }); + + // When a message is sent, check if the current edit/reply relates to that message, + // and update the event_id so that it points to the sent message and not the pending one. + connect( + &events, &EventStore::messageSent, this, [this](std::string txn_id, std::string event_id) { + if (edit_.toStdString() == txn_id) { + edit_ = QString::fromStdString(event_id); + emit editChanged(edit_); + } + if (reply_.toStdString() == txn_id) { + reply_ = QString::fromStdString(event_id); + emit replyChanged(reply_); + } + }); + + connect( + manager_, &TimelineViewManager::initialSyncChanged, &events, &EventStore::enableKeyRequests); + + connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged); + connect(this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged); + connect( + cache::client(), &Cache::verificationStatusChanged, this, &TimelineModel::trustlevelChanged); + + showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); } QHash<int, QByteArray> TimelineModel::roleNames() const { - return { - {Type, "type"}, - {TypeString, "typeString"}, - {IsOnlyEmoji, "isOnlyEmoji"}, - {Body, "body"}, - {FormattedBody, "formattedBody"}, - {PreviousMessageUserId, "previousMessageUserId"}, - {IsSender, "isSender"}, - {UserId, "userId"}, - {UserName, "userName"}, - {PreviousMessageDay, "previousMessageDay"}, - {Day, "day"}, - {Timestamp, "timestamp"}, - {Url, "url"}, - {ThumbnailUrl, "thumbnailUrl"}, - {Blurhash, "blurhash"}, - {Filename, "filename"}, - {Filesize, "filesize"}, - {MimeType, "mimetype"}, - {OriginalHeight, "originalHeight"}, - {OriginalWidth, "originalWidth"}, - {ProportionalHeight, "proportionalHeight"}, - {EventId, "eventId"}, - {State, "status"}, - {IsEdited, "isEdited"}, - {IsEditable, "isEditable"}, - {IsEncrypted, "isEncrypted"}, - {Trustlevel, "trustlevel"}, - {EncryptionError, "encryptionError"}, - {ReplyTo, "replyTo"}, - {Reactions, "reactions"}, - {RoomId, "roomId"}, - {RoomName, "roomName"}, - {RoomTopic, "roomTopic"}, - {CallType, "callType"}, - {Dump, "dump"}, - {RelatedEventCacheBuster, "relatedEventCacheBuster"}, - }; + return { + {Type, "type"}, + {TypeString, "typeString"}, + {IsOnlyEmoji, "isOnlyEmoji"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {PreviousMessageUserId, "previousMessageUserId"}, + {IsSender, "isSender"}, + {UserId, "userId"}, + {UserName, "userName"}, + {PreviousMessageDay, "previousMessageDay"}, + {Day, "day"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Blurhash, "blurhash"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {OriginalHeight, "originalHeight"}, + {OriginalWidth, "originalWidth"}, + {ProportionalHeight, "proportionalHeight"}, + {EventId, "eventId"}, + {State, "status"}, + {IsEdited, "isEdited"}, + {IsEditable, "isEditable"}, + {IsEncrypted, "isEncrypted"}, + {Trustlevel, "trustlevel"}, + {EncryptionError, "encryptionError"}, + {ReplyTo, "replyTo"}, + {Reactions, "reactions"}, + {RoomId, "roomId"}, + {RoomName, "roomName"}, + {RoomTopic, "roomTopic"}, + {CallType, "callType"}, + {Dump, "dump"}, + {RelatedEventCacheBuster, "relatedEventCacheBuster"}, + }; } int TimelineModel::rowCount(const QModelIndex &parent) const { - Q_UNUSED(parent); - return this->events.size(); + Q_UNUSED(parent); + return this->events.size(); } QVariantMap TimelineModel::getDump(QString eventId, QString relatedTo) const { - if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString())) - return data(*event, Dump).toMap(); - return {}; + if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString())) + return data(*event, Dump).toMap(); + return {}; } QVariant TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const { - using namespace mtx::accessors; - namespace acc = mtx::accessors; - - switch (role) { - case IsSender: - return QVariant(acc::sender(event) == http::client()->user_id().to_string()); - case UserId: - return QVariant(QString::fromStdString(acc::sender(event))); - case UserName: - return QVariant(displayName(QString::fromStdString(acc::sender(event)))); - - case Day: { - QDateTime prevDate = origin_server_ts(event); - prevDate.setTime(QTime()); - return QVariant(prevDate.toMSecsSinceEpoch()); - } - case Timestamp: - return QVariant(origin_server_ts(event)); - case Type: - return QVariant(toRoomEventType(event)); - case TypeString: - return QVariant(toRoomEventTypeString(event)); - case IsOnlyEmoji: { - QString qBody = QString::fromStdString(body(event)); - - QVector<uint> utf32_string = qBody.toUcs4(); - int emojiCount = 0; - - for (auto &code : utf32_string) { - if (utils::codepointIsEmoji(code)) { - emojiCount++; - } else { - return QVariant(0); - } - } - - return QVariant(emojiCount); - } - case Body: - return QVariant( - utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped())); - case FormattedBody: { - const static QRegularExpression replyFallback( - "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); - - auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); - - bool isReply = utils::isReply(event); - - auto formattedBody_ = QString::fromStdString(formatted_body(event)); - if (formattedBody_.isEmpty()) { - auto body_ = QString::fromStdString(body(event)); - - if (isReply) { - while (body_.startsWith("> ")) - body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); - if (body_.startsWith('\n')) - body_ = body_.right(body_.size() - 1); - } - formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); - } else { - if (isReply) - formattedBody_ = formattedBody_.remove(replyFallback); - } - - // TODO(Nico): Don't parse html with a regex - const static QRegularExpression matchImgUri( - "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); - formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); - // Same regex but for single quotes around the src - const static QRegularExpression matchImgUri2( - "(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); - formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); - const static QRegularExpression matchEmoticonHeight( - "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); - formattedBody_.replace(matchEmoticonHeight, - QString("\\1 height=\"%1\"\\3").arg(ascent)); - - return QVariant(utils::replaceEmoji( - utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); - } - case Url: - return QVariant(QString::fromStdString(url(event))); - case ThumbnailUrl: - return QVariant(QString::fromStdString(thumbnail_url(event))); - case Blurhash: - return QVariant(QString::fromStdString(blurhash(event))); - case Filename: - return QVariant(QString::fromStdString(filename(event))); - case Filesize: - return QVariant(utils::humanReadableFileSize(filesize(event))); - case MimeType: - return QVariant(QString::fromStdString(mimetype(event))); - case OriginalHeight: - return QVariant(qulonglong{media_height(event)}); - case OriginalWidth: - return QVariant(qulonglong{media_width(event)}); - case ProportionalHeight: { - auto w = media_width(event); - if (w == 0) - w = 1; - - double prop = media_height(event) / (double)w; - - return QVariant(prop > 0 ? prop : 1.); - } - case EventId: { - if (auto replaces = relations(event).replaces()) - return QVariant(QString::fromStdString(replaces.value())); - else - return QVariant(QString::fromStdString(event_id(event))); - } - case State: { - auto id = QString::fromStdString(event_id(event)); - auto containsOthers = [](const auto &vec) { - for (const auto &e : vec) - if (e.second != http::client()->user_id().to_string()) - return true; - return false; - }; - - // only show read receipts for messages not from us - if (acc::sender(event) != http::client()->user_id().to_string()) - return qml_mtx_events::Empty; - else if (!id.isEmpty() && id[0] == "m") - return qml_mtx_events::Sent; - else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_))) - return qml_mtx_events::Read; - else - return qml_mtx_events::Received; - } - case IsEdited: - return QVariant(relations(event).replaces().has_value()); - case IsEditable: - return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == - http::client()->user_id().to_string()); - case IsEncrypted: { - auto id = event_id(event); - auto encrypted_event = events.get(id, "", false); - return encrypted_event && - std::holds_alternative< - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - *encrypted_event); - } - - case Trustlevel: { - auto id = event_id(event); - auto encrypted_event = events.get(id, "", false); - if (encrypted_event) { - if (auto encrypted = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - &*encrypted_event)) { - return olm::calculate_trust( - encrypted->sender, - MegolmSessionIndex(room_id_.toStdString(), encrypted->content)); - } - } - return crypto::Trust::Unverified; - } - - case EncryptionError: - return events.decryptionError(event_id(event)); + using namespace mtx::accessors; + namespace acc = mtx::accessors; + + switch (role) { + case IsSender: + return QVariant(acc::sender(event) == http::client()->user_id().to_string()); + case UserId: + return QVariant(QString::fromStdString(acc::sender(event))); + case UserName: + return QVariant(displayName(QString::fromStdString(acc::sender(event)))); + + case Day: { + QDateTime prevDate = origin_server_ts(event); + prevDate.setTime(QTime()); + return QVariant(prevDate.toMSecsSinceEpoch()); + } + case Timestamp: + return QVariant(origin_server_ts(event)); + case Type: + return QVariant(toRoomEventType(event)); + case TypeString: + return QVariant(toRoomEventTypeString(event)); + case IsOnlyEmoji: { + QString qBody = QString::fromStdString(body(event)); + + QVector<uint> utf32_string = qBody.toUcs4(); + int emojiCount = 0; + + for (auto &code : utf32_string) { + if (utils::codepointIsEmoji(code)) { + emojiCount++; + } else { + return QVariant(0); + } + } + + return QVariant(emojiCount); + } + case Body: + return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)).toHtmlEscaped())); + case FormattedBody: { + const static QRegularExpression replyFallback( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); + + auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); + + bool isReply = utils::isReply(event); + + auto formattedBody_ = QString::fromStdString(formatted_body(event)); + if (formattedBody_.isEmpty()) { + auto body_ = QString::fromStdString(body(event)); + + if (isReply) { + while (body_.startsWith("> ")) + body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); + if (body_.startsWith('\n')) + body_ = body_.right(body_.size() - 1); + } + formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); + } else { + if (isReply) + formattedBody_ = formattedBody_.remove(replyFallback); + } + + // TODO(Nico): Don't parse html with a regex + const static QRegularExpression matchImgUri("(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); + formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); + // Same regex but for single quotes around the src + const static QRegularExpression matchImgUri2("(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); + formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); + const static QRegularExpression matchEmoticonHeight( + "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); + formattedBody_.replace(matchEmoticonHeight, QString("\\1 height=\"%1\"\\3").arg(ascent)); + + return QVariant( + utils::replaceEmoji(utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); + } + case Url: + return QVariant(QString::fromStdString(url(event))); + case ThumbnailUrl: + return QVariant(QString::fromStdString(thumbnail_url(event))); + case Blurhash: + return QVariant(QString::fromStdString(blurhash(event))); + case Filename: + return QVariant(QString::fromStdString(filename(event))); + case Filesize: + return QVariant(utils::humanReadableFileSize(filesize(event))); + case MimeType: + return QVariant(QString::fromStdString(mimetype(event))); + case OriginalHeight: + return QVariant(qulonglong{media_height(event)}); + case OriginalWidth: + return QVariant(qulonglong{media_width(event)}); + case ProportionalHeight: { + auto w = media_width(event); + if (w == 0) + w = 1; + + double prop = media_height(event) / (double)w; + + return QVariant(prop > 0 ? prop : 1.); + } + case EventId: { + if (auto replaces = relations(event).replaces()) + return QVariant(QString::fromStdString(replaces.value())); + else + return QVariant(QString::fromStdString(event_id(event))); + } + case State: { + auto id = QString::fromStdString(event_id(event)); + auto containsOthers = [](const auto &vec) { + for (const auto &e : vec) + if (e.second != http::client()->user_id().to_string()) + return true; + return false; + }; - case ReplyTo: - return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); - case Reactions: { - auto id = relations(event).replaces().value_or(event_id(event)); - return QVariant::fromValue(events.reactions(id)); - } - case RoomId: - return QVariant(room_id_); - case RoomName: - return QVariant( - utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped())); - case RoomTopic: - return QVariant(utils::replaceEmoji( - utils::linkifyMessage(QString::fromStdString(room_topic(event)) - .toHtmlEscaped() - .replace("\n", "<br>")))); - case CallType: - return QVariant(QString::fromStdString(call_type(event))); - case Dump: { - QVariantMap m; - auto names = roleNames(); - - m.insert(names[Type], data(event, static_cast<int>(Type))); - m.insert(names[TypeString], data(event, static_cast<int>(TypeString))); - m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji))); - m.insert(names[Body], data(event, static_cast<int>(Body))); - m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody))); - m.insert(names[IsSender], data(event, static_cast<int>(IsSender))); - m.insert(names[UserId], data(event, static_cast<int>(UserId))); - m.insert(names[UserName], data(event, static_cast<int>(UserName))); - m.insert(names[Day], data(event, static_cast<int>(Day))); - m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp))); - m.insert(names[Url], data(event, static_cast<int>(Url))); - m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); - m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash))); - m.insert(names[Filename], data(event, static_cast<int>(Filename))); - m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); - m.insert(names[MimeType], data(event, static_cast<int>(MimeType))); - m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight))); - m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth))); - m.insert(names[ProportionalHeight], - data(event, static_cast<int>(ProportionalHeight))); - m.insert(names[EventId], data(event, static_cast<int>(EventId))); - m.insert(names[State], data(event, static_cast<int>(State))); - m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited))); - m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable))); - m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted))); - m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo))); - m.insert(names[RoomName], data(event, static_cast<int>(RoomName))); - m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic))); - m.insert(names[CallType], data(event, static_cast<int>(CallType))); - m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError))); - - return QVariant(m); - } - case RelatedEventCacheBuster: - return relatedEventCacheBuster; - default: - return QVariant(); - } + // only show read receipts for messages not from us + if (acc::sender(event) != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (!id.isEmpty() && id[0] == "m") + return qml_mtx_events::Sent; + else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_))) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + } + case IsEdited: + return QVariant(relations(event).replaces().has_value()); + case IsEditable: + return QVariant(!is_state_event(event) && + mtx::accessors::sender(event) == http::client()->user_id().to_string()); + case IsEncrypted: { + auto id = event_id(event); + auto encrypted_event = events.get(id, "", false); + return encrypted_event && + std::holds_alternative<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + *encrypted_event); + } + + case Trustlevel: { + auto id = event_id(event); + auto encrypted_event = events.get(id, "", false); + if (encrypted_event) { + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &*encrypted_event)) { + return olm::calculate_trust( + encrypted->sender, + MegolmSessionIndex(room_id_.toStdString(), encrypted->content)); + } + } + 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: { + auto id = relations(event).replaces().value_or(event_id(event)); + return QVariant::fromValue(events.reactions(id)); + } + case RoomId: + return QVariant(room_id_); + case RoomName: + return QVariant( + utils::replaceEmoji(QString::fromStdString(room_name(event)).toHtmlEscaped())); + case RoomTopic: + return QVariant(utils::replaceEmoji(utils::linkifyMessage( + QString::fromStdString(room_topic(event)).toHtmlEscaped().replace("\n", "<br>")))); + case CallType: + return QVariant(QString::fromStdString(call_type(event))); + case Dump: { + QVariantMap m; + auto names = roleNames(); + + m.insert(names[Type], data(event, static_cast<int>(Type))); + m.insert(names[TypeString], data(event, static_cast<int>(TypeString))); + m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji))); + m.insert(names[Body], data(event, static_cast<int>(Body))); + m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody))); + m.insert(names[IsSender], data(event, static_cast<int>(IsSender))); + m.insert(names[UserId], data(event, static_cast<int>(UserId))); + m.insert(names[UserName], data(event, static_cast<int>(UserName))); + m.insert(names[Day], data(event, static_cast<int>(Day))); + m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp))); + m.insert(names[Url], data(event, static_cast<int>(Url))); + m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); + m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash))); + m.insert(names[Filename], data(event, static_cast<int>(Filename))); + m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); + m.insert(names[MimeType], data(event, static_cast<int>(MimeType))); + m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight))); + m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth))); + m.insert(names[ProportionalHeight], data(event, static_cast<int>(ProportionalHeight))); + m.insert(names[EventId], data(event, static_cast<int>(EventId))); + m.insert(names[State], data(event, static_cast<int>(State))); + m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited))); + m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable))); + m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted))); + m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo))); + m.insert(names[RoomName], data(event, static_cast<int>(RoomName))); + m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic))); + m.insert(names[CallType], data(event, static_cast<int>(CallType))); + m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError))); + + return QVariant(m); + } + case RelatedEventCacheBuster: + return relatedEventCacheBuster; + default: + return QVariant(); + } } QVariant TimelineModel::data(const QModelIndex &index, int role) const { - using namespace mtx::accessors; - namespace acc = mtx::accessors; - if (index.row() < 0 && index.row() >= rowCount()) - return QVariant(); + using namespace mtx::accessors; + namespace acc = mtx::accessors; + if (index.row() < 0 && index.row() >= rowCount()) + return QVariant(); - // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems - if (index.row() + 1 == rowCount() && !m_paginationInProgress) - const_cast<TimelineModel *>(this)->fetchMore(index); + // HACK(Nico): fetchMore likes to break with dynamically sized delegates and reuseItems + if (index.row() + 1 == rowCount() && !m_paginationInProgress) + const_cast<TimelineModel *>(this)->fetchMore(index); - auto event = events.get(rowCount() - index.row() - 1); + auto event = events.get(rowCount() - index.row() - 1); - if (!event) - return ""; - - if (role == PreviousMessageDay || role == PreviousMessageUserId) { - int prevIdx = rowCount() - index.row() - 2; - if (prevIdx < 0) - return QVariant(); - auto tempEv = events.get(prevIdx); - if (!tempEv) - return QVariant(); - if (role == PreviousMessageUserId) - return data(*tempEv, UserId); - else - return data(*tempEv, Day); - } + if (!event) + return ""; - return data(*event, role); + if (role == PreviousMessageDay || role == PreviousMessageUserId) { + int prevIdx = rowCount() - index.row() - 2; + if (prevIdx < 0) + return QVariant(); + auto tempEv = events.get(prevIdx); + if (!tempEv) + return QVariant(); + if (role == PreviousMessageUserId) + return data(*tempEv, UserId); + else + return data(*tempEv, Day); + } + + return data(*event, role); } QVariant TimelineModel::dataById(QString id, int role, QString relatedTo) { - if (auto event = events.get(id.toStdString(), relatedTo.toStdString())) - return data(*event, role); - return QVariant(); + if (auto event = events.get(id.toStdString(), relatedTo.toStdString())) + return data(*event, role); + return QVariant(); } bool TimelineModel::canFetchMore(const QModelIndex &) const { - if (!events.size()) - return true; - if (auto first = events.get(0); - first && - !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first)) - return true; - else + if (!events.size()) + return true; + if (auto first = events.get(0); + first && + !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first)) + return true; + else - return false; + return false; } void TimelineModel::setPaginationInProgress(const bool paginationInProgress) { - if (m_paginationInProgress == paginationInProgress) { - return; - } + if (m_paginationInProgress == paginationInProgress) { + return; + } - m_paginationInProgress = paginationInProgress; - emit paginationInProgressChanged(m_paginationInProgress); + m_paginationInProgress = paginationInProgress; + emit paginationInProgressChanged(m_paginationInProgress); } void TimelineModel::fetchMore(const QModelIndex &) { - if (m_paginationInProgress) { - nhlog::ui()->warn("Already loading older messages"); - return; - } + if (m_paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } - setPaginationInProgress(true); + setPaginationInProgress(true); - events.fetchMore(); + events.fetchMore(); } void TimelineModel::sync(const mtx::responses::JoinedRoom &room) { - this->syncState(room.state); - this->addEvents(room.timeline); + this->syncState(room.state); + this->addEvents(room.timeline); - if (room.unread_notifications.highlight_count != highlight_count || - room.unread_notifications.notification_count != notification_count) { - notification_count = room.unread_notifications.notification_count; - highlight_count = room.unread_notifications.highlight_count; - emit notificationsChanged(); - } + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } } void TimelineModel::syncState(const mtx::responses::State &s) { - using namespace mtx::events; - - for (const auto &e : s.events) { - if (std::holds_alternative<StateEvent<state::Avatar>>(e)) - emit roomAvatarUrlChanged(); - else if (std::holds_alternative<StateEvent<state::Name>>(e)) - emit roomNameChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) - emit roomTopicChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) { - permissions_.invalidate(); - emit permissionsChanged(); - } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { - emit roomAvatarUrlChanged(); - emit roomNameChanged(); - emit roomMemberCountChanged(); - - if (roomMemberCount() <= 2) { - emit isDirectChanged(); - emit directChatOtherUserIdChanged(); - } - } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { - this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - emit encryptionChanged(); - } - } + using namespace mtx::events; + + for (const auto &e : s.events) { + if (std::holds_alternative<StateEvent<state::Avatar>>(e)) + emit roomAvatarUrlChanged(); + else if (std::holds_alternative<StateEvent<state::Name>>(e)) + emit roomNameChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) + emit roomTopicChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { + emit roomAvatarUrlChanged(); + emit roomNameChanged(); + emit roomMemberCountChanged(); + + if (roomMemberCount() <= 2) { + emit isDirectChanged(); + emit directChatOtherUserIdChanged(); + } + } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { + this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + emit encryptionChanged(); + } + } } void TimelineModel::addEvents(const mtx::responses::Timeline &timeline) { - if (timeline.events.empty()) - return; - - events.handleSync(timeline); - - using namespace mtx::events; - - for (auto e : timeline.events) { - if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { - MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content); - - auto result = olm::decryptEvent(index, *encryptedEvent); - if (result.event) - e = result.event.value(); - } + if (timeline.events.empty()) + return; - if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) || - std::holds_alternative<RoomEvent<msg::CallInvite>>(e) || - std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) || - std::holds_alternative<RoomEvent<msg::CallHangUp>>(e)) - std::visit( - [this](auto &event) { - event.room_id = room_id_.toStdString(); - if constexpr (std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<msg::CallAnswer>> || - std::is_same_v<std::decay_t<decltype(event)>, - RoomEvent<msg::CallHangUp>>) - emit newCallEvent(event); - else { - if (event.sender != http::client()->user_id().to_string()) - emit newCallEvent(event); - } - }, - e); - else if (std::holds_alternative<StateEvent<state::Avatar>>(e)) - emit roomAvatarUrlChanged(); - else if (std::holds_alternative<StateEvent<state::Name>>(e)) - emit roomNameChanged(); - else if (std::holds_alternative<StateEvent<state::Topic>>(e)) - emit roomTopicChanged(); - else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) { - permissions_.invalidate(); - emit permissionsChanged(); - } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { - emit roomAvatarUrlChanged(); - emit roomNameChanged(); - emit roomMemberCountChanged(); - } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { - this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - emit encryptionChanged(); - } - } - updateLastMessage(); + events.handleSync(timeline); + + using namespace mtx::events; + + for (auto e : timeline.events) { + if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { + MegolmSessionIndex index(room_id_.toStdString(), encryptedEvent->content); + + auto result = olm::decryptEvent(index, *encryptedEvent); + if (result.event) + e = result.event.value(); + } + + if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) || + std::holds_alternative<RoomEvent<msg::CallInvite>>(e) || + std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) || + std::holds_alternative<RoomEvent<msg::CallHangUp>>(e)) + std::visit( + [this](auto &event) { + event.room_id = room_id_.toStdString(); + if constexpr (std::is_same_v<std::decay_t<decltype(event)>, + RoomEvent<msg::CallAnswer>> || + std::is_same_v<std::decay_t<decltype(event)>, + RoomEvent<msg::CallHangUp>>) + emit newCallEvent(event); + else { + if (event.sender != http::client()->user_id().to_string()) + emit newCallEvent(event); + } + }, + e); + else if (std::holds_alternative<StateEvent<state::Avatar>>(e)) + emit roomAvatarUrlChanged(); + else if (std::holds_alternative<StateEvent<state::Name>>(e)) + emit roomNameChanged(); + else if (std::holds_alternative<StateEvent<state::Topic>>(e)) + emit roomTopicChanged(); + else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) { + permissions_.invalidate(); + emit permissionsChanged(); + } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { + emit roomAvatarUrlChanged(); + emit roomNameChanged(); + emit roomMemberCountChanged(); + } else if (std::holds_alternative<StateEvent<state::Encryption>>(e)) { + this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + emit encryptionChanged(); + } + } + updateLastMessage(); } template<typename T> @@ -896,1216 +876,1191 @@ auto isMessage(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool> { - return true; + return true; } template<typename T> auto isMessage(const mtx::events::Event<T> &) { - return false; + return false; } template<typename T> auto isMessage(const mtx::events::EncryptedEvent<T> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &) { - return true; + return true; } auto isMessage(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &) { - return true; + return true; } // Workaround. We also want to see a room at the top, if we just joined it auto isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e) { - return e.content.membership == mtx::events::state::Membership::Join && - e.state_key == http::client()->user_id().to_string(); + return e.content.membership == mtx::events::state::Membership::Join && + e.state_key == http::client()->user_id().to_string(); } template<typename T> auto isYourJoin(const mtx::events::Event<T> &) { - return false; + return false; } void TimelineModel::updateLastMessage() { - for (auto it = events.size() - 1; it >= 0; --it) { - auto event = events.get(it, decryptDescription); - if (!event) - continue; - - if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { - auto time = mtx::accessors::origin_server_ts(*event); - uint64_t ts = time.toMSecsSinceEpoch(); - auto description = - DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), - QString::fromStdString(http::client()->user_id().to_string()), - tr("You joined this room."), - utils::descriptiveTime(time), - ts, - time}; - if (description != lastMessage_) { - lastMessage_ = description; - emit lastMessageChanged(); - } - return; - } - if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) - continue; - - auto description = utils::getMessageDescription( - *event, - QString::fromStdString(http::client()->user_id().to_string()), - cache::displayName(room_id_, - QString::fromStdString(mtx::accessors::sender(*event)))); - if (description != lastMessage_) { - lastMessage_ = description; - emit lastMessageChanged(); - } - return; + for (auto it = events.size() - 1; it >= 0; --it) { + auto event = events.get(it, decryptDescription); + if (!event) + continue; + + if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { + auto time = mtx::accessors::origin_server_ts(*event); + uint64_t ts = time.toMSecsSinceEpoch(); + auto description = + DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), + QString::fromStdString(http::client()->user_id().to_string()), + tr("You joined this room."), + utils::descriptiveTime(time), + ts, + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } + return; + } + if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) + continue; + + auto description = utils::getMessageDescription( + *event, + QString::fromStdString(http::client()->user_id().to_string()), + cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); } + return; + } } void TimelineModel::setCurrentIndex(int index) { - auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); - if (index != oldIndex) - emit currentIndexChanged(index); + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + if (index != oldIndex) + emit currentIndexChanged(index); - if (!ChatPage::instance()->isActiveWindow()) - return; + if (!ChatPage::instance()->isActiveWindow()) + return; - if (!currentId.startsWith("m")) { - auto oldReadIndex = - cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); - auto nextEventIndexAndId = - cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); + if (!currentId.startsWith("m")) { + auto oldReadIndex = + cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); + auto nextEventIndexAndId = + cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); - if (nextEventIndexAndId && - (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { - readEvent(nextEventIndexAndId->second); - currentReadId = QString::fromStdString(nextEventIndexAndId->second); - } + if (nextEventIndexAndId && (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { + readEvent(nextEventIndexAndId->second); + currentReadId = QString::fromStdString(nextEventIndexAndId->second); } + } } void TimelineModel::readEvent(const std::string &id) { - http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to read_event ({}, {})", - room_id_.toStdString(), - currentId.toStdString()); - } - }); + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to read_event ({}, {})", room_id_.toStdString(), currentId.toStdString()); + } + }); } QString TimelineModel::displayName(QString id) const { - return cache::displayName(room_id_, id).toHtmlEscaped(); + return cache::displayName(room_id_, id).toHtmlEscaped(); } QString TimelineModel::avatarUrl(QString id) const { - return cache::avatarUrl(room_id_, id); + return cache::avatarUrl(room_id_, id); } QString TimelineModel::formatDateSeparator(QDate date) const { - auto now = QDateTime::currentDateTime(); + auto now = QDateTime::currentDateTime(); - QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() == date.year()) { - QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); - fmt = fmt.remove(rx); - } + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - return date.toString(fmt); + return date.toString(fmt); } void TimelineModel::viewRawMessage(QString id) { - auto e = events.get(id.toStdString(), "", false); - if (!e) - return; - std::string ev = mtx::accessors::serialize_event(*e).dump(4); - emit showRawMessageDialog(QString::fromStdString(ev)); + auto e = events.get(id.toStdString(), "", false); + if (!e) + return; + std::string ev = mtx::accessors::serialize_event(*e).dump(4); + emit showRawMessageDialog(QString::fromStdString(ev)); } void TimelineModel::forwardMessage(QString eventId, QString roomId) { - auto e = events.get(eventId.toStdString(), ""); - if (!e) - return; + auto e = events.get(eventId.toStdString(), ""); + if (!e) + return; - emit forwardToRoom(e, roomId); + emit forwardToRoom(e, roomId); } void TimelineModel::viewDecryptedRawMessage(QString id) { - auto e = events.get(id.toStdString(), ""); - if (!e) - return; + auto e = events.get(id.toStdString(), ""); + if (!e) + return; - std::string ev = mtx::accessors::serialize_event(*e).dump(4); - emit showRawMessageDialog(QString::fromStdString(ev)); + std::string ev = mtx::accessors::serialize_event(*e).dump(4); + emit showRawMessageDialog(QString::fromStdString(ev)); } void TimelineModel::openUserProfile(QString userid) { - UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this); - connect( - this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl); - emit manager_->openProfile(userProfile); + UserProfile *userProfile = new UserProfile(room_id_, userid, manager_, this); + connect(this, &TimelineModel::roomAvatarUrlChanged, userProfile, &UserProfile::updateAvatarUrl); + emit manager_->openProfile(userProfile); } void TimelineModel::replyAction(QString id) { - setReply(id); + setReply(id); } void TimelineModel::editAction(QString id) { - setEdit(id); + setEdit(id); } RelatedInfo TimelineModel::relatedInfo(QString id) { - auto event = events.get(id.toStdString(), ""); - if (!event) - return {}; + auto event = events.get(id.toStdString(), ""); + if (!event) + return {}; - return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_); + return utils::stripReplyFallbacks(*event, id.toStdString(), room_id_); } void TimelineModel::showReadReceipts(QString id) { - emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); + emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); } void TimelineModel::redactEvent(QString id) { - if (!id.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - id.toStdString(), - [this, 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 eventRedacted(id); - }); + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, 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 eventRedacted(id); + }); } int TimelineModel::idToIndex(QString id) const { - if (id.isEmpty()) - return -1; + if (id.isEmpty()) + return -1; - auto idx = events.idToIndex(id.toStdString()); - if (idx) - return events.size() - *idx - 1; - else - return -1; + auto idx = events.idToIndex(id.toStdString()); + if (idx) + return events.size() - *idx - 1; + else + return -1; } QString TimelineModel::indexToId(int index) const { - auto id = events.indexToId(events.size() - index - 1); - return id ? QString::fromStdString(*id) : ""; + auto id = events.indexToId(events.size() - index - 1); + return id ? QString::fromStdString(*id) : ""; } // Note: this will only be called for our messages void TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) { - for (const auto &id : event_ids) { - read.insert(id); - int idx = idToIndex(id); - if (idx < 0) { - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + return; } + emit dataChanged(index(idx, 0), index(idx, 0)); + } } template<typename T> void TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType) { - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json doc = {{"type", mtx::events::to_string(eventType)}, - {"content", json(msg.content)}, - {"room_id", room_id}}; - - try { - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - event.event_id = msg.event_id; - event.room_id = room_id; - event.sender = http::client()->user_id().to_string(); - event.type = mtx::events::EventType::RoomEncrypted; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - - emit this->addPendingMessageToStore(event); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } -} - -struct SendMessageVisitor -{ - explicit SendMessageVisitor(TimelineModel *model) - : model_(model) - {} - - template<typename T, mtx::events::EventType Event> - void sendRoomEvent(mtx::events::RoomEvent<T> msg) - { - if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { - auto encInfo = mtx::accessors::file(msg); - if (encInfo) - emit model_->newEncryptedImage(encInfo.value()); - - model_->sendEncryptedMessage(msg, Event); - } else { - msg.type = Event; - emit model_->addPendingMessageToStore(msg); - } - } - - // Do-nothing operator for all unhandled events - template<typename T> - void operator()(const mtx::events::Event<T> &) - {} - - // Operator for m.room.message events that contain a msgtype in their content - template<typename T, - std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> - void operator()(mtx::events::RoomEvent<T> msg) - { - sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg); - } + const auto room_id = room_id_.toStdString(); - // Special operator for reactions, which are a type of m.room.message, but need to be - // handled distinctly for their differences from normal room messages. Specifically, - // reactions need to have the relation outside of ciphertext, or synapse / the homeserver - // cannot handle it correctly. See the MSC for more details: - // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption - void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg) - { - msg.type = mtx::events::EventType::Reaction; - emit model_->addPendingMessageToStore(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event) - { - sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>( - event); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event) - { - sendRoomEvent<mtx::events::msg::CallCandidates, - mtx::events::EventType::CallCandidates>(event); - } + using namespace mtx::events; + using namespace mtx::identifiers; - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event) - { - sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>( - event); - } + json doc = {{"type", mtx::events::to_string(eventType)}, + {"content", json(msg.content)}, + {"room_id", room_id}}; - void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event) - { - sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>( - event); - } + try { + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + event.event_id = msg.event_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationRequest, - mtx::events::EventType::RoomMessage>(msg); - } + emit this->addPendingMessageToStore(event); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationReady, - mtx::events::EventType::KeyVerificationReady>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationStart, - mtx::events::EventType::KeyVerificationStart>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationAccept, - mtx::events::EventType::KeyVerificationAccept>(msg); - } - - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationMac, - mtx::events::EventType::KeyVerificationMac>(msg); - } + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } +} - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationKey, - mtx::events::EventType::KeyVerificationKey>(msg); - } +struct SendMessageVisitor +{ + explicit SendMessageVisitor(TimelineModel *model) + : model_(model) + {} - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationDone, - mtx::events::EventType::KeyVerificationDone>(msg); - } + template<typename T, mtx::events::EventType Event> + void sendRoomEvent(mtx::events::RoomEvent<T> msg) + { + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + auto encInfo = mtx::accessors::file(msg); + if (encInfo) + emit model_->newEncryptedImage(encInfo.value()); - void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) - { - sendRoomEvent<mtx::events::msg::KeyVerificationCancel, - mtx::events::EventType::KeyVerificationCancel>(msg); - } - void operator()(mtx::events::Sticker msg) - { - msg.type = mtx::events::EventType::Sticker; - if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { - model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker); - } else - emit model_->addPendingMessageToStore(msg); - } + model_->sendEncryptedMessage(msg, Event); + } else { + msg.type = Event; + emit model_->addPendingMessageToStore(msg); + } + } + + // Do-nothing operator for all unhandled events + template<typename T> + void operator()(const mtx::events::Event<T> &) + {} + + // Operator for m.room.message events that contain a msgtype in their content + template<typename T, + std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> + void operator()(mtx::events::RoomEvent<T> msg) + { + sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg); + } + + // Special operator for reactions, which are a type of m.room.message, but need to be + // handled distinctly for their differences from normal room messages. Specifically, + // reactions need to have the relation outside of ciphertext, or synapse / the homeserver + // cannot handle it correctly. See the MSC for more details: + // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption + void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg) + { + msg.type = mtx::events::EventType::Reaction; + emit model_->addPendingMessageToStore(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event) + { + sendRoomEvent<mtx::events::msg::CallInvite, mtx::events::EventType::CallInvite>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &event) + { + sendRoomEvent<mtx::events::msg::CallCandidates, mtx::events::EventType::CallCandidates>( + event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &event) + { + sendRoomEvent<mtx::events::msg::CallAnswer, mtx::events::EventType::CallAnswer>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &event) + { + sendRoomEvent<mtx::events::msg::CallHangUp, mtx::events::EventType::CallHangUp>(event); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationRequest, + mtx::events::EventType::RoomMessage>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationReady, + mtx::events::EventType::KeyVerificationReady>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationStart, + mtx::events::EventType::KeyVerificationStart>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationAccept, + mtx::events::EventType::KeyVerificationAccept>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationMac, + mtx::events::EventType::KeyVerificationMac>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationKey, + mtx::events::EventType::KeyVerificationKey>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationDone, + mtx::events::EventType::KeyVerificationDone>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationCancel, + mtx::events::EventType::KeyVerificationCancel>(msg); + } + void operator()(mtx::events::Sticker msg) + { + msg.type = mtx::events::EventType::Sticker; + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker); + } else + emit model_->addPendingMessageToStore(msg); + } - TimelineModel *model_; + TimelineModel *model_; }; void TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) { - std::visit( - [](auto &msg) { - // gets overwritten for reactions and stickers in SendMessageVisitor - msg.type = mtx::events::EventType::RoomMessage; - msg.event_id = "m" + http::client()->generate_txn_id(); - msg.sender = http::client()->user_id().to_string(); - msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - }, - event); + std::visit( + [](auto &msg) { + // gets overwritten for reactions and stickers in SendMessageVisitor + msg.type = mtx::events::EventType::RoomMessage; + msg.event_id = "m" + http::client()->generate_txn_id(); + msg.sender = http::client()->user_id().to_string(); + msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + }, + event); - std::visit(SendMessageVisitor{this}, event); + std::visit(SendMessageVisitor{this}, event); } void TimelineModel::openMedia(QString eventId) { - cacheMedia(eventId, [](QString filename) { - QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); - }); + cacheMedia(eventId, + [](QString filename) { QDesktopServices::openUrl(QUrl::fromLocalFile(filename)); }); } bool TimelineModel::saveMedia(QString eventId) const { - mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); - if (!event) - return false; + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return false; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(*event); + auto encryptionInfo = mtx::accessors::file(*event); - qml_mtx_events::EventType eventType = toRoomEventType(*event); + qml_mtx_events::EventType eventType = toRoomEventType(*event); - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } - const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - const QString downloadsFolder = - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - const QString openLocation = downloadsFolder + "/" + originalFilename; - - const QString filename = QFileDialog::getSaveFileName( - manager_->getWidget(), dialogTitle, openLocation, filterString); - - if (filename.isEmpty()) - return false; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [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, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } + const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + const QString downloadsFolder = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString openLocation = downloadsFolder + "/" + originalFilename; - try { - auto temp = data; - if (encryptionInfo) - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + const QString filename = + QFileDialog::getSaveFileName(manager_->getWidget(), dialogTitle, openLocation, filterString); - QFile file(filename); + if (filename.isEmpty()) + return false; - if (!file.open(QIODevice::WriteOnly)) - return; + const auto url = mxcUrl.toStdString(); - file.write(QByteArray(temp.data(), (int)temp.size())); - file.close(); + http::client()->download(url, + [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, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } - return; - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); - return true; + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); + return true; } void TimelineModel::cacheMedia(QString eventId, std::function<void(const QString)> callback) { - mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); - if (!event) - return; + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(*event); + auto encryptionInfo = mtx::accessors::file(*event); - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - 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(name) - .arg(suffix)); - if (QDir::cleanPath(name) != name) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + 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(name) + .arg(suffix)); + if (QDir::cleanPath(name) != name) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } - QDir().mkpath(filename.path()); + QDir().mkpath(filename.path()); - if (filename.isReadable()) { + if (filename.isReadable()) { #if defined(Q_OS_WIN) - emit mediaCached(mxcUrl, filename.filePath()); + emit mediaCached(mxcUrl, filename.filePath()); #else - emit mediaCached(mxcUrl, "file://" + filename.filePath()); + emit mediaCached(mxcUrl, "file://" + filename.filePath()); #endif - if (callback) { - callback(filename.filePath()); - } - return; + if (callback) { + callback(filename.filePath()); } - - http::client()->download( - url, - [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, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - try { - auto temp = data; - if (encryptionInfo) - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); - - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - 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()); - } + return; + } + + http::client()->download( + url, + [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, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = + mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + 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()); + } #if defined(Q_OS_WIN) - emit mediaCached(mxcUrl, filename.filePath()); + emit mediaCached(mxcUrl, filename.filePath()); #else - emit mediaCached(mxcUrl, "file://" + filename.filePath()); + emit mediaCached(mxcUrl, "file://" + filename.filePath()); #endif - }); + }); } void TimelineModel::cacheMedia(QString eventId) { - cacheMedia(eventId, NULL); + cacheMedia(eventId, NULL); } void TimelineModel::showEvent(QString eventId) { - using namespace std::chrono_literals; - // Direct to eventId - if (eventId[0] == '$') { - int idx = idToIndex(eventId); - if (idx == -1) { - nhlog::ui()->warn("Scrolling to event id {}, failed - no known index", - eventId.toStdString()); - return; - } - eventIdToShow = eventId; - emit scrollTargetChanged(); - showEventTimer.start(50ms); - return; + using namespace std::chrono_literals; + // Direct to eventId + if (eventId[0] == '$') { + int idx = idToIndex(eventId); + if (idx == -1) { + nhlog::ui()->warn("Scrolling to event id {}, failed - no known index", + eventId.toStdString()); + return; } - // to message index - eventId = indexToId(eventId.toInt()); eventIdToShow = eventId; emit scrollTargetChanged(); showEventTimer.start(50ms); return; + } + // to message index + eventId = indexToId(eventId.toInt()); + eventIdToShow = eventId; + emit scrollTargetChanged(); + showEventTimer.start(50ms); + return; } void TimelineModel::eventShown() { - eventIdToShow.clear(); - emit scrollTargetChanged(); + eventIdToShow.clear(); + emit scrollTargetChanged(); } QString TimelineModel::scrollTarget() const { - return eventIdToShow; + return eventIdToShow; } void TimelineModel::scrollTimerEvent() { - if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) { - showEventTimer.stop(); - showEventTimerCounter = 0; - } else { - emit scrollToIndex(idToIndex(eventIdToShow)); - showEventTimerCounter++; - } + if (eventIdToShow.isEmpty() || showEventTimerCounter > 3) { + showEventTimer.stop(); + showEventTimerCounter = 0; + } else { + emit scrollToIndex(idToIndex(eventIdToShow)); + showEventTimerCounter++; + } } void TimelineModel::requestKeyForEvent(QString id) { - auto encrypted_event = events.get(id.toStdString(), "", false); - if (encrypted_event) { - if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - encrypted_event)) - events.requestSession(*ev, true); - } + auto encrypted_event = events.get(id.toStdString(), "", false); + if (encrypted_event) { + if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + encrypted_event)) + events.requestSession(*ev, true); + } } void TimelineModel::copyLinkToEvent(QString eventId) const { - QStringList vias; + QStringList vias; - auto alias = cache::client()->getRoomAliases(room_id_.toStdString()); - QString room; - if (alias) { - room = QString::fromStdString(alias->alias); - if (room.isEmpty() && !alias->alt_aliases.empty()) { - room = QString::fromStdString(alias->alt_aliases.front()); - } + auto alias = cache::client()->getRoomAliases(room_id_.toStdString()); + QString room; + if (alias) { + room = QString::fromStdString(alias->alias); + if (room.isEmpty() && !alias->alt_aliases.empty()) { + room = QString::fromStdString(alias->alt_aliases.front()); } + } - if (room.isEmpty()) - room = room_id_; + if (room.isEmpty()) + room = room_id_; - vias.push_back(QString("via=%1").arg(QString( - QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname()))))); - auto members = cache::getMembers(room_id_.toStdString(), 0, 100); - for (const auto &m : members) { - if (vias.size() >= 4) - break; + vias.push_back(QString("via=%1").arg(QString( + QUrl::toPercentEncoding(QString::fromStdString(http::client()->user_id().hostname()))))); + auto members = cache::getMembers(room_id_.toStdString(), 0, 100); + for (const auto &m : members) { + if (vias.size() >= 4) + break; - auto user_id = - mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString()); - QString server = QString("via=%1").arg( - QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname())))); + auto user_id = mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString()); + QString server = QString("via=%1").arg( + QString(QUrl::toPercentEncoding(QString::fromStdString(user_id.hostname())))); - if (!vias.contains(server)) - vias.push_back(server); - } + if (!vias.contains(server)) + vias.push_back(server); + } - auto link = QString("https://matrix.to/#/%1/%2?%3") - .arg(QString(QUrl::toPercentEncoding(room)), - QString(QUrl::toPercentEncoding(eventId)), - vias.join('&')); + auto link = QString("https://matrix.to/#/%1/%2?%3") + .arg(QString(QUrl::toPercentEncoding(room)), + QString(QUrl::toPercentEncoding(eventId)), + vias.join('&')); - QGuiApplication::clipboard()->setText(link); + QGuiApplication::clipboard()->setText(link); } QString TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg) { - QString temp = - tr("%1 and %2 are typing.", - "Multiple users are typing. First argument is a comma separated list of potentially " - "multiple users. Second argument is the last user of that list. (If only one user is " - "typing, %1 is empty. You should still use it in your string though to silence Qt " - "warnings.)", - (int)users.size()); + QString temp = + tr("%1 and %2 are typing.", + "Multiple users are typing. First argument is a comma separated list of potentially " + "multiple users. Second argument is the last user of that list. (If only one user is " + "typing, %1 is empty. You should still use it in your string though to silence Qt " + "warnings.)", + (int)users.size()); - if (users.empty()) { - return ""; - } + if (users.empty()) { + return ""; + } - QStringList uidWithoutLast; + QStringList uidWithoutLast; - auto formatUser = [this, bg](const QString &user_id) -> QString { - auto uncoloredUsername = utils::replaceEmoji(displayName(user_id)); - QString prefix = - QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); + auto formatUser = [this, bg](const QString &user_id) -> QString { + auto uncoloredUsername = utils::replaceEmoji(displayName(user_id)); + QString prefix = + QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); - // color only parts that don't have a font already specified - QString coloredUsername; - int index = 0; - do { - auto startIndex = uncoloredUsername.indexOf("<font", index); + // color only parts that don't have a font already specified + QString coloredUsername; + int index = 0; + do { + auto startIndex = uncoloredUsername.indexOf("<font", index); - if (startIndex - index != 0) - coloredUsername += - prefix + - uncoloredUsername.midRef( - index, startIndex > 0 ? startIndex - index : -1) + - "</font>"; + if (startIndex - index != 0) + coloredUsername += + prefix + + uncoloredUsername.midRef(index, startIndex > 0 ? startIndex - index : -1) + + "</font>"; - auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); - if (endIndex > 0) - endIndex += sizeof("</font>") - 1; + auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); + if (endIndex > 0) + endIndex += sizeof("</font>") - 1; - if (endIndex - startIndex != 0) - coloredUsername += - uncoloredUsername.midRef(startIndex, endIndex - startIndex); + if (endIndex - startIndex != 0) + coloredUsername += uncoloredUsername.midRef(startIndex, endIndex - startIndex); - index = endIndex; - } while (index > 0 && index < uncoloredUsername.size()); + index = endIndex; + } while (index > 0 && index < uncoloredUsername.size()); - return coloredUsername; - }; + return coloredUsername; + }; - for (size_t i = 0; i + 1 < users.size(); i++) { - uidWithoutLast.append(formatUser(users[i])); - } + for (size_t i = 0; i + 1 < users.size(); i++) { + uidWithoutLast.append(formatUser(users[i])); + } - return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); + return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); } QString TimelineModel::formatJoinRuleEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e); - if (!event) - return ""; - - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); - - switch (event->content.join_rule) { - case mtx::events::state::JoinRule::Public: - return tr("%1 opened the room to the public.").arg(name); - case mtx::events::state::JoinRule::Invite: - return tr("%1 made this room require and invitation to join.").arg(name); - case mtx::events::state::JoinRule::Knock: - return tr("%1 allowed to join this room by knocking.").arg(name); - case mtx::events::state::JoinRule::Restricted: { - QStringList rooms; - for (const auto &r : event->content.allow) { - if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership) - rooms.push_back(QString::fromStdString(r.room_id)); - } - return tr("%1 allowed members of the following rooms to automatically join this " - "room: %2") - .arg(name) - .arg(rooms.join(", ")); - } - default: - // Currently, knock and private are reserved keywords and not implemented in Matrix. - return ""; - } + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e); + if (!event) + return ""; + + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); + + switch (event->content.join_rule) { + case mtx::events::state::JoinRule::Public: + return tr("%1 opened the room to the public.").arg(name); + case mtx::events::state::JoinRule::Invite: + return tr("%1 made this room require and invitation to join.").arg(name); + case mtx::events::state::JoinRule::Knock: + return tr("%1 allowed to join this room by knocking.").arg(name); + case mtx::events::state::JoinRule::Restricted: { + QStringList rooms; + for (const auto &r : event->content.allow) { + if (r.type == mtx::events::state::JoinAllowanceType::RoomMembership) + rooms.push_back(QString::fromStdString(r.room_id)); + } + return tr("%1 allowed members of the following rooms to automatically join this " + "room: %2") + .arg(name) + .arg(rooms.join(", ")); + } + default: + // Currently, knock and private are reserved keywords and not implemented in Matrix. + return ""; + } } QString TimelineModel::formatGuestAccessEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); - if (!event) - return ""; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); + if (!event) + return ""; - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.guest_access) { - case mtx::events::state::AccessState::CanJoin: - return tr("%1 made the room open to guests.").arg(name); - case mtx::events::state::AccessState::Forbidden: - return tr("%1 has closed the room to guest access.").arg(name); - default: - return ""; - } + switch (event->content.guest_access) { + case mtx::events::state::AccessState::CanJoin: + return tr("%1 made the room open to guests.").arg(name); + case mtx::events::state::AccessState::Forbidden: + return tr("%1 has closed the room to guest access.").arg(name); + default: + return ""; + } } QString TimelineModel::formatHistoryVisibilityEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); - if (!event) - return ""; - - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); - - switch (event->content.history_visibility) { - case mtx::events::state::Visibility::WorldReadable: - return tr("%1 made the room history world readable. Events may be now read by " - "non-joined people.") - .arg(name); - case mtx::events::state::Visibility::Shared: - return tr("%1 set the room history visible to members from this point on.") - .arg(name); - case mtx::events::state::Visibility::Invited: - return tr("%1 set the room history visible to members since they were invited.") - .arg(name); - case mtx::events::state::Visibility::Joined: - return tr("%1 set the room history visible to members since they joined the room.") - .arg(name); - default: - return ""; - } + if (!event) + return ""; + + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); + + switch (event->content.history_visibility) { + case mtx::events::state::Visibility::WorldReadable: + return tr("%1 made the room history world readable. Events may be now read by " + "non-joined people.") + .arg(name); + case mtx::events::state::Visibility::Shared: + return tr("%1 set the room history visible to members from this point on.").arg(name); + case mtx::events::state::Visibility::Invited: + return tr("%1 set the room history visible to members since they were invited.").arg(name); + case mtx::events::state::Visibility::Joined: + return tr("%1 set the room history visible to members since they joined the room.") + .arg(name); + default: + return ""; + } } QString TimelineModel::formatPowerLevelEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); - if (!event) - return ""; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); + if (!event) + return ""; - QString user = QString::fromStdString(event->sender); - QString name = utils::replaceEmoji(displayName(user)); + QString user = QString::fromStdString(event->sender); + QString name = utils::replaceEmoji(displayName(user)); - // TODO: power levels rendering is actually a bit complex. work on this later. - return tr("%1 has changed the room's permissions.").arg(name); + // TODO: power levels rendering is actually a bit complex. work on this later. + return tr("%1 has changed the room's permissions.").arg(name); } void TimelineModel::acceptKnock(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return; - if (!permissions_.canInvite()) - return; + if (!permissions_.canInvite()) + return; - if (cache::isRoomMember(event->state_key, room_id_.toStdString())) - return; + if (cache::isRoomMember(event->state_key, room_id_.toStdString())) + return; - using namespace mtx::events::state; - if (event->content.membership != Membership::Knock) - return; + using namespace mtx::events::state; + if (event->content.membership != Membership::Knock) + return; - ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), ""); + ChatPage::instance()->inviteUser(QString::fromStdString(event->state_key), ""); } bool TimelineModel::showAcceptKnockButton(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return false; + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return false; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return false; + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return false; - if (!permissions_.canInvite()) - return false; + if (!permissions_.canInvite()) + return false; - if (cache::isRoomMember(event->state_key, room_id_.toStdString())) - return false; + if (cache::isRoomMember(event->state_key, room_id_.toStdString())) + return false; - using namespace mtx::events::state; - return event->content.membership == Membership::Knock; + using namespace mtx::events::state; + return event->content.membership == Membership::Knock; } QString TimelineModel::formatMemberEvent(QString id) { - mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); - if (!e) - return ""; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return ""; - - mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = - events.get(event->unsigned_data.replaces_state, event->event_id); - if (tempPrevEvent) { - prevEvent = - std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>( - tempPrevEvent); - } - } + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); + if (!event) + return ""; + + mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; + if (!event->unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (tempPrevEvent) { + prevEvent = + std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(tempPrevEvent); + } + } + + QString user = QString::fromStdString(event->state_key); + QString name = utils::replaceEmoji(displayName(user)); + QString rendered; + + // see table https://matrix.org/docs/spec/client_server/latest#m-room-member + using namespace mtx::events::state; + switch (event->content.membership) { + case Membership::Invite: + rendered = tr("%1 was invited.").arg(name); + break; + case Membership::Join: + if (prevEvent && prevEvent->content.membership == Membership::Join) { + QString oldName = utils::replaceEmoji( + QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); + + bool displayNameChanged = + prevEvent->content.display_name != event->content.display_name; + bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url; + + if (displayNameChanged && avatarChanged) + rendered = tr("%1 has changed their avatar and changed their " + "display name to %2.") + .arg(oldName, name); + else if (displayNameChanged) + rendered = tr("%1 has changed their display name to %2.").arg(oldName, name); + else if (avatarChanged) + rendered = tr("%1 changed their avatar.").arg(name); + else + rendered = tr("%1 changed some profile info.").arg(name); + // the case of nothing changed but join follows join shouldn't happen, so + // just show it as join + } else { + if (event->content.join_authorised_via_users_server.empty()) + rendered = tr("%1 joined.").arg(name); + else + rendered = + tr("%1 joined via authorisation from %2's server.") + .arg(name) + .arg(QString::fromStdString(event->content.join_authorised_via_users_server)); + } + break; + case Membership::Leave: + if (!prevEvent) // Should only ever happen temporarily + return ""; + + if (prevEvent->content.membership == Membership::Invite) { + if (event->state_key == event->sender) + rendered = tr("%1 rejected their invite.").arg(name); + else + rendered = tr("Revoked the invite to %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Join) { + if (event->state_key == event->sender) + rendered = tr("%1 left the room.").arg(name); + else + rendered = tr("Kicked %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Ban) { + rendered = tr("Unbanned %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Knock) { + if (event->state_key == event->sender) + rendered = tr("%1 redacted their knock.").arg(name); + else + rendered = tr("Rejected the knock from %1.").arg(name); + } else + return tr("%1 left after having already left!", + "This is a leave event after the user already left and shouldn't " + "happen apart from state resets") + .arg(name); + break; - QString user = QString::fromStdString(event->state_key); - QString name = utils::replaceEmoji(displayName(user)); - QString rendered; - - // see table https://matrix.org/docs/spec/client_server/latest#m-room-member - using namespace mtx::events::state; - switch (event->content.membership) { - case Membership::Invite: - rendered = tr("%1 was invited.").arg(name); - break; - case Membership::Join: - if (prevEvent && prevEvent->content.membership == Membership::Join) { - QString oldName = utils::replaceEmoji( - QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); - - bool displayNameChanged = - prevEvent->content.display_name != event->content.display_name; - bool avatarChanged = - prevEvent->content.avatar_url != event->content.avatar_url; - - if (displayNameChanged && avatarChanged) - rendered = tr("%1 has changed their avatar and changed their " - "display name to %2.") - .arg(oldName, name); - else if (displayNameChanged) - rendered = - tr("%1 has changed their display name to %2.").arg(oldName, name); - else if (avatarChanged) - rendered = tr("%1 changed their avatar.").arg(name); - else - rendered = tr("%1 changed some profile info.").arg(name); - // the case of nothing changed but join follows join shouldn't happen, so - // just show it as join - } else { - if (event->content.join_authorised_via_users_server.empty()) - rendered = tr("%1 joined.").arg(name); - else - rendered = tr("%1 joined via authorisation from %2's server.") - .arg(name) - .arg(QString::fromStdString( - event->content.join_authorised_via_users_server)); - } - break; - case Membership::Leave: - if (!prevEvent) // Should only ever happen temporarily - return ""; - - if (prevEvent->content.membership == Membership::Invite) { - if (event->state_key == event->sender) - rendered = tr("%1 rejected their invite.").arg(name); - else - rendered = tr("Revoked the invite to %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Join) { - if (event->state_key == event->sender) - rendered = tr("%1 left the room.").arg(name); - else - rendered = tr("Kicked %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Ban) { - rendered = tr("Unbanned %1.").arg(name); - } else if (prevEvent->content.membership == Membership::Knock) { - if (event->state_key == event->sender) - rendered = tr("%1 redacted their knock.").arg(name); - else - rendered = tr("Rejected the knock from %1.").arg(name); - } else - return tr("%1 left after having already left!", - "This is a leave event after the user already left and shouldn't " - "happen apart from state resets") - .arg(name); - break; - - case Membership::Ban: - rendered = tr("%1 was banned.").arg(name); - break; - case Membership::Knock: - rendered = tr("%1 knocked.").arg(name); - break; - } + case Membership::Ban: + rendered = tr("%1 was banned.").arg(name); + break; + case Membership::Knock: + rendered = tr("%1 knocked.").arg(name); + break; + } - if (event->content.reason != "") { - rendered += - " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); - } + if (event->content.reason != "") { + rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); + } - return rendered; + return rendered; } void TimelineModel::setEdit(QString newEdit) { - if (newEdit.isEmpty()) { - resetEdit(); - return; - } + if (newEdit.isEmpty()) { + resetEdit(); + return; + } + + if (edit_.isEmpty()) { + this->textBeforeEdit = input()->text(); + this->replyBeforeEdit = reply_; + nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString()); + } + + if (edit_ != newEdit) { + auto ev = events.get(newEdit.toStdString(), ""); + if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { + auto e = *ev; + setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or(""))); + + auto msgType = mtx::accessors::msg_type(e); + if (msgType == mtx::events::MessageType::Text || + msgType == mtx::events::MessageType::Notice || + msgType == mtx::events::MessageType::Emote) { + auto relInfo = relatedInfo(newEdit); + auto editText = relInfo.quoted_body; + + if (!relInfo.quoted_formatted_body.isEmpty()) { + auto matches = + conf::strings::matrixToLink.globalMatch(relInfo.quoted_formatted_body); + std::map<QString, QString> reverseNameMapping; + while (matches.hasNext()) { + auto m = matches.next(); + reverseNameMapping[m.captured(2)] = m.captured(1); + } + + for (const auto &[user, link] : reverseNameMapping) { + // TODO(Nico): html unescape the user name + editText.replace(user, QStringLiteral("[%1](%2)").arg(user, link)); + } + } - if (edit_.isEmpty()) { - this->textBeforeEdit = input()->text(); - this->replyBeforeEdit = reply_; - nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString()); - } + if (msgType == mtx::events::MessageType::Emote) + input()->setText("/me " + editText); + else + input()->setText(editText); + } else { + input()->setText(""); + } - if (edit_ != newEdit) { - auto ev = events.get(newEdit.toStdString(), ""); - if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { - auto e = *ev; - setReply(QString::fromStdString( - mtx::accessors::relations(e).reply_to().value_or(""))); - - auto msgType = mtx::accessors::msg_type(e); - if (msgType == mtx::events::MessageType::Text || - msgType == mtx::events::MessageType::Notice || - msgType == mtx::events::MessageType::Emote) { - auto relInfo = relatedInfo(newEdit); - auto editText = relInfo.quoted_body; - - if (!relInfo.quoted_formatted_body.isEmpty()) { - auto matches = conf::strings::matrixToLink.globalMatch( - relInfo.quoted_formatted_body); - std::map<QString, QString> reverseNameMapping; - while (matches.hasNext()) { - auto m = matches.next(); - reverseNameMapping[m.captured(2)] = m.captured(1); - } - - for (const auto &[user, link] : reverseNameMapping) { - // TODO(Nico): html unescape the user name - editText.replace( - user, QStringLiteral("[%1](%2)").arg(user, link)); - } - } - - if (msgType == mtx::events::MessageType::Emote) - input()->setText("/me " + editText); - else - input()->setText(editText); - } else { - input()->setText(""); - } - - edit_ = newEdit; - } else { - resetReply(); - - input()->setText(""); - edit_ = ""; - } - emit editChanged(edit_); + edit_ = newEdit; + } else { + resetReply(); + + input()->setText(""); + edit_ = ""; } + emit editChanged(edit_); + } } void TimelineModel::resetEdit() { - if (!edit_.isEmpty()) { - edit_ = ""; - emit editChanged(edit_); - nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString()); - input()->setText(textBeforeEdit); - textBeforeEdit.clear(); - if (replyBeforeEdit.isEmpty()) - resetReply(); - else - setReply(replyBeforeEdit); - replyBeforeEdit.clear(); - } + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString()); + input()->setText(textBeforeEdit); + textBeforeEdit.clear(); + if (replyBeforeEdit.isEmpty()) + resetReply(); + else + setReply(replyBeforeEdit); + replyBeforeEdit.clear(); + } } QString TimelineModel::roomName() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return utils::replaceEmoji( - QString::fromStdString(info[room_id_].name).toHtmlEscaped()); + if (!info.count(room_id_)) + return ""; + else + return utils::replaceEmoji(QString::fromStdString(info[room_id_].name).toHtmlEscaped()); } QString TimelineModel::plainRoomName() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return QString::fromStdString(info[room_id_].name); + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); } QString TimelineModel::roomAvatarUrl() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return QString::fromStdString(info[room_id_].avatar_url); + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].avatar_url); } QString TimelineModel::roomTopic() const { - auto info = cache::getRoomInfo({room_id_.toStdString()}); + auto info = cache::getRoomInfo({room_id_.toStdString()}); - if (!info.count(room_id_)) - return ""; - else - return utils::replaceEmoji(utils::linkifyMessage( - QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); + if (!info.count(room_id_)) + return ""; + else + return utils::replaceEmoji( + utils::linkifyMessage(QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); } crypto::Trust TimelineModel::trustlevel() const { - if (!isEncrypted_) - return crypto::Trust::Unverified; + if (!isEncrypted_) + return crypto::Trust::Unverified; - return cache::client()->roomVerificationStatus(room_id_.toStdString()); + return cache::client()->roomVerificationStatus(room_id_.toStdString()); } int TimelineModel::roomMemberCount() const { - return (int)cache::client()->memberCount(room_id_.toStdString()); + return (int)cache::client()->memberCount(room_id_.toStdString()); } QString TimelineModel::directChatOtherUserId() const { - if (roomMemberCount() < 3) { - QString id; - for (auto member : cache::getMembers(room_id_.toStdString())) - if (member.user_id != UserSettings::instance()->userId()) - id = member.user_id; - return id; - } else - return ""; + if (roomMemberCount() < 3) { + QString id; + for (auto member : cache::getMembers(room_id_.toStdString())) + if (member.user_id != UserSettings::instance()->userId()) + id = member.user_id; + return id; + } else + return ""; } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 66e0622e..f16529e2 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -39,95 +39,95 @@ Q_NAMESPACE enum EventType { - // Unsupported event - Unsupported, - /// m.room_key_request - KeyRequest, - /// m.reaction, - Reaction, - /// m.room.aliases - Aliases, - /// m.room.avatar - Avatar, - /// m.call.invite - CallInvite, - /// m.call.answer - CallAnswer, - /// m.call.hangup - CallHangUp, - /// m.call.candidates - CallCandidates, - /// m.room.canonical_alias - CanonicalAlias, - /// m.room.create - RoomCreate, - /// m.room.encrypted. - Encrypted, - /// m.room.encryption. - Encryption, - /// m.room.guest_access - RoomGuestAccess, - /// m.room.history_visibility - RoomHistoryVisibility, - /// m.room.join_rules - RoomJoinRules, - /// m.room.member - Member, - /// m.room.name - Name, - /// m.room.power_levels - PowerLevels, - /// m.room.tombstone - Tombstone, - /// m.room.topic - Topic, - /// m.room.redaction - Redaction, - /// m.room.pinned_events - PinnedEvents, - // m.sticker - Sticker, - // m.tag - Tag, - /// m.room.message - AudioMessage, - EmoteMessage, - FileMessage, - ImageMessage, - LocationMessage, - NoticeMessage, - TextMessage, - VideoMessage, - Redacted, - UnknownMessage, - KeyVerificationRequest, - KeyVerificationStart, - KeyVerificationMac, - KeyVerificationAccept, - KeyVerificationCancel, - KeyVerificationKey, - KeyVerificationDone, - KeyVerificationReady, - //! m.image_pack, currently im.ponies.room_emotes - ImagePackInRoom, - //! m.image_pack, currently im.ponies.user_emotes - ImagePackInAccountData, - //! m.image_pack.rooms, currently im.ponies.emote_rooms - ImagePackRooms, + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.reaction, + Reaction, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.call.invite + CallInvite, + /// m.call.answer + CallAnswer, + /// m.call.hangup + CallHangUp, + /// m.call.candidates + CallCandidates, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + RoomCreate, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + RoomGuestAccess, + /// m.room.history_visibility + RoomHistoryVisibility, + /// m.room.join_rules + RoomJoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, + KeyVerificationRequest, + KeyVerificationStart, + KeyVerificationMac, + KeyVerificationAccept, + KeyVerificationCancel, + KeyVerificationKey, + KeyVerificationDone, + KeyVerificationReady, + //! m.image_pack, currently im.ponies.room_emotes + ImagePackInRoom, + //! m.image_pack, currently im.ponies.user_emotes + ImagePackInAccountData, + //! m.image_pack.rooms, currently im.ponies.emote_rooms + ImagePackRooms, }; Q_ENUM_NS(EventType) mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); enum EventState { - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, }; Q_ENUM_NS(EventState) } @@ -135,330 +135,329 @@ Q_ENUM_NS(EventState) class StateKeeper { public: - StateKeeper(std::function<void()> &&fn) - : fn_(std::move(fn)) - {} + StateKeeper(std::function<void()> &&fn) + : fn_(std::move(fn)) + {} - ~StateKeeper() { fn_(); } + ~StateKeeper() { fn_(); } private: - std::function<void()> fn_; + std::function<void()> fn_; }; struct DecryptionResult { - //! The decrypted content as a normal plaintext event. - mtx::events::collections::TimelineEvents event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; }; class TimelineViewManager; class TimelineModel : public QAbstractListModel { - Q_OBJECT - Q_PROPERTY( - int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY - typingUsersChanged) - Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged) - Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) - Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) - Q_PROPERTY( - bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) - Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged) - Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) - Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) - Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) - Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) - Q_PROPERTY(bool isSpace READ isSpace CONSTANT) - Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged) - Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged) - Q_PROPERTY(QString directChatOtherUserId READ directChatOtherUserId NOTIFY - directChatOtherUserIdChanged) - Q_PROPERTY(InputBar *input READ input CONSTANT) - Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) + Q_OBJECT + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY + typingUsersChanged) + Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged) + Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) + Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) + Q_PROPERTY( + bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) + Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) + Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) + Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) + Q_PROPERTY(bool isSpace READ isSpace CONSTANT) + Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged) + Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged) + Q_PROPERTY( + QString directChatOtherUserId READ directChatOtherUserId NOTIFY directChatOtherUserIdChanged) + Q_PROPERTY(InputBar *input READ input CONSTANT) + Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) public: - explicit TimelineModel(TimelineViewManager *manager, - QString room_id, - QObject *parent = nullptr); - - enum Roles - { - Type, - TypeString, - IsOnlyEmoji, - Body, - FormattedBody, - PreviousMessageUserId, - IsSender, - UserId, - UserName, - PreviousMessageDay, - Day, - Timestamp, - Url, - ThumbnailUrl, - Blurhash, - Filename, - Filesize, - MimeType, - OriginalHeight, - OriginalWidth, - ProportionalHeight, - EventId, - State, - IsEdited, - IsEditable, - IsEncrypted, - Trustlevel, - EncryptionError, - ReplyTo, - Reactions, - RoomId, - RoomName, - RoomTopic, - CallType, - Dump, - RelatedEventCacheBuster, - }; - Q_ENUM(Roles); - - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; - Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo); - - bool canFetchMore(const QModelIndex &) const override; - void fetchMore(const QModelIndex &) override; - - Q_INVOKABLE QString displayName(QString id) const; - Q_INVOKABLE QString avatarUrl(QString id) const; - Q_INVOKABLE QString formatDateSeparator(QDate date) const; - Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg); - Q_INVOKABLE bool showAcceptKnockButton(QString id); - Q_INVOKABLE void acceptKnock(QString id); - Q_INVOKABLE QString formatMemberEvent(QString id); - Q_INVOKABLE QString formatJoinRuleEvent(QString id); - Q_INVOKABLE QString formatHistoryVisibilityEvent(QString id); - Q_INVOKABLE QString formatGuestAccessEvent(QString id); - Q_INVOKABLE QString formatPowerLevelEvent(QString id); - - Q_INVOKABLE void viewRawMessage(QString id); - Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); - Q_INVOKABLE void viewDecryptedRawMessage(QString id); - Q_INVOKABLE void openUserProfile(QString userid); - Q_INVOKABLE void editAction(QString id); - Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void showReadReceipts(QString id); - 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; - Q_INVOKABLE void showEvent(QString eventId); - Q_INVOKABLE void copyLinkToEvent(QString eventId) const; - void cacheMedia(QString eventId, std::function<void(const QString filename)> callback); - Q_INVOKABLE void sendReset() - { - beginResetModel(); - endResetModel(); - } - - Q_INVOKABLE void requestKeyForEvent(QString id); - - std::vector<::Reaction> reactions(const std::string &event_id) - { - auto list = events.reactions(event_id); - std::vector<::Reaction> vec; - for (const auto &r : list) - vec.push_back(r.value<Reaction>()); - return vec; - } - - void updateLastMessage(); - void sync(const mtx::responses::JoinedRoom &room); - void addEvents(const mtx::responses::Timeline &events); - void syncState(const mtx::responses::State &state); - template<class T> - void sendMessageEvent(const T &content, mtx::events::EventType eventType); - RelatedInfo relatedInfo(QString id); - - DescInfo lastMessage() const { return lastMessage_; } - bool isSpace() const { return isSpace_; } - bool isEncrypted() const { return isEncrypted_; } - crypto::Trust trustlevel() const; - int roomMemberCount() const; - bool isDirect() const { return roomMemberCount() <= 2; } - QString directChatOtherUserId() const; - - std::optional<mtx::events::collections::TimelineEvents> eventById(const QString &id) - { - auto e = events.get(id.toStdString(), ""); - if (e) - return *e; - else - return std::nullopt; - } + explicit TimelineModel(TimelineViewManager *manager, + QString room_id, + QObject *parent = nullptr); + + enum Roles + { + Type, + TypeString, + IsOnlyEmoji, + Body, + FormattedBody, + PreviousMessageUserId, + IsSender, + UserId, + UserName, + PreviousMessageDay, + Day, + Timestamp, + Url, + ThumbnailUrl, + Blurhash, + Filename, + Filesize, + MimeType, + OriginalHeight, + OriginalWidth, + ProportionalHeight, + EventId, + State, + IsEdited, + IsEditable, + IsEncrypted, + Trustlevel, + EncryptionError, + ReplyTo, + Reactions, + RoomId, + RoomName, + RoomTopic, + CallType, + Dump, + RelatedEventCacheBuster, + }; + Q_ENUM(Roles); + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; + Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo); + + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; + + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg); + Q_INVOKABLE bool showAcceptKnockButton(QString id); + Q_INVOKABLE void acceptKnock(QString id); + Q_INVOKABLE QString formatMemberEvent(QString id); + Q_INVOKABLE QString formatJoinRuleEvent(QString id); + Q_INVOKABLE QString formatHistoryVisibilityEvent(QString id); + Q_INVOKABLE QString formatGuestAccessEvent(QString id); + Q_INVOKABLE QString formatPowerLevelEvent(QString id); + + Q_INVOKABLE void viewRawMessage(QString id); + Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); + Q_INVOKABLE void viewDecryptedRawMessage(QString id); + Q_INVOKABLE void openUserProfile(QString userid); + Q_INVOKABLE void editAction(QString id); + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void showReadReceipts(QString id); + 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; + Q_INVOKABLE void showEvent(QString eventId); + Q_INVOKABLE void copyLinkToEvent(QString eventId) const; + void cacheMedia(QString eventId, std::function<void(const QString filename)> callback); + Q_INVOKABLE void sendReset() + { + beginResetModel(); + endResetModel(); + } + + Q_INVOKABLE void requestKeyForEvent(QString id); + + std::vector<::Reaction> reactions(const std::string &event_id) + { + auto list = events.reactions(event_id); + std::vector<::Reaction> vec; + for (const auto &r : list) + vec.push_back(r.value<Reaction>()); + return vec; + } + + void updateLastMessage(); + void sync(const mtx::responses::JoinedRoom &room); + void addEvents(const mtx::responses::Timeline &events); + void syncState(const mtx::responses::State &state); + template<class T> + void sendMessageEvent(const T &content, mtx::events::EventType eventType); + RelatedInfo relatedInfo(QString id); + + DescInfo lastMessage() const { return lastMessage_; } + bool isSpace() const { return isSpace_; } + bool isEncrypted() const { return isEncrypted_; } + crypto::Trust trustlevel() const; + int roomMemberCount() const; + bool isDirect() const { return roomMemberCount() <= 2; } + QString directChatOtherUserId() const; + + std::optional<mtx::events::collections::TimelineEvents> eventById(const QString &id) + { + auto e = events.get(id.toStdString(), ""); + if (e) + return *e; + else + return std::nullopt; + } public slots: - void setCurrentIndex(int index); - int currentIndex() const { return idToIndex(currentId); } - void eventShown(); - void markEventsAsRead(const std::vector<QString> &event_ids); - QVariantMap getDump(QString eventId, QString relatedTo) const; - void updateTypingUsers(const std::vector<QString> &users) - { - if (this->typingUsers_ != users) { - this->typingUsers_ = users; - emit typingUsersChanged(typingUsers_); - } + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void eventShown(); + void markEventsAsRead(const std::vector<QString> &event_ids); + QVariantMap getDump(QString eventId, QString relatedTo) const; + void updateTypingUsers(const std::vector<QString> &users) + { + if (this->typingUsers_ != users) { + this->typingUsers_ = users; + emit typingUsersChanged(typingUsers_); } - std::vector<QString> typingUsers() const { return typingUsers_; } - bool paginationInProgress() const { return m_paginationInProgress; } - QString reply() const { return reply_; } - void setReply(QString newReply) - { - if (edit_.startsWith('m')) - return; - - if (reply_ != newReply) { - reply_ = newReply; - emit replyChanged(reply_); - } + } + std::vector<QString> typingUsers() const { return typingUsers_; } + bool paginationInProgress() const { return m_paginationInProgress; } + QString reply() const { return reply_; } + void setReply(QString newReply) + { + if (edit_.startsWith('m')) + return; + + if (reply_ != newReply) { + reply_ = newReply; + emit replyChanged(reply_); } - void resetReply() - { - if (!reply_.isEmpty()) { - reply_ = ""; - emit replyChanged(reply_); - } + } + void resetReply() + { + if (!reply_.isEmpty()) { + reply_ = ""; + emit replyChanged(reply_); } - QString edit() const { return edit_; } - void setEdit(QString newEdit); - void resetEdit(); - void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } - void clearTimeline() { events.clearTimeline(); } - void receivedSessionKey(const std::string &session_key) - { - events.receivedSessionKey(session_key); - } - - QString roomName() const; - QString plainRoomName() const; - QString roomTopic() const; - InputBar *input() { return &input_; } - Permissions *permissions() { return &permissions_; } - QString roomAvatarUrl() const; - QString roomId() const { return room_id_; } - - bool hasMentions() { return highlight_count > 0; } - int notificationCount() { return notification_count; } - - QString scrollTarget() const; + } + QString edit() const { return edit_; } + void setEdit(QString newEdit); + void resetEdit(); + void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } + void clearTimeline() { events.clearTimeline(); } + void receivedSessionKey(const std::string &session_key) + { + events.receivedSessionKey(session_key); + } + + QString roomName() const; + QString plainRoomName() const; + QString roomTopic() const; + InputBar *input() { return &input_; } + Permissions *permissions() { return &permissions_; } + QString roomAvatarUrl() const; + QString roomId() const { return room_id_; } + + bool hasMentions() { return highlight_count > 0; } + int notificationCount() { return notification_count; } + + QString scrollTarget() const; private slots: - void addPendingMessage(mtx::events::collections::TimelineEvents event); - void scrollTimerEvent(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); + void scrollTimerEvent(); signals: - 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); - void replyChanged(QString reply); - void editChanged(QString reply); - void openReadReceiptsDialog(ReadReceiptsProxy *rr); - void showRawMessageDialog(QString rawMessage); - void paginationInProgressChanged(const bool); - void newCallEvent(const mtx::events::collections::TimelineEvents &event); - void scrollToIndex(int index); - - void lastMessageChanged(); - void notificationsChanged(); - - void newMessageToSend(mtx::events::collections::TimelineEvents event); - void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); - void updateFlowEventId(std::string event_id); - - void encryptionChanged(); - void trustlevelChanged(); - void roomNameChanged(); - void plainRoomNameChanged(); - void roomTopicChanged(); - void roomAvatarUrlChanged(); - void roomMemberCountChanged(); - void isDirectChanged(); - void directChatOtherUserIdChanged(); - void permissionsChanged(); - void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); - - void scrollTargetChanged(); + 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); + void replyChanged(QString reply); + void editChanged(QString reply); + void openReadReceiptsDialog(ReadReceiptsProxy *rr); + void showRawMessageDialog(QString rawMessage); + void paginationInProgressChanged(const bool); + void newCallEvent(const mtx::events::collections::TimelineEvents &event); + void scrollToIndex(int index); + + void lastMessageChanged(); + void notificationsChanged(); + + void newMessageToSend(mtx::events::collections::TimelineEvents event); + void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); + void updateFlowEventId(std::string event_id); + + void encryptionChanged(); + void trustlevelChanged(); + void roomNameChanged(); + void plainRoomNameChanged(); + void roomTopicChanged(); + void roomAvatarUrlChanged(); + void roomMemberCountChanged(); + void isDirectChanged(); + void directChatOtherUserIdChanged(); + void permissionsChanged(); + void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); + + void scrollTargetChanged(); private: - template<typename T> - void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType); - void readEvent(const std::string &id); + template<typename T> + void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType); + void readEvent(const std::string &id); - void setPaginationInProgress(const bool paginationInProgress); + void setPaginationInProgress(const bool paginationInProgress); - QSet<QString> read; + QSet<QString> read; - mutable EventStore events; + mutable EventStore events; - QString room_id_; + QString room_id_; - QString currentId, currentReadId; - QString reply_, edit_; - QString textBeforeEdit, replyBeforeEdit; - std::vector<QString> typingUsers_; + QString currentId, currentReadId; + QString reply_, edit_; + QString textBeforeEdit, replyBeforeEdit; + std::vector<QString> typingUsers_; - TimelineViewManager *manager_; + TimelineViewManager *manager_; - InputBar input_{this}; - Permissions permissions_; + InputBar input_{this}; + Permissions permissions_; - QTimer showEventTimer{this}; - QString eventIdToShow; - int showEventTimerCounter = 0; + QTimer showEventTimer{this}; + QString eventIdToShow; + int showEventTimerCounter = 0; - DescInfo lastMessage_{}; + DescInfo lastMessage_{}; - friend struct SendMessageVisitor; + friend struct SendMessageVisitor; - int notification_count = 0, highlight_count = 0; + int notification_count = 0, highlight_count = 0; - unsigned int relatedEventCacheBuster = 0; + unsigned int relatedEventCacheBuster = 0; - bool decryptDescription = true; - bool m_paginationInProgress = false; - bool isSpace_ = false; - bool isEncrypted_ = false; + bool decryptDescription = true; + bool m_paginationInProgress = false; + bool isSpace_ = false; + bool isEncrypted_ = false; }; template<class T> void TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType) { - if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) { - mtx::events::Sticker msgCopy = {}; - msgCopy.content = content; - msgCopy.type = eventType; - emit newMessageToSend(msgCopy); - } else { - mtx::events::RoomEvent<T> msgCopy = {}; - msgCopy.content = content; - msgCopy.type = eventType; - emit newMessageToSend(msgCopy); - } - resetReply(); - resetEdit(); + if constexpr (std::is_same_v<T, mtx::events::msg::StickerImage>) { + mtx::events::Sticker msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } else { + mtx::events::RoomEvent<T> msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } + resetReply(); + resetEdit(); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index ea231b03..8a33dc2b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -67,73 +67,72 @@ template<typename T> static constexpr bool messageWithFileAndUrl(const mtx::events::Event<T> &) { - return is_detected<file_t, T>::value && is_detected<url_t, T>::value; + return is_detected<file_t, T>::value && is_detected<url_t, T>::value; } template<typename T> static constexpr void removeReplyFallback(mtx::events::Event<T> &e) { - if constexpr (is_detected<body_t, T>::value) { - if constexpr (std::is_same_v<std::optional<std::string>, - std::remove_cv_t<decltype(e.content.body)>>) { - if (e.content.body) { - e.content.body = utils::stripReplyFromBody(e.content.body); - } - } else if constexpr (std::is_same_v<std::string, - std::remove_cv_t<decltype(e.content.body)>>) { - e.content.body = utils::stripReplyFromBody(e.content.body); - } + if constexpr (is_detected<body_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.body)>>) { + if (e.content.body) { + e.content.body = utils::stripReplyFromBody(e.content.body); + } + } else if constexpr (std::is_same_v<std::string, + std::remove_cv_t<decltype(e.content.body)>>) { + e.content.body = utils::stripReplyFromBody(e.content.body); } + } - if constexpr (is_detected<formatted_body_t, T>::value) { - if (e.content.format == "org.matrix.custom.html") { - e.content.formatted_body = - utils::stripReplyFromFormattedBody(e.content.formatted_body); - } + if constexpr (is_detected<formatted_body_t, T>::value) { + if (e.content.format == "org.matrix.custom.html") { + e.content.formatted_body = utils::stripReplyFromFormattedBody(e.content.formatted_body); } + } } } void TimelineViewManager::updateColorPalette() { - userColors.clear(); + userColors.clear(); - if (ChatPage::instance()->userSettings()->theme() == "light") { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); - } else if (ChatPage::instance()->userSettings()->theme() == "dark") { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); - } else { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", nullptr); - } + if (ChatPage::instance()->userSettings()->theme() == "light") { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); + } else if (ChatPage::instance()->userSettings()->theme() == "dark") { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); + } } QColor TimelineViewManager::userColor(QString id, QColor background) { - if (!userColors.contains(id)) - userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background))); - return userColors.value(id); + if (!userColors.contains(id)) + userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background))); + return userColors.value(id); } QString TimelineViewManager::userPresence(QString id) const { - if (id.isEmpty()) - return ""; - else - return QString::fromStdString( - mtx::presence::to_string(cache::presenceState(id.toStdString()))); + if (id.isEmpty()) + return ""; + else + return QString::fromStdString( + mtx::presence::to_string(cache::presenceState(id.toStdString()))); } QString TimelineViewManager::userStatus(QString id) const { - return QString::fromStdString(cache::statusMessage(id.toStdString())); + return QString::fromStdString(cache::statusMessage(id.toStdString())); } TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) @@ -146,453 +145,432 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par , rooms_(new RoomlistModel(this)) , communities_(new CommunitiesModel(this)) { - qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationDone>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationKey>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationMac>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationReady>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>(); - qRegisterMetaType<mtx::events::msg::KeyVerificationStart>(); - qRegisterMetaType<CombinedImagePackModel *>(); - - qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>(); - - qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "im.nheko", - 1, - 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, - "im.nheko", - 1, - 0, - "VerificationStatus", - "Can't instantiate enum!"); - - qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); - qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea"); - qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape"); - qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage"); - qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia"); - qmlRegisterUncreatableType<DeviceVerificationFlow>( - "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); - qmlRegisterUncreatableType<UserProfile>( - "im.nheko", - 1, - 0, - "UserProfileModel", - "UserProfile needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<MemberList>( - "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<RoomSettings>( - "im.nheko", - 1, - 0, - "RoomSettingsModel", - "Room Settings needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<TimelineModel>( - "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<ImagePackListModel>( - "im.nheko", - 1, - 0, - "ImagePackListModel", - "ImagePackListModel needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<SingleImagePackModel>( - "im.nheko", - 1, - 0, - "SingleImagePackModel", - "SingleImagePackModel needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<InviteesModel>( - "im.nheko", - 1, - 0, - "InviteesModel", - "InviteesModel needs to be instantiated on the C++ side"); - qmlRegisterUncreatableType<ReadReceiptsProxy>( - "im.nheko", - 1, - 0, - "ReadReceiptsProxy", - "ReadReceiptsProxy needs to be instantiated on the C++ side"); - - static auto self = this; - qmlRegisterSingletonType<MainWindow>( - "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = MainWindow::instance(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType<TimelineViewManager>( - "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = self; - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType<RoomlistModel>( - "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = new FilteredRoomlistModel(self->rooms_); - - connect(self->communities_, - &CommunitiesModel::currentTagIdChanged, - ptr, - &FilteredRoomlistModel::updateFilterTag); - connect(self->communities_, - &CommunitiesModel::hiddenTagsChanged, - ptr, - &FilteredRoomlistModel::updateHiddenTagsAndSpaces); - return ptr; - }); - qmlRegisterSingletonType<RoomlistModel>( - "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = self->communities_; - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType<UserSettings>( - "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = ChatPage::instance()->userSettings().data(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType<CallManager>( - "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * { - auto ptr = ChatPage::instance()->callManager(); - QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); - return ptr; - }); - qmlRegisterSingletonType<Clipboard>( - "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new Clipboard(); - }); - qmlRegisterSingletonType<Nheko>( - "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new Nheko(); - }); - - qRegisterMetaType<mtx::events::collections::TimelineEvents>(); - qRegisterMetaType<std::vector<DeviceInfo>>(); - - qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel"); - qmlRegisterUncreatableType<emoji::Emoji>( - "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models"); - qmlRegisterUncreatableMetaObject(emoji::staticMetaObject, - "im.nheko.EmojiModel", - 1, - 0, - "EmojiCategory", - "Error: Only enums"); - - qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel"); + qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationDone>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationKey>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationMac>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationReady>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationStart>(); + qRegisterMetaType<CombinedImagePackModel *>(); + + qRegisterMetaType<std::vector<mtx::responses::PublicRoomsChunk>>(); + + qmlRegisterUncreatableMetaObject( + qml_mtx_events::staticMetaObject, "im.nheko", 1, 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, + "im.nheko", + 1, + 0, + "VerificationStatus", + "Can't instantiate enum!"); + + qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); + qmlRegisterType<NhekoDropArea>("im.nheko", 1, 0, "NhekoDropArea"); + qmlRegisterType<NhekoCursorShape>("im.nheko", 1, 0, "CursorShape"); + qmlRegisterType<MxcAnimatedImage>("im.nheko", 1, 0, "MxcAnimatedImage"); + qmlRegisterType<MxcMediaProxy>("im.nheko", 1, 0, "MxcMedia"); + qmlRegisterUncreatableType<DeviceVerificationFlow>( + "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); + qmlRegisterUncreatableType<UserProfile>( + "im.nheko", 1, 0, "UserProfileModel", "UserProfile needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<MemberList>( + "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<RoomSettings>( + "im.nheko", + 1, + 0, + "RoomSettingsModel", + "Room Settings needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<TimelineModel>( + "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<ImagePackListModel>( + "im.nheko", + 1, + 0, + "ImagePackListModel", + "ImagePackListModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<SingleImagePackModel>( + "im.nheko", + 1, + 0, + "SingleImagePackModel", + "SingleImagePackModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<InviteesModel>( + "im.nheko", 1, 0, "InviteesModel", "InviteesModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<ReadReceiptsProxy>( + "im.nheko", + 1, + 0, + "ReadReceiptsProxy", + "ReadReceiptsProxy needs to be instantiated on the C++ side"); + + static auto self = this; + qmlRegisterSingletonType<MainWindow>( + "im.nheko", 1, 0, "MainWindow", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = MainWindow::instance(); + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); + qmlRegisterSingletonType<TimelineViewManager>( + "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = self; + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = new FilteredRoomlistModel(self->rooms_); + + connect(self->communities_, + &CommunitiesModel::currentTagIdChanged, + ptr, + &FilteredRoomlistModel::updateFilterTag); + connect(self->communities_, + &CommunitiesModel::hiddenTagsChanged, + ptr, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); + return ptr; + }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = self->communities_; + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); + qmlRegisterSingletonType<UserSettings>( + "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = ChatPage::instance()->userSettings().data(); + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); + qmlRegisterSingletonType<CallManager>( + "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = ChatPage::instance()->callManager(); + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); + qmlRegisterSingletonType<Clipboard>( + "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Clipboard(); + }); + qmlRegisterSingletonType<Nheko>( + "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Nheko(); + }); + + qRegisterMetaType<mtx::events::collections::TimelineEvents>(); + qRegisterMetaType<std::vector<DeviceInfo>>(); + + qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel"); + qmlRegisterUncreatableType<emoji::Emoji>( + "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models"); + qmlRegisterUncreatableMetaObject( + emoji::staticMetaObject, "im.nheko.EmojiModel", 1, 0, "EmojiCategory", "Error: Only enums"); + + qmlRegisterType<RoomDirectoryModel>("im.nheko", 1, 0, "RoomDirectoryModel"); #ifdef USE_QUICK_VIEW - view = new QQuickView(parent); - container = QWidget::createWindowContainer(view, parent); + view = new QQuickView(parent); + container = QWidget::createWindowContainer(view, parent); #else - view = new QQuickWidget(parent); - container = view; - view->setResizeMode(QQuickWidget::SizeRootObjectToView); - container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { - nhlog::ui()->debug("Status changed to {}", status); - }); + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); #endif - container->setMinimumSize(200, 200); - updateColorPalette(); - view->engine()->addImageProvider("MxcImage", imgProvider); - view->engine()->addImageProvider("colorimage", colorImgProvider); - view->engine()->addImageProvider("blurhash", blurhashProvider); - if (JdenticonProvider::isAvailable()) - view->engine()->addImageProvider("jdenticon", jdenticonProvider); - view->setSource(QUrl("qrc:///qml/Root.qml")); - - connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); - connect( - dynamic_cast<ChatPage *>(parent), - &ChatPage::receivedRoomDeviceVerificationRequest, - this, - [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, - TimelineModel *model) { - if (this->isInitialSync_) - return; - - auto event_id = QString::fromStdString(message.event_id); - if (!this->dvList.contains(event_id)) { - if (auto flow = DeviceVerificationFlow::NewInRoomVerification( - this, - model, - message.content, - QString::fromStdString(message.sender), - event_id)) { - dvList[event_id] = flow; - emit newDeviceVerificationRequest(flow.data()); - } - } - }); - connect(dynamic_cast<ChatPage *>(parent), - &ChatPage::receivedDeviceVerificationRequest, - this, - [this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) { - if (this->isInitialSync_) - return; - - if (!msg.transaction_id) - return; - - auto txnid = QString::fromStdString(msg.transaction_id.value()); - if (!this->dvList.contains(txnid)) { - if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( - this, msg, QString::fromStdString(sender), txnid)) { - dvList[txnid] = flow; - emit newDeviceVerificationRequest(flow.data()); - } - } - }); - connect(dynamic_cast<ChatPage *>(parent), - &ChatPage::receivedDeviceVerificationStart, - this, - [this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) { - if (this->isInitialSync_) - return; - - if (!msg.transaction_id) - return; - - auto txnid = QString::fromStdString(msg.transaction_id.value()); - if (!this->dvList.contains(txnid)) { - if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( - this, msg, QString::fromStdString(sender), txnid)) { - dvList[txnid] = flow; - emit newDeviceVerificationRequest(flow.data()); - } - } - }); - connect(parent, &ChatPage::loggedOut, this, [this]() { - isInitialSync_ = true; - emit initialSyncChanged(true); - }); + container->setMinimumSize(200, 200); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->engine()->addImageProvider("blurhash", blurhashProvider); + if (JdenticonProvider::isAvailable()) + view->engine()->addImageProvider("jdenticon", jdenticonProvider); + view->setSource(QUrl("qrc:///qml/Root.qml")); + + connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedRoomDeviceVerificationRequest, + this, + [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, + TimelineModel *model) { + if (this->isInitialSync_) + return; + + auto event_id = QString::fromStdString(message.event_id); + if (!this->dvList.contains(event_id)) { + if (auto flow = DeviceVerificationFlow::NewInRoomVerification( + this, + model, + message.content, + QString::fromStdString(message.sender), + event_id)) { + dvList[event_id] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedDeviceVerificationRequest, + this, + [this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) { + if (this->isInitialSync_) + return; + + if (!msg.transaction_id) + return; + + auto txnid = QString::fromStdString(msg.transaction_id.value()); + if (!this->dvList.contains(txnid)) { + if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( + this, msg, QString::fromStdString(sender), txnid)) { + dvList[txnid] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedDeviceVerificationStart, + this, + [this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) { + if (this->isInitialSync_) + return; + + if (!msg.transaction_id) + return; + + auto txnid = QString::fromStdString(msg.transaction_id.value()); + if (!this->dvList.contains(txnid)) { + if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( + this, msg, QString::fromStdString(sender), txnid)) { + dvList[txnid] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); + connect(parent, &ChatPage::loggedOut, this, [this]() { + isInitialSync_ = true; + emit initialSyncChanged(true); + }); - connect(this, - &TimelineViewManager::openImageOverlayInternalCb, - this, - &TimelineViewManager::openImageOverlayInternal); + connect(this, + &TimelineViewManager::openImageOverlayInternalCb, + this, + &TimelineViewManager::openImageOverlayInternal); } void TimelineViewManager::openRoomMembers(TimelineModel *room) { - if (!room) - return; - MemberList *memberList = new MemberList(room->roomId(), this); - emit openRoomMembersDialog(memberList, room); + if (!room) + return; + MemberList *memberList = new MemberList(room->roomId(), this); + emit openRoomMembersDialog(memberList, room); } void TimelineViewManager::openRoomSettings(QString room_id) { - RoomSettings *settings = new RoomSettings(room_id, this); - connect(rooms_->getRoomById(room_id).data(), - &TimelineModel::roomAvatarUrlChanged, - settings, - &RoomSettings::avatarChanged); - emit openRoomSettingsDialog(settings); + RoomSettings *settings = new RoomSettings(room_id, this); + connect(rooms_->getRoomById(room_id).data(), + &TimelineModel::roomAvatarUrlChanged, + settings, + &RoomSettings::avatarChanged); + emit openRoomSettingsDialog(settings); } void TimelineViewManager::openInviteUsers(QString roomId) { - InviteesModel *model = new InviteesModel{this}; - connect(model, &InviteesModel::accept, this, [this, model, roomId]() { - emit inviteUsers(roomId, model->mxids()); - }); - emit openInviteUsersDialog(model); + InviteesModel *model = new InviteesModel{this}; + connect(model, &InviteesModel::accept, this, [this, model, roomId]() { + emit inviteUsers(roomId, model->mxids()); + }); + emit openInviteUsersDialog(model); } void TimelineViewManager::openGlobalUserProfile(QString userId) { - UserProfile *profile = new UserProfile{QString{}, userId, this}; - emit openProfile(profile); + UserProfile *profile = new UserProfile{QString{}, userId, this}; + emit openProfile(profile); } void TimelineViewManager::setVideoCallItem() { - WebRTCSession::instance().setVideoItem( - view->rootObject()->findChild<QQuickItem *>("videoCallItem")); + WebRTCSession::instance().setVideoItem( + view->rootObject()->findChild<QQuickItem *>("videoCallItem")); } void TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res) { - this->rooms_->sync(rooms_res); - this->communities_->sync(rooms_res); + this->rooms_->sync(rooms_res); + this->communities_->sync(rooms_res); - if (isInitialSync_) { - this->isInitialSync_ = false; - emit initialSyncChanged(false); - } + if (isInitialSync_) { + this->isInitialSync_ = false; + emit initialSyncChanged(false); + } } void TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) { - if (auto room = rooms_->getRoomById(room_id)) { - if (rooms_->currentRoom() != room) { - rooms_->setCurrentRoom(room_id); - container->setFocus(); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } - - room->showEvent(event_id); + if (auto room = rooms_->getRoomById(room_id)) { + if (rooms_->currentRoom() != room) { + rooms_->setCurrentRoom(room_id); + container->setFocus(); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); } + + room->showEvent(event_id); + } } QString TimelineViewManager::escapeEmoji(QString str) const { - return utils::replaceEmoji(str); + return utils::replaceEmoji(str); } void TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) { - if (mxcUrl.isEmpty()) { - return; - } + if (mxcUrl.isEmpty()) { + return; + } - MxcImageProvider::download( - mxcUrl.remove("mxc://"), QSize(), [this, eventId](QString, QSize, QImage img, QString) { - if (img.isNull()) { - nhlog::ui()->error("Error when retrieving image for overlay."); - return; - } + MxcImageProvider::download( + mxcUrl.remove("mxc://"), QSize(), [this, eventId](QString, QSize, QImage img, QString) { + if (img.isNull()) { + nhlog::ui()->error("Error when retrieving image for overlay."); + return; + } - emit openImageOverlayInternalCb(eventId, std::move(img)); - }); + emit openImageOverlayInternalCb(eventId, std::move(img)); + }); } void TimelineViewManager::openImagePackSettings(QString roomid) { - emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this)); + emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this)); } void TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) { - auto pixmap = QPixmap::fromImage(img); + auto pixmap = QPixmap::fromImage(img); - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->showFullScreen(); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->showFullScreen(); - auto room = rooms_->currentRoom(); - connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { - // hide the overlay while presenting the save dialog for better - // cross platform support. - imgDialog->hide(); + auto room = rooms_->currentRoom(); + connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { + // hide the overlay while presenting the save dialog for better + // cross platform support. + imgDialog->hide(); - if (!room->saveMedia(eventId)) { - imgDialog->show(); - } else { - imgDialog->close(); - } - }); + if (!room->saveMedia(eventId)) { + imgDialog->show(); + } else { + imgDialog->close(); + } + }); } void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { - MainWindow::instance()->openLeaveRoomDialog(roomid); + MainWindow::instance()->openLeaveRoomDialog(roomid); } void TimelineViewManager::verifyUser(QString userid) { - auto joined_rooms = cache::joinedRooms(); - auto room_infos = cache::getRoomInfo(joined_rooms); - - for (std::string room_id : joined_rooms) { - if ((room_infos[QString::fromStdString(room_id)].member_count == 2) && - cache::isRoomEncrypted(room_id)) { - auto room_members = cache::roomMembers(room_id); - if (std::find(room_members.begin(), - room_members.end(), - (userid).toStdString()) != room_members.end()) { - if (auto model = - rooms_->getRoomById(QString::fromStdString(room_id))) { - auto flow = - DeviceVerificationFlow::InitiateUserVerification( - this, model.data(), userid); - connect(model.data(), - &TimelineModel::updateFlowEventId, - this, - [this, flow](std::string eventId) { - dvList[QString::fromStdString(eventId)] = - flow; - }); - emit newDeviceVerificationRequest(flow.data()); - return; - } - } + auto joined_rooms = cache::joinedRooms(); + auto room_infos = cache::getRoomInfo(joined_rooms); + + for (std::string room_id : joined_rooms) { + if ((room_infos[QString::fromStdString(room_id)].member_count == 2) && + cache::isRoomEncrypted(room_id)) { + auto room_members = cache::roomMembers(room_id); + if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != + room_members.end()) { + if (auto model = rooms_->getRoomById(QString::fromStdString(room_id))) { + auto flow = + DeviceVerificationFlow::InitiateUserVerification(this, model.data(), userid); + connect(model.data(), + &TimelineModel::updateFlowEventId, + this, + [this, flow](std::string eventId) { + dvList[QString::fromStdString(eventId)] = flow; + }); + emit newDeviceVerificationRequest(flow.data()); + return; } + } } + } - emit ChatPage::instance()->showNotification( - tr("No encrypted private chat found with this user. Create an " - "encrypted private chat with this user and try again.")); + emit ChatPage::instance()->showNotification( + tr("No encrypted private chat found with this user. Create an " + "encrypted private chat with this user and try again.")); } void TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow) { - for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) { - if ((*it).second == flow) { - dvList.remove((*it).first); - return; - } + for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) { + if ((*it).second == flow) { + dvList.remove((*it).first); + return; } + } } void TimelineViewManager::verifyDevice(QString userid, QString deviceid) { - auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid); - this->dvList[flow->transactionId()] = flow; - emit newDeviceVerificationRequest(flow.data()); + auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid); + this->dvList[flow->transactionId()] = flow; + emit newDeviceVerificationRequest(flow.data()); } void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids) { - if (auto room = rooms_->getRoomById(room_id)) { - room->markEventsAsRead(event_ids); - } + if (auto room = rooms_->getRoomById(room_id)) { + room->markEventsAsRead(event_ids); + } } void TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) { - if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) { - room->receivedSessionKey(session_id); - } + if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) { + room->receivedSessionKey(session_id); + } } void TimelineViewManager::initializeRoomlist() { - rooms_->initializeRooms(); - communities_->initializeSidebar(); + rooms_->initializeRooms(); + communities_->initializeSidebar(); } void @@ -600,178 +578,175 @@ TimelineViewManager::queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody) { - if (auto room = rooms_->getRoomById(roomid)) { - room->setReply(repliedToEvent); - room->input()->message(replyBody); - } + if (auto room = rooms_->getRoomById(roomid)) { + room->setReply(repliedToEvent); + room->input()->message(replyBody); + } } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) { - if (auto room = rooms_->getRoomById(roomid)) - room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &callCandidates) { - if (auto room = rooms_->getRoomById(roomid)) - room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &callAnswer) { - if (auto room = rooms_->getRoomById(roomid)) - room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &callHangUp) { - if (auto room = rooms_->getRoomById(roomid)) - room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } void TimelineViewManager::focusMessageInput() { - emit focusInput(); + emit focusInput(); } QObject * TimelineViewManager::completerFor(QString completerName, QString roomId) { - if (completerName == "user") { - auto userModel = new UsersModel(roomId.toStdString()); - auto proxy = new CompletionProxyModel(userModel); - userModel->setParent(proxy); - return proxy; - } else if (completerName == "emoji") { - auto emojiModel = new emoji::EmojiModel(); - auto proxy = new CompletionProxyModel(emojiModel); - emojiModel->setParent(proxy); - return proxy; - } else if (completerName == "allemoji") { - auto emojiModel = new emoji::EmojiModel(); - auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4); - emojiModel->setParent(proxy); - return proxy; - } else if (completerName == "room") { - auto roomModel = new RoomsModel(false); - auto proxy = new CompletionProxyModel(roomModel, 4); - roomModel->setParent(proxy); - return proxy; - } else if (completerName == "roomAliases") { - auto roomModel = new RoomsModel(true); - auto proxy = new CompletionProxyModel(roomModel); - roomModel->setParent(proxy); - return proxy; - } else if (completerName == "stickers") { - auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true); - auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4); - stickerModel->setParent(proxy); - return proxy; - } - return nullptr; + if (completerName == "user") { + auto userModel = new UsersModel(roomId.toStdString()); + auto proxy = new CompletionProxyModel(userModel); + userModel->setParent(proxy); + return proxy; + } else if (completerName == "emoji") { + auto emojiModel = new emoji::EmojiModel(); + auto proxy = new CompletionProxyModel(emojiModel); + emojiModel->setParent(proxy); + return proxy; + } else if (completerName == "allemoji") { + auto emojiModel = new emoji::EmojiModel(); + auto proxy = new CompletionProxyModel(emojiModel, 1, static_cast<size_t>(-1) / 4); + emojiModel->setParent(proxy); + return proxy; + } else if (completerName == "room") { + auto roomModel = new RoomsModel(false); + auto proxy = new CompletionProxyModel(roomModel, 4); + roomModel->setParent(proxy); + return proxy; + } else if (completerName == "roomAliases") { + auto roomModel = new RoomsModel(true); + auto proxy = new CompletionProxyModel(roomModel); + roomModel->setParent(proxy); + return proxy; + } else if (completerName == "stickers") { + auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true); + auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast<size_t>(-1) / 4); + stickerModel->setParent(proxy); + return proxy; + } + return nullptr; } void TimelineViewManager::focusTimeline() { - getWidget()->setFocus(); + getWidget()->setFocus(); } void TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId) { - auto room = rooms_->getRoomById(roomId); - auto content = mtx::accessors::url(*e); - std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); - - if (encryptionInfo) { - http::client()->download( - content, - [this, roomId, e, encryptionInfo](const std::string &res, - const std::string &content_type, - const std::string &originalFilename, - mtx::http::RequestErr err) { - if (err) - return; - - auto data = mtx::crypto::to_string( - mtx::crypto::decrypt_file(res, encryptionInfo.value())); - - http::client()->upload( - data, - content_type, - originalFilename, - [this, roomId, e](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) mutable { - if (err) { - nhlog::net()->warn("failed to upload media: {} {} ({})", - err->matrix_error.error, - to_string(err->matrix_error.errcode), - static_cast<int>(err->status_code)); - return; - } - - std::visit( - [this, roomId, url = res.content_uri](auto ev) { - if constexpr (mtx::events::message_content_to_type< - decltype(ev.content)> == - mtx::events::EventType::RoomMessage) { - if constexpr (messageWithFileAndUrl(ev)) { - ev.content.relations.relations - .clear(); - ev.content.file.reset(); - ev.content.url = url; - } - - if (auto room = rooms_->getRoomById(roomId)) { - removeReplyFallback(ev); - ev.content.relations.relations - .clear(); - room->sendMessageEvent( - ev.content, - mtx::events::EventType:: - RoomMessage); - } - } - }, - *e); - }); + auto room = rooms_->getRoomById(roomId); + auto content = mtx::accessors::url(*e); + std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); + + if (encryptionInfo) { + http::client()->download( + content, + [this, roomId, e, encryptionInfo](const std::string &res, + const std::string &content_type, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) + return; + + auto data = + mtx::crypto::to_string(mtx::crypto::decrypt_file(res, encryptionInfo.value())); + + http::client()->upload( + data, + content_type, + originalFilename, + [this, roomId, e](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) mutable { + if (err) { + nhlog::net()->warn("failed to upload media: {} {} ({})", + err->matrix_error.error, + to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + return; + } + + std::visit( + [this, roomId, url = res.content_uri](auto ev) { + using namespace mtx::events; + if constexpr (EventType::RoomMessage == + message_content_to_type<decltype(ev.content)> || + EventType::Sticker == + message_content_to_type<decltype(ev.content)>) { + if constexpr (messageWithFileAndUrl(ev)) { + ev.content.relations.relations.clear(); + ev.content.file.reset(); + ev.content.url = url; + } + + if (auto room = rooms_->getRoomById(roomId)) { + removeReplyFallback(ev); + ev.content.relations.relations.clear(); + room->sendMessageEvent(ev.content, + mtx::events::EventType::RoomMessage); + } + } + }, + *e); + }); - return; - }); + return; + }); - return; - } + return; + } - std::visit( - [room](auto e) { - if constexpr (mtx::events::message_content_to_type<decltype(e.content)> == - mtx::events::EventType::RoomMessage) { - e.content.relations.relations.clear(); - removeReplyFallback(e); - room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage); - } - }, - *e); + std::visit( + [room](auto e) { + if constexpr (mtx::events::message_content_to_type<decltype(e.content)> == + mtx::events::EventType::RoomMessage) { + e.content.relations.relations.clear(); + removeReplyFallback(e); + room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage); + } + }, + *e); } //! WORKAROUND(Nico): for https://bugreports.qt.io/browse/QTBUG-93281 void TimelineViewManager::fixImageRendering(QQuickTextDocument *t, QQuickItem *i) { - if (t) { - QObject::connect( - t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument())); - } + if (t) { + QObject::connect(t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument())); + } } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 8991de55..f7b01315 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -38,123 +38,121 @@ class ImagePackListModel; class TimelineViewManager : public QObject { - Q_OBJECT + Q_OBJECT - Q_PROPERTY( - bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) - Q_PROPERTY( - bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) + Q_PROPERTY( + bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged) public: - TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); - QWidget *getWidget() const { return container; } + TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); + QWidget *getWidget() const { return container; } - void sync(const mtx::responses::Rooms &rooms); + void sync(const mtx::responses::Rooms &rooms); - MxcImageProvider *imageProvider() { return imgProvider; } - CallManager *callManager() { return callManager_; } + MxcImageProvider *imageProvider() { return imgProvider; } + CallManager *callManager() { return callManager_; } - void clearAll() { rooms_->clear(); } + void clearAll() { rooms_->clear(); } - Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - bool isWindowFocused() const { return isWindowFocused_; } - Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); - Q_INVOKABLE void openImagePackSettings(QString roomid); - Q_INVOKABLE QColor userColor(QString id, QColor background); - Q_INVOKABLE QString escapeEmoji(QString str) const; - Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } + bool isWindowFocused() const { return isWindowFocused_; } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); + Q_INVOKABLE void openImagePackSettings(QString roomid); + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } - Q_INVOKABLE QString userPresence(QString id) const; - Q_INVOKABLE QString userStatus(QString id) const; + Q_INVOKABLE QString userPresence(QString id) const; + Q_INVOKABLE QString userStatus(QString id) const; - Q_INVOKABLE void openRoomMembers(TimelineModel *room); - Q_INVOKABLE void openRoomSettings(QString room_id); - Q_INVOKABLE void openInviteUsers(QString roomId); - Q_INVOKABLE void openGlobalUserProfile(QString userId); + Q_INVOKABLE void openRoomMembers(TimelineModel *room); + Q_INVOKABLE void openRoomSettings(QString room_id); + Q_INVOKABLE void openInviteUsers(QString roomId); + Q_INVOKABLE void openGlobalUserProfile(QString userId); - Q_INVOKABLE void focusMessageInput(); - Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; - Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); + Q_INVOKABLE void focusMessageInput(); + Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; + Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); - Q_INVOKABLE void fixImageRendering(QQuickTextDocument *t, QQuickItem *i); + Q_INVOKABLE void fixImageRendering(QQuickTextDocument *t, QQuickItem *i); - void verifyUser(QString userid); - void verifyDevice(QString userid, QString deviceid); + void verifyUser(QString userid); + void verifyDevice(QString userid, QString deviceid); signals: - void activeTimelineChanged(TimelineModel *timeline); - void initialSyncChanged(bool isInitialSync); - void replyingEventChanged(QString replyingEvent); - void replyClosed(); - void newDeviceVerificationRequest(DeviceVerificationFlow *flow); - void inviteUsers(QString roomId, QStringList users); - void showRoomList(); - void narrowViewChanged(); - void focusChanged(); - void focusInput(); - void openImageOverlayInternalCb(QString eventId, QImage img); - void openRoomMembersDialog(MemberList *members, TimelineModel *room); - void openRoomSettingsDialog(RoomSettings *settings); - void openInviteUsersDialog(InviteesModel *invitees); - void openProfile(UserProfile *profile); - void showImagePackSettings(ImagePackListModel *packlist); + void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); + void replyingEventChanged(QString replyingEvent); + void replyClosed(); + void newDeviceVerificationRequest(DeviceVerificationFlow *flow); + void inviteUsers(QString roomId, QStringList users); + void showRoomList(); + void narrowViewChanged(); + void focusChanged(); + void focusInput(); + void openImageOverlayInternalCb(QString eventId, QImage img); + void openRoomMembersDialog(MemberList *members, TimelineModel *room); + void openRoomSettingsDialog(RoomSettings *settings); + void openInviteUsersDialog(InviteesModel *invitees); + void openProfile(UserProfile *profile); + void showImagePackSettings(ImagePackListModel *packlist); public slots: - void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void initializeRoomlist(); - void chatFocusChanged(bool focused) - { - isWindowFocused_ = focused; - emit focusChanged(); - } - - void showEvent(const QString &room_id, const QString &event_id); - void focusTimeline(); - - void updateColorPalette(); - void queueReply(const QString &roomid, - const QString &repliedToEvent, - const QString &replyBody); - void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); - void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); - void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); - void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); - - void setVideoCallItem(); - - QObject *completerFor(QString completerName, QString roomId = ""); - void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); - - RoomlistModel *rooms() { return rooms_; } + void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); + void receivedSessionKey(const std::string &room_id, const std::string &session_id); + void initializeRoomlist(); + void chatFocusChanged(bool focused) + { + isWindowFocused_ = focused; + emit focusChanged(); + } + + void showEvent(const QString &room_id, const QString &event_id); + void focusTimeline(); + + void updateColorPalette(); + void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody); + void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); + void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); + void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); + void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); + + void setVideoCallItem(); + + QObject *completerFor(QString completerName, QString roomId = ""); + void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); + + RoomlistModel *rooms() { return rooms_; } private slots: - void openImageOverlayInternal(QString eventId, QImage img); + void openImageOverlayInternal(QString eventId, QImage img); private: #ifdef USE_QUICK_VIEW - QQuickView *view; + QQuickView *view; #else - QQuickWidget *view; + QQuickWidget *view; #endif - QWidget *container; + QWidget *container; - MxcImageProvider *imgProvider; - ColorImageProvider *colorImgProvider; - BlurhashProvider *blurhashProvider; - JdenticonProvider *jdenticonProvider; + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + BlurhashProvider *blurhashProvider; + JdenticonProvider *jdenticonProvider; - CallManager *callManager_ = nullptr; + CallManager *callManager_ = nullptr; - bool isInitialSync_ = true; - bool isWindowFocused_ = false; + bool isInitialSync_ = true; + bool isWindowFocused_ = false; - RoomlistModel *rooms_ = nullptr; - CommunitiesModel *communities_ = nullptr; + RoomlistModel *rooms_ = nullptr; + CommunitiesModel *communities_ = nullptr; - QHash<QString, QColor> userColors; + QHash<QString, QColor> userColors; - QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList; + QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList; }; Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept) Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel) diff --git a/src/ui/Badge.cpp b/src/ui/Badge.cpp index 66210d06..1b5aba2f 100644 --- a/src/ui/Badge.cpp +++ b/src/ui/Badge.cpp @@ -9,214 +9,214 @@ Badge::Badge(QWidget *parent) : OverlayWidget(parent) { - init(); + init(); } Badge::Badge(const QIcon &icon, QWidget *parent) : OverlayWidget(parent) { - init(); - setIcon(icon); + init(); + setIcon(icon); } Badge::Badge(const QString &text, QWidget *parent) : OverlayWidget(parent) { - init(); - setText(text); + init(); + setText(text); } void Badge::init() { - x_ = 0; - y_ = 0; - // TODO: Make padding configurable. - padding_ = 5; - diameter_ = 24; + x_ = 0; + y_ = 0; + // TODO: Make padding configurable. + padding_ = 5; + diameter_ = 24; - setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_TransparentForMouseEvents); - QFont _font(font()); - _font.setPointSizeF(7.5); - _font.setStyleName("Bold"); + QFont _font(font()); + _font.setPointSizeF(7.5); + _font.setStyleName("Bold"); - setFont(_font); - setText(""); + setFont(_font); + setText(""); } QString Badge::text() const { - return text_; + return text_; } QIcon Badge::icon() const { - return icon_; + return icon_; } QSize Badge::sizeHint() const { - const int d = diameter(); - return QSize(d + 4, d + 4); + const int d = diameter(); + return QSize(d + 4, d + 4); } qreal Badge::relativeYPosition() const { - return y_; + return y_; } qreal Badge::relativeXPosition() const { - return x_; + return x_; } QPointF Badge::relativePosition() const { - return QPointF(x_, y_); + return QPointF(x_, y_); } QColor Badge::backgroundColor() const { - if (!background_color_.isValid()) - return QColor("black"); + if (!background_color_.isValid()) + return QColor("black"); - return background_color_; + return background_color_; } QColor Badge::textColor() const { - if (!text_color_.isValid()) - return QColor("white"); + if (!text_color_.isValid()) + return QColor("white"); - return text_color_; + return text_color_; } void Badge::setTextColor(const QColor &color) { - text_color_ = color; + text_color_ = color; } void Badge::setBackgroundColor(const QColor &color) { - background_color_ = color; + background_color_ = color; } void Badge::setRelativePosition(const QPointF &pos) { - setRelativePosition(pos.x(), pos.y()); + setRelativePosition(pos.x(), pos.y()); } void Badge::setRelativePosition(qreal x, qreal y) { - x_ = x; - y_ = y; - update(); + x_ = x; + y_ = y; + update(); } void Badge::setRelativeXPosition(qreal x) { - x_ = x; - update(); + x_ = x; + update(); } void Badge::setRelativeYPosition(qreal y) { - y_ = y; - update(); + y_ = y; + update(); } void Badge::setIcon(const QIcon &icon) { - icon_ = icon; - update(); + icon_ = icon; + update(); } void Badge::setText(const QString &text) { - text_ = text; + text_ = text; - if (!icon_.isNull()) - icon_ = QIcon(); + if (!icon_.isNull()) + icon_ = QIcon(); - size_ = fontMetrics().size(Qt::TextShowMnemonic, text); + size_ = fontMetrics().size(Qt::TextShowMnemonic, text); - update(); + update(); } void Badge::setDiameter(int diameter) { - if (diameter > 0) { - diameter_ = diameter; - update(); - } + if (diameter > 0) { + diameter_ = diameter; + update(); + } } void Badge::paintEvent(QPaintEvent *) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.translate(x_, y_); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.translate(x_, y_); - QBrush brush; - brush.setStyle(Qt::SolidPattern); + QBrush brush; + brush.setStyle(Qt::SolidPattern); - painter.setBrush(brush); - painter.setPen(Qt::NoPen); + painter.setBrush(brush); + painter.setPen(Qt::NoPen); - const int d = diameter(); + const int d = diameter(); - QRectF r(0, 0, d, d); - r.translate(QPointF((width() - d), (height() - d)) / 2); + QRectF r(0, 0, d, d); + r.translate(QPointF((width() - d), (height() - d)) / 2); - if (icon_.isNull()) { - QPen pen; - // TODO: Make badge width configurable. - pen.setWidth(1); - pen.setColor(textColor()); + if (icon_.isNull()) { + QPen pen; + // TODO: Make badge width configurable. + pen.setWidth(1); + pen.setColor(textColor()); - painter.setPen(pen); - painter.drawEllipse(r); + painter.setPen(pen); + painter.drawEllipse(r); - painter.setPen(textColor()); - painter.setBrush(Qt::NoBrush); - painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_); - } else { - painter.drawEllipse(r); - QRectF q(0, 0, 16, 16); - q.moveCenter(r.center()); - QPixmap pixmap = icon().pixmap(16, 16); - QPainter icon(&pixmap); - icon.setCompositionMode(QPainter::CompositionMode_SourceIn); - icon.fillRect(pixmap.rect(), textColor()); - painter.drawPixmap(q.toRect(), pixmap); - } + painter.setPen(textColor()); + painter.setBrush(Qt::NoBrush); + painter.drawText(r.translated(0, -0.5), Qt::AlignCenter, text_); + } else { + painter.drawEllipse(r); + QRectF q(0, 0, 16, 16); + q.moveCenter(r.center()); + QPixmap pixmap = icon().pixmap(16, 16); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), textColor()); + painter.drawPixmap(q.toRect(), pixmap); + } } int Badge::diameter() const { - if (icon_.isNull()) { - return qMax(size_.width(), size_.height()) + padding_; - } + if (icon_.isNull()) { + return qMax(size_.width(), size_.height()) + padding_; + } - return diameter_; + return diameter_; } diff --git a/src/ui/Badge.h b/src/ui/Badge.h index 98e16873..15b69b58 100644 --- a/src/ui/Badge.h +++ b/src/ui/Badge.h @@ -13,54 +13,54 @@ class Badge : public OverlayWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) + Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) public: - explicit Badge(QWidget *parent = nullptr); - explicit Badge(const QIcon &icon, QWidget *parent = nullptr); - explicit Badge(const QString &text, QWidget *parent = nullptr); + explicit Badge(QWidget *parent = nullptr); + explicit Badge(const QIcon &icon, QWidget *parent = nullptr); + explicit Badge(const QString &text, QWidget *parent = nullptr); - void setBackgroundColor(const QColor &color); - void setTextColor(const QColor &color); - void setIcon(const QIcon &icon); - void setRelativePosition(const QPointF &pos); - void setRelativePosition(qreal x, qreal y); - void setRelativeXPosition(qreal x); - void setRelativeYPosition(qreal y); - void setText(const QString &text); - void setDiameter(int diameter); + void setBackgroundColor(const QColor &color); + void setTextColor(const QColor &color); + void setIcon(const QIcon &icon); + void setRelativePosition(const QPointF &pos); + void setRelativePosition(qreal x, qreal y); + void setRelativeXPosition(qreal x); + void setRelativeYPosition(qreal y); + void setText(const QString &text); + void setDiameter(int diameter); - QIcon icon() const; - QString text() const; - QColor backgroundColor() const; - QColor textColor() const; - QPointF relativePosition() const; - QSize sizeHint() const override; - qreal relativeXPosition() const; - qreal relativeYPosition() const; + QIcon icon() const; + QString text() const; + QColor backgroundColor() const; + QColor textColor() const; + QPointF relativePosition() const; + QSize sizeHint() const override; + qreal relativeXPosition() const; + qreal relativeYPosition() const; - int diameter() const; + int diameter() const; protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - void init(); + void init(); - QColor background_color_; - QColor text_color_; + QColor background_color_; + QColor text_color_; - QIcon icon_; - QSize size_; - QString text_; + QIcon icon_; + QSize size_; + QString text_; - int padding_; - int diameter_; + int padding_; + int diameter_; - qreal x_; - qreal y_; + qreal x_; + qreal y_; }; diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp index a413e3f7..31d13b93 100644 --- a/src/ui/DropShadow.cpp +++ b/src/ui/DropShadow.cpp @@ -19,94 +19,89 @@ DropShadow::draw(QPainter &painter, qreal width, qreal height) { - painter.setPen(Qt::NoPen); + painter.setPen(Qt::NoPen); - QLinearGradient gradient; - gradient.setColorAt(startPosition, start); - gradient.setColorAt(endPosition0, end); + QLinearGradient gradient; + gradient.setColorAt(startPosition, start); + gradient.setColorAt(endPosition0, end); - // Right - QPointF right0(width - margin, height / 2); - QPointF right1(width, height / 2); - gradient.setStart(right0); - gradient.setFinalStop(right1); - painter.setBrush(QBrush(gradient)); - // Deprecated in 5.13: painter.drawRoundRect( - // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - - // margin)), 0.0, 0.0); - painter.drawRoundedRect( - QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - 0.0, - 0.0); + // Right + QPointF right0(width - margin, height / 2); + QPointF right1(width, height / 2); + gradient.setStart(right0); + gradient.setFinalStop(right1); + painter.setBrush(QBrush(gradient)); + // Deprecated in 5.13: painter.drawRoundRect( + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - + // margin)), 0.0, 0.0); + painter.drawRoundedRect( + QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, 0.0); - // Left - QPointF left0(margin, height / 2); - QPointF left1(0, height / 2); - gradient.setStart(left0); - gradient.setFinalStop(left1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); + // Left + QPointF left0(margin, height / 2); + QPointF left1(0, height / 2); + gradient.setStart(left0); + gradient.setFinalStop(left1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); - // Top - QPointF top0(width / 2, margin); - QPointF top1(width / 2, 0); - gradient.setStart(top0); - gradient.setFinalStop(top1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); + // Top + QPointF top0(width / 2, margin); + QPointF top1(width / 2, 0); + gradient.setStart(top0); + gradient.setFinalStop(top1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); - // Bottom - QPointF bottom0(width / 2, height - margin); - QPointF bottom1(width / 2, height); - gradient.setStart(bottom0); - gradient.setFinalStop(bottom1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0); + // Bottom + QPointF bottom0(width / 2, height - margin); + QPointF bottom1(width / 2, height); + gradient.setStart(bottom0); + gradient.setFinalStop(bottom1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0); - // BottomRight - QPointF bottomright0(width - margin, height - margin); - QPointF bottomright1(width, height); - gradient.setStart(bottomright0); - gradient.setFinalStop(bottomright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); + // BottomRight + QPointF bottomright0(width - margin, height - margin); + QPointF bottomright1(width, height); + gradient.setStart(bottomright0); + gradient.setFinalStop(bottomright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); - // BottomLeft - QPointF bottomleft0(margin, height - margin); - QPointF bottomleft1(0, height); - gradient.setStart(bottomleft0); - gradient.setFinalStop(bottomleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); + // BottomLeft + QPointF bottomleft0(margin, height - margin); + QPointF bottomleft1(0, height); + gradient.setStart(bottomleft0); + gradient.setFinalStop(bottomleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); - // TopLeft - QPointF topleft0(margin, margin); - QPointF topleft1(0, 0); - gradient.setStart(topleft0); - gradient.setFinalStop(topleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); + // TopLeft + QPointF topleft0(margin, margin); + QPointF topleft1(0, 0); + gradient.setStart(topleft0); + gradient.setFinalStop(topleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); - // TopRight - QPointF topright0(width - margin, margin); - QPointF topright1(width, 0); - gradient.setStart(topright0); - gradient.setFinalStop(topright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); + // TopRight + QPointF topright0(width - margin, margin); + QPointF topright1(width, 0); + gradient.setStart(topright0); + gradient.setFinalStop(topright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); - // Widget - painter.setBrush(QBrush("#FFFFFF")); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundedRect( - QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), - radius, - radius); + // Widget + painter.setBrush(QBrush("#FFFFFF")); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( + QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), radius, radius); } diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h index 4ace4731..f089d0b3 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h @@ -11,14 +11,14 @@ class QPainter; class DropShadow { public: - static void draw(QPainter &painter, - qint16 margin, - qreal radius, - QColor start, - QColor end, - qreal startPosition, - qreal endPosition0, - qreal endPosition1, - qreal width, - qreal height); + static void draw(QPainter &painter, + qint16 margin, + qreal radius, + QColor start, + QColor end, + qreal startPosition, + qreal endPosition0, + qreal endPosition1, + qreal width, + qreal height); }; diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp index c036401b..4d19c8bb 100644 --- a/src/ui/FlatButton.cpp +++ b/src/ui/FlatButton.cpp @@ -30,60 +30,60 @@ static QString removeKDEAccelerators(QString text) { - return text.remove(QChar('&')); + return text.remove(QChar('&')); } void FlatButton::init() { - ripple_overlay_ = new RippleOverlay(this); - state_machine_ = new FlatButtonStateMachine(this); - role_ = ui::Role::Default; - ripple_style_ = ui::RippleStyle::PositionedRipple; - icon_placement_ = ui::ButtonIconPlacement::LeftIcon; - overlay_style_ = ui::OverlayStyle::GrayOverlay; - bg_mode_ = Qt::TransparentMode; - fixed_ripple_radius_ = 64; - corner_radius_ = 3; - base_opacity_ = 0.13; - font_size_ = 10; // 10.5; - use_fixed_ripple_radius_ = false; - - setStyle(&ThemeManager::instance()); - setAttribute(Qt::WA_Hover); - setMouseTracking(true); - setCursor(QCursor(Qt::PointingHandCursor)); + ripple_overlay_ = new RippleOverlay(this); + state_machine_ = new FlatButtonStateMachine(this); + role_ = ui::Role::Default; + ripple_style_ = ui::RippleStyle::PositionedRipple; + icon_placement_ = ui::ButtonIconPlacement::LeftIcon; + overlay_style_ = ui::OverlayStyle::GrayOverlay; + bg_mode_ = Qt::TransparentMode; + fixed_ripple_radius_ = 64; + corner_radius_ = 3; + base_opacity_ = 0.13; + font_size_ = 10; // 10.5; + use_fixed_ripple_radius_ = false; - QPainterPath path; - path.addRoundedRect(rect(), corner_radius_, corner_radius_); + setStyle(&ThemeManager::instance()); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + setCursor(QCursor(Qt::PointingHandCursor)); + + QPainterPath path; + path.addRoundedRect(rect(), corner_radius_, corner_radius_); - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); + ripple_overlay_->setClipPath(path); + ripple_overlay_->setClipping(true); - state_machine_->setupProperties(); - state_machine_->startAnimations(); + state_machine_->setupProperties(); + state_machine_->startAnimations(); } FlatButton::FlatButton(QWidget *parent, ui::ButtonPreset preset) : QPushButton(parent) { - init(); - applyPreset(preset); + init(); + applyPreset(preset); } FlatButton::FlatButton(const QString &text, QWidget *parent, ui::ButtonPreset preset) : QPushButton(text, parent) { - init(); - applyPreset(preset); + init(); + applyPreset(preset); } FlatButton::FlatButton(const QString &text, ui::Role role, QWidget *parent, ui::ButtonPreset preset) : QPushButton(text, parent) { - init(); - applyPreset(preset); - setRole(role); + init(); + applyPreset(preset); + setRole(role); } FlatButton::~FlatButton() {} @@ -91,406 +91,406 @@ FlatButton::~FlatButton() {} void FlatButton::applyPreset(ui::ButtonPreset preset) { - switch (preset) { - case ui::ButtonPreset::FlatPreset: - setOverlayStyle(ui::OverlayStyle::NoOverlay); - break; - case ui::ButtonPreset::CheckablePreset: - setOverlayStyle(ui::OverlayStyle::NoOverlay); - setCheckable(true); - break; - default: - break; - } + switch (preset) { + case ui::ButtonPreset::FlatPreset: + setOverlayStyle(ui::OverlayStyle::NoOverlay); + break; + case ui::ButtonPreset::CheckablePreset: + setOverlayStyle(ui::OverlayStyle::NoOverlay); + setCheckable(true); + break; + default: + break; + } } void FlatButton::setRole(ui::Role role) { - role_ = role; - state_machine_->setupProperties(); + role_ = role; + state_machine_->setupProperties(); } ui::Role FlatButton::role() const { - return role_; + return role_; } void FlatButton::setForegroundColor(const QColor &color) { - foreground_color_ = color; + foreground_color_ = color; } QColor FlatButton::foregroundColor() const { - if (!foreground_color_.isValid()) { - if (bg_mode_ == Qt::OpaqueMode) { - return ThemeManager::instance().themeColor("BrightWhite"); - } - - switch (role_) { - case ui::Role::Primary: - return ThemeManager::instance().themeColor("Blue"); - case ui::Role::Secondary: - return ThemeManager::instance().themeColor("Gray"); - case ui::Role::Default: - default: - return ThemeManager::instance().themeColor("Black"); - } + if (!foreground_color_.isValid()) { + if (bg_mode_ == Qt::OpaqueMode) { + return ThemeManager::instance().themeColor("BrightWhite"); + } + + switch (role_) { + case ui::Role::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Role::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::Role::Default: + default: + return ThemeManager::instance().themeColor("Black"); } + } - return foreground_color_; + return foreground_color_; } void FlatButton::setBackgroundColor(const QColor &color) { - background_color_ = color; + background_color_ = color; } QColor FlatButton::backgroundColor() const { - if (!background_color_.isValid()) { - switch (role_) { - case ui::Role::Primary: - return ThemeManager::instance().themeColor("Blue"); - case ui::Role::Secondary: - return ThemeManager::instance().themeColor("Gray"); - case ui::Role::Default: - default: - return ThemeManager::instance().themeColor("Black"); - } + if (!background_color_.isValid()) { + switch (role_) { + case ui::Role::Primary: + return ThemeManager::instance().themeColor("Blue"); + case ui::Role::Secondary: + return ThemeManager::instance().themeColor("Gray"); + case ui::Role::Default: + default: + return ThemeManager::instance().themeColor("Black"); } + } - return background_color_; + return background_color_; } void FlatButton::setOverlayColor(const QColor &color) { - overlay_color_ = color; - setOverlayStyle(ui::OverlayStyle::TintedOverlay); + overlay_color_ = color; + setOverlayStyle(ui::OverlayStyle::TintedOverlay); } QColor FlatButton::overlayColor() const { - if (!overlay_color_.isValid()) { - return foregroundColor(); - } + if (!overlay_color_.isValid()) { + return foregroundColor(); + } - return overlay_color_; + return overlay_color_; } void FlatButton::setDisabledForegroundColor(const QColor &color) { - disabled_color_ = color; + disabled_color_ = color; } QColor FlatButton::disabledForegroundColor() const { - if (!disabled_color_.isValid()) { - return ThemeManager::instance().themeColor("FadedWhite"); - } + if (!disabled_color_.isValid()) { + return ThemeManager::instance().themeColor("FadedWhite"); + } - return disabled_color_; + return disabled_color_; } void FlatButton::setDisabledBackgroundColor(const QColor &color) { - disabled_background_color_ = color; + disabled_background_color_ = color; } QColor FlatButton::disabledBackgroundColor() const { - if (!disabled_background_color_.isValid()) { - return ThemeManager::instance().themeColor("FadedWhite"); - } + if (!disabled_background_color_.isValid()) { + return ThemeManager::instance().themeColor("FadedWhite"); + } - return disabled_background_color_; + return disabled_background_color_; } void FlatButton::setFontSize(qreal size) { - font_size_ = size; + font_size_ = size; - QFont f(font()); - f.setPointSizeF(size); - setFont(f); + QFont f(font()); + f.setPointSizeF(size); + setFont(f); - update(); + update(); } qreal FlatButton::fontSize() const { - return font_size_; + return font_size_; } void FlatButton::setOverlayStyle(ui::OverlayStyle style) { - overlay_style_ = style; - update(); + overlay_style_ = style; + update(); } ui::OverlayStyle FlatButton::overlayStyle() const { - return overlay_style_; + return overlay_style_; } void FlatButton::setRippleStyle(ui::RippleStyle style) { - ripple_style_ = style; + ripple_style_ = style; } ui::RippleStyle FlatButton::rippleStyle() const { - return ripple_style_; + return ripple_style_; } void FlatButton::setIconPlacement(ui::ButtonIconPlacement placement) { - icon_placement_ = placement; - update(); + icon_placement_ = placement; + update(); } ui::ButtonIconPlacement FlatButton::iconPlacement() const { - return icon_placement_; + return icon_placement_; } void FlatButton::setCornerRadius(qreal radius) { - corner_radius_ = radius; - updateClipPath(); - update(); + corner_radius_ = radius; + updateClipPath(); + update(); } qreal FlatButton::cornerRadius() const { - return corner_radius_; + return corner_radius_; } void FlatButton::setBackgroundMode(Qt::BGMode mode) { - bg_mode_ = mode; - state_machine_->setupProperties(); + bg_mode_ = mode; + state_machine_->setupProperties(); } Qt::BGMode FlatButton::backgroundMode() const { - return bg_mode_; + return bg_mode_; } void FlatButton::setBaseOpacity(qreal opacity) { - base_opacity_ = opacity; - state_machine_->setupProperties(); + base_opacity_ = opacity; + state_machine_->setupProperties(); } qreal FlatButton::baseOpacity() const { - return base_opacity_; + return base_opacity_; } void FlatButton::setCheckable(bool value) { - state_machine_->updateCheckedStatus(); - state_machine_->setCheckedOverlayProgress(0); + state_machine_->updateCheckedStatus(); + state_machine_->setCheckedOverlayProgress(0); - QPushButton::setCheckable(value); + QPushButton::setCheckable(value); } void FlatButton::setHasFixedRippleRadius(bool value) { - use_fixed_ripple_radius_ = value; + use_fixed_ripple_radius_ = value; } bool FlatButton::hasFixedRippleRadius() const { - return use_fixed_ripple_radius_; + return use_fixed_ripple_radius_; } void FlatButton::setFixedRippleRadius(qreal radius) { - fixed_ripple_radius_ = radius; - setHasFixedRippleRadius(true); + fixed_ripple_radius_ = radius; + setHasFixedRippleRadius(true); } QSize FlatButton::sizeHint() const { - ensurePolished(); + ensurePolished(); - QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); + QSize label(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); - int w = 20 + label.width(); - int h = label.height(); + int w = 20 + label.width(); + int h = label.height(); - if (!icon().isNull()) { - w += iconSize().width() + FlatButton::IconPadding; - h = qMax(h, iconSize().height()); - } + if (!icon().isNull()) { + w += iconSize().width() + FlatButton::IconPadding; + h = qMax(h, iconSize().height()); + } - return QSize(w, 20 + h); + return QSize(w, 20 + h); } void FlatButton::checkStateSet() { - state_machine_->updateCheckedStatus(); - QPushButton::checkStateSet(); + state_machine_->updateCheckedStatus(); + QPushButton::checkStateSet(); } void FlatButton::mousePressEvent(QMouseEvent *event) { - if (ui::RippleStyle::NoRipple != ripple_style_) { - QPoint pos; - qreal radiusEndValue; + if (ui::RippleStyle::NoRipple != ripple_style_) { + QPoint pos; + qreal radiusEndValue; - if (ui::RippleStyle::CenteredRipple == ripple_style_) { - pos = rect().center(); - } else { - pos = event->pos(); - } + if (ui::RippleStyle::CenteredRipple == ripple_style_) { + pos = rect().center(); + } else { + pos = event->pos(); + } - if (use_fixed_ripple_radius_) { - radiusEndValue = fixed_ripple_radius_; - } else { - radiusEndValue = static_cast<qreal>(width()) / 2; - } + if (use_fixed_ripple_radius_) { + radiusEndValue = fixed_ripple_radius_; + } else { + radiusEndValue = static_cast<qreal>(width()) / 2; + } - Ripple *ripple = new Ripple(pos); + Ripple *ripple = new Ripple(pos); - ripple->setRadiusEndValue(radiusEndValue); - ripple->setOpacityStartValue(0.35); - ripple->setColor(foregroundColor()); - ripple->radiusAnimation()->setDuration(250); - ripple->opacityAnimation()->setDuration(250); + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.35); + ripple->setColor(foregroundColor()); + ripple->radiusAnimation()->setDuration(250); + ripple->opacityAnimation()->setDuration(250); - ripple_overlay_->addRipple(ripple); - } + ripple_overlay_->addRipple(ripple); + } - QPushButton::mousePressEvent(event); + QPushButton::mousePressEvent(event); } void FlatButton::mouseReleaseEvent(QMouseEvent *event) { - QPushButton::mouseReleaseEvent(event); - state_machine_->updateCheckedStatus(); + QPushButton::mouseReleaseEvent(event); + state_machine_->updateCheckedStatus(); } void FlatButton::resizeEvent(QResizeEvent *event) { - QPushButton::resizeEvent(event); - updateClipPath(); + QPushButton::resizeEvent(event); + updateClipPath(); } void FlatButton::paintEvent(QPaintEvent *event) { - Q_UNUSED(event) + Q_UNUSED(event) - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - const qreal cr = corner_radius_; + const qreal cr = corner_radius_; - if (cr > 0) { - QPainterPath path; - path.addRoundedRect(rect(), cr, cr); + if (cr > 0) { + QPainterPath path; + path.addRoundedRect(rect(), cr, cr); - painter.setClipPath(path); - painter.setClipping(true); - } + painter.setClipPath(path); + painter.setClipping(true); + } - paintBackground(&painter); + paintBackground(&painter); - painter.setOpacity(1); - painter.setClipping(false); + painter.setOpacity(1); + painter.setClipping(false); - paintForeground(&painter); + paintForeground(&painter); } void FlatButton::paintBackground(QPainter *painter) { - const qreal overlayOpacity = state_machine_->overlayOpacity(); - const qreal checkedProgress = state_machine_->checkedOverlayProgress(); + const qreal overlayOpacity = state_machine_->overlayOpacity(); + const qreal checkedProgress = state_machine_->checkedOverlayProgress(); - if (Qt::OpaqueMode == bg_mode_) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - - if (isEnabled()) { - brush.setColor(backgroundColor()); - } else { - brush.setColor(disabledBackgroundColor()); - } + if (Qt::OpaqueMode == bg_mode_) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); - painter->setOpacity(1); - painter->setBrush(brush); - painter->setPen(Qt::NoPen); - painter->drawRect(rect()); + if (isEnabled()) { + brush.setColor(backgroundColor()); + } else { + brush.setColor(disabledBackgroundColor()); } - QBrush brush; - brush.setStyle(Qt::SolidPattern); + painter->setOpacity(1); + painter->setBrush(brush); painter->setPen(Qt::NoPen); + painter->drawRect(rect()); + } - if (!isEnabled()) { - return; - } + QBrush brush; + brush.setStyle(Qt::SolidPattern); + painter->setPen(Qt::NoPen); - if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) { - if (ui::OverlayStyle::TintedOverlay == overlay_style_) { - brush.setColor(overlayColor()); - } else { - brush.setColor(Qt::gray); - } + if (!isEnabled()) { + return; + } - painter->setOpacity(overlayOpacity); - painter->setBrush(brush); - painter->drawRect(rect()); + if ((ui::OverlayStyle::NoOverlay != overlay_style_) && (overlayOpacity > 0)) { + if (ui::OverlayStyle::TintedOverlay == overlay_style_) { + brush.setColor(overlayColor()); + } else { + brush.setColor(Qt::gray); } - if (isCheckable() && checkedProgress > 0) { - const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7; - brush.setColor(foregroundColor()); - painter->setOpacity(q * checkedProgress); - painter->setBrush(brush); - QRect r(rect()); - r.setHeight(static_cast<qreal>(r.height()) * checkedProgress); - painter->drawRect(r); - } + painter->setOpacity(overlayOpacity); + painter->setBrush(brush); + painter->drawRect(rect()); + } + + if (isCheckable() && checkedProgress > 0) { + const qreal q = Qt::TransparentMode == bg_mode_ ? 0.45 : 0.7; + brush.setColor(foregroundColor()); + painter->setOpacity(q * checkedProgress); + painter->setBrush(brush); + QRect r(rect()); + r.setHeight(static_cast<qreal>(r.height()) * checkedProgress); + painter->drawRect(r); + } } #define COLOR_INTERPOLATE(CH) (1 - progress) * source.CH() + progress *dest.CH() @@ -498,64 +498,63 @@ FlatButton::paintBackground(QPainter *painter) void FlatButton::paintForeground(QPainter *painter) { - if (isEnabled()) { - painter->setPen(foregroundColor()); - const qreal progress = state_machine_->checkedOverlayProgress(); - - if (isCheckable() && progress > 0) { - QColor source = foregroundColor(); - QColor dest = - Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor(); - if (qFuzzyCompare(1, progress)) { - painter->setPen(dest); - } else { - painter->setPen(QColor(COLOR_INTERPOLATE(red), - COLOR_INTERPOLATE(green), - COLOR_INTERPOLATE(blue), - COLOR_INTERPOLATE(alpha))); - } - } - } else { - painter->setPen(disabledForegroundColor()); + if (isEnabled()) { + painter->setPen(foregroundColor()); + const qreal progress = state_machine_->checkedOverlayProgress(); + + if (isCheckable() && progress > 0) { + QColor source = foregroundColor(); + QColor dest = Qt::TransparentMode == bg_mode_ ? Qt::white : backgroundColor(); + if (qFuzzyCompare(1, progress)) { + painter->setPen(dest); + } else { + painter->setPen(QColor(COLOR_INTERPOLATE(red), + COLOR_INTERPOLATE(green), + COLOR_INTERPOLATE(blue), + COLOR_INTERPOLATE(alpha))); + } } + } else { + painter->setPen(disabledForegroundColor()); + } - if (icon().isNull()) { - painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text())); - return; - } + if (icon().isNull()) { + painter->drawText(rect(), Qt::AlignCenter, removeKDEAccelerators(text())); + return; + } - QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); - QSize base(size() - textSize); + QSize textSize(fontMetrics().size(Qt::TextSingleLine, removeKDEAccelerators(text()))); + QSize base(size() - textSize); - const int iw = iconSize().width() + IconPadding; - QPoint pos((base.width() - iw) / 2, 0); + const int iw = iconSize().width() + IconPadding; + QPoint pos((base.width() - iw) / 2, 0); - QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize); - QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize()); + QRect textGeometry(pos + QPoint(0, base.height() / 2), textSize); + QRect iconGeometry(pos + QPoint(0, (height() - iconSize().height()) / 2), iconSize()); - /* if (ui::LeftIcon == icon_placement_) { */ - /* textGeometry.translate(iw, 0); */ - /* } else { */ - /* iconGeometry.translate(textSize.width() + IconPadding, 0); */ - /* } */ + /* if (ui::LeftIcon == icon_placement_) { */ + /* textGeometry.translate(iw, 0); */ + /* } else { */ + /* iconGeometry.translate(textSize.width() + IconPadding, 0); */ + /* } */ - painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text())); + painter->drawText(textGeometry, Qt::AlignCenter, removeKDEAccelerators(text())); - QPixmap pixmap = icon().pixmap(iconSize()); - QPainter icon(&pixmap); - icon.setCompositionMode(QPainter::CompositionMode_SourceIn); - icon.fillRect(pixmap.rect(), painter->pen().color()); - painter->drawPixmap(iconGeometry, pixmap); + QPixmap pixmap = icon().pixmap(iconSize()); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), painter->pen().color()); + painter->drawPixmap(iconGeometry, pixmap); } void FlatButton::updateClipPath() { - const qreal radius = corner_radius_; + const qreal radius = corner_radius_; - QPainterPath path; - path.addRoundedRect(rect(), radius, radius); - ripple_overlay_->setClipPath(path); + QPainterPath path; + path.addRoundedRect(rect(), radius, radius); + ripple_overlay_->setClipPath(path); } FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent) @@ -575,45 +574,45 @@ FlatButtonStateMachine::FlatButtonStateMachine(FlatButton *parent) , checked_overlay_progress_(parent->isChecked() ? 1 : 0) , was_checked_(false) { - Q_ASSERT(parent); + Q_ASSERT(parent); - parent->installEventFilter(this); + parent->installEventFilter(this); - config_state_->setInitialState(neutral_state_); - addState(top_level_state_); - setInitialState(top_level_state_); + config_state_->setInitialState(neutral_state_); + addState(top_level_state_); + setInitialState(top_level_state_); - checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_); - QSignalTransition *transition; - QPropertyAnimation *animation; + checkable_state_->setInitialState(parent->isChecked() ? checked_state_ : unchecked_state_); + QSignalTransition *transition; + QPropertyAnimation *animation; - transition = new QSignalTransition(this, SIGNAL(buttonChecked())); - transition->setTargetState(checked_state_); - unchecked_state_->addTransition(transition); + transition = new QSignalTransition(this, SIGNAL(buttonChecked())); + transition->setTargetState(checked_state_); + unchecked_state_->addTransition(transition); - animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); - animation->setDuration(200); - transition->addAnimation(animation); + animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); + animation->setDuration(200); + transition->addAnimation(animation); - transition = new QSignalTransition(this, SIGNAL(buttonUnchecked())); - transition->setTargetState(unchecked_state_); - checked_state_->addTransition(transition); + transition = new QSignalTransition(this, SIGNAL(buttonUnchecked())); + transition->setTargetState(unchecked_state_); + checked_state_->addTransition(transition); - animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); - animation->setDuration(200); - transition->addAnimation(animation); + animation = new QPropertyAnimation(this, "checkedOverlayProgress", this); + animation->setDuration(200); + transition->addAnimation(animation); - addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_); - addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_); - addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_); - addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_); - addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_); - addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_); - addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_); - addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_); - addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_); - addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_); - addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_); + addTransition(button_, QEvent::FocusIn, neutral_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusOut, neutral_focused_state_, neutral_state_); + addTransition(button_, QEvent::Enter, neutral_state_, hovered_state_); + addTransition(button_, QEvent::Leave, hovered_state_, neutral_state_); + addTransition(button_, QEvent::Enter, neutral_focused_state_, hovered_focused_state_); + addTransition(button_, QEvent::Leave, hovered_focused_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusIn, hovered_state_, hovered_focused_state_); + addTransition(button_, QEvent::FocusOut, hovered_focused_state_, hovered_state_); + addTransition(this, SIGNAL(buttonPressed()), hovered_state_, pressed_state_); + addTransition(button_, QEvent::Leave, pressed_state_, neutral_focused_state_); + addTransition(button_, QEvent::FocusOut, pressed_state_, hovered_state_); } FlatButtonStateMachine::~FlatButtonStateMachine() {} @@ -621,73 +620,73 @@ FlatButtonStateMachine::~FlatButtonStateMachine() {} void FlatButtonStateMachine::setOverlayOpacity(qreal opacity) { - overlay_opacity_ = opacity; - button_->update(); + overlay_opacity_ = opacity; + button_->update(); } void FlatButtonStateMachine::setCheckedOverlayProgress(qreal opacity) { - checked_overlay_progress_ = opacity; - button_->update(); + checked_overlay_progress_ = opacity; + button_->update(); } void FlatButtonStateMachine::startAnimations() { - start(); + start(); } void FlatButtonStateMachine::setupProperties() { - QColor overlayColor; + QColor overlayColor; - if (Qt::TransparentMode == button_->backgroundMode()) { - overlayColor = button_->backgroundColor(); - } else { - overlayColor = button_->foregroundColor(); - } + if (Qt::TransparentMode == button_->backgroundMode()) { + overlayColor = button_->backgroundColor(); + } else { + overlayColor = button_->foregroundColor(); + } - const qreal baseOpacity = button_->baseOpacity(); + const qreal baseOpacity = button_->baseOpacity(); - neutral_state_->assignProperty(this, "overlayOpacity", 0); - neutral_focused_state_->assignProperty(this, "overlayOpacity", 0); - hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity); - hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity); - pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity); - checked_state_->assignProperty(this, "checkedOverlayProgress", 1); - unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0); + neutral_state_->assignProperty(this, "overlayOpacity", 0); + neutral_focused_state_->assignProperty(this, "overlayOpacity", 0); + hovered_state_->assignProperty(this, "overlayOpacity", baseOpacity); + hovered_focused_state_->assignProperty(this, "overlayOpacity", baseOpacity); + pressed_state_->assignProperty(this, "overlayOpacity", baseOpacity); + checked_state_->assignProperty(this, "checkedOverlayProgress", 1); + unchecked_state_->assignProperty(this, "checkedOverlayProgress", 0); - button_->update(); + button_->update(); } void FlatButtonStateMachine::updateCheckedStatus() { - const bool checked = button_->isChecked(); - if (was_checked_ != checked) { - was_checked_ = checked; - if (checked) { - emit buttonChecked(); - } else { - emit buttonUnchecked(); - } + const bool checked = button_->isChecked(); + if (was_checked_ != checked) { + was_checked_ = checked; + if (checked) { + emit buttonChecked(); + } else { + emit buttonUnchecked(); } + } } bool FlatButtonStateMachine::eventFilter(QObject *watched, QEvent *event) { - if (QEvent::FocusIn == event->type()) { - QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event); - if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) { - emit buttonPressed(); - return true; - } + if (QEvent::FocusIn == event->type()) { + QFocusEvent *focusEvent = static_cast<QFocusEvent *>(event); + if (focusEvent && Qt::MouseFocusReason == focusEvent->reason()) { + emit buttonPressed(); + return true; } + } - return QStateMachine::eventFilter(watched, event); + return QStateMachine::eventFilter(watched, event); } void @@ -696,7 +695,7 @@ FlatButtonStateMachine::addTransition(QObject *object, QState *fromState, QState *toState) { - addTransition(new QSignalTransition(object, signal), fromState, toState); + addTransition(new QSignalTransition(object, signal), fromState, toState); } void @@ -705,7 +704,7 @@ FlatButtonStateMachine::addTransition(QObject *object, QState *fromState, QState *toState) { - addTransition(new QEventTransition(object, eventType), fromState, toState); + addTransition(new QEventTransition(object, eventType), fromState, toState); } void @@ -713,13 +712,13 @@ FlatButtonStateMachine::addTransition(QAbstractTransition *transition, QState *fromState, QState *toState) { - transition->setTargetState(toState); + transition->setTargetState(toState); - QPropertyAnimation *animation; + QPropertyAnimation *animation; - animation = new QPropertyAnimation(this, "overlayOpacity", this); - animation->setDuration(150); - transition->addAnimation(animation); + animation = new QPropertyAnimation(this, "overlayOpacity", this); + animation->setDuration(150); + transition->addAnimation(animation); - fromState->addTransition(transition); + fromState->addTransition(transition); } diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h index c79945b7..b39c94ac 100644 --- a/src/ui/FlatButton.h +++ b/src/ui/FlatButton.h @@ -14,174 +14,171 @@ class FlatButton; class FlatButtonStateMachine : public QStateMachine { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity) - Q_PROPERTY( - qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress) + Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity) + Q_PROPERTY( + qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress) public: - explicit FlatButtonStateMachine(FlatButton *parent); - ~FlatButtonStateMachine() override; + explicit FlatButtonStateMachine(FlatButton *parent); + ~FlatButtonStateMachine() override; - void setOverlayOpacity(qreal opacity); - void setCheckedOverlayProgress(qreal opacity); + void setOverlayOpacity(qreal opacity); + void setCheckedOverlayProgress(qreal opacity); - inline qreal overlayOpacity() const; - inline qreal checkedOverlayProgress() const; + inline qreal overlayOpacity() const; + inline qreal checkedOverlayProgress() const; - void startAnimations(); - void setupProperties(); - void updateCheckedStatus(); + void startAnimations(); + void setupProperties(); + void updateCheckedStatus(); signals: - void buttonPressed(); - void buttonChecked(); - void buttonUnchecked(); + void buttonPressed(); + void buttonChecked(); + void buttonUnchecked(); protected: - bool eventFilter(QObject *watched, QEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; private: - void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState); - void addTransition(QObject *object, - QEvent::Type eventType, - QState *fromState, - QState *toState); - void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState); - - FlatButton *const button_; - - QState *const top_level_state_; - QState *const config_state_; - QState *const checkable_state_; - QState *const checked_state_; - QState *const unchecked_state_; - QState *const neutral_state_; - QState *const neutral_focused_state_; - QState *const hovered_state_; - QState *const hovered_focused_state_; - QState *const pressed_state_; - - qreal overlay_opacity_; - qreal checked_overlay_progress_; - - bool was_checked_; + void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState); + void addTransition(QObject *object, QEvent::Type eventType, QState *fromState, QState *toState); + void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState); + + FlatButton *const button_; + + QState *const top_level_state_; + QState *const config_state_; + QState *const checkable_state_; + QState *const checked_state_; + QState *const unchecked_state_; + QState *const neutral_state_; + QState *const neutral_focused_state_; + QState *const hovered_state_; + QState *const hovered_focused_state_; + QState *const pressed_state_; + + qreal overlay_opacity_; + qreal checked_overlay_progress_; + + bool was_checked_; }; inline qreal FlatButtonStateMachine::overlayOpacity() const { - return overlay_opacity_; + return overlay_opacity_; } inline qreal FlatButtonStateMachine::checkedOverlayProgress() const { - return checked_overlay_progress_; + return checked_overlay_progress_; } class FlatButton : public QPushButton { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor) - Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ - disabledForegroundColor) - Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ - disabledBackgroundColor) - Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) + Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor) + Q_PROPERTY( + QColor disabledForegroundColor WRITE setDisabledForegroundColor READ disabledForegroundColor) + Q_PROPERTY( + QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ disabledBackgroundColor) + Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) public: - explicit FlatButton(QWidget *parent = nullptr, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - explicit FlatButton(const QString &text, - QWidget *parent = nullptr, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - FlatButton(const QString &text, - ui::Role role, - QWidget *parent = nullptr, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - ~FlatButton() override; - - void applyPreset(ui::ButtonPreset preset); - - void setBackgroundColor(const QColor &color); - void setBackgroundMode(Qt::BGMode mode); - void setBaseOpacity(qreal opacity); - void setCheckable(bool value); - void setCornerRadius(qreal radius); - void setDisabledBackgroundColor(const QColor &color); - void setDisabledForegroundColor(const QColor &color); - void setFixedRippleRadius(qreal radius); - void setFontSize(qreal size); - void setForegroundColor(const QColor &color); - void setHasFixedRippleRadius(bool value); - void setIconPlacement(ui::ButtonIconPlacement placement); - void setOverlayColor(const QColor &color); - void setOverlayStyle(ui::OverlayStyle style); - void setRippleStyle(ui::RippleStyle style); - void setRole(ui::Role role); - - QColor foregroundColor() const; - QColor backgroundColor() const; - QColor overlayColor() const; - QColor disabledForegroundColor() const; - QColor disabledBackgroundColor() const; - - qreal fontSize() const; - qreal cornerRadius() const; - qreal baseOpacity() const; - - bool hasFixedRippleRadius() const; - - ui::Role role() const; - ui::OverlayStyle overlayStyle() const; - ui::RippleStyle rippleStyle() const; - ui::ButtonIconPlacement iconPlacement() const; - - Qt::BGMode backgroundMode() const; - - QSize sizeHint() const override; + explicit FlatButton(QWidget *parent = nullptr, + ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); + explicit FlatButton(const QString &text, + QWidget *parent = nullptr, + ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); + FlatButton(const QString &text, + ui::Role role, + QWidget *parent = nullptr, + ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); + ~FlatButton() override; + + void applyPreset(ui::ButtonPreset preset); + + void setBackgroundColor(const QColor &color); + void setBackgroundMode(Qt::BGMode mode); + void setBaseOpacity(qreal opacity); + void setCheckable(bool value); + void setCornerRadius(qreal radius); + void setDisabledBackgroundColor(const QColor &color); + void setDisabledForegroundColor(const QColor &color); + void setFixedRippleRadius(qreal radius); + void setFontSize(qreal size); + void setForegroundColor(const QColor &color); + void setHasFixedRippleRadius(bool value); + void setIconPlacement(ui::ButtonIconPlacement placement); + void setOverlayColor(const QColor &color); + void setOverlayStyle(ui::OverlayStyle style); + void setRippleStyle(ui::RippleStyle style); + void setRole(ui::Role role); + + QColor foregroundColor() const; + QColor backgroundColor() const; + QColor overlayColor() const; + QColor disabledForegroundColor() const; + QColor disabledBackgroundColor() const; + + qreal fontSize() const; + qreal cornerRadius() const; + qreal baseOpacity() const; + + bool hasFixedRippleRadius() const; + + ui::Role role() const; + ui::OverlayStyle overlayStyle() const; + ui::RippleStyle rippleStyle() const; + ui::ButtonIconPlacement iconPlacement() const; + + Qt::BGMode backgroundMode() const; + + QSize sizeHint() const override; protected: - int IconPadding = 0; + int IconPadding = 0; - void checkStateSet() override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; + void checkStateSet() override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; - virtual void paintBackground(QPainter *painter); - virtual void paintForeground(QPainter *painter); - virtual void updateClipPath(); + virtual void paintBackground(QPainter *painter); + virtual void paintForeground(QPainter *painter); + virtual void updateClipPath(); - void init(); + void init(); private: - RippleOverlay *ripple_overlay_; - FlatButtonStateMachine *state_machine_; + RippleOverlay *ripple_overlay_; + FlatButtonStateMachine *state_machine_; - ui::Role role_; - ui::RippleStyle ripple_style_; - ui::ButtonIconPlacement icon_placement_; - ui::OverlayStyle overlay_style_; + ui::Role role_; + ui::RippleStyle ripple_style_; + ui::ButtonIconPlacement icon_placement_; + ui::OverlayStyle overlay_style_; - Qt::BGMode bg_mode_; + Qt::BGMode bg_mode_; - QColor background_color_; - QColor foreground_color_; - QColor overlay_color_; - QColor disabled_color_; - QColor disabled_background_color_; + QColor background_color_; + QColor foreground_color_; + QColor overlay_color_; + QColor disabled_color_; + QColor disabled_background_color_; - qreal fixed_ripple_radius_; - qreal corner_radius_; - qreal base_opacity_; - qreal font_size_; + qreal fixed_ripple_radius_; + qreal corner_radius_; + qreal base_opacity_; + qreal font_size_; - bool use_fixed_ripple_radius_; + bool use_fixed_ripple_radius_; }; diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp index 95b6ae1d..3f88e313 100644 --- a/src/ui/FloatingButton.cpp +++ b/src/ui/FloatingButton.cpp @@ -10,91 +10,91 @@ FloatingButton::FloatingButton(const QIcon &icon, QWidget *parent) : RaisedButton(parent) { - setFixedSize(DIAMETER, DIAMETER); - setGeometry(buttonGeometry()); + setFixedSize(DIAMETER, DIAMETER); + setGeometry(buttonGeometry()); - if (parentWidget()) - parentWidget()->installEventFilter(this); + if (parentWidget()) + parentWidget()->installEventFilter(this); - setFixedRippleRadius(50); - setIcon(icon); - raise(); + setFixedRippleRadius(50); + setIcon(icon); + raise(); } QRect FloatingButton::buttonGeometry() const { - QWidget *parent = parentWidget(); + QWidget *parent = parentWidget(); - if (!parent) - return QRect(); + if (!parent) + return QRect(); - return QRect(parent->width() - (OFFSET_X + DIAMETER), - parent->height() - (OFFSET_Y + DIAMETER), - DIAMETER, - DIAMETER); + return QRect(parent->width() - (OFFSET_X + DIAMETER), + parent->height() - (OFFSET_Y + DIAMETER), + DIAMETER, + DIAMETER); } bool FloatingButton::event(QEvent *event) { - if (!parent()) - return RaisedButton::event(event); - - switch (event->type()) { - case QEvent::ParentChange: { - parent()->installEventFilter(this); - setGeometry(buttonGeometry()); - break; - } - case QEvent::ParentAboutToChange: { - parent()->installEventFilter(this); - break; - } - default: - break; - } - + if (!parent()) return RaisedButton::event(event); + + switch (event->type()) { + case QEvent::ParentChange: { + parent()->installEventFilter(this); + setGeometry(buttonGeometry()); + break; + } + case QEvent::ParentAboutToChange: { + parent()->installEventFilter(this); + break; + } + default: + break; + } + + return RaisedButton::event(event); } bool FloatingButton::eventFilter(QObject *obj, QEvent *event) { - const QEvent::Type type = event->type(); + const QEvent::Type type = event->type(); - if (QEvent::Move == type || QEvent::Resize == type) - setGeometry(buttonGeometry()); + if (QEvent::Move == type || QEvent::Resize == type) + setGeometry(buttonGeometry()); - return RaisedButton::eventFilter(obj, event); + return RaisedButton::eventFilter(obj, event); } void FloatingButton::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - QRect square = QRect(0, 0, DIAMETER, DIAMETER); - square.moveCenter(rect().center()); + QRect square = QRect(0, 0, DIAMETER, DIAMETER); + square.moveCenter(rect().center()); - QPainter p(this); - p.setRenderHints(QPainter::Antialiasing); + QPainter p(this); + p.setRenderHints(QPainter::Antialiasing); - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor()); + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor()); - p.setBrush(brush); - p.setPen(Qt::NoPen); - p.drawEllipse(square); + p.setBrush(brush); + p.setPen(Qt::NoPen); + p.drawEllipse(square); - QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE); - iconGeometry.moveCenter(square.center()); + QRect iconGeometry(0, 0, ICON_SIZE, ICON_SIZE); + iconGeometry.moveCenter(square.center()); - QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE)); - QPainter icon(&pixmap); - icon.setCompositionMode(QPainter::CompositionMode_SourceIn); - icon.fillRect(pixmap.rect(), foregroundColor()); + QPixmap pixmap = icon().pixmap(QSize(ICON_SIZE, ICON_SIZE)); + QPainter icon(&pixmap); + icon.setCompositionMode(QPainter::CompositionMode_SourceIn); + icon.fillRect(pixmap.rect(), foregroundColor()); - p.drawPixmap(iconGeometry, pixmap); + p.drawPixmap(iconGeometry, pixmap); } diff --git a/src/ui/FloatingButton.h b/src/ui/FloatingButton.h index b59b3854..df14dd2c 100644 --- a/src/ui/FloatingButton.h +++ b/src/ui/FloatingButton.h @@ -14,17 +14,17 @@ constexpr int OFFSET_Y = 20; class FloatingButton : public RaisedButton { - Q_OBJECT + Q_OBJECT public: - FloatingButton(const QIcon &icon, QWidget *parent = nullptr); + FloatingButton(const QIcon &icon, QWidget *parent = nullptr); - QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); }; - QRect buttonGeometry() const; + QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); }; + QRect buttonGeometry() const; protected: - bool event(QEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; + bool event(QEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; }; diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index ebe0e63f..e238a4d2 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -19,60 +19,60 @@ constexpr int HMargin = 20; InfoMessage::InfoMessage(QWidget *parent) : QWidget{parent} { - initFont(); + initFont(); } InfoMessage::InfoMessage(QString msg, QWidget *parent) : QWidget{parent} , msg_{msg} { - initFont(); + initFont(); - QFontMetrics fm{font()}; - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; - height_ = fm.ascent() + 2 * VPadding; + QFontMetrics fm{font()}; + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; + height_ = fm.ascent() + 2 * VPadding; - setFixedHeight(height_ + 2 * HMargin); + setFixedHeight(height_ + 2 * HMargin); } void InfoMessage::paintEvent(QPaintEvent *) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.setFont(font()); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setFont(font()); - // Center the box horizontally & vertically. - auto textRegion = QRectF(width() / 2 - width_ / 2, HMargin, width_, height_); + // Center the box horizontally & vertically. + auto textRegion = QRectF(width() / 2 - width_ / 2, HMargin, width_, height_); - QPainterPath ppath; - ppath.addRoundedRect(textRegion, height_ / 2, height_ / 2); + QPainterPath ppath; + ppath.addRoundedRect(textRegion, height_ / 2, height_ / 2); - p.setPen(Qt::NoPen); - p.fillPath(ppath, boxColor()); - p.drawPath(ppath); + p.setPen(Qt::NoPen); + p.fillPath(ppath, boxColor()); + p.drawPath(ppath); - p.setPen(QPen(textColor())); - p.drawText(textRegion, Qt::AlignCenter, msg_); + p.setPen(QPen(textColor())); + p.drawText(textRegion, Qt::AlignCenter, msg_); } DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) : InfoMessage{parent} { - auto now = QDateTime::currentDateTime(); + auto now = QDateTime::currentDateTime(); - QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() == datetime.date().year()) { - QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); - fmt = fmt.remove(rx); - } + if (now.date().year() == datetime.date().year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - msg_ = datetime.date().toString(fmt); + msg_ = datetime.date().toString(fmt); - QFontMetrics fm{font()}; - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; - height_ = fm.ascent() + 2 * VPadding; + QFontMetrics fm{font()}; + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; + height_ = fm.ascent() + 2 * VPadding; - setFixedHeight(height_ + 2 * HMargin); + setFixedHeight(height_ + 2 * HMargin); } diff --git a/src/ui/InfoMessage.h b/src/ui/InfoMessage.h index cc0c57dc..486812a2 100644 --- a/src/ui/InfoMessage.h +++ b/src/ui/InfoMessage.h @@ -10,47 +10,47 @@ class InfoMessage : public QWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor boxColor WRITE setBoxColor READ boxColor) + Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) + Q_PROPERTY(QColor boxColor WRITE setBoxColor READ boxColor) public: - explicit InfoMessage(QWidget *parent = nullptr); - InfoMessage(QString msg, QWidget *parent = nullptr); + explicit InfoMessage(QWidget *parent = nullptr); + InfoMessage(QString msg, QWidget *parent = nullptr); - void setTextColor(QColor color) { textColor_ = color; } - void setBoxColor(QColor color) { boxColor_ = color; } - void saveDatetime(QDateTime datetime) { datetime_ = datetime; } + void setTextColor(QColor color) { textColor_ = color; } + void setBoxColor(QColor color) { boxColor_ = color; } + void saveDatetime(QDateTime datetime) { datetime_ = datetime; } - QColor textColor() const { return textColor_; } - QColor boxColor() const { return boxColor_; } - QDateTime datetime() const { return datetime_; } + QColor textColor() const { return textColor_; } + QColor boxColor() const { return boxColor_; } + QDateTime datetime() const { return datetime_; } protected: - void paintEvent(QPaintEvent *event) override; - void initFont() - { - QFont f; - f.setWeight(QFont::Medium); - setFont(f); - } + void paintEvent(QPaintEvent *event) override; + void initFont() + { + QFont f; + f.setWeight(QFont::Medium); + setFont(f); + } - int width_; - int height_; + int width_; + int height_; - QString msg_; + QString msg_; - QDateTime datetime_; + QDateTime datetime_; - QColor textColor_ = QColor("black"); - QColor boxColor_ = QColor("white"); + QColor textColor_ = QColor("black"); + QColor boxColor_ = QColor("white"); }; class DateSeparator : public InfoMessage { - Q_OBJECT + Q_OBJECT public: - DateSeparator(QDateTime datetime, QWidget *parent = nullptr); + DateSeparator(QDateTime datetime, QWidget *parent = nullptr); }; diff --git a/src/ui/Label.cpp b/src/ui/Label.cpp index 2e8f8e1d..220fe2f0 100644 --- a/src/ui/Label.cpp +++ b/src/ui/Label.cpp @@ -17,16 +17,16 @@ Label::Label(const QString &text, QWidget *parent, Qt::WindowFlags f) void Label::mousePressEvent(QMouseEvent *e) { - pressPosition_ = e->pos(); - emit pressed(e); - QLabel::mousePressEvent(e); + pressPosition_ = e->pos(); + emit pressed(e); + QLabel::mousePressEvent(e); } void Label::mouseReleaseEvent(QMouseEvent *e) { - emit released(e); - if (pressPosition_ == e->pos()) - emit clicked(e); - QLabel::mouseReleaseEvent(e); + emit released(e); + if (pressPosition_ == e->pos()) + emit clicked(e); + QLabel::mouseReleaseEvent(e); } diff --git a/src/ui/Label.h b/src/ui/Label.h index a3eb511b..b6e76b77 100644 --- a/src/ui/Label.h +++ b/src/ui/Label.h @@ -8,22 +8,22 @@ class Label : public QLabel { - Q_OBJECT + Q_OBJECT public: - explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); - explicit Label(const QString &text, - QWidget *parent = Q_NULLPTR, - Qt::WindowFlags f = Qt::WindowFlags()); + explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + explicit Label(const QString &text, + QWidget *parent = Q_NULLPTR, + Qt::WindowFlags f = Qt::WindowFlags()); signals: - void clicked(QMouseEvent *e); - void pressed(QMouseEvent *e); - void released(QMouseEvent *e); + void clicked(QMouseEvent *e); + void pressed(QMouseEvent *e); + void released(QMouseEvent *e); protected: - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; - QPoint pressPosition_; + QPoint pressPosition_; }; diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp index fb3c761c..7581ec83 100644 --- a/src/ui/LoadingIndicator.cpp +++ b/src/ui/LoadingIndicator.cpp @@ -14,70 +14,70 @@ LoadingIndicator::LoadingIndicator(QWidget *parent) , angle_(0) , color_(Qt::black) { - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - setFocusPolicy(Qt::NoFocus); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setFocusPolicy(Qt::NoFocus); - timer_ = new QTimer(this); - connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout())); + timer_ = new QTimer(this); + connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout())); } void LoadingIndicator::paintEvent(QPaintEvent *e) { - Q_UNUSED(e) + Q_UNUSED(e) - if (!timer_->isActive()) - return; + if (!timer_->isActive()) + return; - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - int width = qMin(this->width(), this->height()); + int width = qMin(this->width(), this->height()); - int outerRadius = (width - 4) * 0.5f; - int innerRadius = outerRadius * 0.78f; + int outerRadius = (width - 4) * 0.5f; + int innerRadius = outerRadius * 0.78f; - int capsuleRadius = (outerRadius - innerRadius) / 2; + int capsuleRadius = (outerRadius - innerRadius) / 2; - for (int i = 0; i < 8; ++i) { - QColor color = color_; + for (int i = 0; i < 8; ++i) { + QColor color = color_; - color.setAlphaF(1.0f - (i / 8.0f)); + color.setAlphaF(1.0f - (i / 8.0f)); - painter.setPen(Qt::NoPen); - painter.setBrush(color); + painter.setPen(Qt::NoPen); + painter.setBrush(color); - qreal radius = capsuleRadius * (1.0f - (i / 16.0f)); + qreal radius = capsuleRadius * (1.0f - (i / 16.0f)); - painter.save(); + painter.save(); - painter.translate(rect().center()); - painter.rotate(angle_ - i * 45.0f); + painter.translate(rect().center()); + painter.rotate(angle_ - i * 45.0f); - QPointF center = QPointF(-capsuleRadius, -innerRadius); - painter.drawEllipse(center, radius * 2, radius * 2); + QPointF center = QPointF(-capsuleRadius, -innerRadius); + painter.drawEllipse(center, radius * 2, radius * 2); - painter.restore(); - } + painter.restore(); + } } void LoadingIndicator::start() { - timer_->start(interval_); - show(); + timer_->start(interval_); + show(); } void LoadingIndicator::stop() { - timer_->stop(); - hide(); + timer_->stop(); + hide(); } void LoadingIndicator::onTimeout() { - angle_ = (angle_ + 45) % 360; - repaint(); + angle_ = (angle_ + 45) % 360; + repaint(); } diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h index ba56b449..458ecd40 100644 --- a/src/ui/LoadingIndicator.h +++ b/src/ui/LoadingIndicator.h @@ -12,30 +12,30 @@ class QTimer; class QPaintEvent; class LoadingIndicator : public QWidget { - Q_OBJECT - Q_PROPERTY(QColor color READ color WRITE setColor) + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor) public: - LoadingIndicator(QWidget *parent = nullptr); + LoadingIndicator(QWidget *parent = nullptr); - void paintEvent(QPaintEvent *e) override; + void paintEvent(QPaintEvent *e) override; - void start(); - void stop(); + void start(); + void stop(); - QColor color() { return color_; } - void setColor(QColor color) { color_ = color; } + QColor color() { return color_; } + void setColor(QColor color) { color_ = color; } - int interval() { return interval_; } - void setInterval(int interval) { interval_ = interval; } + int interval() { return interval_; } + void setInterval(int interval) { interval_ = interval; } private slots: - void onTimeout(); + void onTimeout(); private: - int interval_; - int angle_; + int interval_; + int angle_; - QColor color_; - QTimer *timer_; + QColor color_; + QTimer *timer_; }; diff --git a/src/ui/Menu.h b/src/ui/Menu.h index fd2946dd..d1ac2b80 100644 --- a/src/ui/Menu.h +++ b/src/ui/Menu.h @@ -10,16 +10,16 @@ class Menu : public QMenu { - Q_OBJECT + Q_OBJECT public: - Menu(QWidget *parent = nullptr) - : QMenu(parent){}; + Menu(QWidget *parent = nullptr) + : QMenu(parent){}; protected: - void leaveEvent(QEvent *e) override - { - hide(); + void leaveEvent(QEvent *e) override + { + hide(); - QMenu::leaveEvent(e); - } + QMenu::leaveEvent(e); + } }; diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp index c691bab0..72758653 100644 --- a/src/ui/MxcAnimatedImage.cpp +++ b/src/ui/MxcAnimatedImage.cpp @@ -19,160 +19,157 @@ void MxcAnimatedImage::startDownload() { - if (!room_) - return; - if (eventId_.isEmpty()) - return; - - auto event = room_->eventById(eventId_); - if (!event) { - nhlog::ui()->error("Failed to load media for event {}, event not found.", - eventId_.toStdString()); - return; - } + if (!room_) + return; + if (eventId_.isEmpty()) + return; - QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8(); + auto event = room_->eventById(eventId_); + if (!event) { + nhlog::ui()->error("Failed to load media for event {}, event not found.", + eventId_.toStdString()); + return; + } - animatable_ = QMovie::supportedFormats().contains(mimeType.split('/').back()); - animatableChanged(); + QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8(); - if (!animatable_) - return; + animatable_ = QMovie::supportedFormats().contains(mimeType.split('/').back()); + animatableChanged(); - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + if (!animatable_) + return; - auto encryptionInfo = mtx::accessors::file(*event); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - return; - } + auto encryptionInfo = mtx::accessors::file(*event); - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - const auto name = QString(mxcUrl).remove("mxc://"); - QFileInfo filename(QString("%1/media_cache/media/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(name) - .arg(suffix)); - if (QDir::cleanPath(name) != name) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + const auto name = QString(mxcUrl).remove("mxc://"); + QFileInfo filename(QString("%1/media_cache/media/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(name) + .arg(suffix)); + if (QDir::cleanPath(name) != name) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + QPointer<MxcAnimatedImage> self = this; + + auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) { + if (!self) + return; - QDir().mkpath(filename.path()); - - QPointer<MxcAnimatedImage> self = this; - - auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) { - if (!self) - return; - - if (buffer.isOpen()) { - movie.stop(); - movie.setDevice(nullptr); - buffer.close(); - } - - if (encryptionInfo) { - QByteArray ba = device.readAll(); - std::string temp(ba.constData(), ba.size()); - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); - buffer.setData(temp.data(), temp.size()); - } else { - buffer.setData(device.readAll()); - } - buffer.open(QIODevice::ReadOnly); - buffer.reset(); - - QTimer::singleShot(0, this, [this, mimeType] { - nhlog::ui()->info("Playing movie with size: {}, {}", - buffer.bytesAvailable(), - buffer.isOpen()); - movie.setFormat(mimeType); - movie.setDevice(&buffer); - if (play_) - movie.start(); - else - movie.jumpToFrame(0); - emit loadedChanged(); - update(); - }); - }; - - if (filename.isReadable()) { - QFile f(filename.filePath()); - if (f.open(QIODevice::ReadOnly)) { - processBuffer(f); - return; - } + if (buffer.isOpen()) { + movie.stop(); + movie.setDevice(nullptr); + buffer.close(); } - http::client()->download( - url, - [filename, url, processBuffer](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve media {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - QByteArray ba(data.data(), (int)data.size()); - file.write(ba); - file.close(); - - QBuffer buf(&ba); - buf.open(QBuffer::ReadOnly); - processBuffer(buf); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); + if (encryptionInfo) { + QByteArray ba = device.readAll(); + std::string temp(ba.constData(), ba.size()); + temp = mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + buffer.setData(temp.data(), temp.size()); + } else { + buffer.setData(device.readAll()); + } + buffer.open(QIODevice::ReadOnly); + buffer.reset(); + + QTimer::singleShot(0, this, [this, mimeType] { + nhlog::ui()->info( + "Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); + movie.setFormat(mimeType); + movie.setDevice(&buffer); + if (play_) + movie.start(); + else + movie.jumpToFrame(0); + emit loadedChanged(); + update(); + }); + }; + + if (filename.isReadable()) { + QFile f(filename.filePath()); + if (f.open(QIODevice::ReadOnly)) { + processBuffer(f); + return; + } + } + + http::client()->download(url, + [filename, url, processBuffer](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve media {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + QByteArray ba(data.data(), (int)data.size()); + file.write(ba); + file.close(); + + QBuffer buf(&ba); + buf.open(QBuffer::ReadOnly); + processBuffer(buf); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } QSGNode * MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) { - if (!imageDirty) - return oldNode; - - imageDirty = false; - QSGImageNode *n = static_cast<QSGImageNode *>(oldNode); - if (!n) { - n = window()->createImageNode(); - n->setOwnsTexture(true); - // n->setFlags(QSGNode::OwnedByParent | QSGNode::OwnsGeometry | - // GSGNode::OwnsMaterial); - n->setFlags(QSGNode::OwnedByParent); - } - - // n->setTexture(nullptr); - auto img = movie.currentImage(); - if (!img.isNull()) - n->setTexture(window()->createTextureFromImage(img)); - else { - delete n; - return nullptr; - } - - n->setSourceRect(img.rect()); - n->setRect(QRect(0, 0, width(), height())); - n->setFiltering(QSGTexture::Linear); - n->setMipmapFiltering(QSGTexture::Linear); - - return n; + if (!imageDirty) + return oldNode; + + imageDirty = false; + QSGImageNode *n = static_cast<QSGImageNode *>(oldNode); + if (!n) { + n = window()->createImageNode(); + n->setOwnsTexture(true); + // n->setFlags(QSGNode::OwnedByParent | QSGNode::OwnsGeometry | + // GSGNode::OwnsMaterial); + n->setFlags(QSGNode::OwnedByParent); + } + + // n->setTexture(nullptr); + auto img = movie.currentImage(); + if (!img.isNull()) + n->setTexture(window()->createTextureFromImage(img)); + else { + delete n; + return nullptr; + } + + n->setSourceRect(img.rect()); + n->setRect(QRect(0, 0, width(), height())); + n->setFiltering(QSGTexture::Linear); + n->setMipmapFiltering(QSGTexture::Linear); + + return n; } diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h index 2e0489ad..c3ca24d1 100644 --- a/src/ui/MxcAnimatedImage.h +++ b/src/ui/MxcAnimatedImage.h @@ -14,78 +14,78 @@ class TimelineModel; // This is an AnimatedImage, that can draw encrypted images class MxcAnimatedImage : public QQuickItem { - Q_OBJECT - Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) - Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) - Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged) - Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) - Q_PROPERTY(bool play READ play WRITE setPlay NOTIFY playChanged) + Q_OBJECT + Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) + Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) + Q_PROPERTY(bool animatable READ animatable NOTIFY animatableChanged) + Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) + Q_PROPERTY(bool play READ play WRITE setPlay NOTIFY playChanged) public: - MxcAnimatedImage(QQuickItem *parent = nullptr) - : QQuickItem(parent) - { - connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload); - connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload); - connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame); - setFlag(QQuickItem::ItemHasContents); - // setAcceptHoverEvents(true); - } + MxcAnimatedImage(QQuickItem *parent = nullptr) + : QQuickItem(parent) + { + connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload); + connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload); + connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame); + setFlag(QQuickItem::ItemHasContents); + // setAcceptHoverEvents(true); + } - bool animatable() const { return animatable_; } - bool loaded() const { return buffer.size() > 0; } - bool play() const { return play_; } - QString eventId() const { return eventId_; } - TimelineModel *room() const { return room_; } - void setEventId(QString newEventId) - { - if (eventId_ != newEventId) { - eventId_ = newEventId; - emit eventIdChanged(); - } + bool animatable() const { return animatable_; } + bool loaded() const { return buffer.size() > 0; } + bool play() const { return play_; } + QString eventId() const { return eventId_; } + TimelineModel *room() const { return room_; } + void setEventId(QString newEventId) + { + if (eventId_ != newEventId) { + eventId_ = newEventId; + emit eventIdChanged(); } - void setRoom(TimelineModel *room) - { - if (room_ != room) { - room_ = room; - emit roomChanged(); - } + } + void setRoom(TimelineModel *room) + { + if (room_ != room) { + room_ = room; + emit roomChanged(); } - void setPlay(bool newPlay) - { - if (play_ != newPlay) { - play_ = newPlay; - movie.setPaused(!play_); - emit playChanged(); - } + } + void setPlay(bool newPlay) + { + if (play_ != newPlay) { + play_ = newPlay; + movie.setPaused(!play_); + emit playChanged(); } + } - QSGNode *updatePaintNode(QSGNode *oldNode, - QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override; + QSGNode *updatePaintNode(QSGNode *oldNode, + QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override; signals: - void roomChanged(); - void eventIdChanged(); - void animatableChanged(); - void loadedChanged(); - void playChanged(); + void roomChanged(); + void eventIdChanged(); + void animatableChanged(); + void loadedChanged(); + void playChanged(); private slots: - void startDownload(); - void newFrame(int frame) - { - currentFrame = frame; - imageDirty = true; - update(); - } + void startDownload(); + void newFrame(int frame) + { + currentFrame = frame; + imageDirty = true; + update(); + } private: - TimelineModel *room_ = nullptr; - QString eventId_; - QString filename_; - bool animatable_ = false; - QBuffer buffer; - QMovie movie; - int currentFrame = 0; - bool imageDirty = true; - bool play_ = true; + TimelineModel *room_ = nullptr; + QString eventId_; + QString filename_; + bool animatable_ = false; + QBuffer buffer; + QMovie movie; + int currentFrame = 0; + bool imageDirty = true; + bool play_ = true; }; diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp index dc65de7c..db8c0f1f 100644 --- a/src/ui/MxcMediaProxy.cpp +++ b/src/ui/MxcMediaProxy.cpp @@ -21,122 +21,119 @@ void MxcMediaProxy::setVideoSurface(QAbstractVideoSurface *surface) { - qDebug() << "Changing surface"; - m_surface = surface; - setVideoOutput(m_surface); + qDebug() << "Changing surface"; + m_surface = surface; + setVideoOutput(m_surface); } QAbstractVideoSurface * MxcMediaProxy::getVideoSurface() { - return m_surface; + return m_surface; } void MxcMediaProxy::startDownload() { - if (!room_) - return; - if (eventId_.isEmpty()) - return; - - auto event = room_->eventById(eventId_); - if (!event) { - nhlog::ui()->error("Failed to load media for event {}, event not found.", - eventId_.toStdString()); - return; + if (!room_) + return; + if (eventId_.isEmpty()) + return; + + auto event = room_->eventById(eventId_); + if (!event) { + nhlog::ui()->error("Failed to load media for event {}, event not found.", + eventId_.toStdString()); + return; + } + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); + + auto encryptionInfo = mtx::accessors::file(*event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + const auto name = QString(mxcUrl).remove("mxc://"); + QFileInfo filename(QString("%1/media_cache/media/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(name) + .arg(suffix)); + if (QDir::cleanPath(name) != name) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + QPointer<MxcMediaProxy> self = this; + + auto processBuffer = [this, encryptionInfo, filename, self](QIODevice &device) { + if (!self) + return; + + if (encryptionInfo) { + QByteArray ba = device.readAll(); + std::string temp(ba.constData(), ba.size()); + temp = mtx::crypto::to_string(mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + buffer.setData(temp.data(), temp.size()); + } else { + buffer.setData(device.readAll()); } - - QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - - auto encryptionInfo = mtx::accessors::file(*event); - - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - const auto name = QString(mxcUrl).remove("mxc://"); - QFileInfo filename(QString("%1/media_cache/media/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(name) - .arg(suffix)); - if (QDir::cleanPath(name) != name) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - QPointer<MxcMediaProxy> self = this; - - auto processBuffer = [this, encryptionInfo, filename, self](QIODevice &device) { - if (!self) - return; - - if (encryptionInfo) { - QByteArray ba = device.readAll(); - std::string temp(ba.constData(), ba.size()); - temp = mtx::crypto::to_string( - mtx::crypto::decrypt_file(temp, encryptionInfo.value())); - buffer.setData(temp.data(), temp.size()); - } else { - buffer.setData(device.readAll()); - } - buffer.open(QIODevice::ReadOnly); - buffer.reset(); - - QTimer::singleShot(0, this, [this, filename] { - nhlog::ui()->info("Playing buffer with size: {}, {}", - buffer.bytesAvailable(), - buffer.isOpen()); - this->setMedia(QMediaContent(filename.fileName()), &buffer); - emit loadedChanged(); - }); - }; - - if (filename.isReadable()) { - QFile f(filename.filePath()); - if (f.open(QIODevice::ReadOnly)) { - processBuffer(f); - return; - } + buffer.open(QIODevice::ReadOnly); + buffer.reset(); + + QTimer::singleShot(0, this, [this, filename] { + nhlog::ui()->info( + "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); + this->setMedia(QMediaContent(filename.fileName()), &buffer); + emit loadedChanged(); + }); + }; + + if (filename.isReadable()) { + QFile f(filename.filePath()); + if (f.open(QIODevice::ReadOnly)) { + processBuffer(f); + return; } - - http::client()->download( - url, - [filename, url, processBuffer](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve media {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - QByteArray ba(data.data(), (int)data.size()); - file.write(ba); - file.close(); - - QBuffer buf(&ba); - buf.open(QBuffer::ReadOnly); - processBuffer(buf); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); + } + + http::client()->download(url, + [filename, url, processBuffer](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve media {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + QByteArray ba(data.data(), (int)data.size()); + file.write(ba); + file.close(); + + QBuffer buf(&ba); + buf.open(QBuffer::ReadOnly); + processBuffer(buf); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } diff --git a/src/ui/MxcMediaProxy.h b/src/ui/MxcMediaProxy.h index 14541815..18152c75 100644 --- a/src/ui/MxcMediaProxy.h +++ b/src/ui/MxcMediaProxy.h @@ -20,61 +20,58 @@ class TimelineModel; // need the videoSurface property, so that part is really easy! class MxcMediaProxy : public QMediaPlayer { - Q_OBJECT - Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) - Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) - Q_PROPERTY(QAbstractVideoSurface *videoSurface READ getVideoSurface WRITE setVideoSurface) - Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) + Q_OBJECT + Q_PROPERTY(TimelineModel *roomm READ room WRITE setRoom NOTIFY roomChanged REQUIRED) + Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged) + Q_PROPERTY(QAbstractVideoSurface *videoSurface READ getVideoSurface WRITE setVideoSurface) + Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) public: - MxcMediaProxy(QObject *parent = nullptr) - : QMediaPlayer(parent) - { - connect(this, &MxcMediaProxy::eventIdChanged, &MxcMediaProxy::startDownload); - connect(this, &MxcMediaProxy::roomChanged, &MxcMediaProxy::startDownload); - connect(this, - qOverload<QMediaPlayer::Error>(&MxcMediaProxy::error), - [this](QMediaPlayer::Error error) { - nhlog::ui()->info("Media player error {} and errorStr {}", - error, - this->errorString().toStdString()); - }); - connect(this, - &MxcMediaProxy::mediaStatusChanged, - [this](QMediaPlayer::MediaStatus status) { - nhlog::ui()->info( - "Media player status {} and error {}", status, this->error()); - }); - } + MxcMediaProxy(QObject *parent = nullptr) + : QMediaPlayer(parent) + { + connect(this, &MxcMediaProxy::eventIdChanged, &MxcMediaProxy::startDownload); + connect(this, &MxcMediaProxy::roomChanged, &MxcMediaProxy::startDownload); + connect(this, + qOverload<QMediaPlayer::Error>(&MxcMediaProxy::error), + [this](QMediaPlayer::Error error) { + nhlog::ui()->info("Media player error {} and errorStr {}", + error, + this->errorString().toStdString()); + }); + connect(this, &MxcMediaProxy::mediaStatusChanged, [this](QMediaPlayer::MediaStatus status) { + nhlog::ui()->info("Media player status {} and error {}", status, this->error()); + }); + } - bool loaded() const { return buffer.size() > 0; } - QString eventId() const { return eventId_; } - TimelineModel *room() const { return room_; } - void setEventId(QString newEventId) - { - eventId_ = newEventId; - emit eventIdChanged(); - } - void setRoom(TimelineModel *room) - { - room_ = room; - emit roomChanged(); - } - void setVideoSurface(QAbstractVideoSurface *surface); - QAbstractVideoSurface *getVideoSurface(); + bool loaded() const { return buffer.size() > 0; } + QString eventId() const { return eventId_; } + TimelineModel *room() const { return room_; } + void setEventId(QString newEventId) + { + eventId_ = newEventId; + emit eventIdChanged(); + } + void setRoom(TimelineModel *room) + { + room_ = room; + emit roomChanged(); + } + void setVideoSurface(QAbstractVideoSurface *surface); + QAbstractVideoSurface *getVideoSurface(); signals: - void roomChanged(); - void eventIdChanged(); - void loadedChanged(); - void newBuffer(QMediaContent, QIODevice *buf); + void roomChanged(); + void eventIdChanged(); + void loadedChanged(); + void newBuffer(QMediaContent, QIODevice *buf); private slots: - void startDownload(); + void startDownload(); private: - TimelineModel *room_ = nullptr; - QString eventId_; - QString filename_; - QBuffer buffer; - QAbstractVideoSurface *m_surface = nullptr; + TimelineModel *room_ = nullptr; + QString eventId_; + QString filename_; + QBuffer buffer; + QAbstractVideoSurface *m_surface = nullptr; }; diff --git a/src/ui/NhekoCursorShape.cpp b/src/ui/NhekoCursorShape.cpp index b36eedbd..70991757 100644 --- a/src/ui/NhekoCursorShape.cpp +++ b/src/ui/NhekoCursorShape.cpp @@ -14,16 +14,16 @@ NhekoCursorShape::NhekoCursorShape(QQuickItem *parent) Qt::CursorShape NhekoCursorShape::cursorShape() const { - return cursor().shape(); + return cursor().shape(); } void NhekoCursorShape::setCursorShape(Qt::CursorShape cursorShape) { - if (currentShape_ == cursorShape) - return; + if (currentShape_ == cursorShape) + return; - currentShape_ = cursorShape; - setCursor(cursorShape); - emit cursorShapeChanged(); + currentShape_ = cursorShape; + setCursor(cursorShape); + emit cursorShapeChanged(); } diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h index 6f6a2b82..b3a0a1ba 100644 --- a/src/ui/NhekoCursorShape.h +++ b/src/ui/NhekoCursorShape.h @@ -11,20 +11,20 @@ class NhekoCursorShape : public QQuickItem { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY - cursorShapeChanged) + Q_PROPERTY( + Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY cursorShapeChanged) public: - explicit NhekoCursorShape(QQuickItem *parent = 0); + explicit NhekoCursorShape(QQuickItem *parent = 0); private: - Qt::CursorShape cursorShape() const; - void setCursorShape(Qt::CursorShape cursorShape); + Qt::CursorShape cursorShape() const; + void setCursorShape(Qt::CursorShape cursorShape); - Qt::CursorShape currentShape_; + Qt::CursorShape currentShape_; signals: - void cursorShapeChanged(); + void cursorShapeChanged(); }; diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp index bbcedd7e..b1b53c3d 100644 --- a/src/ui/NhekoDropArea.cpp +++ b/src/ui/NhekoDropArea.cpp @@ -16,28 +16,28 @@ NhekoDropArea::NhekoDropArea(QQuickItem *parent) : QQuickItem(parent) { - setFlags(ItemAcceptsDrops); + setFlags(ItemAcceptsDrops); } void NhekoDropArea::dragEnterEvent(QDragEnterEvent *event) { - event->acceptProposedAction(); + event->acceptProposedAction(); } void NhekoDropArea::dragMoveEvent(QDragMoveEvent *event) { - event->acceptProposedAction(); + event->acceptProposedAction(); } void NhekoDropArea::dropEvent(QDropEvent *event) { - if (event) { - auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_); - if (model) { - model->input()->insertMimeData(event->mimeData()); - } + if (event) { + auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_); + if (model) { + model->input()->insertMimeData(event->mimeData()); } + } } diff --git a/src/ui/NhekoDropArea.h b/src/ui/NhekoDropArea.h index 9fbf1737..7b3de5c9 100644 --- a/src/ui/NhekoDropArea.h +++ b/src/ui/NhekoDropArea.h @@ -6,29 +6,29 @@ class NhekoDropArea : public QQuickItem { - Q_OBJECT - Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged) + Q_OBJECT + Q_PROPERTY(QString roomid READ roomid WRITE setRoomid NOTIFY roomidChanged) public: - NhekoDropArea(QQuickItem *parent = nullptr); + NhekoDropArea(QQuickItem *parent = nullptr); signals: - void roomidChanged(QString roomid); + void roomidChanged(QString roomid); public slots: - void setRoomid(QString roomid) - { - if (roomid_ != roomid) { - roomid_ = roomid; - emit roomidChanged(roomid); - } + void setRoomid(QString roomid) + { + if (roomid_ != roomid) { + roomid_ = roomid; + emit roomidChanged(roomid); } - QString roomid() const { return roomid_; } + } + QString roomid() const { return roomid_; } protected: - void dragEnterEvent(QDragEnterEvent *event) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; private: - QString roomid_; + QString roomid_; }; diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index 355f187b..d6824996 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -17,93 +17,93 @@ Nheko::Nheko() { - connect( - UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged); - connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile); + connect( + UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged); + connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile); } void Nheko::updateUserProfile() { - if (cache::client() && cache::client()->isInitialized()) - currentUser_.reset( - new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager())); - else - currentUser_.reset(); - emit profileChanged(); + if (cache::client() && cache::client()->isInitialized()) + currentUser_.reset( + new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager())); + else + currentUser_.reset(); + emit profileChanged(); } QPalette Nheko::colors() const { - return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString()); + return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString()); } QPalette Nheko::inactiveColors() const { - auto p = colors(); - p.setCurrentColorGroup(QPalette::ColorGroup::Inactive); - return p; + auto p = colors(); + p.setCurrentColorGroup(QPalette::ColorGroup::Inactive); + return p; } Theme Nheko::theme() const { - return Theme(UserSettings::instance()->theme().toStdString()); + return Theme(UserSettings::instance()->theme().toStdString()); } void Nheko::openLink(QString link) const { - QUrl url(link); - // Open externally if we couldn't handle it internally - if (!ChatPage::instance()->handleMatrixUri(url)) { - QDesktopServices::openUrl(url); - } + QUrl url(link); + // Open externally if we couldn't handle it internally + if (!ChatPage::instance()->handleMatrixUri(url)) { + QDesktopServices::openUrl(url); + } } void Nheko::setStatusMessage(QString msg) const { - ChatPage::instance()->setStatus(msg); + ChatPage::instance()->setStatus(msg); } UserProfile * Nheko::currentUser() const { - nhlog::ui()->debug("Profile requested"); + nhlog::ui()->debug("Profile requested"); - return currentUser_.get(); + return currentUser_.get(); } void Nheko::showUserSettingsPage() const { - ChatPage::instance()->showUserSettingsPage(); + ChatPage::instance()->showUserSettingsPage(); } void Nheko::openLogoutDialog() const { - MainWindow::instance()->openLogoutDialog(); + MainWindow::instance()->openLogoutDialog(); } void Nheko::openCreateRoomDialog() const { - MainWindow::instance()->openCreateRoomDialog( - [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); }); + MainWindow::instance()->openCreateRoomDialog( + [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); }); } void Nheko::openJoinRoomDialog() const { - MainWindow::instance()->openJoinRoomDialog( - [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); + MainWindow::instance()->openJoinRoomDialog( + [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); } void Nheko::reparent(QWindow *win) const { - win->setTransientParent(MainWindow::instance()->windowHandle()); + win->setTransientParent(MainWindow::instance()->windowHandle()); } diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index d4d119dc..aa8435d1 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -15,51 +15,51 @@ class QWindow; class Nheko : public QObject { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged) - Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged) - Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged) - Q_PROPERTY(int avatarSize READ avatarSize CONSTANT) - Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT) - Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT) - Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT) + Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged) + Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged) + Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged) + Q_PROPERTY(int avatarSize READ avatarSize CONSTANT) + Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT) + Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT) + Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT) - Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged) + Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged) public: - Nheko(); + Nheko(); - QPalette colors() const; - QPalette inactiveColors() const; - Theme theme() const; + QPalette colors() const; + QPalette inactiveColors() const; + Theme theme() const; - int avatarSize() const { return 40; } + int avatarSize() const { return 40; } - int paddingSmall() const { return 4; } - int paddingMedium() const { return 8; } - int paddingLarge() const { return 20; } - UserProfile *currentUser() const; + int paddingSmall() const { return 4; } + int paddingMedium() const { return 8; } + int paddingLarge() const { return 20; } + UserProfile *currentUser() const; - Q_INVOKABLE QFont monospaceFont() const - { - return QFontDatabase::systemFont(QFontDatabase::FixedFont); - } - Q_INVOKABLE void openLink(QString link) const; - Q_INVOKABLE void setStatusMessage(QString msg) const; - Q_INVOKABLE void showUserSettingsPage() const; - Q_INVOKABLE void openLogoutDialog() const; - Q_INVOKABLE void openCreateRoomDialog() const; - Q_INVOKABLE void openJoinRoomDialog() const; - Q_INVOKABLE void reparent(QWindow *win) const; + Q_INVOKABLE QFont monospaceFont() const + { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } + Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void setStatusMessage(QString msg) const; + Q_INVOKABLE void showUserSettingsPage() const; + Q_INVOKABLE void openLogoutDialog() const; + Q_INVOKABLE void openCreateRoomDialog() const; + Q_INVOKABLE void openJoinRoomDialog() const; + Q_INVOKABLE void reparent(QWindow *win) const; public slots: - void updateUserProfile(); + void updateUserProfile(); signals: - void colorsChanged(); - void profileChanged(); + void colorsChanged(); + void profileChanged(); private: - QScopedPointer<UserProfile> currentUser_; + QScopedPointer<UserProfile> currentUser_; }; diff --git a/src/ui/OverlayModal.cpp b/src/ui/OverlayModal.cpp index f5f28732..6534c4bc 100644 --- a/src/ui/OverlayModal.cpp +++ b/src/ui/OverlayModal.cpp @@ -12,50 +12,50 @@ OverlayModal::OverlayModal(QWidget *parent) : OverlayWidget(parent) , color_{QColor(30, 30, 30, 170)} { - layout_ = new QVBoxLayout(this); - layout_->setSpacing(0); - layout_->setContentsMargins(10, 40, 10, 20); - setContentAlignment(Qt::AlignCenter); + layout_ = new QVBoxLayout(this); + layout_->setSpacing(0); + layout_->setContentsMargins(10, 40, 10, 20); + setContentAlignment(Qt::AlignCenter); } void OverlayModal::setWidget(QWidget *widget) { - // Delete the previous widget - if (layout_->count() > 0) { - QLayoutItem *item; - while ((item = layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } + // Delete the previous widget + if (layout_->count() > 0) { + QLayoutItem *item; + while ((item = layout_->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; } + } - layout_->addWidget(widget); - content_ = widget; - content_->setFocus(); + layout_->addWidget(widget); + content_ = widget; + content_->setFocus(); } void OverlayModal::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - QPainter painter(this); - painter.fillRect(rect(), color_); + QPainter painter(this); + painter.fillRect(rect(), color_); } void OverlayModal::mousePressEvent(QMouseEvent *e) { - if (isDismissible_ && content_ && !content_->geometry().contains(e->pos())) - hide(); + if (isDismissible_ && content_ && !content_->geometry().contains(e->pos())) + hide(); } void OverlayModal::keyPressEvent(QKeyEvent *event) { - if (event->key() == Qt::Key_Escape) { - event->accept(); - hide(); - } + if (event->key() == Qt::Key_Escape) { + event->accept(); + hide(); + } } diff --git a/src/ui/OverlayModal.h b/src/ui/OverlayModal.h index 005614fa..5c15f17f 100644 --- a/src/ui/OverlayModal.h +++ b/src/ui/OverlayModal.h @@ -15,25 +15,25 @@ class OverlayModal : public OverlayWidget { public: - OverlayModal(QWidget *parent); + OverlayModal(QWidget *parent); - void setColor(QColor color) { color_ = color; } - void setDismissible(bool state) { isDismissible_ = state; } + void setColor(QColor color) { color_ = color; } + void setDismissible(bool state) { isDismissible_ = state; } - void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); } - void setWidget(QWidget *widget); + void setContentAlignment(QFlags<Qt::AlignmentFlag> flag) { layout_->setAlignment(flag); } + void setWidget(QWidget *widget); protected: - void paintEvent(QPaintEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; private: - QWidget *content_; - QVBoxLayout *layout_; + QWidget *content_; + QVBoxLayout *layout_; - QColor color_; + QColor color_; - //! Decides whether or not the modal can be removed by clicking into it. - bool isDismissible_ = true; + //! Decides whether or not the modal can be removed by clicking into it. + bool isDismissible_ = true; }; diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp index c8c95581..4e338753 100644 --- a/src/ui/OverlayWidget.cpp +++ b/src/ui/OverlayWidget.cpp @@ -10,69 +10,69 @@ OverlayWidget::OverlayWidget(QWidget *parent) : QWidget(parent) { - if (parent) { - parent->installEventFilter(this); - setGeometry(overlayGeometry()); - raise(); - } + if (parent) { + parent->installEventFilter(this); + setGeometry(overlayGeometry()); + raise(); + } } bool OverlayWidget::event(QEvent *event) { - if (!parent()) - return QWidget::event(event); + if (!parent()) + return QWidget::event(event); - switch (event->type()) { - case QEvent::ParentChange: { - parent()->installEventFilter(this); - setGeometry(overlayGeometry()); - break; - } - case QEvent::ParentAboutToChange: { - parent()->removeEventFilter(this); - break; - } - default: - break; - } + switch (event->type()) { + case QEvent::ParentChange: { + parent()->installEventFilter(this); + setGeometry(overlayGeometry()); + break; + } + case QEvent::ParentAboutToChange: { + parent()->removeEventFilter(this); + break; + } + default: + break; + } - return QWidget::event(event); + return QWidget::event(event); } bool OverlayWidget::eventFilter(QObject *obj, QEvent *event) { - switch (event->type()) { - case QEvent::Move: - case QEvent::Resize: - setGeometry(overlayGeometry()); - break; - default: - break; - } + switch (event->type()) { + case QEvent::Move: + case QEvent::Resize: + setGeometry(overlayGeometry()); + break; + default: + break; + } - return QWidget::eventFilter(obj, event); + return QWidget::eventFilter(obj, event); } QRect OverlayWidget::overlayGeometry() const { - QWidget *widget = parentWidget(); + QWidget *widget = parentWidget(); - if (!widget) - return QRect(); + if (!widget) + return QRect(); - return widget->rect(); + return widget->rect(); } void OverlayWidget::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h index 05bb8696..5f023e35 100644 --- a/src/ui/OverlayWidget.h +++ b/src/ui/OverlayWidget.h @@ -11,15 +11,15 @@ class QPainter; class OverlayWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit OverlayWidget(QWidget *parent = nullptr); + explicit OverlayWidget(QWidget *parent = nullptr); protected: - bool event(QEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; + bool event(QEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; - QRect overlayGeometry() const; - void paintEvent(QPaintEvent *event) override; + QRect overlayGeometry() const; + void paintEvent(QPaintEvent *event) override; }; diff --git a/src/ui/Painter.h b/src/ui/Painter.h index 9f974116..f78b55e5 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h @@ -13,147 +13,141 @@ class Painter : public QPainter { public: - explicit Painter(QPaintDevice *device) - : QPainter(device) - {} - - void drawTextLeft(int x, int y, const QString &text) - { - QFontMetrics m(fontMetrics()); - drawText(x, y + m.ascent(), text); - } - - void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) - { - QFontMetrics m(fontMetrics()); - if (textWidth < 0) { - textWidth = m.horizontalAdvance(text); - } - drawText((outerw - x - textWidth), y + m.ascent(), text); - } - - void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from) - { - drawPixmap(QPoint(x, y), pix, from); - } - - void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from) - { - return drawPixmapLeft(p.x(), p.y(), pix, from); - } - - void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from) - { - drawPixmap(QRect(x, y, w, h), pix, from); - } - - void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from) - { - return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from); - } - - void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix) - { - Q_UNUSED(outerw); - drawPixmap(QPoint(x, y), pix); - } - - void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix) - { - return drawPixmapLeft(p.x(), p.y(), outerw, pix); - } - - void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from) - { - drawPixmap( - QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from); - } - - void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from) - { - return drawPixmapRight(p.x(), p.y(), outerw, pix, from); - } - void drawPixmapRight(int x, - int y, - int w, - int h, - int outerw, - const QPixmap &pix, - const QRect &from) - { - drawPixmap(QRect((outerw - x - w), y, w, h), pix, from); - } - - void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from) - { - return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from); - } - - void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix) - { - drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix); - } - - void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix) - { - return drawPixmapRight(p.x(), p.y(), outerw, pix); - } - - void drawAvatar(const QPixmap &pix, int w, int h, int d) - { - QPainterPath pp; - pp.addEllipse((w - d) / 2, (h - d) / 2, d, d); - - QRect region((w - d) / 2, (h - d) / 2, d, d); - - setClipPath(pp); - drawPixmap(region, pix); - } - - void drawLetterAvatar(const QString &c, - const QColor &penColor, - const QColor &brushColor, - int w, - int h, - int d) - { - QRect region((w - d) / 2, (h - d) / 2, d, d); - - setPen(Qt::NoPen); - setBrush(brushColor); - - drawEllipse(region.center(), d / 2, d / 2); - - setBrush(Qt::NoBrush); - drawEllipse(region.center(), d / 2, d / 2); - - setPen(penColor); - drawText(region.translated(0, -1), Qt::AlignCenter, c); - } + explicit Painter(QPaintDevice *device) + : QPainter(device) + {} + + void drawTextLeft(int x, int y, const QString &text) + { + QFontMetrics m(fontMetrics()); + drawText(x, y + m.ascent(), text); + } + + void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) + { + QFontMetrics m(fontMetrics()); + if (textWidth < 0) { + textWidth = m.horizontalAdvance(text); + } + drawText((outerw - x - textWidth), y + m.ascent(), text); + } + + void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from) + { + drawPixmap(QPoint(x, y), pix, from); + } + + void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from) + { + return drawPixmapLeft(p.x(), p.y(), pix, from); + } + + void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from) + { + drawPixmap(QRect(x, y, w, h), pix, from); + } + + void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from) + { + return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from); + } + + void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix) + { + Q_UNUSED(outerw); + drawPixmap(QPoint(x, y), pix); + } + + void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix) + { + return drawPixmapLeft(p.x(), p.y(), outerw, pix); + } + + void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from) + { + drawPixmap(QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from); + } + + void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from) + { + return drawPixmapRight(p.x(), p.y(), outerw, pix, from); + } + void + drawPixmapRight(int x, int y, int w, int h, int outerw, const QPixmap &pix, const QRect &from) + { + drawPixmap(QRect((outerw - x - w), y, w, h), pix, from); + } + + void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from) + { + return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from); + } + + void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix) + { + drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix); + } + + void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix) + { + return drawPixmapRight(p.x(), p.y(), outerw, pix); + } + + void drawAvatar(const QPixmap &pix, int w, int h, int d) + { + QPainterPath pp; + pp.addEllipse((w - d) / 2, (h - d) / 2, d, d); + + QRect region((w - d) / 2, (h - d) / 2, d, d); + + setClipPath(pp); + drawPixmap(region, pix); + } + + void drawLetterAvatar(const QString &c, + const QColor &penColor, + const QColor &brushColor, + int w, + int h, + int d) + { + QRect region((w - d) / 2, (h - d) / 2, d, d); + + setPen(Qt::NoPen); + setBrush(brushColor); + + drawEllipse(region.center(), d / 2, d / 2); + + setBrush(Qt::NoBrush); + drawEllipse(region.center(), d / 2, d / 2); + + setPen(penColor); + drawText(region.translated(0, -1), Qt::AlignCenter, c); + } }; class PainterHighQualityEnabler { public: - PainterHighQualityEnabler(Painter &p) - : _painter(p) - { - hints_ = QPainter::Antialiasing | QPainter::SmoothPixmapTransform | - QPainter::TextAntialiasing; + PainterHighQualityEnabler(Painter &p) + : _painter(p) + { + hints_ = + QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing; - _painter.setRenderHints(hints_); - } + _painter.setRenderHints(hints_); + } - ~PainterHighQualityEnabler() - { - if (hints_) - _painter.setRenderHints(hints_, false); - } + ~PainterHighQualityEnabler() + { + if (hints_) + _painter.setRenderHints(hints_, false); + } - PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete; - PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete; + PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete; + PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete; private: - Painter &_painter; - QPainter::RenderHints hints_ = {}; + Painter &_painter; + QPainter::RenderHints hints_ = {}; }; diff --git a/src/ui/RaisedButton.cpp b/src/ui/RaisedButton.cpp index 563cb8e5..fd0cbd76 100644 --- a/src/ui/RaisedButton.cpp +++ b/src/ui/RaisedButton.cpp @@ -10,68 +10,68 @@ void RaisedButton::init() { - shadow_state_machine_ = new QStateMachine(this); - normal_state_ = new QState; - pressed_state_ = new QState; - effect_ = new QGraphicsDropShadowEffect; + shadow_state_machine_ = new QStateMachine(this); + normal_state_ = new QState; + pressed_state_ = new QState; + effect_ = new QGraphicsDropShadowEffect; - effect_->setBlurRadius(7); - effect_->setOffset(QPointF(0, 2)); - effect_->setColor(QColor(0, 0, 0, 75)); + effect_->setBlurRadius(7); + effect_->setOffset(QPointF(0, 2)); + effect_->setColor(QColor(0, 0, 0, 75)); - setBackgroundMode(Qt::OpaqueMode); - setMinimumHeight(42); - setGraphicsEffect(effect_); - setBaseOpacity(0.3); + setBackgroundMode(Qt::OpaqueMode); + setMinimumHeight(42); + setGraphicsEffect(effect_); + setBaseOpacity(0.3); - shadow_state_machine_->addState(normal_state_); - shadow_state_machine_->addState(pressed_state_); + shadow_state_machine_->addState(normal_state_); + shadow_state_machine_->addState(pressed_state_); - normal_state_->assignProperty(effect_, "offset", QPointF(0, 2)); - normal_state_->assignProperty(effect_, "blurRadius", 7); + normal_state_->assignProperty(effect_, "offset", QPointF(0, 2)); + normal_state_->assignProperty(effect_, "blurRadius", 7); - pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5)); - pressed_state_->assignProperty(effect_, "blurRadius", 29); + pressed_state_->assignProperty(effect_, "offset", QPointF(0, 5)); + pressed_state_->assignProperty(effect_, "blurRadius", 29); - QAbstractTransition *transition; + QAbstractTransition *transition; - transition = new QEventTransition(this, QEvent::MouseButtonPress); - transition->setTargetState(pressed_state_); - normal_state_->addTransition(transition); + transition = new QEventTransition(this, QEvent::MouseButtonPress); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); - transition = new QEventTransition(this, QEvent::MouseButtonDblClick); - transition->setTargetState(pressed_state_); - normal_state_->addTransition(transition); + transition = new QEventTransition(this, QEvent::MouseButtonDblClick); + transition->setTargetState(pressed_state_); + normal_state_->addTransition(transition); - transition = new QEventTransition(this, QEvent::MouseButtonRelease); - transition->setTargetState(normal_state_); - pressed_state_->addTransition(transition); + transition = new QEventTransition(this, QEvent::MouseButtonRelease); + transition->setTargetState(normal_state_); + pressed_state_->addTransition(transition); - QPropertyAnimation *animation; + QPropertyAnimation *animation; - animation = new QPropertyAnimation(effect_, "offset", this); - animation->setDuration(100); - shadow_state_machine_->addDefaultAnimation(animation); + animation = new QPropertyAnimation(effect_, "offset", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); - animation = new QPropertyAnimation(effect_, "blurRadius", this); - animation->setDuration(100); - shadow_state_machine_->addDefaultAnimation(animation); + animation = new QPropertyAnimation(effect_, "blurRadius", this); + animation->setDuration(100); + shadow_state_machine_->addDefaultAnimation(animation); - shadow_state_machine_->setInitialState(normal_state_); - shadow_state_machine_->start(); + shadow_state_machine_->setInitialState(normal_state_); + shadow_state_machine_->start(); } RaisedButton::RaisedButton(QWidget *parent) : FlatButton(parent) { - init(); + init(); } RaisedButton::RaisedButton(const QString &text, QWidget *parent) : FlatButton(parent) { - init(); - setText(text); + init(); + setText(text); } RaisedButton::~RaisedButton() {} @@ -79,15 +79,15 @@ RaisedButton::~RaisedButton() {} bool RaisedButton::event(QEvent *event) { - if (QEvent::EnabledChange == event->type()) { - if (isEnabled()) { - shadow_state_machine_->start(); - effect_->setEnabled(true); - } else { - shadow_state_machine_->stop(); - effect_->setEnabled(false); - } + if (QEvent::EnabledChange == event->type()) { + if (isEnabled()) { + shadow_state_machine_->start(); + effect_->setEnabled(true); + } else { + shadow_state_machine_->stop(); + effect_->setEnabled(false); } + } - return FlatButton::event(event); + return FlatButton::event(event); } diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h index dcb579bb..277fa7ff 100644 --- a/src/ui/RaisedButton.h +++ b/src/ui/RaisedButton.h @@ -12,21 +12,21 @@ class RaisedButton : public FlatButton { - Q_OBJECT + Q_OBJECT public: - explicit RaisedButton(QWidget *parent = nullptr); - explicit RaisedButton(const QString &text, QWidget *parent = nullptr); - ~RaisedButton() override; + explicit RaisedButton(QWidget *parent = nullptr); + explicit RaisedButton(const QString &text, QWidget *parent = nullptr); + ~RaisedButton() override; protected: - bool event(QEvent *event) override; + bool event(QEvent *event) override; private: - void init(); + void init(); - QStateMachine *shadow_state_machine_; - QState *normal_state_; - QState *pressed_state_; - QGraphicsDropShadowEffect *effect_; + QStateMachine *shadow_state_machine_; + QState *normal_state_; + QState *pressed_state_; + QGraphicsDropShadowEffect *effect_; }; diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp index f0455f0b..72cf2da2 100644 --- a/src/ui/Ripple.cpp +++ b/src/ui/Ripple.cpp @@ -14,7 +14,7 @@ Ripple::Ripple(const QPoint ¢er, QObject *parent) , opacity_(0) , center_(center) { - init(); + init(); } Ripple::Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent) @@ -26,86 +26,86 @@ Ripple::Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent) , opacity_(0) , center_(center) { - init(); + init(); } void Ripple::setRadius(qreal radius) { - Q_ASSERT(overlay_); + Q_ASSERT(overlay_); - if (radius_ == radius) - return; + if (radius_ == radius) + return; - radius_ = radius; - overlay_->update(); + radius_ = radius; + overlay_->update(); } void Ripple::setOpacity(qreal opacity) { - Q_ASSERT(overlay_); + Q_ASSERT(overlay_); - if (opacity_ == opacity) - return; + if (opacity_ == opacity) + return; - opacity_ = opacity; - overlay_->update(); + opacity_ = opacity; + overlay_->update(); } void Ripple::setColor(const QColor &color) { - if (brush_.color() == color) - return; + if (brush_.color() == color) + return; - brush_.setColor(color); + brush_.setColor(color); - if (overlay_) - overlay_->update(); + if (overlay_) + overlay_->update(); } void Ripple::setBrush(const QBrush &brush) { - brush_ = brush; + brush_ = brush; - if (overlay_) - overlay_->update(); + if (overlay_) + overlay_->update(); } void Ripple::destroy() { - Q_ASSERT(overlay_); + Q_ASSERT(overlay_); - overlay_->removeRipple(this); + overlay_->removeRipple(this); } QPropertyAnimation * Ripple::animate(const QByteArray &property, const QEasingCurve &easing, int duration) { - QPropertyAnimation *animation = new QPropertyAnimation; - animation->setTargetObject(this); - animation->setPropertyName(property); - animation->setEasingCurve(easing); - animation->setDuration(duration); + QPropertyAnimation *animation = new QPropertyAnimation; + animation->setTargetObject(this); + animation->setPropertyName(property); + animation->setEasingCurve(easing); + animation->setDuration(duration); - addAnimation(animation); + addAnimation(animation); - return animation; + return animation; } void Ripple::init() { - setOpacityStartValue(0.5); - setOpacityEndValue(0); - setRadiusStartValue(0); - setRadiusEndValue(300); + setOpacityStartValue(0.5); + setOpacityEndValue(0); + setRadiusStartValue(0); + setRadiusEndValue(300); - brush_.setColor(Qt::black); - brush_.setStyle(Qt::SolidPattern); + brush_.setColor(Qt::black); + brush_.setStyle(Qt::SolidPattern); - connect(this, SIGNAL(finished()), this, SLOT(destroy())); + connect(this, SIGNAL(finished()), this, SLOT(destroy())); } diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h index 2ad42b9e..df09caba 100644 --- a/src/ui/Ripple.h +++ b/src/ui/Ripple.h @@ -14,136 +14,136 @@ class RippleOverlay; class Ripple : public QParallelAnimationGroup { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(qreal radius WRITE setRadius READ radius) - Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) + Q_PROPERTY(qreal radius WRITE setRadius READ radius) + Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) public: - explicit Ripple(const QPoint ¢er, QObject *parent = nullptr); - Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = nullptr); + explicit Ripple(const QPoint ¢er, QObject *parent = nullptr); + Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = nullptr); - inline void setOverlay(RippleOverlay *overlay); + inline void setOverlay(RippleOverlay *overlay); - void setRadius(qreal radius); - void setOpacity(qreal opacity); - void setColor(const QColor &color); - void setBrush(const QBrush &brush); + void setRadius(qreal radius); + void setOpacity(qreal opacity); + void setColor(const QColor &color); + void setBrush(const QBrush &brush); - inline qreal radius() const; - inline qreal opacity() const; - inline QColor color() const; - inline QBrush brush() const; - inline QPoint center() const; + inline qreal radius() const; + inline qreal opacity() const; + inline QColor color() const; + inline QBrush brush() const; + inline QPoint center() const; - inline QPropertyAnimation *radiusAnimation() const; - inline QPropertyAnimation *opacityAnimation() const; + inline QPropertyAnimation *radiusAnimation() const; + inline QPropertyAnimation *opacityAnimation() const; - inline void setOpacityStartValue(qreal value); - inline void setOpacityEndValue(qreal value); - inline void setRadiusStartValue(qreal value); - inline void setRadiusEndValue(qreal value); - inline void setDuration(int msecs); + inline void setOpacityStartValue(qreal value); + inline void setOpacityEndValue(qreal value); + inline void setRadiusStartValue(qreal value); + inline void setRadiusEndValue(qreal value); + inline void setDuration(int msecs); protected slots: - void destroy(); + void destroy(); private: - Q_DISABLE_COPY(Ripple) + Q_DISABLE_COPY(Ripple) - QPropertyAnimation *animate(const QByteArray &property, - const QEasingCurve &easing = QEasingCurve::OutQuad, - int duration = 800); + QPropertyAnimation *animate(const QByteArray &property, + const QEasingCurve &easing = QEasingCurve::OutQuad, + int duration = 800); - void init(); + void init(); - RippleOverlay *overlay_; + RippleOverlay *overlay_; - QPropertyAnimation *const radius_anim_; - QPropertyAnimation *const opacity_anim_; + QPropertyAnimation *const radius_anim_; + QPropertyAnimation *const opacity_anim_; - qreal radius_; - qreal opacity_; + qreal radius_; + qreal opacity_; - QPoint center_; - QBrush brush_; + QPoint center_; + QBrush brush_; }; inline void Ripple::setOverlay(RippleOverlay *overlay) { - overlay_ = overlay; + overlay_ = overlay; } inline qreal Ripple::radius() const { - return radius_; + return radius_; } inline qreal Ripple::opacity() const { - return opacity_; + return opacity_; } inline QColor Ripple::color() const { - return brush_.color(); + return brush_.color(); } inline QBrush Ripple::brush() const { - return brush_; + return brush_; } inline QPoint Ripple::center() const { - return center_; + return center_; } inline QPropertyAnimation * Ripple::radiusAnimation() const { - return radius_anim_; + return radius_anim_; } inline QPropertyAnimation * Ripple::opacityAnimation() const { - return opacity_anim_; + return opacity_anim_; } inline void Ripple::setOpacityStartValue(qreal value) { - opacity_anim_->setStartValue(value); + opacity_anim_->setStartValue(value); } inline void Ripple::setOpacityEndValue(qreal value) { - opacity_anim_->setEndValue(value); + opacity_anim_->setEndValue(value); } inline void Ripple::setRadiusStartValue(qreal value) { - radius_anim_->setStartValue(value); + radius_anim_->setStartValue(value); } inline void Ripple::setRadiusEndValue(qreal value) { - radius_anim_->setEndValue(value); + radius_anim_->setEndValue(value); } inline void Ripple::setDuration(int msecs) { - radius_anim_->setDuration(msecs); - opacity_anim_->setDuration(msecs); + radius_anim_->setDuration(msecs); + opacity_anim_->setDuration(msecs); } diff --git a/src/ui/RippleOverlay.cpp b/src/ui/RippleOverlay.cpp index 00915deb..6745cc37 100644 --- a/src/ui/RippleOverlay.cpp +++ b/src/ui/RippleOverlay.cpp @@ -11,56 +11,56 @@ RippleOverlay::RippleOverlay(QWidget *parent) : OverlayWidget(parent) , use_clip_(false) { - setAttribute(Qt::WA_TransparentForMouseEvents); - setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_NoSystemBackground); } void RippleOverlay::addRipple(Ripple *ripple) { - ripple->setOverlay(this); - ripples_.push_back(ripple); - ripple->start(); + ripple->setOverlay(this); + ripples_.push_back(ripple); + ripple->start(); } void RippleOverlay::addRipple(const QPoint &position, qreal radius) { - Ripple *ripple = new Ripple(position); - ripple->setRadiusEndValue(radius); - addRipple(ripple); + Ripple *ripple = new Ripple(position); + ripple->setRadiusEndValue(radius); + addRipple(ripple); } void RippleOverlay::removeRipple(Ripple *ripple) { - if (ripples_.removeOne(ripple)) - delete ripple; + if (ripples_.removeOne(ripple)) + delete ripple; } void RippleOverlay::paintEvent(QPaintEvent *event) { - Q_UNUSED(event) + Q_UNUSED(event) - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.setPen(Qt::NoPen); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); - if (use_clip_) - painter.setClipPath(clip_path_); + if (use_clip_) + painter.setClipPath(clip_path_); - for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it) - paintRipple(&painter, *it); + for (auto it = ripples_.constBegin(); it != ripples_.constEnd(); ++it) + paintRipple(&painter, *it); } void RippleOverlay::paintRipple(QPainter *painter, Ripple *ripple) { - const qreal radius = ripple->radius(); - const QPointF center = ripple->center(); + const qreal radius = ripple->radius(); + const QPointF center = ripple->center(); - painter->setOpacity(ripple->opacity()); - painter->setBrush(ripple->brush()); - painter->drawEllipse(center, radius, radius); + painter->setOpacity(ripple->opacity()); + painter->setBrush(ripple->brush()); + painter->drawEllipse(center, radius, radius); } diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h index 7ae3e4f1..3256c28d 100644 --- a/src/ui/RippleOverlay.h +++ b/src/ui/RippleOverlay.h @@ -12,50 +12,50 @@ class Ripple; class RippleOverlay : public OverlayWidget { - Q_OBJECT + Q_OBJECT public: - explicit RippleOverlay(QWidget *parent = nullptr); + explicit RippleOverlay(QWidget *parent = nullptr); - void addRipple(Ripple *ripple); - void addRipple(const QPoint &position, qreal radius = 300); + void addRipple(Ripple *ripple); + void addRipple(const QPoint &position, qreal radius = 300); - void removeRipple(Ripple *ripple); + void removeRipple(Ripple *ripple); - inline void setClipping(bool enable); - inline bool hasClipping() const; + inline void setClipping(bool enable); + inline bool hasClipping() const; - inline void setClipPath(const QPainterPath &path); + inline void setClipPath(const QPainterPath &path); protected: - void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; private: - Q_DISABLE_COPY(RippleOverlay) + Q_DISABLE_COPY(RippleOverlay) - void paintRipple(QPainter *painter, Ripple *ripple); + void paintRipple(QPainter *painter, Ripple *ripple); - QList<Ripple *> ripples_; - QPainterPath clip_path_; - bool use_clip_; + QList<Ripple *> ripples_; + QPainterPath clip_path_; + bool use_clip_; }; inline void RippleOverlay::setClipping(bool enable) { - use_clip_ = enable; - update(); + use_clip_ = enable; + update(); } inline bool RippleOverlay::hasClipping() const { - return use_clip_; + return use_clip_; } inline void RippleOverlay::setClipPath(const QPainterPath &path) { - clip_path_ = path; - update(); + clip_path_ = path; + update(); } diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp index 2fb93325..ed8a7cd8 100644 --- a/src/ui/RoomSettings.cpp +++ b/src/ui/RoomSettings.cpp @@ -27,479 +27,473 @@ EditModal::EditModal(const QString &roomId, QWidget *parent) : QWidget(parent) , roomId_{roomId} { - setAutoFillBackground(true); - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4); - setMinimumWidth(conf::window::minModalWidth); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - - auto layout = new QVBoxLayout(this); - - applyBtn_ = new QPushButton(tr("Apply"), this); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - cancelBtn_->setDefault(true); - - auto btnLayout = new QHBoxLayout; - btnLayout->addStretch(1); - btnLayout->setSpacing(15); - btnLayout->addWidget(cancelBtn_); - btnLayout->addWidget(applyBtn_); - - nameInput_ = new TextField(this); - nameInput_->setLabel(tr("Name").toUpper()); - topicInput_ = new TextField(this); - topicInput_->setLabel(tr("Topic").toUpper()); - - errorField_ = new QLabel(this); - errorField_->setWordWrap(true); - errorField_->hide(); - - layout->addWidget(nameInput_); - layout->addWidget(topicInput_); - layout->addLayout(btnLayout, 1); - - auto labelLayout = new QHBoxLayout; - labelLayout->setAlignment(Qt::AlignHCenter); - labelLayout->addWidget(errorField_); - layout->addLayout(labelLayout); - - connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked); - connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close); - - auto window = QApplication::activeWindow(); - - if (window != nullptr) { - auto center = window->frameGeometry().center(); - move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); - } + setAutoFillBackground(true); + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + QFont largeFont; + largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4); + setMinimumWidth(conf::window::minModalWidth); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + auto layout = new QVBoxLayout(this); + + applyBtn_ = new QPushButton(tr("Apply"), this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + cancelBtn_->setDefault(true); + + auto btnLayout = new QHBoxLayout; + btnLayout->addStretch(1); + btnLayout->setSpacing(15); + btnLayout->addWidget(cancelBtn_); + btnLayout->addWidget(applyBtn_); + + nameInput_ = new TextField(this); + nameInput_->setLabel(tr("Name").toUpper()); + topicInput_ = new TextField(this); + topicInput_->setLabel(tr("Topic").toUpper()); + + errorField_ = new QLabel(this); + errorField_->setWordWrap(true); + errorField_->hide(); + + layout->addWidget(nameInput_); + layout->addWidget(topicInput_); + layout->addLayout(btnLayout, 1); + + auto labelLayout = new QHBoxLayout; + labelLayout->setAlignment(Qt::AlignHCenter); + labelLayout->addWidget(errorField_); + layout->addLayout(labelLayout); + + connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked); + connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close); + + auto window = QApplication::activeWindow(); + + if (window != nullptr) { + auto center = window->frameGeometry().center(); + move(center.x() - (width() * 0.5), center.y() - (height() * 0.5)); + } } void EditModal::topicEventSent(const QString &topic) { - errorField_->hide(); - emit topicChanged(topic); - close(); + errorField_->hide(); + emit topicChanged(topic); + close(); } void EditModal::nameEventSent(const QString &name) { - errorField_->hide(); - emit nameChanged(name); - close(); + errorField_->hide(); + emit nameChanged(name); + close(); } void EditModal::error(const QString &msg) { - errorField_->setText(msg); - errorField_->show(); + errorField_->setText(msg); + errorField_->show(); } void EditModal::applyClicked() { - // Check if the values are changed from the originals. - auto newName = nameInput_->text().trimmed(); - auto newTopic = topicInput_->text().trimmed(); + // Check if the values are changed from the originals. + auto newName = nameInput_->text().trimmed(); + auto newTopic = topicInput_->text().trimmed(); - errorField_->hide(); + errorField_->hide(); - if (newName == initialName_ && newTopic == initialTopic_) { - close(); - return; - } + if (newName == initialName_ && newTopic == initialTopic_) { + close(); + return; + } - using namespace mtx::events; - auto proxy = std::make_shared<ThreadProxy>(); - connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent); - connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent); - connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error); - - if (newName != initialName_ && !newName.isEmpty()) { - state::Name body; - body.name = newName.toStdString(); - - http::client()->send_state_event( - roomId_.toStdString(), - body, - [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit proxy->error( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit proxy->nameEventSent(newName); - }); - } + using namespace mtx::events; + auto proxy = std::make_shared<ThreadProxy>(); + connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent); + connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent); + connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error); - if (newTopic != initialTopic_ && !newTopic.isEmpty()) { - state::Topic body; - body.topic = newTopic.toStdString(); - - http::client()->send_state_event( - roomId_.toStdString(), - body, - [proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit proxy->error( - QString::fromStdString(err->matrix_error.error)); - return; - } - - emit proxy->topicEventSent(newTopic); - }); - } + if (newName != initialName_ && !newName.isEmpty()) { + state::Name body; + body.name = newName.toStdString(); + + http::client()->send_state_event( + roomId_.toStdString(), + body, + [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit proxy->error(QString::fromStdString(err->matrix_error.error)); + return; + } + + emit proxy->nameEventSent(newName); + }); + } + + if (newTopic != initialTopic_ && !newTopic.isEmpty()) { + state::Topic body; + body.topic = newTopic.toStdString(); + + http::client()->send_state_event( + roomId_.toStdString(), + body, + [proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit proxy->error(QString::fromStdString(err->matrix_error.error)); + return; + } + + emit proxy->topicEventSent(newTopic); + }); + } } void EditModal::setFields(const QString &roomName, const QString &roomTopic) { - initialName_ = roomName; - initialTopic_ = roomTopic; + initialName_ = roomName; + initialTopic_ = roomTopic; - nameInput_->setText(roomName); - topicInput_->setText(roomTopic); + nameInput_->setText(roomName); + topicInput_->setText(roomTopic); } RoomSettings::RoomSettings(QString roomid, QObject *parent) : QObject(parent) , roomid_{std::move(roomid)} { - retrieveRoomInfo(); - - // get room setting notifications - http::client()->get_pushrules( - "global", - "override", - roomid_.toStdString(), - [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { - if (err) { - if (err->status_code == 404) - http::client()->get_pushrules( - "global", - "room", - roomid_.toStdString(), - [this](const mtx::pushrules::PushRule &rule, - mtx::http::RequestErr &err) { - if (err) { - notifications_ = 2; // all messages - emit notificationsChanged(); - return; - } - - if (rule.enabled) { - notifications_ = 1; // mentions only - emit notificationsChanged(); - } - }); - return; - } - - if (rule.enabled) { - notifications_ = 0; // muted - emit notificationsChanged(); - } else { - notifications_ = 2; // all messages - emit notificationsChanged(); - } - }); - - // access rules - if (info_.join_rule == state::JoinRule::Public) { - if (info_.guest_access) { - accessRules_ = 0; - } else { - accessRules_ = 1; - } - } else if (info_.join_rule == state::JoinRule::Invite) { - accessRules_ = 2; - } else if (info_.join_rule == state::JoinRule::Knock) { - accessRules_ = 3; - } else if (info_.join_rule == state::JoinRule::Restricted) { - accessRules_ = 4; + retrieveRoomInfo(); + + // get room setting notifications + http::client()->get_pushrules( + "global", + "override", + roomid_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { + if (err) { + if (err->status_code == 404) + http::client()->get_pushrules( + "global", + "room", + roomid_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { + if (err) { + notifications_ = 2; // all messages + emit notificationsChanged(); + return; + } + + if (rule.enabled) { + notifications_ = 1; // mentions only + emit notificationsChanged(); + } + }); + return; + } + + if (rule.enabled) { + notifications_ = 0; // muted + emit notificationsChanged(); + } else { + notifications_ = 2; // all messages + emit notificationsChanged(); + } + }); + + // access rules + if (info_.join_rule == state::JoinRule::Public) { + if (info_.guest_access) { + accessRules_ = 0; + } else { + accessRules_ = 1; } - emit accessJoinRulesChanged(); + } else if (info_.join_rule == state::JoinRule::Invite) { + accessRules_ = 2; + } else if (info_.join_rule == state::JoinRule::Knock) { + accessRules_ = 3; + } else if (info_.join_rule == state::JoinRule::Restricted) { + accessRules_ = 4; + } + emit accessJoinRulesChanged(); } QString RoomSettings::roomName() const { - return utils::replaceEmoji(QString::fromStdString(info_.name).toHtmlEscaped()); + return utils::replaceEmoji(QString::fromStdString(info_.name).toHtmlEscaped()); } QString RoomSettings::roomTopic() const { - return utils::replaceEmoji(utils::linkifyMessage( - QString::fromStdString(info_.topic).toHtmlEscaped().replace("\n", "<br>"))); + return utils::replaceEmoji(utils::linkifyMessage( + QString::fromStdString(info_.topic).toHtmlEscaped().replace("\n", "<br>"))); } QString RoomSettings::roomId() const { - return roomid_; + return roomid_; } QString RoomSettings::roomVersion() const { - return QString::fromStdString(info_.version); + return QString::fromStdString(info_.version); } bool RoomSettings::isLoading() const { - return isLoading_; + return isLoading_; } QString RoomSettings::roomAvatarUrl() { - return QString::fromStdString(info_.avatar_url); + return QString::fromStdString(info_.avatar_url); } int RoomSettings::memberCount() const { - return info_.member_count; + return info_.member_count; } void RoomSettings::retrieveRoomInfo() { - try { - usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString()); - info_ = cache::singleRoomInfo(roomid_.toStdString()); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve room info from cache: {}", - roomid_.toStdString()); - } + try { + usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString()); + info_ = cache::singleRoomInfo(roomid_.toStdString()); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve room info from cache: {}", roomid_.toStdString()); + } } int RoomSettings::notifications() { - return notifications_; + return notifications_; } int RoomSettings::accessJoinRules() { - return accessRules_; + return accessRules_; } void RoomSettings::enableEncryption() { - if (usesEncryption_) - return; - - const auto room_id = roomid_.toStdString(); - http::client()->enable_encryption( - room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - int status_code = static_cast<int>(err->status_code); - nhlog::net()->warn("failed to enable encryption in room ({}): {} {}", - room_id, - err->matrix_error.error, - status_code); - emit displayError( - tr("Failed to enable encryption: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - usesEncryption_ = false; - emit encryptionChanged(); - return; - } - - nhlog::net()->info("enabled encryption on room ({})", room_id); - }); - - usesEncryption_ = true; - emit encryptionChanged(); + if (usesEncryption_) + return; + + const auto room_id = roomid_.toStdString(); + http::client()->enable_encryption( + room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + int status_code = static_cast<int>(err->status_code); + nhlog::net()->warn("failed to enable encryption in room ({}): {} {}", + room_id, + err->matrix_error.error, + status_code); + emit displayError(tr("Failed to enable encryption: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + usesEncryption_ = false; + emit encryptionChanged(); + return; + } + + nhlog::net()->info("enabled encryption on room ({})", room_id); + }); + + usesEncryption_ = true; + emit encryptionChanged(); } bool RoomSettings::canChangeJoinRules() const { - try { - return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, - roomid_.toStdString(), - utils::localUser().toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->warn("lmdb error: {}", e.what()); - } - - return false; + try { + return cache::hasEnoughPowerLevel( + {EventType::RoomJoinRules}, roomid_.toStdString(), utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; } bool RoomSettings::canChangeNameAndTopic() const { - try { - return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic}, - roomid_.toStdString(), - utils::localUser().toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->warn("lmdb error: {}", e.what()); - } - - return false; + try { + return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic}, + roomid_.toStdString(), + utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; } bool RoomSettings::canChangeAvatar() const { - try { - return cache::hasEnoughPowerLevel( - {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->warn("lmdb error: {}", e.what()); - } - - return false; + try { + return cache::hasEnoughPowerLevel( + {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->warn("lmdb error: {}", e.what()); + } + + return false; } bool RoomSettings::isEncryptionEnabled() const { - return usesEncryption_; + return usesEncryption_; } bool RoomSettings::supportsKnocking() const { - return info_.version != "" && info_.version != "1" && info_.version != "2" && - info_.version != "3" && info_.version != "4" && info_.version != "5" && - info_.version != "6"; + return info_.version != "" && info_.version != "1" && info_.version != "2" && + info_.version != "3" && info_.version != "4" && info_.version != "5" && + info_.version != "6"; } bool RoomSettings::supportsRestricted() const { - return info_.version != "" && info_.version != "1" && info_.version != "2" && - info_.version != "3" && info_.version != "4" && info_.version != "5" && - info_.version != "6" && info_.version != "7"; + return info_.version != "" && info_.version != "1" && info_.version != "2" && + info_.version != "3" && info_.version != "4" && info_.version != "5" && + info_.version != "6" && info_.version != "7"; } void RoomSettings::openEditModal() { - retrieveRoomInfo(); - - auto modal = new EditModal(roomid_); - modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic)); - modal->raise(); - modal->show(); - connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) { - info_.name = newName.toStdString(); - emit roomNameChanged(); - }); - - connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) { - info_.topic = newTopic.toStdString(); - emit roomTopicChanged(); - }); + retrieveRoomInfo(); + + auto modal = new EditModal(roomid_); + modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic)); + modal->raise(); + modal->show(); + connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) { + info_.name = newName.toStdString(); + emit roomNameChanged(); + }); + + connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) { + info_.topic = newTopic.toStdString(); + emit roomTopicChanged(); + }); } void RoomSettings::changeNotifications(int currentIndex) { - notifications_ = currentIndex; - - std::string room_id = roomid_.toStdString(); - if (notifications_ == 0) { - // mute room - // delete old rule first, then add new rule - mtx::pushrules::PushRule rule; - rule.actions = {mtx::pushrules::actions::dont_notify{}}; - mtx::pushrules::PushCondition condition; - condition.kind = "event_match"; - condition.key = "room_id"; - condition.pattern = room_id; - rule.conditions = {condition}; - - http::client()->put_pushrules( - "global", "override", room_id, rule, [room_id](mtx::http::RequestErr &err) { - if (err) - nhlog::net()->error("failed to set pushrule for room {}: {} {}", - room_id, - static_cast<int>(err->status_code), - err->matrix_error.error); - http::client()->delete_pushrules( - "global", "room", room_id, [room_id](mtx::http::RequestErr &) {}); - }); - } else if (notifications_ == 1) { - // mentions only - // delete old rule first, then add new rule - mtx::pushrules::PushRule rule; - rule.actions = {mtx::pushrules::actions::dont_notify{}}; - http::client()->put_pushrules( - "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) { - if (err) - nhlog::net()->error("failed to set pushrule for room {}: {} {}", - room_id, - static_cast<int>(err->status_code), - err->matrix_error.error); - http::client()->delete_pushrules( - "global", "override", room_id, [room_id](mtx::http::RequestErr &) {}); - }); - } else { - // all messages - http::client()->delete_pushrules( - "global", "override", room_id, [room_id](mtx::http::RequestErr &) { - http::client()->delete_pushrules( - "global", "room", room_id, [room_id](mtx::http::RequestErr &) {}); - }); - } + notifications_ = currentIndex; + + std::string room_id = roomid_.toStdString(); + if (notifications_ == 0) { + // mute room + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + mtx::pushrules::PushCondition condition; + condition.kind = "event_match"; + condition.key = "room_id"; + condition.pattern = room_id; + rule.conditions = {condition}; + + http::client()->put_pushrules( + "global", "override", room_id, rule, [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error("failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) {}); + }); + } else if (notifications_ == 1) { + // mentions only + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + http::client()->put_pushrules( + "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error("failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", "override", room_id, [room_id](mtx::http::RequestErr &) {}); + }); + } else { + // all messages + http::client()->delete_pushrules( + "global", "override", room_id, [room_id](mtx::http::RequestErr &) { + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) {}); + }); + } } void RoomSettings::changeAccessRules(int index) { - using namespace mtx::events::state; - - auto guest_access = [](int index) -> state::GuestAccess { - state::GuestAccess event; - - if (index == 0) - event.guest_access = state::AccessState::CanJoin; - else - event.guest_access = state::AccessState::Forbidden; - - return event; - }(index); - - auto join_rule = [](int index) -> state::JoinRules { - state::JoinRules event; - - switch (index) { - case 0: - case 1: - event.join_rule = state::JoinRule::Public; - break; - case 2: - event.join_rule = state::JoinRule::Invite; - break; - case 3: - event.join_rule = state::JoinRule::Knock; - break; - case 4: - event.join_rule = state::JoinRule::Restricted; - break; - default: - event.join_rule = state::JoinRule::Invite; - } + using namespace mtx::events::state; + + auto guest_access = [](int index) -> state::GuestAccess { + state::GuestAccess event; + + if (index == 0) + event.guest_access = state::AccessState::CanJoin; + else + event.guest_access = state::AccessState::Forbidden; + + return event; + }(index); + + auto join_rule = [](int index) -> state::JoinRules { + state::JoinRules event; + + switch (index) { + case 0: + case 1: + event.join_rule = state::JoinRule::Public; + break; + case 2: + event.join_rule = state::JoinRule::Invite; + break; + case 3: + event.join_rule = state::JoinRule::Knock; + break; + case 4: + event.join_rule = state::JoinRule::Restricted; + break; + default: + event.join_rule = state::JoinRule::Invite; + } - return event; - }(index); + return event; + }(index); - updateAccessRules(roomid_.toStdString(), join_rule, guest_access); + updateAccessRules(roomid_.toStdString(), join_rule, guest_access); } void @@ -507,139 +501,134 @@ RoomSettings::updateAccessRules(const std::string &room_id, const mtx::events::state::JoinRules &join_rule, const mtx::events::state::GuestAccess &guest_access) { - isLoading_ = true; - emit loadingChanged(); + isLoading_ = true; + emit loadingChanged(); + + http::client()->send_state_event( + room_id, + join_rule, + [this, room_id, guest_access](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send m.room.join_rule: {} {}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit displayError(QString::fromStdString(err->matrix_error.error)); + isLoading_ = false; + emit loadingChanged(); + return; + } + + http::client()->send_state_event( + room_id, + guest_access, + [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send m.room.guest_access: {} {}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit displayError(QString::fromStdString(err->matrix_error.error)); + } - http::client()->send_state_event( - room_id, - join_rule, - [this, room_id, guest_access](const mtx::responses::EventId &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send m.room.join_rule: {} {}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit displayError(QString::fromStdString(err->matrix_error.error)); - isLoading_ = false; - emit loadingChanged(); - return; - } - - http::client()->send_state_event( - room_id, - guest_access, - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send m.room.guest_access: {} {}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit displayError( - QString::fromStdString(err->matrix_error.error)); - } - - isLoading_ = false; - emit loadingChanged(); - }); - }); + isLoading_ = false; + emit loadingChanged(); + }); + }); } void RoomSettings::stopLoading() { - isLoading_ = false; - emit loadingChanged(); + isLoading_ = false; + emit loadingChanged(); } void RoomSettings::avatarChanged() { - retrieveRoomInfo(); - emit avatarUrlChanged(); + retrieveRoomInfo(); + emit avatarUrlChanged(); } void RoomSettings::updateAvatar() { - const QString picturesFolder = - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); - const QString fileName = QFileDialog::getOpenFileName( - nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); - - if (fileName.isEmpty()) - return; - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - - const auto format = mime.name().split("/")[0]; - - QFile file{fileName, this}; - if (format != "image") { - emit displayError(tr("The selected file is not an image")); - return; - } - - if (!file.open(QIODevice::ReadOnly)) { - emit displayError(tr("Error while reading file: %1").arg(file.errorString())); - return; - } - - isLoading_ = true; - emit loadingChanged(); - - // Events emitted from the http callbacks (different threads) will - // be queued back into the UI thread through this proxy object. - auto proxy = std::make_shared<ThreadProxy>(); - connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError); - connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading); - - const auto bin = file.peek(file.size()); - const auto payload = std::string(bin.data(), bin.size()); - const auto dimensions = QImageReader(&file).size(); - - // First we need to create a new mxc URI - // (i.e upload media to the Matrix content repository) for the new avatar. - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fileName).fileName().toStdString(), - [proxy = std::move(proxy), - dimensions, - payload, - mimetype = mime.name().toStdString(), - size = payload.size(), - room_id = roomid_.toStdString(), - content = std::move(bin)](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit proxy->stopLoading(); - emit proxy->error( - tr("Failed to upload image: %s") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - using namespace mtx::events; - state::Avatar avatar_event; - avatar_event.image_info.w = dimensions.width(); - avatar_event.image_info.h = dimensions.height(); - avatar_event.image_info.mimetype = mimetype; - avatar_event.image_info.size = size; - avatar_event.url = res.content_uri; - - http::client()->send_state_event( - room_id, - avatar_event, - [content = std::move(content), proxy = std::move(proxy)]( - const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit proxy->error( - tr("Failed to upload image: %s") + const QString picturesFolder = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString fileName = QFileDialog::getOpenFileName( + nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + const auto format = mime.name().split("/")[0]; + + QFile file{fileName, this}; + if (format != "image") { + emit displayError(tr("The selected file is not an image")); + return; + } + + if (!file.open(QIODevice::ReadOnly)) { + emit displayError(tr("Error while reading file: %1").arg(file.errorString())); + return; + } + + isLoading_ = true; + emit loadingChanged(); + + // Events emitted from the http callbacks (different threads) will + // be queued back into the UI thread through this proxy object. + auto proxy = std::make_shared<ThreadProxy>(); + connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError); + connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading); + + const auto bin = file.peek(file.size()); + const auto payload = std::string(bin.data(), bin.size()); + const auto dimensions = QImageReader(&file).size(); + + // First we need to create a new mxc URI + // (i.e upload media to the Matrix content repository) for the new avatar. + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fileName).fileName().toStdString(), + [proxy = std::move(proxy), + dimensions, + payload, + mimetype = mime.name().toStdString(), + size = payload.size(), + room_id = roomid_.toStdString(), + content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { + if (err) { + emit proxy->stopLoading(); + emit proxy->error(tr("Failed to upload image: %s") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + using namespace mtx::events; + state::Avatar avatar_event; + avatar_event.image_info.w = dimensions.width(); + avatar_event.image_info.h = dimensions.height(); + avatar_event.image_info.mimetype = mimetype; + avatar_event.image_info.size = size; + avatar_event.url = res.content_uri; + + http::client()->send_state_event( + room_id, + avatar_event, + [content = std::move(content), + proxy = std::move(proxy)](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit proxy->error(tr("Failed to upload image: %s") .arg(QString::fromStdString(err->matrix_error.error))); - return; - } + return; + } - emit proxy->stopLoading(); - }); - }); + emit proxy->stopLoading(); + }); + }); } diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h index ab768ffe..6ec645e0 100644 --- a/src/ui/RoomSettings.h +++ b/src/ui/RoomSettings.h @@ -19,121 +19,121 @@ class TextField; /// outside of main with the UI code. class ThreadProxy : public QObject { - Q_OBJECT + Q_OBJECT signals: - void error(const QString &msg); - void nameEventSent(const QString &); - void topicEventSent(const QString &); - void stopLoading(); + void error(const QString &msg); + void nameEventSent(const QString &); + void topicEventSent(const QString &); + void stopLoading(); }; class EditModal : public QWidget { - Q_OBJECT + Q_OBJECT public: - EditModal(const QString &roomId, QWidget *parent = nullptr); + EditModal(const QString &roomId, QWidget *parent = nullptr); - void setFields(const QString &roomName, const QString &roomTopic); + void setFields(const QString &roomName, const QString &roomTopic); signals: - void nameChanged(const QString &roomName); - void topicChanged(const QString &topic); + void nameChanged(const QString &roomName); + void topicChanged(const QString &topic); private slots: - void topicEventSent(const QString &topic); - void nameEventSent(const QString &name); - void error(const QString &msg); + void topicEventSent(const QString &topic); + void nameEventSent(const QString &name); + void error(const QString &msg); - void applyClicked(); + void applyClicked(); private: - QString roomId_; - QString initialName_; - QString initialTopic_; + QString roomId_; + QString initialName_; + QString initialTopic_; - QLabel *errorField_; + QLabel *errorField_; - TextField *nameInput_; - TextField *topicInput_; + TextField *nameInput_; + TextField *topicInput_; - QPushButton *applyBtn_; - QPushButton *cancelBtn_; + QPushButton *applyBtn_; + QPushButton *cancelBtn_; }; class RoomSettings : public QObject { - Q_OBJECT - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT) - Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) - Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) - Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(int memberCount READ memberCount CONSTANT) - Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged) - Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged) - Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) - Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT) - Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT) - Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT) - Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged) - Q_PROPERTY(bool supportsKnocking READ supportsKnocking CONSTANT) - Q_PROPERTY(bool supportsRestricted READ supportsRestricted CONSTANT) + Q_OBJECT + Q_PROPERTY(QString roomId READ roomId CONSTANT) + Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT) + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(int memberCount READ memberCount CONSTANT) + Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged) + Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged) + Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) + Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT) + Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT) + Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT) + Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged) + Q_PROPERTY(bool supportsKnocking READ supportsKnocking CONSTANT) + Q_PROPERTY(bool supportsRestricted READ supportsRestricted CONSTANT) public: - RoomSettings(QString roomid, QObject *parent = nullptr); - - QString roomId() const; - QString roomName() const; - QString roomTopic() const; - QString roomVersion() const; - QString roomAvatarUrl(); - int memberCount() const; - int notifications(); - int accessJoinRules(); - bool isLoading() const; - //! Whether the user has enough power level to send m.room.join_rules events. - bool canChangeJoinRules() const; - //! Whether the user has enough power level to send m.room.name & m.room.topic events. - bool canChangeNameAndTopic() const; - //! Whether the user has enough power level to send m.room.avatar event. - bool canChangeAvatar() const; - bool isEncryptionEnabled() const; - bool supportsKnocking() const; - bool supportsRestricted() const; - - Q_INVOKABLE void enableEncryption(); - Q_INVOKABLE void updateAvatar(); - Q_INVOKABLE void openEditModal(); - Q_INVOKABLE void changeAccessRules(int index); - Q_INVOKABLE void changeNotifications(int currentIndex); + RoomSettings(QString roomid, QObject *parent = nullptr); + + QString roomId() const; + QString roomName() const; + QString roomTopic() const; + QString roomVersion() const; + QString roomAvatarUrl(); + int memberCount() const; + int notifications(); + int accessJoinRules(); + bool isLoading() const; + //! Whether the user has enough power level to send m.room.join_rules events. + bool canChangeJoinRules() const; + //! Whether the user has enough power level to send m.room.name & m.room.topic events. + bool canChangeNameAndTopic() const; + //! Whether the user has enough power level to send m.room.avatar event. + bool canChangeAvatar() const; + bool isEncryptionEnabled() const; + bool supportsKnocking() const; + bool supportsRestricted() const; + + Q_INVOKABLE void enableEncryption(); + Q_INVOKABLE void updateAvatar(); + Q_INVOKABLE void openEditModal(); + Q_INVOKABLE void changeAccessRules(int index); + Q_INVOKABLE void changeNotifications(int currentIndex); signals: - void loadingChanged(); - void roomNameChanged(); - void roomTopicChanged(); - void avatarUrlChanged(); - void encryptionChanged(); - void notificationsChanged(); - void accessJoinRulesChanged(); - void displayError(const QString &errorMessage); + void loadingChanged(); + void roomNameChanged(); + void roomTopicChanged(); + void avatarUrlChanged(); + void encryptionChanged(); + void notificationsChanged(); + void accessJoinRulesChanged(); + void displayError(const QString &errorMessage); public slots: - void stopLoading(); - void avatarChanged(); + void stopLoading(); + void avatarChanged(); private: - void retrieveRoomInfo(); - void updateAccessRules(const std::string &room_id, - const mtx::events::state::JoinRules &, - const mtx::events::state::GuestAccess &); + void retrieveRoomInfo(); + void updateAccessRules(const std::string &room_id, + const mtx::events::state::JoinRules &, + const mtx::events::state::GuestAccess &); private: - QString roomid_; - bool usesEncryption_ = false; - bool isLoading_ = false; - RoomInfo info_; - int notifications_ = 0; - int accessRules_ = 0; + QString roomid_; + bool usesEncryption_ = false; + bool isLoading_ = false; + RoomInfo info_; + int notifications_ = 0; + int accessRules_ = 0; }; diff --git a/src/ui/SnackBar.cpp b/src/ui/SnackBar.cpp index 18990c47..90187154 100644 --- a/src/ui/SnackBar.cpp +++ b/src/ui/SnackBar.cpp @@ -15,121 +15,121 @@ SnackBar::SnackBar(QWidget *parent) : OverlayWidget(parent) , offset_anim(this, "offset", this) { - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.2); - font.setWeight(50); - setFont(font); + QFont font; + font.setPointSizeF(font.pointSizeF() * 1.2); + font.setWeight(50); + setFont(font); - boxHeight_ = QFontMetrics(font).height() * 2; - offset_ = STARTING_OFFSET; - position_ = SnackBarPosition::Top; + boxHeight_ = QFontMetrics(font).height() * 2; + offset_ = STARTING_OFFSET; + position_ = SnackBarPosition::Top; - hideTimer_.setSingleShot(true); + hideTimer_.setSingleShot(true); - offset_anim.setStartValue(1.0); - offset_anim.setEndValue(0.0); - offset_anim.setDuration(100); - offset_anim.setEasingCurve(QEasingCurve::OutCubic); + offset_anim.setStartValue(1.0); + offset_anim.setEndValue(0.0); + offset_anim.setDuration(100); + offset_anim.setEasingCurve(QEasingCurve::OutCubic); - connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); }); - connect( - &offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); }); + connect(this, &SnackBar::offsetChanged, this, [this]() mutable { repaint(); }); + connect( + &offset_anim, &QPropertyAnimation::finished, this, [this]() { hideTimer_.start(10000); }); - connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage())); + connect(&hideTimer_, SIGNAL(timeout()), this, SLOT(hideMessage())); - hide(); + hide(); } void SnackBar::start() { - if (messages_.empty()) - return; + if (messages_.empty()) + return; - show(); - raise(); + show(); + raise(); - offset_anim.start(); + offset_anim.start(); } void SnackBar::hideMessage() { - stopTimers(); - hide(); + stopTimers(); + hide(); - if (!messages_.empty()) - // Moving on to the next message. - messages_.pop_front(); + if (!messages_.empty()) + // Moving on to the next message. + messages_.pop_front(); - // Resetting the starting position of the widget. - offset_ = STARTING_OFFSET; + // Resetting the starting position of the widget. + offset_ = STARTING_OFFSET; - if (!messages_.empty()) - start(); + if (!messages_.empty()) + start(); } void SnackBar::stopTimers() { - hideTimer_.stop(); + hideTimer_.stop(); } void SnackBar::showMessage(const QString &msg) { - messages_.push_back(msg); + messages_.push_back(msg); - // There is already an active message. - if (isVisible()) - return; + // There is already an active message. + if (isVisible()) + return; - start(); + start(); } void SnackBar::mousePressEvent(QMouseEvent *) { - hideMessage(); + hideMessage(); } void SnackBar::paintEvent(QPaintEvent *event) { - Q_UNUSED(event) + Q_UNUSED(event) - if (messages_.empty()) - return; + if (messages_.empty()) + return; - auto message_ = messages_.front(); + auto message_ = messages_.front(); - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(bgColor_); - p.setBrush(brush); + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(bgColor_); + p.setBrush(brush); - QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_); + QRect r(0, 0, std::max(MIN_WIDTH, width() * MIN_WIDTH_PERCENTAGE), boxHeight_); - p.setPen(Qt::white); - QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_); + p.setPen(Qt::white); + QRect br = p.boundingRect(r, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_); - p.setPen(Qt::NoPen); - r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING); + p.setPen(Qt::NoPen); + r = br.united(r).adjusted(-BOX_PADDING, -BOX_PADDING, BOX_PADDING, BOX_PADDING); - const qreal s = 1 - offset_; + const qreal s = 1 - offset_; - if (position_ == SnackBarPosition::Bottom) - p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2, - height() - BOX_PADDING - s * (r.height())); - else - p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2, - s * (r.height()) - 2 * BOX_PADDING); + if (position_ == SnackBarPosition::Bottom) + p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2, + height() - BOX_PADDING - s * (r.height())); + else + p.translate((width() - (r.width() - 2 * BOX_PADDING)) / 2, + s * (r.height()) - 2 * BOX_PADDING); - br.moveCenter(r.center()); - p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4); - p.setPen(textColor_); - p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_); + br.moveCenter(r.center()); + p.drawRoundedRect(r.adjusted(0, 0, 0, 4), 4, 4); + p.setPen(textColor_); + p.drawText(br, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, message_); } diff --git a/src/ui/SnackBar.h b/src/ui/SnackBar.h index 8d127933..0459950f 100644 --- a/src/ui/SnackBar.h +++ b/src/ui/SnackBar.h @@ -14,79 +14,79 @@ enum class SnackBarPosition { - Bottom, - Top, + Bottom, + Top, }; class SnackBar : public OverlayWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor bgColor READ backgroundColor WRITE setBackgroundColor) - Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor) - Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged) + Q_PROPERTY(QColor bgColor READ backgroundColor WRITE setBackgroundColor) + Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor) + Q_PROPERTY(double offset READ offset WRITE setOffset NOTIFY offsetChanged) public: - explicit SnackBar(QWidget *parent); - - QColor backgroundColor() const { return bgColor_; } - void setBackgroundColor(const QColor &color) - { - bgColor_ = color; - update(); - } - - QColor textColor() const { return textColor_; } - void setTextColor(const QColor &color) - { - textColor_ = color; - update(); - } - void setPosition(SnackBarPosition pos) - { - position_ = pos; - update(); - } - - double offset() { return offset_; } - void setOffset(double offset) - { - if (offset != offset_) { - offset_ = offset; - emit offsetChanged(); - } + explicit SnackBar(QWidget *parent); + + QColor backgroundColor() const { return bgColor_; } + void setBackgroundColor(const QColor &color) + { + bgColor_ = color; + update(); + } + + QColor textColor() const { return textColor_; } + void setTextColor(const QColor &color) + { + textColor_ = color; + update(); + } + void setPosition(SnackBarPosition pos) + { + position_ = pos; + update(); + } + + double offset() { return offset_; } + void setOffset(double offset) + { + if (offset != offset_) { + offset_ = offset; + emit offsetChanged(); } + } public slots: - void showMessage(const QString &msg); + void showMessage(const QString &msg); signals: - void offsetChanged(); + void offsetChanged(); protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; private slots: - void hideMessage(); + void hideMessage(); private: - void stopTimers(); - void start(); + void stopTimers(); + void start(); - QColor bgColor_; - QColor textColor_; + QColor bgColor_; + QColor textColor_; - qreal bgOpacity_; - qreal offset_; + qreal bgOpacity_; + qreal offset_; - std::deque<QString> messages_; + std::deque<QString> messages_; - QTimer hideTimer_; + QTimer hideTimer_; - double boxHeight_; + double boxHeight_; - QPropertyAnimation offset_anim; + QPropertyAnimation offset_anim; - SnackBarPosition position_; + SnackBarPosition position_; }; diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp index 7d015e89..8f1a6aa5 100644 --- a/src/ui/TextField.cpp +++ b/src/ui/TextField.cpp @@ -15,363 +15,363 @@ TextField::TextField(QWidget *parent) : QLineEdit(parent) { - // Get rid of the focus border on macOS. - setAttribute(Qt::WA_MacShowFocusRect, 0); + // Get rid of the focus border on macOS. + setAttribute(Qt::WA_MacShowFocusRect, 0); - QPalette pal; + QPalette pal; - state_machine_ = new TextFieldStateMachine(this); - label_ = nullptr; - label_font_size_ = 15; - show_label_ = false; - background_color_ = pal.color(QPalette::Window); - is_valid_ = true; + state_machine_ = new TextFieldStateMachine(this); + label_ = nullptr; + label_font_size_ = 15; + show_label_ = false; + background_color_ = pal.color(QPalette::Window); + is_valid_ = true; - setFrame(false); - setAttribute(Qt::WA_Hover); - setMouseTracking(true); - setTextMargins(0, 4, 0, 6); + setFrame(false); + setAttribute(Qt::WA_Hover); + setMouseTracking(true); + setTextMargins(0, 4, 0, 6); - state_machine_->start(); - QCoreApplication::processEvents(); + state_machine_->start(); + QCoreApplication::processEvents(); } void TextField::setBackgroundColor(const QColor &color) { - background_color_ = color; + background_color_ = color; } QColor TextField::backgroundColor() const { - return background_color_; + return background_color_; } void TextField::setShowLabel(bool value) { - if (show_label_ == value) { - return; - } - - show_label_ = value; - - if (!label_ && value) { - label_ = new TextFieldLabel(this); - state_machine_->setLabel(label_); - } - - if (value) { - setContentsMargins(0, 23, 0, 0); - } else { - setContentsMargins(0, 0, 0, 0); - } + if (show_label_ == value) { + return; + } + + show_label_ = value; + + if (!label_ && value) { + label_ = new TextFieldLabel(this); + state_machine_->setLabel(label_); + } + + if (value) { + setContentsMargins(0, 23, 0, 0); + } else { + setContentsMargins(0, 0, 0, 0); + } } bool TextField::hasLabel() const { - return show_label_; + return show_label_; } void TextField::setValid(bool valid) { - is_valid_ = valid; + is_valid_ = valid; } bool TextField::isValid() const { - QString s = text(); - int pos = 0; - if (regexp_.pattern().isEmpty()) { - return is_valid_; - } - QRegularExpressionValidator v(QRegularExpression(regexp_), 0); - return v.validate(s, pos) == QValidator::Acceptable; + QString s = text(); + int pos = 0; + if (regexp_.pattern().isEmpty()) { + return is_valid_; + } + QRegularExpressionValidator v(QRegularExpression(regexp_), 0); + return v.validate(s, pos) == QValidator::Acceptable; } void TextField::setLabelFontSize(qreal size) { - label_font_size_ = size; + label_font_size_ = size; - if (label_) { - QFont font(label_->font()); - font.setPointSizeF(size); - label_->setFont(font); - label_->update(); - } + if (label_) { + QFont font(label_->font()); + font.setPointSizeF(size); + label_->setFont(font); + label_->update(); + } } qreal TextField::labelFontSize() const { - return label_font_size_; + return label_font_size_; } void TextField::setLabel(const QString &label) { - label_text_ = label; - setShowLabel(true); - label_->update(); + label_text_ = label; + setShowLabel(true); + label_->update(); } QString TextField::label() const { - return label_text_; + return label_text_; } void TextField::setLabelColor(const QColor &color) { - label_color_ = color; - update(); + label_color_ = color; + update(); } QColor TextField::labelColor() const { - if (!label_color_.isValid()) { - return QPalette().color(QPalette::Text); - } + if (!label_color_.isValid()) { + return QPalette().color(QPalette::Text); + } - return label_color_; + return label_color_; } void TextField::setInkColor(const QColor &color) { - ink_color_ = color; - update(); + ink_color_ = color; + update(); } QColor TextField::inkColor() const { - if (!ink_color_.isValid()) { - return QPalette().color(QPalette::Text); - } + if (!ink_color_.isValid()) { + return QPalette().color(QPalette::Text); + } - return ink_color_; + return ink_color_; } void TextField::setUnderlineColor(const QColor &color) { - underline_color_ = color; - update(); + underline_color_ = color; + update(); } void TextField::setRegexp(const QRegularExpression ®exp) { - regexp_ = regexp; + regexp_ = regexp; } QColor TextField::underlineColor() const { - if (!underline_color_.isValid()) { - if ((hasAcceptableInput() && isValid()) || !isModified()) - return QPalette().color(QPalette::Highlight); - else - return Qt::red; - } - - return underline_color_; + if (!underline_color_.isValid()) { + if ((hasAcceptableInput() && isValid()) || !isModified()) + return QPalette().color(QPalette::Highlight); + else + return Qt::red; + } + + return underline_color_; } bool TextField::event(QEvent *event) { - switch (event->type()) { - case QEvent::Resize: - case QEvent::Move: { - if (label_) - label_->setGeometry(rect()); - break; - } - default: - break; - } - - return QLineEdit::event(event); + switch (event->type()) { + case QEvent::Resize: + case QEvent::Move: { + if (label_) + label_->setGeometry(rect()); + break; + } + default: + break; + } + + return QLineEdit::event(event); } void TextField::paintEvent(QPaintEvent *event) { - QLineEdit::paintEvent(event); + QLineEdit::paintEvent(event); - QPainter painter(this); + QPainter painter(this); - if (text().isEmpty()) { - painter.setOpacity(1 - state_machine_->progress()); - painter.fillRect(rect(), backgroundColor()); - } + if (text().isEmpty()) { + painter.setOpacity(1 - state_machine_->progress()); + painter.fillRect(rect(), backgroundColor()); + } - const int y = height() - 1; - const int wd = width() - 5; + const int y = height() - 1; + const int wd = width() - 5; - QPen pen; - pen.setWidth(1); - pen.setColor(underlineColor()); - painter.setPen(pen); - painter.setOpacity(1); - painter.drawLine(2, y, wd, y); + QPen pen; + pen.setWidth(1); + pen.setColor(underlineColor()); + painter.setPen(pen); + painter.setOpacity(1); + painter.drawLine(2, y, wd, y); - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(inkColor()); + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(inkColor()); - const qreal progress = state_machine_->progress(); + const qreal progress = state_machine_->progress(); - if (progress > 0) { - painter.setPen(Qt::NoPen); - painter.setBrush(brush); - const int w = (1 - progress) * static_cast<qreal>(wd / 2); - painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2); - } + if (progress > 0) { + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + const int w = (1 - progress) * static_cast<qreal>(wd / 2); + painter.drawRect(w + 2.5, height() - 2, wd - 2 * w, 2); + } } TextFieldStateMachine::TextFieldStateMachine(TextField *parent) : QStateMachine(parent) , text_field_(parent) { - normal_state_ = new QState; - focused_state_ = new QState; + normal_state_ = new QState; + focused_state_ = new QState; - label_ = nullptr; - offset_anim_ = nullptr; - color_anim_ = nullptr; - progress_ = 0.0; + label_ = nullptr; + offset_anim_ = nullptr; + color_anim_ = nullptr; + progress_ = 0.0; - addState(normal_state_); - addState(focused_state_); + addState(normal_state_); + addState(focused_state_); - setInitialState(normal_state_); + setInitialState(normal_state_); - QEventTransition *transition; - QPropertyAnimation *animation; + QEventTransition *transition; + QPropertyAnimation *animation; - transition = new QEventTransition(parent, QEvent::FocusIn); - transition->setTargetState(focused_state_); - normal_state_->addTransition(transition); + transition = new QEventTransition(parent, QEvent::FocusIn); + transition->setTargetState(focused_state_); + normal_state_->addTransition(transition); - animation = new QPropertyAnimation(this, "progress", this); - animation->setEasingCurve(QEasingCurve::InCubic); - animation->setDuration(310); - transition->addAnimation(animation); + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->setDuration(310); + transition->addAnimation(animation); - transition = new QEventTransition(parent, QEvent::FocusOut); - transition->setTargetState(normal_state_); - focused_state_->addTransition(transition); + transition = new QEventTransition(parent, QEvent::FocusOut); + transition->setTargetState(normal_state_); + focused_state_->addTransition(transition); - animation = new QPropertyAnimation(this, "progress", this); - animation->setEasingCurve(QEasingCurve::OutCubic); - animation->setDuration(310); - transition->addAnimation(animation); + animation = new QPropertyAnimation(this, "progress", this); + animation->setEasingCurve(QEasingCurve::OutCubic); + animation->setDuration(310); + transition->addAnimation(animation); - normal_state_->assignProperty(this, "progress", 0); - focused_state_->assignProperty(this, "progress", 1); + normal_state_->assignProperty(this, "progress", 0); + focused_state_->assignProperty(this, "progress", 1); - setupProperties(); + setupProperties(); - connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties())); + connect(text_field_, SIGNAL(textChanged(QString)), this, SLOT(setupProperties())); } void TextFieldStateMachine::setLabel(TextFieldLabel *label) { - if (label_) { - delete label_; - } - - if (offset_anim_) { - removeDefaultAnimation(offset_anim_); - delete offset_anim_; - } - - if (color_anim_) { - removeDefaultAnimation(color_anim_); - delete color_anim_; - } - - label_ = label; - - if (label_) { - offset_anim_ = new QPropertyAnimation(label_, "offset", this); - offset_anim_->setDuration(210); - offset_anim_->setEasingCurve(QEasingCurve::OutCubic); - addDefaultAnimation(offset_anim_); - - color_anim_ = new QPropertyAnimation(label_, "color", this); - color_anim_->setDuration(210); - addDefaultAnimation(color_anim_); - } - - setupProperties(); + if (label_) { + delete label_; + } + + if (offset_anim_) { + removeDefaultAnimation(offset_anim_); + delete offset_anim_; + } + + if (color_anim_) { + removeDefaultAnimation(color_anim_); + delete color_anim_; + } + + label_ = label; + + if (label_) { + offset_anim_ = new QPropertyAnimation(label_, "offset", this); + offset_anim_->setDuration(210); + offset_anim_->setEasingCurve(QEasingCurve::OutCubic); + addDefaultAnimation(offset_anim_); + + color_anim_ = new QPropertyAnimation(label_, "color", this); + color_anim_->setDuration(210); + addDefaultAnimation(color_anim_); + } + + setupProperties(); } void TextFieldStateMachine::setupProperties() { - if (label_) { - const int m = text_field_->textMargins().top(); - - if (text_field_->text().isEmpty()) { - normal_state_->assignProperty(label_, "offset", QPointF(0, 26)); - } else { - normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); - } - - focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); - focused_state_->assignProperty(label_, "color", text_field_->inkColor()); - normal_state_->assignProperty(label_, "color", text_field_->labelColor()); - - if (0 != label_->offset().y() && !text_field_->text().isEmpty()) { - label_->setOffset(QPointF(0, 0 - m)); - } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 && - text_field_->text().isEmpty()) { - label_->setOffset(QPointF(0, 26)); - } + if (label_) { + const int m = text_field_->textMargins().top(); + + if (text_field_->text().isEmpty()) { + normal_state_->assignProperty(label_, "offset", QPointF(0, 26)); + } else { + normal_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + } + + focused_state_->assignProperty(label_, "offset", QPointF(0, 0 - m)); + focused_state_->assignProperty(label_, "color", text_field_->inkColor()); + normal_state_->assignProperty(label_, "color", text_field_->labelColor()); + + if (0 != label_->offset().y() && !text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 0 - m)); + } else if (!text_field_->hasFocus() && label_->offset().y() <= 0 && + text_field_->text().isEmpty()) { + label_->setOffset(QPointF(0, 26)); } + } - text_field_->update(); + text_field_->update(); } TextFieldLabel::TextFieldLabel(TextField *parent) : QWidget(parent) , text_field_(parent) { - x_ = 0; - y_ = 26; - scale_ = 1; - color_ = parent->labelColor(); - - QFont font; - font.setWeight(60); - font.setLetterSpacing(QFont::PercentageSpacing, 102); - setFont(font); + x_ = 0; + y_ = 26; + scale_ = 1; + color_ = parent->labelColor(); + + QFont font; + font.setWeight(60); + font.setLetterSpacing(QFont::PercentageSpacing, 102); + setFont(font); } void TextFieldLabel::paintEvent(QPaintEvent *) { - if (!text_field_->hasLabel()) - return; + if (!text_field_->hasLabel()) + return; - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - painter.scale(scale_, scale_); - painter.setPen(color_); - painter.setOpacity(1); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.scale(scale_, scale_); + painter.setPen(color_); + painter.setOpacity(1); - QPointF pos(2 + x_, height() - 36 + y_); - painter.drawText(pos.x(), pos.y(), text_field_->label()); + QPointF pos(2 + x_, height() - 36 + y_); + painter.drawText(pos.x(), pos.y(), text_field_->label()); } diff --git a/src/ui/TextField.h b/src/ui/TextField.h index ac4c396e..47257019 100644 --- a/src/ui/TextField.h +++ b/src/ui/TextField.h @@ -18,163 +18,163 @@ class TextFieldStateMachine; class TextField : public QLineEdit { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) - Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor) - Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) + Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) + Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor) + Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) + Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit TextField(QWidget *parent = nullptr); - - void setInkColor(const QColor &color); - void setBackgroundColor(const QColor &color); - void setLabel(const QString &label); - void setLabelColor(const QColor &color); - void setLabelFontSize(qreal size); - void setShowLabel(bool value); - void setUnderlineColor(const QColor &color); - void setRegexp(const QRegularExpression ®exp); - void setValid(bool valid); - - QColor inkColor() const; - QColor labelColor() const; - QColor underlineColor() const; - QColor backgroundColor() const; - QString label() const; - bool hasLabel() const; - bool isValid() const; - qreal labelFontSize() const; + explicit TextField(QWidget *parent = nullptr); + + void setInkColor(const QColor &color); + void setBackgroundColor(const QColor &color); + void setLabel(const QString &label); + void setLabelColor(const QColor &color); + void setLabelFontSize(qreal size); + void setShowLabel(bool value); + void setUnderlineColor(const QColor &color); + void setRegexp(const QRegularExpression ®exp); + void setValid(bool valid); + + QColor inkColor() const; + QColor labelColor() const; + QColor underlineColor() const; + QColor backgroundColor() const; + QString label() const; + bool hasLabel() const; + bool isValid() const; + qreal labelFontSize() const; protected: - bool event(QEvent *event) override; - void paintEvent(QPaintEvent *event) override; + bool event(QEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - void init(); - - QColor ink_color_; - QColor background_color_; - QColor label_color_; - QColor underline_color_; - QString label_text_; - TextFieldLabel *label_; - TextFieldStateMachine *state_machine_; - bool show_label_; - QRegularExpression regexp_; - bool is_valid_; - qreal label_font_size_; + void init(); + + QColor ink_color_; + QColor background_color_; + QColor label_color_; + QColor underline_color_; + QString label_text_; + TextFieldLabel *label_; + TextFieldStateMachine *state_machine_; + bool show_label_; + QRegularExpression regexp_; + bool is_valid_; + qreal label_font_size_; }; class TextFieldLabel : public QWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(qreal scale WRITE setScale READ scale) - Q_PROPERTY(QPointF offset WRITE setOffset READ offset) - Q_PROPERTY(QColor color WRITE setColor READ color) + Q_PROPERTY(qreal scale WRITE setScale READ scale) + Q_PROPERTY(QPointF offset WRITE setOffset READ offset) + Q_PROPERTY(QColor color WRITE setColor READ color) public: - TextFieldLabel(TextField *parent); + TextFieldLabel(TextField *parent); - inline void setColor(const QColor &color); - inline void setOffset(const QPointF &pos); - inline void setScale(qreal scale); + inline void setColor(const QColor &color); + inline void setOffset(const QPointF &pos); + inline void setScale(qreal scale); - inline QColor color() const; - inline QPointF offset() const; - inline qreal scale() const; + inline QColor color() const; + inline QPointF offset() const; + inline qreal scale() const; protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - TextField *const text_field_; + TextField *const text_field_; - QColor color_; - qreal scale_; - qreal x_; - qreal y_; + QColor color_; + qreal scale_; + qreal x_; + qreal y_; }; inline void TextFieldLabel::setColor(const QColor &color) { - color_ = color; - update(); + color_ = color; + update(); } inline void TextFieldLabel::setOffset(const QPointF &pos) { - x_ = pos.x(); - y_ = pos.y(); - update(); + x_ = pos.x(); + y_ = pos.y(); + update(); } inline void TextFieldLabel::setScale(qreal scale) { - scale_ = scale; - update(); + scale_ = scale; + update(); } inline QPointF TextFieldLabel::offset() const { - return QPointF(x_, y_); + return QPointF(x_, y_); } inline qreal TextFieldLabel::scale() const { - return scale_; + return scale_; } inline QColor TextFieldLabel::color() const { - return color_; + return color_; } class TextFieldStateMachine : public QStateMachine { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(qreal progress WRITE setProgress READ progress) + Q_PROPERTY(qreal progress WRITE setProgress READ progress) public: - TextFieldStateMachine(TextField *parent); + TextFieldStateMachine(TextField *parent); - inline void setProgress(qreal progress); - void setLabel(TextFieldLabel *label); + inline void setProgress(qreal progress); + void setLabel(TextFieldLabel *label); - inline qreal progress() const; + inline qreal progress() const; public slots: - void setupProperties(); + void setupProperties(); private: - QPropertyAnimation *color_anim_; - QPropertyAnimation *offset_anim_; + QPropertyAnimation *color_anim_; + QPropertyAnimation *offset_anim_; - QState *focused_state_; - QState *normal_state_; + QState *focused_state_; + QState *normal_state_; - TextField *text_field_; - TextFieldLabel *label_; + TextField *text_field_; + TextFieldLabel *label_; - qreal progress_; + qreal progress_; }; inline void TextFieldStateMachine::setProgress(qreal progress) { - progress_ = progress; - text_field_->update(); + progress_ = progress; + text_field_->update(); } inline qreal TextFieldStateMachine::progress() const { - return progress_; + return progress_; } diff --git a/src/ui/TextLabel.cpp b/src/ui/TextLabel.cpp index 3568e15c..340d3b8f 100644 --- a/src/ui/TextLabel.cpp +++ b/src/ui/TextLabel.cpp @@ -14,12 +14,12 @@ bool ContextMenuFilter::eventFilter(QObject *obj, QEvent *event) { - if (event->type() == QEvent::MouseButtonPress) { - emit contextMenuIsOpening(); - return true; - } + if (event->type() == QEvent::MouseButtonPress) { + emit contextMenuIsOpening(); + return true; + } - return QObject::eventFilter(obj, event); + return QObject::eventFilter(obj, event); } TextLabel::TextLabel(QWidget *parent) @@ -29,94 +29,94 @@ TextLabel::TextLabel(QWidget *parent) TextLabel::TextLabel(const QString &text, QWidget *parent) : QTextBrowser(parent) { - document()->setDefaultStyleSheet(QString("a {color: %1; }").arg(utils::linkColor())); - - setText(text); - setOpenExternalLinks(true); - - // Make it look and feel like an ordinary label. - setReadOnly(true); - setFrameStyle(QFrame::NoFrame); - QPalette pal = palette(); - pal.setColor(QPalette::Base, Qt::transparent); - setPalette(pal); - - // Wrap anywhere but prefer words, adjust minimum height on the fly. - setLineWrapMode(QTextEdit::WidgetWidth); - setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - &TextLabel::adjustHeight); - document()->setDocumentMargin(0); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - setFixedHeight(0); - - connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation); - - auto filter = new ContextMenuFilter(this); - installEventFilter(filter); - connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() { - contextMenuRequested_ = true; - }); + document()->setDefaultStyleSheet(QString("a {color: %1; }").arg(utils::linkColor())); + + setText(text); + setOpenExternalLinks(true); + + // Make it look and feel like an ordinary label. + setReadOnly(true); + setFrameStyle(QFrame::NoFrame); + QPalette pal = palette(); + pal.setColor(QPalette::Base, Qt::transparent); + setPalette(pal); + + // Wrap anywhere but prefer words, adjust minimum height on the fly. + setLineWrapMode(QTextEdit::WidgetWidth); + setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + connect(document()->documentLayout(), + &QAbstractTextDocumentLayout::documentSizeChanged, + this, + &TextLabel::adjustHeight); + document()->setDocumentMargin(0); + + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + setFixedHeight(0); + + connect(this, &TextLabel::linkActivated, this, &TextLabel::handleLinkActivation); + + auto filter = new ContextMenuFilter(this); + installEventFilter(filter); + connect(filter, &ContextMenuFilter::contextMenuIsOpening, this, [this]() { + contextMenuRequested_ = true; + }); } void TextLabel::focusOutEvent(QFocusEvent *e) { - QTextBrowser::focusOutEvent(e); + QTextBrowser::focusOutEvent(e); - // We keep the selection available for the context menu. - if (contextMenuRequested_) { - contextMenuRequested_ = false; - return; - } + // We keep the selection available for the context menu. + if (contextMenuRequested_) { + contextMenuRequested_ = false; + return; + } - QTextCursor cursor = textCursor(); - cursor.clearSelection(); - setTextCursor(cursor); + QTextCursor cursor = textCursor(); + cursor.clearSelection(); + setTextCursor(cursor); } void TextLabel::mousePressEvent(QMouseEvent *e) { - link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString(); - QTextBrowser::mousePressEvent(e); + link_ = (e->button() & Qt::LeftButton) ? anchorAt(e->pos()) : QString(); + QTextBrowser::mousePressEvent(e); } void TextLabel::mouseReleaseEvent(QMouseEvent *e) { - if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) { - emit linkActivated(link_); - return; - } + if (e->button() & Qt::LeftButton && !link_.isEmpty() && anchorAt(e->pos()) == link_) { + emit linkActivated(link_); + return; + } - QTextBrowser::mouseReleaseEvent(e); + QTextBrowser::mouseReleaseEvent(e); } void TextLabel::wheelEvent(QWheelEvent *event) { - event->ignore(); + event->ignore(); } void TextLabel::handleLinkActivation(const QUrl &url) { - auto parts = url.toString().split('/'); - auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); }; + auto parts = url.toString().split('/'); + auto defaultHandler = [](const QUrl &url) { QDesktopServices::openUrl(url); }; - if (url.host() != "matrix.to" || parts.isEmpty()) - return defaultHandler(url); + if (url.host() != "matrix.to" || parts.isEmpty()) + return defaultHandler(url); - try { - using namespace mtx::identifiers; - parse<User>(parts.last().toStdString()); - } catch (const std::exception &) { - return defaultHandler(url); - } + try { + using namespace mtx::identifiers; + parse<User>(parts.last().toStdString()); + } catch (const std::exception &) { + return defaultHandler(url); + } - emit userProfileTriggered(parts.last()); + emit userProfileTriggered(parts.last()); } diff --git a/src/ui/TextLabel.h b/src/ui/TextLabel.h index bc095823..313ad97c 100644 --- a/src/ui/TextLabel.h +++ b/src/ui/TextLabel.h @@ -15,45 +15,45 @@ class QWheelEvent; class ContextMenuFilter : public QObject { - Q_OBJECT + Q_OBJECT public: - explicit ContextMenuFilter(QWidget *parent) - : QObject(parent) - {} + explicit ContextMenuFilter(QWidget *parent) + : QObject(parent) + {} signals: - void contextMenuIsOpening(); + void contextMenuIsOpening(); protected: - bool eventFilter(QObject *obj, QEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; }; class TextLabel : public QTextBrowser { - Q_OBJECT + Q_OBJECT public: - TextLabel(const QString &text, QWidget *parent = nullptr); - TextLabel(QWidget *parent = nullptr); + TextLabel(const QString &text, QWidget *parent = nullptr); + TextLabel(QWidget *parent = nullptr); - void wheelEvent(QWheelEvent *event) override; - void clearLinks() { link_.clear(); } + void wheelEvent(QWheelEvent *event) override; + void clearLinks() { link_.clear(); } protected: - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; private slots: - void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); } - void handleLinkActivation(const QUrl &link); + void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); } + void handleLinkActivation(const QUrl &link); signals: - void userProfileTriggered(const QString &user_id); - void linkActivated(const QUrl &link); + void userProfileTriggered(const QString &user_id); + void linkActivated(const QUrl &link); private: - QString link_; - bool contextMenuRequested_ = false; + QString link_; + bool contextMenuRequested_ = false; }; diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index 732a0443..d6f0b72f 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -9,66 +9,66 @@ Q_DECLARE_METATYPE(Theme) QPalette Theme::paletteFromTheme(std::string_view theme) { - [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme"); - static QPalette original; - if (theme == "light") { - QPalette lightActive( - /*windowText*/ QColor("#333"), - /*button*/ QColor("white"), - /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(70, 77, 93), - /*mid*/ QColor(220, 220, 220), - /*text*/ QColor("#333"), - /*bright_text*/ QColor("#f2f5f8"), - /*base*/ QColor("#fff"), - /*window*/ QColor("white")); - lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); - lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5")); - lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); - lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); - lightActive.setColor(QPalette::Link, QColor("#0077b5")); - lightActive.setColor(QPalette::ButtonText, QColor("#555459")); - return lightActive; - } else if (theme == "dark") { - QPalette darkActive( - /*windowText*/ QColor("#caccd1"), - /*button*/ QColor(0xff, 0xff, 0xff), - /*light*/ QColor("#caccd1"), - /*dark*/ QColor(60, 70, 77), - /*mid*/ QColor("#202228"), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor("#f4f5f8"), - /*base*/ QColor("#202228"), - /*window*/ QColor("#2d3139")); - darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); - darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8")); - darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); - darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); - darkActive.setColor(QPalette::Link, QColor("#38a3d8")); - darkActive.setColor(QPalette::ButtonText, "#828284"); - return darkActive; - } else { - return original; - } + [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme"); + static QPalette original; + if (theme == "light") { + QPalette lightActive( + /*windowText*/ QColor("#333"), + /*button*/ QColor("white"), + /*light*/ QColor(0xef, 0xef, 0xef), + /*dark*/ QColor(70, 77, 93), + /*mid*/ QColor(220, 220, 220), + /*text*/ QColor("#333"), + /*bright_text*/ QColor("#f2f5f8"), + /*base*/ QColor("#fff"), + /*window*/ QColor("white")); + lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); + lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5")); + lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); + lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); + lightActive.setColor(QPalette::Link, QColor("#0077b5")); + lightActive.setColor(QPalette::ButtonText, QColor("#555459")); + return lightActive; + } else if (theme == "dark") { + QPalette darkActive( + /*windowText*/ QColor("#caccd1"), + /*button*/ QColor(0xff, 0xff, 0xff), + /*light*/ QColor("#caccd1"), + /*dark*/ QColor(60, 70, 77), + /*mid*/ QColor("#202228"), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor("#f4f5f8"), + /*base*/ QColor("#202228"), + /*window*/ QColor("#2d3139")); + darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); + darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8")); + darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); + darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); + darkActive.setColor(QPalette::Link, QColor("#38a3d8")); + darkActive.setColor(QPalette::ButtonText, "#828284"); + return darkActive; + } else { + return original; + } } Theme::Theme(std::string_view theme) { - auto p = paletteFromTheme(theme); - separator_ = p.mid().color(); - if (theme == "light") { - sidebarBackground_ = QColor("#233649"); - alternateButton_ = QColor("#ccc"); - red_ = QColor("#a82353"); - } else if (theme == "dark") { - sidebarBackground_ = QColor("#2d3139"); - alternateButton_ = QColor("#414A59"); - red_ = QColor("#a82353"); - } else { - sidebarBackground_ = p.window().color(); - alternateButton_ = p.dark().color(); - red_ = QColor("red"); - } + auto p = paletteFromTheme(theme); + separator_ = p.mid().color(); + if (theme == "light") { + sidebarBackground_ = QColor("#233649"); + alternateButton_ = QColor("#ccc"); + red_ = QColor("#a82353"); + } else if (theme == "dark") { + sidebarBackground_ = QColor("#2d3139"); + alternateButton_ = QColor("#414A59"); + red_ = QColor("#a82353"); + } else { + sidebarBackground_ = p.window().color(); + alternateButton_ = p.dark().color(); + red_ = QColor("red"); + } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h index cc39714b..f3e7c287 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -16,62 +16,62 @@ const int AvatarSize = 40; enum class ButtonPreset { - FlatPreset, - CheckablePreset + FlatPreset, + CheckablePreset }; enum class RippleStyle { - CenteredRipple, - PositionedRipple, - NoRipple + CenteredRipple, + PositionedRipple, + NoRipple }; enum class OverlayStyle { - NoOverlay, - TintedOverlay, - GrayOverlay + NoOverlay, + TintedOverlay, + GrayOverlay }; enum class Role { - Default, - Primary, - Secondary + Default, + Primary, + Secondary }; enum class ButtonIconPlacement { - LeftIcon, - RightIcon + LeftIcon, + RightIcon }; enum class ProgressType { - DeterminateProgress, - IndeterminateProgress + DeterminateProgress, + IndeterminateProgress }; } // namespace ui class Theme : public QPalette { - Q_GADGET - Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) - Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) - Q_PROPERTY(QColor separator READ separator CONSTANT) - Q_PROPERTY(QColor red READ red CONSTANT) + Q_GADGET + Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) + Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) + Q_PROPERTY(QColor separator READ separator CONSTANT) + Q_PROPERTY(QColor red READ red CONSTANT) public: - Theme() {} - explicit Theme(std::string_view theme); - static QPalette paletteFromTheme(std::string_view theme); + Theme() {} + explicit Theme(std::string_view theme); + static QPalette paletteFromTheme(std::string_view theme); - QColor sidebarBackground() const { return sidebarBackground_; } - QColor alternateButton() const { return alternateButton_; } - QColor separator() const { return separator_; } - QColor red() const { return red_; } + QColor sidebarBackground() const { return sidebarBackground_; } + QColor alternateButton() const { return alternateButton_; } + QColor separator() const { return separator_; } + QColor red() const { return red_; } private: - QColor sidebarBackground_, separator_, red_, alternateButton_; + QColor sidebarBackground_, separator_, red_, alternateButton_; }; diff --git a/src/ui/ThemeManager.cpp b/src/ui/ThemeManager.cpp index b7b3df40..0c84777a 100644 --- a/src/ui/ThemeManager.cpp +++ b/src/ui/ThemeManager.cpp @@ -11,32 +11,32 @@ ThemeManager::ThemeManager() {} QColor ThemeManager::themeColor(const QString &key) const { - if (key == "Black") - return QColor("#171919"); - - else if (key == "BrightWhite") - return QColor("#EBEBEB"); - else if (key == "FadedWhite") - return QColor("#C9C9C9"); - else if (key == "MediumWhite") - return QColor("#929292"); - - else if (key == "BrightGreen") - return QColor("#1C3133"); - else if (key == "DarkGreen") - return QColor("#577275"); - else if (key == "LightGreen") - return QColor("#46A451"); - - else if (key == "Gray") - return QColor("#5D6565"); - else if (key == "Red") - return QColor("#E22826"); - else if (key == "Blue") - return QColor("#81B3A9"); - - else if (key == "Transparent") - return QColor(0, 0, 0, 0); - - return (QColor(0, 0, 0, 0)); + if (key == "Black") + return QColor("#171919"); + + else if (key == "BrightWhite") + return QColor("#EBEBEB"); + else if (key == "FadedWhite") + return QColor("#C9C9C9"); + else if (key == "MediumWhite") + return QColor("#929292"); + + else if (key == "BrightGreen") + return QColor("#1C3133"); + else if (key == "DarkGreen") + return QColor("#577275"); + else if (key == "LightGreen") + return QColor("#46A451"); + + else if (key == "Gray") + return QColor("#5D6565"); + else if (key == "Red") + return QColor("#E22826"); + else if (key == "Blue") + return QColor("#81B3A9"); + + else if (key == "Transparent") + return QColor(0, 0, 0, 0); + + return (QColor(0, 0, 0, 0)); } diff --git a/src/ui/ThemeManager.h b/src/ui/ThemeManager.h index cbb355fd..5e86c68f 100644 --- a/src/ui/ThemeManager.h +++ b/src/ui/ThemeManager.h @@ -8,23 +8,23 @@ class ThemeManager : public QCommonStyle { - Q_OBJECT + Q_OBJECT public: - inline static ThemeManager &instance(); + inline static ThemeManager &instance(); - QColor themeColor(const QString &key) const; + QColor themeColor(const QString &key) const; private: - ThemeManager(); + ThemeManager(); - ThemeManager(ThemeManager const &); - void operator=(ThemeManager const &); + ThemeManager(ThemeManager const &); + void operator=(ThemeManager const &); }; inline ThemeManager & ThemeManager::instance() { - static ThemeManager instance; - return instance; + static ThemeManager instance; + return instance; } diff --git a/src/ui/ToggleButton.cpp b/src/ui/ToggleButton.cpp index 33bf8f92..04f752d7 100644 --- a/src/ui/ToggleButton.cpp +++ b/src/ui/ToggleButton.cpp @@ -12,84 +12,84 @@ void Toggle::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); } Toggle::Toggle(QWidget *parent) : QAbstractButton{parent} { - init(); + init(); - connect(this, &QAbstractButton::toggled, this, &Toggle::setState); + connect(this, &QAbstractButton::toggled, this, &Toggle::setState); } void Toggle::setState(bool isEnabled) { - setChecked(isEnabled); - thumb_->setShift(isEnabled ? Position::Left : Position::Right); - setupProperties(); + setChecked(isEnabled); + thumb_->setShift(isEnabled ? Position::Left : Position::Right); + setupProperties(); } void Toggle::init() { - track_ = new ToggleTrack(this); - thumb_ = new ToggleThumb(this); + track_ = new ToggleTrack(this); + thumb_ = new ToggleThumb(this); - setCursor(QCursor(Qt::PointingHandCursor)); - setCheckable(true); - setChecked(false); + setCursor(QCursor(Qt::PointingHandCursor)); + setCheckable(true); + setChecked(false); - setState(false); - setupProperties(); + setState(false); + setupProperties(); - QCoreApplication::processEvents(); + QCoreApplication::processEvents(); } void Toggle::setupProperties() { - if (isEnabled()) { - Position position = thumb_->shift(); + if (isEnabled()) { + Position position = thumb_->shift(); - thumb_->setThumbColor(trackColor()); + thumb_->setThumbColor(trackColor()); - if (position == Position::Left) - track_->setTrackColor(activeColor()); - else if (position == Position::Right) - track_->setTrackColor(inactiveColor()); - } + if (position == Position::Left) + track_->setTrackColor(activeColor()); + else if (position == Position::Right) + track_->setTrackColor(inactiveColor()); + } - update(); + update(); } void Toggle::setDisabledColor(const QColor &color) { - disabledColor_ = color; - setupProperties(); + disabledColor_ = color; + setupProperties(); } void Toggle::setActiveColor(const QColor &color) { - activeColor_ = color; - setupProperties(); + activeColor_ = color; + setupProperties(); } void Toggle::setInactiveColor(const QColor &color) { - inactiveColor_ = color; - setupProperties(); + inactiveColor_ = color; + setupProperties(); } void Toggle::setTrackColor(const QColor &color) { - trackColor_ = color; - setupProperties(); + trackColor_ = color; + setupProperties(); } ToggleThumb::ToggleThumb(Toggle *parent) @@ -98,119 +98,119 @@ ToggleThumb::ToggleThumb(Toggle *parent) , position_{Position::Right} , offset_{0} { - parent->installEventFilter(this); + parent->installEventFilter(this); } void ToggleThumb::setShift(Position position) { - if (position_ != position) { - position_ = position; - updateOffset(); - } + if (position_ != position) { + position_ = position; + updateOffset(); + } } bool ToggleThumb::eventFilter(QObject *obj, QEvent *event) { - const QEvent::Type type = event->type(); + const QEvent::Type type = event->type(); - if (QEvent::Resize == type || QEvent::Move == type) { - setGeometry(toggle_->rect().adjusted(8, 8, -8, -8)); - updateOffset(); - } + if (QEvent::Resize == type || QEvent::Move == type) { + setGeometry(toggle_->rect().adjusted(8, 8, -8, -8)); + updateOffset(); + } - return QWidget::eventFilter(obj, event); + return QWidget::eventFilter(obj, event); } void ToggleThumb::paintEvent(QPaintEvent *event) { - Q_UNUSED(event) + Q_UNUSED(event) - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(toggle_->isEnabled() ? thumbColor_ : Qt::white); + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(toggle_->isEnabled() ? thumbColor_ : Qt::white); - painter.setBrush(brush); - painter.setPen(Qt::NoPen); + painter.setBrush(brush); + painter.setPen(Qt::NoPen); - int s; - QRectF r; + int s; + QRectF r; - s = height() - 10; - r = QRectF(5 + offset_, 5, s, s); + s = height() - 10; + r = QRectF(5 + offset_, 5, s, s); - painter.drawEllipse(r); + painter.drawEllipse(r); - if (!toggle_->isEnabled()) { - brush.setColor(toggle_->disabledColor()); - painter.setBrush(brush); - painter.drawEllipse(r); - } + if (!toggle_->isEnabled()) { + brush.setColor(toggle_->disabledColor()); + painter.setBrush(brush); + painter.drawEllipse(r); + } } void ToggleThumb::updateOffset() { - const QSize s(size()); - offset_ = position_ == Position::Left ? static_cast<qreal>(s.width() - s.height()) : 0; - update(); + const QSize s(size()); + offset_ = position_ == Position::Left ? static_cast<qreal>(s.width() - s.height()) : 0; + update(); } ToggleTrack::ToggleTrack(Toggle *parent) : QWidget{parent} , toggle_{parent} { - Q_ASSERT(parent); + Q_ASSERT(parent); - parent->installEventFilter(this); + parent->installEventFilter(this); } void ToggleTrack::setTrackColor(const QColor &color) { - trackColor_ = color; - update(); + trackColor_ = color; + update(); } bool ToggleTrack::eventFilter(QObject *obj, QEvent *event) { - const QEvent::Type type = event->type(); + const QEvent::Type type = event->type(); - if (QEvent::Resize == type || QEvent::Move == type) { - setGeometry(toggle_->rect()); - } + if (QEvent::Resize == type || QEvent::Move == type) { + setGeometry(toggle_->rect()); + } - return QWidget::eventFilter(obj, event); + return QWidget::eventFilter(obj, event); } void ToggleTrack::paintEvent(QPaintEvent *event) { - Q_UNUSED(event) + Q_UNUSED(event) - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - QBrush brush; - if (toggle_->isEnabled()) { - brush.setColor(trackColor_); - painter.setOpacity(0.8); - } else { - brush.setColor(toggle_->disabledColor()); - painter.setOpacity(0.6); - } + QBrush brush; + if (toggle_->isEnabled()) { + brush.setColor(trackColor_); + painter.setOpacity(0.8); + } else { + brush.setColor(toggle_->disabledColor()); + painter.setOpacity(0.6); + } - brush.setStyle(Qt::SolidPattern); - painter.setBrush(brush); - painter.setPen(Qt::NoPen); + brush.setStyle(Qt::SolidPattern); + painter.setBrush(brush); + painter.setPen(Qt::NoPen); - const int h = height() / 2; - const QRect r(0, h / 2, width(), h); - painter.drawRoundedRect(r.adjusted(14, 4, -14, -4), h / 2 - 4, h / 2 - 4); + const int h = height() / 2; + const QRect r(0, h / 2, width(), h); + painter.drawRoundedRect(r.adjusted(14, 4, -14, -4), h / 2 - 4, h / 2 - 4); } diff --git a/src/ui/ToggleButton.h b/src/ui/ToggleButton.h index 2413b086..15d5e192 100644 --- a/src/ui/ToggleButton.h +++ b/src/ui/ToggleButton.h @@ -12,103 +12,103 @@ class ToggleThumb; enum class Position { - Left, - Right + Left, + Right }; class Toggle : public QAbstractButton { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor activeColor WRITE setActiveColor READ activeColor) - Q_PROPERTY(QColor disabledColor WRITE setDisabledColor READ disabledColor) - Q_PROPERTY(QColor inactiveColor WRITE setInactiveColor READ inactiveColor) - Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) + Q_PROPERTY(QColor activeColor WRITE setActiveColor READ activeColor) + Q_PROPERTY(QColor disabledColor WRITE setDisabledColor READ disabledColor) + Q_PROPERTY(QColor inactiveColor WRITE setInactiveColor READ inactiveColor) + Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) public: - Toggle(QWidget *parent = nullptr); + Toggle(QWidget *parent = nullptr); - void setState(bool isEnabled); + void setState(bool isEnabled); - void setActiveColor(const QColor &color); - void setDisabledColor(const QColor &color); - void setInactiveColor(const QColor &color); - void setTrackColor(const QColor &color); + void setActiveColor(const QColor &color); + void setDisabledColor(const QColor &color); + void setInactiveColor(const QColor &color); + void setTrackColor(const QColor &color); - QColor activeColor() const { return activeColor_; }; - QColor disabledColor() const { return disabledColor_; }; - QColor inactiveColor() const { return inactiveColor_; }; - QColor trackColor() const { return trackColor_.isValid() ? trackColor_ : QColor("#eee"); }; + QColor activeColor() const { return activeColor_; }; + QColor disabledColor() const { return disabledColor_; }; + QColor inactiveColor() const { return inactiveColor_; }; + QColor trackColor() const { return trackColor_.isValid() ? trackColor_ : QColor("#eee"); }; - QSize sizeHint() const override { return QSize(64, 48); }; + QSize sizeHint() const override { return QSize(64, 48); }; protected: - void paintEvent(QPaintEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - void init(); - void setupProperties(); + void init(); + void setupProperties(); - ToggleTrack *track_; - ToggleThumb *thumb_; + ToggleTrack *track_; + ToggleThumb *thumb_; - QColor disabledColor_; - QColor activeColor_; - QColor inactiveColor_; - QColor trackColor_; + QColor disabledColor_; + QColor activeColor_; + QColor inactiveColor_; + QColor trackColor_; }; class ToggleThumb : public QWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor thumbColor WRITE setThumbColor READ thumbColor) + Q_PROPERTY(QColor thumbColor WRITE setThumbColor READ thumbColor) public: - ToggleThumb(Toggle *parent); + ToggleThumb(Toggle *parent); - Position shift() const { return position_; }; - qreal offset() const { return offset_; }; - QColor thumbColor() const { return thumbColor_; }; + Position shift() const { return position_; }; + qreal offset() const { return offset_; }; + QColor thumbColor() const { return thumbColor_; }; - void setShift(Position position); - void setThumbColor(const QColor &color) - { - thumbColor_ = color; - update(); - }; + void setShift(Position position); + void setThumbColor(const QColor &color) + { + thumbColor_ = color; + update(); + }; protected: - bool eventFilter(QObject *obj, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - void updateOffset(); + void updateOffset(); - Toggle *const toggle_; - QColor thumbColor_; + Toggle *const toggle_; + QColor thumbColor_; - Position position_; - qreal offset_; + Position position_; + qreal offset_; }; class ToggleTrack : public QWidget { - Q_OBJECT + Q_OBJECT - Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) + Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) public: - ToggleTrack(Toggle *parent); + ToggleTrack(Toggle *parent); - void setTrackColor(const QColor &color); - QColor trackColor() const { return trackColor_; }; + void setTrackColor(const QColor &color); + QColor trackColor() const { return trackColor_; }; protected: - bool eventFilter(QObject *obj, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; + void paintEvent(QPaintEvent *event) override; private: - Toggle *const toggle_; - QColor trackColor_; + Toggle *const toggle_; + QColor trackColor_; }; diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index fbd0f4f7..58150ed7 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -27,210 +27,200 @@ UserProfile::UserProfile(QString roomid, , manager(manager_) , model(parent) { - globalAvatarUrl = ""; - - connect(this, - &UserProfile::globalUsernameRetrieved, - this, - &UserProfile::setGlobalUsername, - Qt::QueuedConnection); - - if (isGlobalUserProfile()) { - getGlobalProfileData(); - } - - if (!cache::client() || !cache::client()->isDatabaseReady() || - !ChatPage::instance()->timelineManager()) - return; - - connect(cache::client(), - &Cache::verificationStatusChanged, - this, - [this](const std::string &user_id) { - if (user_id != this->userid_.toStdString()) - return; - - auto status = cache::verificationStatus(user_id); - if (!status) - return; - this->isUserVerified = status->user_verified; - emit userStatusChanged(); - - for (auto &deviceInfo : deviceList_.deviceList_) { - deviceInfo.verification_status = - std::find(status->verified_devices.begin(), - status->verified_devices.end(), - deviceInfo.device_id.toStdString()) == - status->verified_devices.end() - ? verification::UNVERIFIED - : verification::VERIFIED; - } - deviceList_.reset(deviceList_.deviceList_); - emit devicesChanged(); - }); - fetchDeviceList(this->userid_); + globalAvatarUrl = ""; + + connect(this, + &UserProfile::globalUsernameRetrieved, + this, + &UserProfile::setGlobalUsername, + Qt::QueuedConnection); + + if (isGlobalUserProfile()) { + getGlobalProfileData(); + } + + if (!cache::client() || !cache::client()->isDatabaseReady() || + !ChatPage::instance()->timelineManager()) + return; + + connect( + cache::client(), &Cache::verificationStatusChanged, this, [this](const std::string &user_id) { + if (user_id != this->userid_.toStdString()) + return; + + auto status = cache::verificationStatus(user_id); + if (!status) + return; + this->isUserVerified = status->user_verified; + emit userStatusChanged(); + + for (auto &deviceInfo : deviceList_.deviceList_) { + deviceInfo.verification_status = + std::find(status->verified_devices.begin(), + status->verified_devices.end(), + deviceInfo.device_id.toStdString()) == status->verified_devices.end() + ? verification::UNVERIFIED + : verification::VERIFIED; + } + deviceList_.reset(deviceList_.deviceList_); + emit devicesChanged(); + }); + fetchDeviceList(this->userid_); } QHash<int, QByteArray> DeviceInfoModel::roleNames() const { - return { - {DeviceId, "deviceId"}, - {DeviceName, "deviceName"}, - {VerificationStatus, "verificationStatus"}, - }; + return { + {DeviceId, "deviceId"}, + {DeviceName, "deviceName"}, + {VerificationStatus, "verificationStatus"}, + }; } QVariant DeviceInfoModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0) - return {}; - - switch (role) { - case DeviceId: - return deviceList_[index.row()].device_id; - case DeviceName: - return deviceList_[index.row()].display_name; - case VerificationStatus: - return QVariant::fromValue(deviceList_[index.row()].verification_status); - default: - return {}; - } + if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0) + return {}; + + switch (role) { + case DeviceId: + return deviceList_[index.row()].device_id; + case DeviceName: + return deviceList_[index.row()].display_name; + case VerificationStatus: + return QVariant::fromValue(deviceList_[index.row()].verification_status); + default: + return {}; + } } void DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList) { - beginResetModel(); - this->deviceList_ = std::move(deviceList); - endResetModel(); + beginResetModel(); + this->deviceList_ = std::move(deviceList); + endResetModel(); } DeviceInfoModel * UserProfile::deviceList() { - return &this->deviceList_; + return &this->deviceList_; } QString UserProfile::userid() { - return this->userid_; + return this->userid_; } QString UserProfile::displayName() { - return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_); + return isGlobalUserProfile() ? globalUsername : cache::displayName(roomid_, userid_); } QString UserProfile::avatarUrl() { - return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_); + return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_); } bool UserProfile::isGlobalUserProfile() const { - return roomid_ == ""; + return roomid_ == ""; } crypto::Trust UserProfile::getUserStatus() { - return isUserVerified; + return isUserVerified; } bool UserProfile::userVerificationEnabled() const { - return hasMasterKey; + return hasMasterKey; } bool UserProfile::isSelf() const { - return this->userid_ == utils::localUser(); + return this->userid_ == utils::localUser(); } void UserProfile::fetchDeviceList(const QString &userID) { - auto localUser = utils::localUser(); - - if (!cache::client() || !cache::client()->isDatabaseReady()) - return; - - cache::client()->query_keys( - userID.toStdString(), - [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys, - mtx::http::RequestErr err) { - if (err) { - 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 - cache::client()->query_keys( - utils::localUser().toStdString(), - [other_user_id, other_user_keys, this](const UserKeyCache &, - mtx::http::RequestErr err) { - using namespace mtx; - std::string local_user_id = utils::localUser().toStdString(); - - if (err) { - nhlog::net()->warn( - "failed to query device keys: {},{}", - mtx::errors::to_string(err->matrix_error.errcode), - static_cast<int>(err->status_code)); - return; - } - - this->hasMasterKey = !other_user_keys.master_keys.keys.empty(); - - std::vector<DeviceInfo> deviceInfo; - auto devices = other_user_keys.device_keys; - auto verificationStatus = - cache::client()->verificationStatus(other_user_id); - - isUserVerified = verificationStatus.user_verified; - emit userStatusChanged(); - - for (const auto &d : devices) { - auto device = d.second; - verification::Status verified = - verification::Status::UNVERIFIED; - - if (std::find(verificationStatus.verified_devices.begin(), - verificationStatus.verified_devices.end(), - device.device_id) != - verificationStatus.verified_devices.end() && - mtx::crypto::verify_identity_signature( - device, - DeviceId(device.device_id), - UserId(other_user_id))) - verified = verification::Status::VERIFIED; - - deviceInfo.push_back( - {QString::fromStdString(d.first), - QString::fromStdString( - device.unsigned_info.device_display_name), - verified}); - } - - this->deviceList_.queueReset(std::move(deviceInfo)); - emit devicesChanged(); - }); - }); + auto localUser = utils::localUser(); + + if (!cache::client() || !cache::client()->isDatabaseReady()) + return; + + cache::client()->query_keys( + userID.toStdString(), + [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys, + mtx::http::RequestErr err) { + if (err) { + 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 + cache::client()->query_keys( + utils::localUser().toStdString(), + [other_user_id, other_user_keys, this](const UserKeyCache &, + mtx::http::RequestErr err) { + using namespace mtx; + std::string local_user_id = utils::localUser().toStdString(); + + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + mtx::errors::to_string(err->matrix_error.errcode), + static_cast<int>(err->status_code)); + return; + } + + this->hasMasterKey = !other_user_keys.master_keys.keys.empty(); + + std::vector<DeviceInfo> deviceInfo; + auto devices = other_user_keys.device_keys; + auto verificationStatus = cache::client()->verificationStatus(other_user_id); + + isUserVerified = verificationStatus.user_verified; + emit userStatusChanged(); + + for (const auto &d : devices) { + auto device = d.second; + verification::Status verified = verification::Status::UNVERIFIED; + + if (std::find(verificationStatus.verified_devices.begin(), + verificationStatus.verified_devices.end(), + device.device_id) != verificationStatus.verified_devices.end() && + mtx::crypto::verify_identity_signature( + device, DeviceId(device.device_id), UserId(other_user_id))) + verified = verification::Status::VERIFIED; + + deviceInfo.push_back( + {QString::fromStdString(d.first), + QString::fromStdString(device.unsigned_info.device_display_name), + verified}); + } + + this->deviceList_.queueReset(std::move(deviceInfo)); + emit devicesChanged(); + }); + }); } void UserProfile::banUser() { - ChatPage::instance()->banUser(this->userid_, ""); + ChatPage::instance()->banUser(this->userid_, ""); } // void ignoreUser(){ @@ -240,188 +230,180 @@ UserProfile::banUser() void UserProfile::kickUser() { - ChatPage::instance()->kickUser(this->userid_, ""); + ChatPage::instance()->kickUser(this->userid_, ""); } void UserProfile::startChat() { - ChatPage::instance()->startChat(this->userid_); + ChatPage::instance()->startChat(this->userid_); } void UserProfile::changeUsername(QString username) { - if (isGlobalUserProfile()) { - // change global - http::client()->set_displayname( - username.toStdString(), [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("could not change username"); - return; - } - }); - } else { - // change room username - mtx::events::state::Member member; - member.display_name = username.toStdString(); - member.avatar_url = - cache::avatarUrl(roomid_, - QString::fromStdString(http::client()->user_id().to_string())) - .toStdString(); - member.membership = mtx::events::state::Membership::Join; - - updateRoomMemberState(std::move(member)); - } + if (isGlobalUserProfile()) { + // change global + http::client()->set_displayname(username.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("could not change username"); + return; + } + }); + } else { + // change room username + mtx::events::state::Member member; + member.display_name = username.toStdString(); + member.avatar_url = + cache::avatarUrl(roomid_, QString::fromStdString(http::client()->user_id().to_string())) + .toStdString(); + member.membership = mtx::events::state::Membership::Join; + + updateRoomMemberState(std::move(member)); + } } void UserProfile::verify(QString device) { - if (!device.isEmpty()) - manager->verifyDevice(userid_, device); - else { - manager->verifyUser(userid_); - } + if (!device.isEmpty()) + manager->verifyDevice(userid_, device); + else { + manager->verifyUser(userid_); + } } void UserProfile::unverify(QString device) { - cache::markDeviceUnverified(userid_.toStdString(), device.toStdString()); + cache::markDeviceUnverified(userid_.toStdString(), device.toStdString()); } void UserProfile::setGlobalUsername(const QString &globalUser) { - globalUsername = globalUser; - emit displayNameChanged(); + globalUsername = globalUser; + emit displayNameChanged(); } void UserProfile::changeAvatar() { - const QString picturesFolder = - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); - const QString fileName = QFileDialog::getOpenFileName( - nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); - - if (fileName.isEmpty()) - return; - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - - const auto format = mime.name().split("/")[0]; - - QFile file{fileName, this}; - if (format != "image") { - emit displayError(tr("The selected file is not an image")); - return; - } - - if (!file.open(QIODevice::ReadOnly)) { - emit displayError(tr("Error while reading file: %1").arg(file.errorString())); - return; - } - - const auto bin = file.peek(file.size()); - const auto payload = std::string(bin.data(), bin.size()); - - isLoading_ = true; - emit loadingChanged(); - - // First we need to create a new mxc URI - // (i.e upload media to the Matrix content repository) for the new avatar. - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fileName).fileName().toStdString(), - [this, - payload, - mimetype = mime.name().toStdString(), - size = payload.size(), - room_id = roomid_.toStdString(), - content = std::move(bin)](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { + const QString picturesFolder = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString fileName = QFileDialog::getOpenFileName( + nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + const auto format = mime.name().split("/")[0]; + + QFile file{fileName, this}; + if (format != "image") { + emit displayError(tr("The selected file is not an image")); + return; + } + + if (!file.open(QIODevice::ReadOnly)) { + emit displayError(tr("Error while reading file: %1").arg(file.errorString())); + return; + } + + const auto bin = file.peek(file.size()); + const auto payload = std::string(bin.data(), bin.size()); + + isLoading_ = true; + emit loadingChanged(); + + // First we need to create a new mxc URI + // (i.e upload media to the Matrix content repository) for the new avatar. + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fileName).fileName().toStdString(), + [this, + payload, + mimetype = mime.name().toStdString(), + size = payload.size(), + room_id = roomid_.toStdString(), + content = std::move(bin)](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to upload image", err->matrix_error.error); + return; + } + + if (isGlobalUserProfile()) { + http::client()->set_avatar_url(res.content_uri, [this](mtx::http::RequestErr err) { if (err) { - nhlog::ui()->error("Failed to upload image", err->matrix_error.error); - return; + nhlog::ui()->error("Failed to set user avatar url", err->matrix_error.error); } - if (isGlobalUserProfile()) { - http::client()->set_avatar_url( - res.content_uri, [this](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error("Failed to set user avatar url", - err->matrix_error.error); - } - - isLoading_ = false; - emit loadingChanged(); - getGlobalProfileData(); - }); - } else { - // change room username - mtx::events::state::Member member; - member.display_name = cache::displayName(roomid_, userid_).toStdString(); - member.avatar_url = res.content_uri; - member.membership = mtx::events::state::Membership::Join; - - updateRoomMemberState(std::move(member)); - } - }); + isLoading_ = false; + emit loadingChanged(); + getGlobalProfileData(); + }); + } else { + // change room username + mtx::events::state::Member member; + member.display_name = cache::displayName(roomid_, userid_).toStdString(); + member.avatar_url = res.content_uri; + member.membership = mtx::events::state::Membership::Join; + + updateRoomMemberState(std::move(member)); + } + }); } void UserProfile::updateRoomMemberState(mtx::events::state::Member member) { - http::client()->send_state_event(roomid_.toStdString(), - http::client()->user_id().to_string(), - member, - [](mtx::responses::EventId, mtx::http::RequestErr err) { - if (err) - nhlog::net()->error( - "Failed to update room member state : ", - err->matrix_error.error); - }); + http::client()->send_state_event( + roomid_.toStdString(), + http::client()->user_id().to_string(), + member, + [](mtx::responses::EventId, mtx::http::RequestErr err) { + if (err) + nhlog::net()->error("Failed to update room member state : ", err->matrix_error.error); + }); } void UserProfile::updateAvatarUrl() { - isLoading_ = false; - emit loadingChanged(); + isLoading_ = false; + emit loadingChanged(); - emit avatarUrlChanged(); + emit avatarUrlChanged(); } bool UserProfile::isLoading() const { - return isLoading_; + return isLoading_; } void UserProfile::getGlobalProfileData() { - http::client()->get_profile( - userid_.toStdString(), - [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve profile info for {}", - userid_.toStdString()); - return; - } - - emit globalUsernameRetrieved(QString::fromStdString(res.display_name)); - globalAvatarUrl = QString::fromStdString(res.avatar_url); - emit avatarUrlChanged(); - }); + http::client()->get_profile( + userid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve profile info for {}", userid_.toStdString()); + return; + } + + emit globalUsernameRetrieved(QString::fromStdString(res.display_name)); + globalAvatarUrl = QString::fromStdString(res.avatar_url); + emit avatarUrlChanged(); + }); } void UserProfile::openGlobalProfile() { - emit manager->openGlobalUserProfile(userid_); + emit manager->openGlobalUserProfile(userid_); } diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h index fd8772d5..a148c431 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h @@ -18,9 +18,9 @@ Q_NAMESPACE enum Status { - VERIFIED, - UNVERIFIED, - BLOCKED + VERIFIED, + UNVERIFIED, + BLOCKED }; Q_ENUM_NS(Status) } @@ -32,128 +32,127 @@ class TimelineViewManager; class DeviceInfo { public: - DeviceInfo(const QString deviceID, - const QString displayName, - verification::Status verification_status_) - : device_id(deviceID) - , display_name(displayName) - , verification_status(verification_status_) - {} - DeviceInfo() - : verification_status(verification::UNVERIFIED) - {} - - QString device_id; - QString display_name; - - verification::Status verification_status; + DeviceInfo(const QString deviceID, + const QString displayName, + verification::Status verification_status_) + : device_id(deviceID) + , display_name(displayName) + , verification_status(verification_status_) + {} + DeviceInfo() + : verification_status(verification::UNVERIFIED) + {} + + QString device_id; + QString display_name; + + verification::Status verification_status; }; class DeviceInfoModel : public QAbstractListModel { - Q_OBJECT + Q_OBJECT public: - enum Roles - { - DeviceId, - DeviceName, - VerificationStatus, - }; - - explicit DeviceInfoModel(QObject *parent = nullptr) - { - (void)parent; - connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset); - }; - QHash<int, QByteArray> roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)deviceList_.size(); - } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + enum Roles + { + DeviceId, + DeviceName, + VerificationStatus, + }; + + explicit DeviceInfoModel(QObject *parent = nullptr) + { + (void)parent; + connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset); + }; + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)deviceList_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; signals: - void queueReset(const std::vector<DeviceInfo> &deviceList); + void queueReset(const std::vector<DeviceInfo> &deviceList); public slots: - void reset(const std::vector<DeviceInfo> &deviceList); + void reset(const std::vector<DeviceInfo> &deviceList); private: - std::vector<DeviceInfo> deviceList_; + std::vector<DeviceInfo> deviceList_; - friend class UserProfile; + friend class UserProfile; }; class UserProfile : public QObject { - Q_OBJECT - Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) - Q_PROPERTY(QString userid READ userid CONSTANT) - Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) - Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) - Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) - Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) - Q_PROPERTY( - bool userVerificationEnabled READ userVerificationEnabled NOTIFY userStatusChanged) - Q_PROPERTY(bool isSelf READ isSelf CONSTANT) - Q_PROPERTY(TimelineModel *room READ room CONSTANT) + Q_OBJECT + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) + Q_PROPERTY(QString userid READ userid CONSTANT) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) + Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) + Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) + Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) + Q_PROPERTY(bool userVerificationEnabled READ userVerificationEnabled NOTIFY userStatusChanged) + Q_PROPERTY(bool isSelf READ isSelf CONSTANT) + Q_PROPERTY(TimelineModel *room READ room CONSTANT) public: - UserProfile(QString roomid, - QString userid, - TimelineViewManager *manager_, - TimelineModel *parent = nullptr); - - DeviceInfoModel *deviceList(); - - QString userid(); - QString displayName(); - QString avatarUrl(); - bool isGlobalUserProfile() const; - crypto::Trust getUserStatus(); - bool userVerificationEnabled() const; - bool isSelf() const; - bool isLoading() const; - TimelineModel *room() const { return model; } - - Q_INVOKABLE void verify(QString device = ""); - Q_INVOKABLE void unverify(QString device = ""); - Q_INVOKABLE void fetchDeviceList(const QString &userID); - Q_INVOKABLE void banUser(); - // Q_INVOKABLE void ignoreUser(); - Q_INVOKABLE void kickUser(); - Q_INVOKABLE void startChat(); - Q_INVOKABLE void changeUsername(QString username); - Q_INVOKABLE void changeAvatar(); - Q_INVOKABLE void openGlobalProfile(); + UserProfile(QString roomid, + QString userid, + TimelineViewManager *manager_, + TimelineModel *parent = nullptr); + + DeviceInfoModel *deviceList(); + + QString userid(); + QString displayName(); + QString avatarUrl(); + bool isGlobalUserProfile() const; + crypto::Trust getUserStatus(); + bool userVerificationEnabled() const; + bool isSelf() const; + bool isLoading() const; + TimelineModel *room() const { return model; } + + Q_INVOKABLE void verify(QString device = ""); + Q_INVOKABLE void unverify(QString device = ""); + Q_INVOKABLE void fetchDeviceList(const QString &userID); + Q_INVOKABLE void banUser(); + // Q_INVOKABLE void ignoreUser(); + Q_INVOKABLE void kickUser(); + Q_INVOKABLE void startChat(); + Q_INVOKABLE void changeUsername(QString username); + Q_INVOKABLE void changeAvatar(); + Q_INVOKABLE void openGlobalProfile(); signals: - void userStatusChanged(); - void loadingChanged(); - void displayNameChanged(); - void avatarUrlChanged(); - void displayError(const QString &errorMessage); - void globalUsernameRetrieved(const QString &globalUser); - void devicesChanged(); + void userStatusChanged(); + void loadingChanged(); + void displayNameChanged(); + void avatarUrlChanged(); + void displayError(const QString &errorMessage); + void globalUsernameRetrieved(const QString &globalUser); + void devicesChanged(); public slots: - void updateAvatarUrl(); + void updateAvatarUrl(); protected slots: - void setGlobalUsername(const QString &globalUser); + void setGlobalUsername(const QString &globalUser); private: - void updateRoomMemberState(mtx::events::state::Member member); - void getGlobalProfileData(); + void updateRoomMemberState(mtx::events::state::Member member); + void getGlobalProfileData(); private: - QString roomid_, userid_; - QString globalUsername; - QString globalAvatarUrl; - DeviceInfoModel deviceList_; - crypto::Trust isUserVerified = crypto::Trust::Unverified; - bool hasMasterKey = false; - bool isLoading_ = false; - TimelineViewManager *manager; - TimelineModel *model; + QString roomid_, userid_; + QString globalUsername; + QString globalAvatarUrl; + DeviceInfoModel deviceList_; + crypto::Trust isUserVerified = crypto::Trust::Unverified; + bool hasMasterKey = false; + bool isLoading_ = false; + TimelineViewManager *manager; + TimelineModel *model; }; |