summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp275
-rw-r--r--src/Cache.h19
-rw-r--r--src/CacheCryptoStructs.h10
-rw-r--r--src/Cache_p.h44
-rw-r--r--src/ChatPage.cpp30
-rw-r--r--src/ChatPage.h5
-rw-r--r--src/MainWindow.cpp20
-rw-r--r--src/MainWindow.h6
-rw-r--r--src/Olm.cpp523
-rw-r--r--src/Olm.h17
-rw-r--r--src/RoomInfoListItem.cpp8
-rw-r--r--src/RoomInfoListItem.h7
-rw-r--r--src/RoomList.cpp16
-rw-r--r--src/RoomList.h1
-rw-r--r--src/TextInputWidget.cpp3
-rw-r--r--src/UserSettingsPage.cpp126
-rw-r--r--src/UserSettingsPage.h20
-rw-r--r--src/main.cpp41
-rw-r--r--src/timeline/EventStore.cpp60
-rw-r--r--src/timeline/EventStore.h9
-rw-r--r--src/timeline/TimelineModel.cpp222
-rw-r--r--src/timeline/TimelineModel.h10
-rw-r--r--src/timeline/TimelineViewManager.cpp32
-rw-r--r--src/timeline/TimelineViewManager.h6
24 files changed, 823 insertions, 687 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 088b6fc6..993fbfe7 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -40,7 +40,7 @@ //! Should be changed when a breaking change occurs in the cache format. //! This will reset client's data. -static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.07.05"); +static const std::string CURRENT_CACHE_FORMAT_VERSION("2020.10.20"); static const std::string SECRET("secret"); static lmdb::val NEXT_BATCH_KEY("next_batch"); @@ -437,7 +437,9 @@ Cache::getOutboundMegolmSession(const std::string &room_id) // void -Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +Cache::saveOlmSession(const std::string &curve25519, + mtx::crypto::OlmSessionPtr session, + uint64_t timestamp) { using namespace mtx::crypto; @@ -447,7 +449,11 @@ Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr const auto pickled = pickle<SessionObject>(session.get(), SECRET); const auto session_id = mtx::crypto::session_id(session.get()); - lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled)); + StoredOlmSession stored_session; + stored_session.pickled_session = pickled; + stored_session.last_message_ts = timestamp; + + lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(json(stored_session).dump())); txn.commit(); } @@ -466,13 +472,44 @@ Cache::getOlmSession(const std::string &curve25519, const std::string &session_i txn.commit(); if (found) { - auto data = std::string(pickled.data(), pickled.size()); - return unpickle<SessionObject>(data, SECRET); + std::string_view raw(pickled.data(), pickled.size()); + auto data = json::parse(raw).get<StoredOlmSession>(); + return unpickle<SessionObject>(data.pickled_session, SECRET); } return std::nullopt; } +std::optional<mtx::crypto::OlmSessionPtr> +Cache::getLatestOlmSession(const std::string &curve25519) +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + std::string session_id, pickled_session; + std::vector<std::string> res; + + std::optional<StoredOlmSession> currentNewest; + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(session_id, pickled_session, MDB_NEXT)) { + auto data = + json::parse(std::string_view(pickled_session.data(), pickled_session.size())) + .get<StoredOlmSession>(); + if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts) + currentNewest = data; + } + cursor.close(); + + txn.commit(); + + return currentNewest + ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session, SECRET)) + : std::nullopt; +} + std::vector<std::string> Cache::getOlmSessions(const std::string &curve25519) { @@ -828,6 +865,80 @@ Cache::runMigrations() 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 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, dbName.c_str()); + + std::string 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(); + + olmDb.drop(txn, true); + + auto newDbName = dbName; + newDbName.erase(0, sizeof("olm_sessions") - 1); + newDbName = "olm_sessions.v2" + newDbName; + + auto newDb = lmdb::dbi::open(txn, newDbName.c_str(), MDB_CREATE); + + for (const auto &[key, value] : sessions) { + nhlog::db()->debug("{}\n{}", key, json(value).dump()); + lmdb::dbi_put(txn, + newDb, + lmdb::val(key), + lmdb::val(json(value).dump())); + } + } + 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; + }}, }; nhlog::db()->info("Running migrations, this may take a while!"); @@ -1638,7 +1749,7 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) lmdb::dbi orderDb{0}; try { - orderDb = getOrderToMessageDb(txn, room_id); + 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, @@ -2169,34 +2280,22 @@ Cache::joinedRooms() return room_ids; } -void -Cache::populateMembers() +std::optional<MemberInfo> +Cache::getMember(const std::string &room_id, const std::string &user_id) { - auto rooms = joinedRooms(); - nhlog::db()->info("loading {} rooms", rooms.size()); - - auto txn = lmdb::txn::begin(env_); - - for (const auto &room : rooms) { - const auto roomid = QString::fromStdString(room); - - auto membersdb = getMembersDb(txn, room); - auto cursor = lmdb::cursor::open(txn, membersdb); - - std::string user_id, info; - while (cursor.get(user_id, info, MDB_NEXT)) { - MemberInfo m = json::parse(info); + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - const auto userid = QString::fromStdString(user_id); + auto membersdb = getMembersDb(txn, room_id); - insertDisplayName(roomid, userid, QString::fromStdString(m.name)); - insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url)); + lmdb::val info; + if (lmdb::dbi_get(txn, membersdb, lmdb::val(user_id), info)) { + MemberInfo m = json::parse(std::string_view(info.data(), info.size())); + return m; } - - cursor.close(); + } catch (...) { } - - txn.commit(); + return std::nullopt; } std::vector<RoomSearchResult> @@ -2613,8 +2712,19 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message } } - if (res.chunk.empty()) + if (res.chunk.empty()) { + if (lmdb::dbi_get(txn, orderDb, lmdb::val(&index, sizeof(index)), val)) { + auto orderEntry = json::parse(std::string_view(val.data(), val.size())); + orderEntry["prev_batch"] = res.end; + lmdb::dbi_put(txn, + orderDb, + lmdb::val(&index, sizeof(index)), + lmdb::val(orderEntry.dump())); + nhlog::db()->debug("saving '{}'", orderEntry.dump()); + txn.commit(); + } return index; + } std::string event_id_val; for (const auto &e : res.chunk) { @@ -3034,15 +3144,12 @@ Cache::roomMembers(const std::string &room_id) return members; } -QHash<QString, QString> Cache::DisplayNames; -QHash<QString, QString> Cache::AvatarUrls; - QString Cache::displayName(const QString &room_id, const QString &user_id) { - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - if (DisplayNames.contains(fmt)) - return DisplayNames[fmt]; + if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); + info && !info->name.empty()) + return QString::fromStdString(info->name); return user_id; } @@ -3050,9 +3157,8 @@ Cache::displayName(const QString &room_id, const QString &user_id) std::string Cache::displayName(const std::string &room_id, const std::string &user_id) { - auto fmt = QString::fromStdString(room_id + " " + user_id); - if (DisplayNames.contains(fmt)) - return DisplayNames[fmt].toStdString(); + if (auto info = getMember(room_id, user_id); info && !info->name.empty()) + return info->name; return user_id; } @@ -3060,41 +3166,11 @@ Cache::displayName(const std::string &room_id, const std::string &user_id) QString Cache::avatarUrl(const QString &room_id, const QString &user_id) { - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - if (AvatarUrls.contains(fmt)) - return AvatarUrls[fmt]; - - return QString(); -} + if (auto info = getMember(room_id.toStdString(), user_id.toStdString()); + info && !info->avatar_url.empty()) + return QString::fromStdString(info->avatar_url); -void -Cache::insertDisplayName(const QString &room_id, - const QString &user_id, - const QString &display_name) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - DisplayNames.insert(fmt, display_name); -} - -void -Cache::removeDisplayName(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - DisplayNames.remove(fmt); -} - -void -Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - AvatarUrls.insert(fmt, avatar_url); -} - -void -Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - AvatarUrls.remove(fmt); + return ""; } mtx::presence::PresenceState @@ -3629,6 +3705,19 @@ from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) 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; +} +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>(); +} + namespace cache { void init(const QString &user_id) @@ -3669,28 +3758,6 @@ avatarUrl(const QString &room_id, const QString &user_id) return instance_->avatarUrl(room_id, user_id); } -void -removeDisplayName(const QString &room_id, const QString &user_id) -{ - instance_->removeDisplayName(room_id, user_id); -} -void -removeAvatarUrl(const QString &room_id, const QString &user_id) -{ - instance_->removeAvatarUrl(room_id, user_id); -} - -void -insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name) -{ - instance_->insertDisplayName(room_id, user_id, display_name); -} -void -insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) -{ - instance_->insertAvatarUrl(room_id, user_id, avatar_url); -} - mtx::presence::PresenceState presenceState(const std::string &user_id) { @@ -3702,13 +3769,6 @@ statusMessage(const std::string &user_id) return instance_->statusMessage(user_id); } -//! Load saved data for the display names & avatars. -void -populateMembers() -{ - instance_->populateMembers(); -} - // user cache stores user keys std::optional<UserKeyCache> userKeys(const std::string &user_id) @@ -4114,9 +4174,11 @@ inboundMegolmSessionExists(const MegolmSessionIndex &index) // Olm Sessions // void -saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +saveOlmSession(const std::string &curve25519, + mtx::crypto::OlmSessionPtr session, + uint64_t timestamp) { - instance_->saveOlmSession(curve25519, std::move(session)); + instance_->saveOlmSession(curve25519, std::move(session), timestamp); } std::vector<std::string> getOlmSessions(const std::string &curve25519) @@ -4128,6 +4190,11 @@ getOlmSession(const std::string &curve25519, const std::string &session_id) { return instance_->getOlmSession(curve25519, session_id); } +std::optional<mtx::crypto::OlmSessionPtr> +getLatestOlmSession(const std::string &curve25519) +{ + return instance_->getLatestOlmSession(curve25519); +} void saveOlmAccount(const std::string &pickled) diff --git a/src/Cache.h b/src/Cache.h
index cd96708e..98e6cb75 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -44,16 +44,6 @@ displayName(const QString &room_id, const QString &user_id); QString avatarUrl(const QString &room_id, const QString &user_id); -void -removeDisplayName(const QString &room_id, const QString &user_id); -void -removeAvatarUrl(const QString &room_id, const QString &user_id); - -void -insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name); -void -insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); - // presence mtx::presence::PresenceState presenceState(const std::string &user_id); @@ -74,9 +64,6 @@ markDeviceVerified(const std::string &user_id, const std::string &device); void markDeviceUnverified(const std::string &user_id, const std::string &device); -//! Load saved data for the display names & avatars. -void -populateMembers(); std::vector<std::string> joinedRooms(); @@ -292,11 +279,15 @@ inboundMegolmSessionExists(const MegolmSessionIndex &index); // Olm Sessions // void -saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); +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); diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 935d6493..a693e233 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h
@@ -66,6 +66,16 @@ struct OlmSessionStorage std::mutex group_inbound_mtx; }; +struct StoredOlmSession +{ + std::uint64_t last_message_ts = 0; + std::string pickled_session; +}; +void +to_json(nlohmann::json &obj, const StoredOlmSession &msg); +void +from_json(const nlohmann::json &obj, StoredOlmSession &msg); + //! Verification status of a single user struct VerificationStatus { diff --git a/src/Cache_p.h b/src/Cache_p.h
index b3f4c58c..a32793ea 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -46,9 +46,9 @@ class Cache : public QObject public: Cache(const QString &userId, QObject *parent = nullptr); - static std::string displayName(const std::string &room_id, const std::string &user_id); - static QString displayName(const QString &room_id, const QString &user_id); - static QString avatarUrl(const QString &room_id, const QString &user_id); + 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); @@ -71,18 +71,6 @@ public: void markDeviceVerified(const std::string &user_id, const std::string &device); void markDeviceUnverified(const std::string &user_id, const std::string &device); - static void removeDisplayName(const QString &room_id, const QString &user_id); - static void removeAvatarUrl(const QString &room_id, const QString &user_id); - - static void insertDisplayName(const QString &room_id, - const QString &user_id, - const QString &display_name); - static void insertAvatarUrl(const QString &room_id, - const QString &user_id, - const QString &avatar_url); - - //! Load saved data for the display names & avatars. - void populateMembers(); std::vector<std::string> joinedRooms(); QMap<QString, RoomInfo> roomInfo(bool withInvites = true); @@ -266,10 +254,14 @@ public: // // Olm Sessions // - void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + 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(); @@ -304,6 +296,8 @@ private: QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + 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); DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); void saveTimelineMessages(lmdb::txn &txn, @@ -360,25 +354,12 @@ private: lmdb::val(e->state_key), lmdb::val(json(tmp).dump())); - insertDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e->state_key), - QString::fromStdString(display_name)); - - insertAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e->state_key), - QString::fromStdString(e->content.avatar_url)); - break; } default: { lmdb::dbi_del( txn, membersdb, lmdb::val(e->state_key), lmdb::val("")); - removeDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e->state_key)); - removeAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e->state_key)); - break; } } @@ -565,7 +546,7 @@ private: lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) { return lmdb::dbi::open( - txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + txn, std::string("olm_sessions.v2/" + curve25519_key).c_str(), MDB_CREATE); } QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) @@ -598,9 +579,6 @@ private: QString localUserId_; QString cacheDirectory_; - static QHash<QString, QString> DisplayNames; - static QHash<QString, QString> AvatarUrls; - OlmSessionStorage session_storage; VerificationStorage verification_storage; }; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index e0ac31ab..c86c6128 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -73,6 +73,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) { setObjectName("chatPage"); + instance_ = this; + qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); qRegisterMetaType<std::optional<RelatedInfo>>(); qRegisterMetaType<mtx::presence::PresenceState>(); @@ -124,7 +126,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) contentLayout_->setSpacing(0); contentLayout_->setMargin(0); - view_manager_ = new TimelineViewManager(userSettings_, &callManager_, this); + view_manager_ = new TimelineViewManager(&callManager_, this); contentLayout_->addWidget(view_manager_->getWidget()); @@ -270,7 +272,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(room_list_, SIGNAL(totalUnreadMessageCountUpdated(int)), this, - SLOT(showUnreadMessageNotification(int))); + SIGNAL(unreadMessages(int))); connect(text_input_, &TextInputWidget::sendTextMessage, @@ -593,8 +595,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connectCallMessage<mtx::events::msg::CallCandidates>(); connectCallMessage<mtx::events::msg::CallAnswer>(); connectCallMessage<mtx::events::msg::CallHangUp>(); - - instance_ = this; } void @@ -629,7 +629,7 @@ ChatPage::resetUI() user_info_widget_->reset(); view_manager_->clearAll(); - showUnreadMessageNotification(0); + emit unreadMessages(0); } void @@ -755,18 +755,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } void -ChatPage::showUnreadMessageNotification(int count) -{ - emit unreadMessages(count); - - // TODO: Make the default title a const. - if (count == 0) - emit changeWindowTitle("nheko"); - else - emit changeWindowTitle(QString("nheko (%1)").arg(count)); -} - -void ChatPage::loadStateFromCache() { emit contentLoaded(); @@ -777,8 +765,6 @@ ChatPage::loadStateFromCache() cache::restoreSessions(); olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - cache::populateMembers(); - emit initializeEmptyViews(cache::roomMessages()); emit initializeRoomList(cache::roomInfo()); emit initializeMentions(cache::getTimelineMentions()); @@ -1253,6 +1239,12 @@ ChatPage::unbanUser(QString userid, QString reason) } void +ChatPage::receivedSessionKey(const std::string &room_id, const std::string &session_id) +{ + view_manager_->receivedSessionKey(room_id, session_id); +} + +void ChatPage::sendTypingNotifications() { if (!userSettings_->typingNotifications()) diff --git a/src/ChatPage.h b/src/ChatPage.h
index f0e12ab5..a29cea7b 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h
@@ -107,6 +107,8 @@ public slots: void banUser(QString userid, QString reason); void unbanUser(QString userid, QString reason); + void receivedSessionKey(const std::string &room_id, const std::string &session_id); + signals: void connectionLost(); void connectionRestored(); @@ -128,7 +130,7 @@ signals: void contentLoaded(); void closing(); - void changeWindowTitle(const QString &msg); + void changeWindowTitle(const int); void unreadMessages(int count); void showNotification(const QString &msg); void showLoginPage(const QString &msg); @@ -186,7 +188,6 @@ signals: void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message); private slots: - void showUnreadMessageNotification(int count); void logout(); void removeRoom(const QString &room_id); void dropToLoginPage(const QString &msg); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index b6ad8bbe..f7c9fbf0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -53,10 +53,11 @@ MainWindow *MainWindow::instance_ = nullptr; -MainWindow::MainWindow(QWidget *parent) +MainWindow::MainWindow(const QString profile, QWidget *parent) : QMainWindow(parent) + , profile_{profile} { - setWindowTitle("nheko"); + setWindowTitle(0); setObjectName("MainWindow"); modal_ = new OverlayModal(this); @@ -103,8 +104,7 @@ MainWindow::MainWindow(QWidget *parent) connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); connect( chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); - connect( - chat_page_, SIGNAL(changeWindowTitle(QString)), this, SLOT(setWindowTitle(QString))); + 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_->loginError(msg); @@ -179,6 +179,18 @@ MainWindow::MainWindow(QWidget *parent) } void +MainWindow::setWindowTitle(int notificationCount) +{ + QString name = "nheko"; + if (!profile_.isEmpty()) + name += " | " + profile_; + if (notificationCount > 0) { + name.append(QString{" (%1)"}.arg(notificationCount)); + } + QMainWindow::setWindowTitle(name); +} + +void MainWindow::showEvent(QShowEvent *event) { adjustSideBars(); diff --git a/src/MainWindow.h b/src/MainWindow.h
index e66f299f..2f9ff897 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h
@@ -62,7 +62,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: - explicit MainWindow(QWidget *parent = nullptr); + explicit MainWindow(const QString name, QWidget *parent = nullptr); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); @@ -113,6 +113,8 @@ private slots: void showOverlayProgressBar(); void removeOverlayProgressBar(); + virtual void setWindowTitle(int notificationCount); + private: bool loadJdenticonPlugin(); @@ -147,4 +149,6 @@ private: LoadingIndicator *spinner_ = nullptr; JdenticonInterface *jdenticonInteface_ = nullptr; + + QString profile_; }; diff --git a/src/Olm.cpp b/src/Olm.cpp
index f4cb2209..6e68bd42 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -1,14 +1,15 @@ +#include "Olm.h" + #include <QObject> #include <variant> -#include "Olm.h" - #include "Cache.h" #include "Cache_p.h" #include "ChatPage.h" #include "DeviceVerificationFlow.h" #include "Logging.h" #include "MatrixClient.h" +#include "UserSettingsPage.h" #include "Utils.h" static const std::string STORAGE_SECRET_KEY("secret"); @@ -46,7 +47,7 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven if (msg_type == to_string(mtx::events::EventType::RoomEncrypted)) { try { - OlmMessage olm_msg = j_msg; + olm::OlmMessage olm_msg = j_msg; handle_olm_message(std::move(olm_msg)); } catch (const nlohmann::json::exception &e) { nhlog::crypto()->warn( @@ -55,10 +56,6 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven nhlog::crypto()->warn("validation error for olm message: {} {}", e.what(), j_msg.dump(2)); - - nhlog::crypto()->warn("validation error for olm message: {} {}", - e.what(), - j_msg.dump(2)); } } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { @@ -249,7 +246,10 @@ handle_pre_key_olm_message(const std::string &sender, nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); try { - cache::saveOlmSession(sender_key, std::move(inbound_session)); + 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()); @@ -317,7 +317,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip try { text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - cache::saveOlmSession(id, std::move(session.value())); + 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, @@ -366,6 +369,8 @@ create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::R nhlog::crypto()->info( "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); + + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } void @@ -389,9 +394,10 @@ import_inbound_megolm_session( return; } - // TODO(Nico): Reload messages encrypted with this key. nhlog::crypto()->info( "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); + + ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } void @@ -402,48 +408,24 @@ mark_keys_as_published() } void -request_keys(const std::string &room_id, const std::string &event_id) -{ - nhlog::crypto()->info("requesting keys for event {} at {}", event_id, room_id); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id](const mtx::events::collections::TimelineEvents &res, - mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - if (!std::holds_alternative<EncryptedEvent<msg::Encrypted>>(res)) { - nhlog::net()->info( - "retrieved event is not encrypted: {} from {}", event_id, room_id); - return; - } - - olm::send_key_request_for(room_id, std::get<EncryptedEvent<msg::Encrypted>>(res)); - }); -} - -void -send_key_request_for(const std::string &room_id, - const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) +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: {}", json(e).dump(2)); + 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 = mtx::events::msg::RequestAction::Request; + request.action = !cancel ? mtx::events::msg::RequestAction::Request + : mtx::events::msg::RequestAction::Cancellation; request.algorithm = MEGOLM_ALGO; - request.room_id = room_id; + request.room_id = e.room_id; request.sender_key = e.content.sender_key; request.session_id = e.content.session_id; - request.request_id = "key_request." + http::client()->generate_txn_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)); @@ -493,19 +475,18 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR nhlog::crypto()->warn("requested session not found in room: {}", req.content.room_id); - nhlog::crypto()->warn("requested session not found in room: {}", - req.content.room_id); - return; } // Check that the requested session_id and the one we have saved match. - const auto session = cache::getOutboundMegolmSession(req.content.room_id); - if (req.content.session_id != session.data.session_id) { - nhlog::crypto()->warn("session id of retrieved session doesn't match the request: " - "requested({}), ours({})", - req.content.session_id, - session.data.session_id); + MegolmSessionIndex index{}; + index.room_id = req.content.room_id; + index.session_id = req.content.session_id; + index.sender_key = olm::client()->identity_keys().curve25519; + + const auto session = cache::getInboundMegolmSession(index); + if (!session) { + nhlog::crypto()->warn("No session with id {} in db", req.content.session_id); return; } @@ -517,168 +498,56 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR return; } - if (!utils::respondsToKeyRequests(req.content.room_id)) { + // check if device is verified + auto verificationStatus = cache::verificationStatus(req.sender); + bool verifiedDevice = false; + if (verificationStatus && + ChatPage::instance()->userSettings()->shareKeysWithTrustedUsers()) { + for (const auto &dev : verificationStatus->verified_devices) { + if (dev == req.content.requesting_device_id) { + verifiedDevice = true; + nhlog::crypto()->debug("Verified device: {}", dev); + break; + } + } + } + + if (!utils::respondsToKeyRequests(req.content.room_id) && !verifiedDevice) { nhlog::crypto()->debug("ignoring all key requests for room {}", req.content.room_id); return; } + auto session_key = mtx::crypto::export_session(session); // // Prepare the m.room_key event. // - auto payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", req.content.room_id}, - {"session_id", req.content.session_id}, - {"session_key", session.data.session_key}}; - - send_megolm_key_to_device(req.sender, req.content.requesting_device_id, payload); + 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 = olm::client()->identity_keys().ed25519; + forward_key.forwarding_curve25519_key_chain = {}; + + send_megolm_key_to_device(req.sender, req.content.requesting_device_id, forward_key); } void send_megolm_key_to_device(const std::string &user_id, const std::string &device_id, - const json &payload) + const mtx::events::msg::ForwardedRoomKey &payload) { - mtx::requests::QueryKeys req; - req.device_keys[user_id] = {device_id}; - - http::client()->query_keys( - req, - [payload, user_id, device_id](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()->warn("retrieved device keys from {}, {}", user_id, device_id); - - if (res.device_keys.empty()) { - nhlog::net()->warn("no devices retrieved {}", user_id); - return; - } - - auto device = res.device_keys.begin()->second; - if (device.empty()) { - nhlog::net()->warn("no keys retrieved from user, device {}", user_id); - return; - } - - const auto device_keys = device.begin()->second.keys; - const auto curveKey = "curve25519:" + device_id; - const auto edKey = "ed25519:" + device_id; - - 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); - return; - } + mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> room_key; + room_key.content = payload; + room_key.type = mtx::events::EventType::ForwardedRoomKey; - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature(json(device.begin()->second), - DeviceId(device_id), - UserId(user_id))) { - nhlog::crypto()->warn("failed to verify identity keys: {}", - json(device).dump(2)); - return; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn("failed to parse device key json: {}", e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn("failed to verify device key json: {}", e.what()); - return; - } - - auto room_key = olm::client() - ->create_room_key_event(UserId(user_id), pks.ed25519, payload) - .dump(); - - mtx::requests::ClaimKeys claim_keys; - claim_keys.one_time_keys[user_id][device_id] = mtx::crypto::SIGNED_CURVE25519; - - http::client()->claim_keys( - claim_keys, - [room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast<int>(err->status_code)); - return; - } - - nhlog::net()->info("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->info("no one-time keys found for user_id: {}", - user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->info("no one-time keys found for user_id: {}", - user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - if (retrieved_devices.empty()) { - nhlog::net()->info("claiming keys for {}: no retrieved devices", - device_id); - return; - } - - json body; - body["messages"][user_id] = json::object(); - - auto device = retrieved_devices.begin()->second; - nhlog::net()->debug("{} : \n {}", device_id, device.dump(2)); - - json device_msg; - - try { - auto olm_session = olm::client()->create_outbound_session( - pks.curve25519, device.begin()->at("key")); - - device_msg = olm::client()->create_olm_encrypted_content( - olm_session.get(), room_key, pks.curve25519); - - cache::saveOlmSession(pks.curve25519, std::move(olm_session)); - } catch (const json::exception &e) { - nhlog::crypto()->warn("creating outbound session: {}", - e.what()); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn("creating outbound session: {}", - e.what()); - return; - } - - body["messages"][user_id][device_id] = device_msg; - - nhlog::net()->info( - "sending m.room_key event to {}:{}", user_id, device_id); - http::client()->send_to_device( - "m.room.encrypted", body, [user_id](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 send to {}", user_id); - }); - }); - }); + std::map<std::string, std::vector<std::string>> targets; + targets[user_id] = {device_id}; + send_encrypted_to_device_messages(targets, room_key); } DecryptionResult @@ -727,4 +596,258 @@ decryptEvent(const MegolmSessionIndex &index, return {std::nullopt, std::nullopt, std::move(te.data)}; } + +//! Send encrypted to device messages, targets is a map from userid to device ids or {} for all +//! devices +void +send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::string>> targets, + const mtx::events::collections::DeviceEvents &event, + bool force_new_session) +{ + 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; + + for (const auto &[user, devices] : targets) { + auto deviceKeys = cache::client()->userKeys(user); + + // 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); + + auto session = + cache::getLatestOlmSession(d.keys.at("curve25519:" + device)); + if (!session || force_new_session) { + 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); + 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), + d.keys.at("curve25519:" + device)) + .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 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; + } + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + auto id_key = pks.at(user_id).at(device_id).curve25519; + 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), + pks.at(user_id).at(device_id).ed25519, + 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); + } + }); + }; + }; + + 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](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); + + 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; + } + + deviceKeys[user_id].emplace(device_id, pks); + claim_keys.one_time_keys[user.first][device_id] = + mtx::crypto::SIGNED_CURVE25519; + + nhlog::net()->info("{}", device_id.get()); + nhlog::net()->info(" curve25519 {}", pks.curve25519); + nhlog::net()->info(" ed25519 {}", pks.ed25519); + } + } + + http::client()->claim_keys(claim_keys, BindPks(deviceKeys)); + }); + } +} + } // namespace olm diff --git a/src/Olm.h b/src/Olm.h
index 7b97039b..322affa1 100644 --- a/src/Olm.h +++ b/src/Olm.h
@@ -96,11 +96,9 @@ mark_keys_as_published(); //! Request the encryption keys from sender's device for the given event. void -request_keys(const std::string &room_id, const std::string &event_id); - -void -send_key_request_for(const std::string &room_id, - const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &); +send_key_request_for(mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> e, + const std::string &request_id, + bool cancel = false); void handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyRequest> &); @@ -108,6 +106,13 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR void send_megolm_key_to_device(const std::string &user_id, const std::string &device_id, - const json &payload); + const mtx::events::msg::ForwardedRoomKey &payload); + +//! Send encrypted to device messages, targets is a map from userid to device ids or {} for all +//! devices +void +send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::string>> targets, + const mtx::events::collections::DeviceEvents &event, + bool force_new_session = false); } // namespace olm diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index f234b59b..985ab1b9 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp
@@ -203,10 +203,7 @@ RoomInfoListItem::init(QWidget *parent) }); } -RoomInfoListItem::RoomInfoListItem(QString room_id, - const RoomInfo &info, - QSharedPointer<UserSettings> userSettings, - QWidget *parent) +RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent) : QWidget(parent) , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} , roomId_(std::move(room_id)) @@ -214,7 +211,6 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, , isPressed_(false) , unreadMsgCount_(0) , unreadHighlightedMsgCount_(0) - , settings(userSettings) { init(parent); } @@ -451,7 +447,7 @@ RoomInfoListItem::calculateImportance() const // returns ImportanceDisabled or Invite if (isInvite()) { return Invite; - } else if (!settings->sortByImportance()) { + } else if (!ChatPage::instance()->userSettings()->sortByImportance()) { return ImportanceDisabled; } else if (unreadHighlightedMsgCount_) { return NewMentions; diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index e609f4d8..da5a1bc4 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h
@@ -64,10 +64,7 @@ class RoomInfoListItem : public QWidget Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) public: - RoomInfoListItem(QString room_id, - const RoomInfo &info, - QSharedPointer<UserSettings> userSettings, - QWidget *parent = nullptr); + RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr); void updateUnreadMessageCount(int count, int highlightedCount); void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; @@ -220,6 +217,4 @@ private: QColor bubbleBgColor_; QColor bubbleFgColor_; - - QSharedPointer<UserSettings> settings; }; diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index b4c507b5..8c9e296f 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp
@@ -35,7 +35,6 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent) : QWidget(parent) - , settings(userSettings) { topLayout_ = new QVBoxLayout(this); topLayout_->setSpacing(0); @@ -76,7 +75,7 @@ RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent) void RoomList::addRoom(const QString &room_id, const RoomInfo &info) { - auto room_item = new RoomInfoListItem(room_id, info, settings, scrollArea_); + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); room_item->setRoomName(QString::fromStdString(std::move(info.name))); connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); @@ -84,7 +83,7 @@ RoomList::addRoom(const QString &room_id, const RoomInfo &info) MainWindow::instance()->openLeaveRoomDialog(room_id); }); - QSharedPointer<RoomInfoListItem> roomWidget(room_item); + QSharedPointer<RoomInfoListItem> roomWidget(room_item, &QObject::deleteLater); rooms_.emplace(room_id, roomWidget); rooms_sort_cache_.push_back(roomWidget); @@ -164,11 +163,6 @@ RoomList::initialize(const QMap<QString, RoomInfo> &info) // prevent flickering and save time sorting over and over again setUpdatesEnabled(false); - disconnect(settings.data(), - &UserSettings::roomSortingChanged, - this, - &RoomList::sortRoomsByLastMessage); - for (auto it = info.begin(); it != info.end(); it++) { if (it.value().is_invite) addInvitedRoom(it.key(), it.value()); @@ -179,10 +173,6 @@ RoomList::initialize(const QMap<QString, RoomInfo> &info) for (auto it = info.begin(); it != info.end(); it++) updateRoomDescription(it.key(), it.value().msgInfo); - connect(settings.data(), - &UserSettings::roomSortingChanged, - this, - &RoomList::sortRoomsByLastMessage); setUpdatesEnabled(true); if (rooms_.empty()) @@ -505,7 +495,7 @@ RoomList::updateRoom(const QString &room_id, const RoomInfo &info) void RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) { - auto room_item = new RoomInfoListItem(room_id, info, settings, scrollArea_); + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite); connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite); diff --git a/src/RoomList.h b/src/RoomList.h
index d3470666..d50c7de1 100644 --- a/src/RoomList.h +++ b/src/RoomList.h
@@ -104,5 +104,4 @@ private: QString selectedRoom_; bool isSortPending_ = false; - QSharedPointer<UserSettings> settings; }; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index 22e8aafc..e6a10f0a 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp
@@ -165,6 +165,9 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) MacHelper::showEmojiWindow(); #endif + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_U) + QTextEdit::setText(""); + if (!isModifier) { if (!typingTimer_->isActive()) emit startedTyping(); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index f04193c9..308ec9a2 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -17,7 +17,9 @@ #include <QApplication> #include <QComboBox> +#include <QCoreApplication> #include <QFileDialog> +#include <QFontComboBox> #include <QFormLayout> #include <QInputDialog> #include <QLabel> @@ -73,8 +75,11 @@ UserSettings::load() font_ = settings.value("user/font_family", "default").toString(); avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); - baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); + shareKeysWithTrustedUsers_ = + settings.value("user/share_keys_with_trusted_users", true).toBool(); + 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(); presence_ = settings.value("user/presence", QVariant::fromValue(Presence::AutomaticPresence)) .value<Presence>(); @@ -125,6 +130,16 @@ UserSettings::setStartInTray(bool state) } void +UserSettings::setMobileMode(bool state) +{ + if (state == mobileMode_) + return; + mobileMode_ = state; + emit mobileModeChanged(state); + save(); +} + +void UserSettings::setGroupView(bool state) { if (groupView_ != state) @@ -296,6 +311,17 @@ UserSettings::setUseStunServer(bool useStunServer) } void +UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) +{ + if (shareKeys == shareKeysWithTrustedUsers_) + return; + + shareKeysWithTrustedUsers_ = shareKeys; + emit shareKeysWithTrustedUsersChanged(shareKeys); + save(); +} + +void UserSettings::setMicrophone(QString microphone) { if (microphone == microphone_) @@ -347,17 +373,18 @@ UserSettings::applyTheme() /*windowText*/ QColor("#333"), /*button*/ QColor("#333"), /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(220, 220, 220), - /*mid*/ QColor(110, 110, 110), + /*dark*/ QColor(110, 110, 110), + /*mid*/ QColor(220, 220, 220), /*text*/ QColor("#333"), /*bright_text*/ QColor("#333"), - /*base*/ QColor("#eee"), + /*base*/ QColor("#fff"), /*window*/ QColor("white")); + lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); 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(Qt::gray)); + lightActive.setColor(QPalette::ButtonText, QColor("#495057")); QApplication::setPalette(lightActive); } else if (this->theme() == "dark") { stylefile.setFileName(":/styles/styles/nheko-dark.qss"); @@ -365,17 +392,18 @@ UserSettings::applyTheme() /*windowText*/ QColor("#caccd1"), /*button*/ QColor(0xff, 0xff, 0xff), /*light*/ QColor("#caccd1"), - /*dark*/ QColor("#2d3139"), - /*mid*/ QColor(110, 110, 110), + /*dark*/ QColor(110, 110, 110), + /*mid*/ QColor("#202228"), /*text*/ QColor("#caccd1"), /*bright_text*/ QColor(0xff, 0xff, 0xff), - /*base*/ QColor("#2d3139"), - /*window*/ QColor("#202228")); + /*base*/ QColor("#202228"), + /*window*/ QColor("#2d3139")); + darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); darkActive.setColor(QPalette::Link, QColor("#38a3d8")); - darkActive.setColor(QPalette::ButtonText, QColor(0x90, 0x90, 0x90)); + darkActive.setColor(QPalette::ButtonText, "#727274"); QApplication::setPalette(darkActive); } else { stylefile.setFileName(":/styles/styles/system.qss"); @@ -408,6 +436,8 @@ UserSettings::save() settings.setValue("avatar_circles", avatarCircles_); settings.setValue("decrypt_sidebar", decryptSidebar_); + settings.setValue("share_keys_with_trusted_users", shareKeysWithTrustedUsers_); + settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); settings.setValue("minor_events", sortByImportance_); @@ -456,6 +486,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge 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; @@ -476,30 +509,32 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = 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}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + shareKeysWithTrustedUsers_ = 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}; + 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}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; if (!settings_->tray()) startInTrayToggle_->setDisabled(true); @@ -517,10 +552,6 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); QFontDatabase fontDb; - auto fontFamilies = fontDb.families(); - for (const auto &family : fontFamilies) { - fontSelectionCombo_->addItem(family); - } // TODO: Is there a way to limit to just emojis, rather than // all emoji fonts? @@ -666,6 +697,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge formLayout_->addRow(uiLabel_); formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Mobile mode"), + mobileMode_, + tr("Will prevent text selection in the timeline to make scrolling easier.")); #if !defined(Q_OS_MAC) boxWrap(tr("Scale factor"), scaleFactorCombo_, @@ -702,6 +736,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge formLayout_->addRow(new HorizontalLine{this}); boxWrap(tr("Device ID"), deviceIdValue_); boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); + boxWrap( + tr("Share keys with trusted users"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified.")); formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); @@ -793,6 +831,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge settings_->setStartInTray(!disabled); }); + connect(mobileMode_, &Toggle::toggled, this, [this](bool disabled) { + settings_->setMobileMode(!disabled); + }); + connect(groupViewToggle_, &Toggle::toggled, this, [this](bool disabled) { settings_->setGroupView(!disabled); }); @@ -802,6 +844,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge emit decryptSidebarChanged(); }); + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool disabled) { + settings_->setShareKeysWithTrustedUsers(!disabled); + }); + connect(avatarCircles_, &Toggle::toggled, this, [this](bool disabled) { settings_->setAvatarCircles(!disabled); }); @@ -876,10 +922,12 @@ UserSettingsPage::showEvent(QShowEvent *) startInTrayToggle_->setState(!settings_->startInTray()); groupViewToggle_->setState(!settings_->groupView()); decryptSidebar_->setState(!settings_->decryptSidebar()); + shareKeysWithTrustedUsers_->setState(!settings_->shareKeysWithTrustedUsers()); 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()); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 9d291303..0bd0ac84 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h
@@ -27,6 +27,7 @@ class Toggle; class QLabel; class QFormLayout; class QComboBox; +class QFontComboBox; class QSpinBox; class QHBoxLayout; class QVBoxLayout; @@ -66,19 +67,22 @@ class UserSettings : public QObject decryptSidebarChanged) Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY timelineMaxWidthChanged) + 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( - bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) 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( + bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE + setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) public: UserSettings(); @@ -100,6 +104,7 @@ public: 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); @@ -120,6 +125,7 @@ public: void setCameraResolution(QString resolution); void setCameraFrameRate(QString frameRate); void setUseStunServer(bool state); + void setShareKeysWithTrustedUsers(bool state); QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool messageHoverHighlight() const { return messageHoverHighlight_; } @@ -133,6 +139,7 @@ public: 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_; } @@ -150,6 +157,7 @@ public: QString cameraResolution() const { return cameraResolution_; } QString cameraFrameRate() const { return cameraFrameRate_; } bool useStunServer() const { return useStunServer_; } + bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } signals: void groupViewStateChanged(bool state); @@ -168,6 +176,7 @@ signals: void avatarCirclesChanged(bool state); void decryptSidebarChanged(bool state); void timelineMaxWidthChanged(int state); + void mobileModeChanged(bool mode); void fontSizeChanged(double state); void fontChanged(QString state); void emojiFontChanged(QString state); @@ -177,6 +186,7 @@ signals: void cameraResolutionChanged(QString resolution); void cameraFrameRateChanged(QString frameRate); void useStunServerChanged(bool state); + void shareKeysWithTrustedUsersChanged(bool state); private: // Default to system theme if QT_QPA_PLATFORMTHEME var is set. @@ -199,6 +209,8 @@ private: bool hasAlertOnNotification_; bool avatarCircles_; bool decryptSidebar_; + bool shareKeysWithTrustedUsers_; + bool mobileMode_; int timelineMaxWidth_; double baseFontSize_; QString font_; @@ -264,13 +276,15 @@ private: Toggle *avatarCircles_; Toggle *useStunServer_; Toggle *decryptSidebar_; + Toggle *shareKeysWithTrustedUsers_; + Toggle *mobileMode_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; QComboBox *themeCombo_; QComboBox *scaleFactorCombo_; QComboBox *fontSizeCombo_; - QComboBox *fontSelectionCombo_; + QFontComboBox *fontSelectionCombo_; QComboBox *emojiFontSelectionCombo_; QComboBox *microphoneCombo_; QComboBox *cameraCombo_; diff --git a/src/main.cpp b/src/main.cpp
index e02ffa36..6fbccf5c 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -104,10 +104,32 @@ createCacheDirectory() int main(int argc, char *argv[]) { - // needed for settings so need to register before any settings are read to prevent warings + // needed for settings so need to register before any settings are read to prevent warnings qRegisterMetaType<UserSettings::Presence>(); - QCoreApplication::setApplicationName("nheko"); + // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name + // parsed before the app name is set. + QString appName{"nheko"}; + for (int i = 0; i < argc; ++i) { + if (QString{argv[i]}.startsWith("--profile=")) { + QString q{argv[i]}; + q.remove("--profile="); + appName += "-" + q; + } else if (QString{argv[i]}.startsWith("--p=")) { + QString q{argv[i]}; + q.remove("-p="); + appName += "-" + q; + } else if (QString{argv[i]} == "--profile" || QString{argv[i]} == "-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 + appName += "-" + QString{argv[i]}; + } + } + } + + QCoreApplication::setApplicationName(appName); QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setOrganizationName("nheko"); QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); @@ -137,6 +159,19 @@ main(int argc, char *argv[]) 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(":/logos/nheko.png")); @@ -181,7 +216,7 @@ main(int argc, char *argv[]) appTranslator.load(QLocale(), "nheko", "_", ":/translations"); app.installTranslator(&appTranslator); - MainWindow w; + MainWindow w{(appName == "nheko" ? "" : appName.remove("nheko-"))}; // Move the MainWindow to the center w.move(screenCenter(w.width(), w.height())); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index d3c5c3fa..38292f49 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -54,6 +54,12 @@ EventStore::EventStore(std::string room_id, QObject *) &EventStore::oldMessagesRetrieved, this, [this](const mtx::responses::Messages &res) { + if (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(); @@ -210,6 +216,28 @@ EventStore::clearTimeline() } void +EventStore::receivedSessionKey(const std::string &session_id) +{ + if (!pending_key_requests.count(session_id)) + return; + + auto request = pending_key_requests.at(session_id); + pending_key_requests.erase(session_id); + + olm::send_key_request_for(request.events.front(), request.request_id, true); + + 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()) @@ -291,18 +319,6 @@ EventStore::handleSync(const mtx::responses::Timeline &events) *d_event)) { handle_room_verification(*d_event); } - // else { - // // only the key.verification.ready sent by localuser's other - // device - // // is of significance as it is used for detecting accepted request - // if (std::get_if<mtx::events::RoomEvent< - // mtx::events::msg::KeyVerificationReady>>(d_event)) { - // auto msg = std::get_if<mtx::events::RoomEvent< - // mtx::events::msg::KeyVerificationReady>>(d_event); - // ChatPage::instance()->receivedDeviceVerificationReady( - // msg->content); - // } - //} } } } @@ -498,7 +514,7 @@ EventStore::decryptEvent(const IdIndex &idx, if (decryptionResult.error) { switch (*decryptionResult.error) { - case olm::DecryptionErrorCode::MissingSession: + case olm::DecryptionErrorCode::MissingSession: { dummy.content.body = tr("-- Encrypted Event (No keys found for decryption) --", "Placeholder, when the message was not decrypted yet or can't be " @@ -509,8 +525,21 @@ EventStore::decryptEvent(const IdIndex &idx, index.session_id, e.sender); // TODO: Check if this actually works and look in key backup - olm::send_key_request_for(room_id_, e); + auto copy = e; + copy.room_id = room_id_; + if (pending_key_requests.count(e.content.session_id)) { + pending_key_requests.at(e.content.session_id) + .events.push_back(copy); + } else { + PendingKeyRequests request; + request.request_id = + "key_request." + http::client()->generate_txn_id(); + request.events.push_back(copy); + olm::send_key_request_for(copy, request.request_id); + pending_key_requests[e.content.session_id] = request; + } break; + } case olm::DecryptionErrorCode::DbError: nhlog::db()->critical( "failed to retrieve megolm session with index ({}, {}, {})", @@ -687,6 +716,9 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) void EventStore::fetchMore() { + if (noMoreMessages) + return; + mtx::http::MessagesOpts opts; opts.room_id = room_id_; opts.from = cache::client()->previousBatchToken(room_id_); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index 954e271c..2d5fb1be 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -104,6 +104,7 @@ signals: public slots: void addPending(mtx::events::collections::TimelineEvents event); + void receivedSessionKey(const std::string &session_id); void clearTimeline(); private: @@ -121,6 +122,14 @@ private: 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; + }; + std::map<std::string, PendingKeyRequests> pending_key_requests; + std::string current_txn; int current_txn_error_count = 0; + bool noMoreMessages = false; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 359e95bc..8b80ea51 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -930,11 +930,12 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events:: const auto session_id = mtx::crypto::session_id(outbound_session.get()); const auto session_key = mtx::crypto::session_key(outbound_session.get()); - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; + mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload; + megolm_payload.content.algorithm = "m.megolm.v1.aes-sha2"; + megolm_payload.content.room_id = room_id; + megolm_payload.content.session_id = session_id; + megolm_payload.content.session_key = session_key; + megolm_payload.type = mtx::events::EventType::RoomKey; // Saving the new megolm session. // TODO: Maybe it's too early to save. @@ -958,122 +959,29 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events:: const auto members = cache::roomMembers(room_id); nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - auto keeper = - std::make_shared<StateKeeper>([room_id, doc, txn_id = msg.event_id, this]() { - 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 = txn_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); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } - }); - - mtx::requests::QueryKeys req; + std::map<std::string, std::vector<std::string>> targets; for (const auto &member : members) - req.device_keys[member] = {}; + targets[member] = {}; - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, txn_id = msg.event_id, this]( - 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)); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - return; - } + olm::send_encrypted_to_device_messages(targets, megolm_payload); - mtx::requests::ClaimKeys claim_keys; - - // Mapping from user id to a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map<std::string, std::map<std::string, std::string>> room_key_msgs; - 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); - - 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 room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs[user_id].emplace(device_id, room_key); - deviceKeys[user_id].emplace(device_id, pks); - claim_keys.one_time_keys[user.first][device_id] = - mtx::crypto::SIGNED_CURVE25519; - - nhlog::net()->info("{}", device_id.get()); - nhlog::net()->info(" curve25519 {}", pks.curve25519); - nhlog::net()->info(" ed25519 {}", pks.ed25519); - } - } + 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(); - http::client()->claim_keys(claim_keys, - std::bind(&TimelineModel::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - std::placeholders::_1, - std::placeholders::_2)); - }); + emit this->addPendingMessageToStore(event); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save megolm outbound session: {}", + e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } // TODO: Let the user know about the errors. } catch (const lmdb::error &e) { @@ -1089,86 +997,6 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events:: } } -void -TimelineModel::handleClaimedKeys( - std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::map<std::string, std::string>> &room_keys, - const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast<int>(err->status_code)); - return; - } - - // Payload with all the to_device message to be sent. - nlohmann::json body; - - 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); - return; - } - - 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; - } - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - auto id_key = pks.at(user_id).at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), - room_keys.at(user_id).at(device_id), - pks.at(user_id).at(device_id).curve25519); - - try { - cache::saveOlmSession(id_key, std::move(s)); - } 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()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - } - - http::client()->send_to_device( - mtx::events::to_string(mtx::events::EventType::RoomEncrypted), - http::client()->generate_txn_id(), - body, - [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} - struct SendMessageVisitor { explicit SendMessageVisitor(TimelineModel *model) diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3234a20c..e1fb9196 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -264,6 +264,10 @@ public slots: } 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 roomTopic() const; @@ -297,12 +301,6 @@ signals: private: template<typename T> void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType); - void handleClaimedKeys( - std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::map<std::string, std::string>> &room_keys, - const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); void readEvent(const std::string &id); void setPaginationInProgress(const bool paginationInProgress); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 353f7065..598af31e 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -30,7 +30,7 @@ namespace msgs = mtx::events::msg; void TimelineViewManager::updateEncryptedDescriptions() { - auto decrypt = settings->decryptSidebar(); + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); QHash<QString, QSharedPointer<TimelineModel>>::iterator i; for (i = models.begin(); i != models.end(); ++i) { auto ptr = i.value(); @@ -47,10 +47,10 @@ TimelineViewManager::updateColorPalette() { userColors.clear(); - if (settings->theme() == "light") { + if (ChatPage::instance()->userSettings()->theme() == "light") { view->rootContext()->setContextProperty("currentActivePalette", QPalette()); view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); - } else if (settings->theme() == "dark") { + } else if (ChatPage::instance()->userSettings()->theme() == "dark") { view->rootContext()->setContextProperty("currentActivePalette", QPalette()); view->rootContext()->setContextProperty("currentInactivePalette", QPalette()); } else { @@ -84,14 +84,11 @@ TimelineViewManager::userStatus(QString id) const return QString::fromStdString(cache::statusMessage(id.toStdString())); } -TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, - CallManager *callManager, - ChatPage *parent) +TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) : imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) , blurhashProvider(new BlurhashProvider()) , callManager_(callManager) - , settings(userSettings) { qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); @@ -133,7 +130,7 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin }); qmlRegisterSingletonType<UserSettings>( "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { - return self->settings.data(); + return ChatPage::instance()->userSettings().data(); }); qRegisterMetaType<mtx::events::collections::TimelineEvents>(); @@ -295,7 +292,8 @@ TimelineViewManager::addRoom(const QString &room_id) { if (!models.contains(room_id)) { QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); - newRoom->setDecryptDescription(settings->decryptSidebar()); + newRoom->setDecryptDescription( + ChatPage::instance()->userSettings()->decryptSidebar()); connect(newRoom.data(), &TimelineModel::newEncryptedImage, @@ -454,6 +452,15 @@ TimelineViewManager::updateReadReceipts(const QString &room_id, } void +TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) +{ + auto room = models.find(QString::fromStdString(room_id)); + if (room != models.end()) { + room.value()->receivedSessionKey(session_id); + } +} + +void TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) { for (const auto &e : msgs) { @@ -472,7 +479,7 @@ TimelineViewManager::queueTextMessage(const QString &msg) mtx::events::msg::Text text = {}; text.body = msg.trimmed().toStdString(); - if (settings->markdown()) { + if (ChatPage::instance()->userSettings()->markdown()) { text.formatted_body = utils::markdownToHtml(msg).toStdString(); // Don't send formatted_body, when we don't need to @@ -500,7 +507,7 @@ TimelineViewManager::queueTextMessage(const QString &msg) // NOTE(Nico): rich replies always need a formatted_body! text.format = "org.matrix.custom.html"; - if (settings->markdown()) + if (ChatPage::instance()->userSettings()->markdown()) text.formatted_body = utils::getFormattedQuoteBody(related, utils::markdownToHtml(msg)) .toStdString(); @@ -523,7 +530,8 @@ TimelineViewManager::queueEmoteMessage(const QString &msg) mtx::events::msg::Emote emote; emote.body = msg.trimmed().toStdString(); - if (html != msg.trimmed().toHtmlEscaped() && settings->markdown()) { + if (html != msg.trimmed().toHtmlEscaped() && + ChatPage::instance()->userSettings()->markdown()) { emote.formatted_body = html.toStdString(); emote.format = "org.matrix.custom.html"; } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 1a2d4c4e..895c4b39 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -42,9 +42,7 @@ class TimelineViewManager : public QObject Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) public: - TimelineViewManager(QSharedPointer<UserSettings> userSettings, - CallManager *callManager, - ChatPage *parent = nullptr); + TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr); QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); @@ -98,6 +96,7 @@ signals: 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 initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); void setHistoryView(const QString &room_id); @@ -180,7 +179,6 @@ private: bool isInitialSync_ = true; bool isNarrowView_ = false; - QSharedPointer<UserSettings> settings; QHash<QString, QColor> userColors; QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;