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;
|