summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp244
-rw-r--r--src/Cache.h12
-rw-r--r--src/CacheCryptoStructs.h30
-rw-r--r--src/Cache_p.h14
-rw-r--r--src/ChatPage.cpp1
-rw-r--r--src/LoginPage.cpp58
-rw-r--r--src/LoginPage.h6
-rw-r--r--src/MainWindow.cpp2
-rw-r--r--src/Olm.cpp190
-rw-r--r--src/RegisterPage.cpp123
-rw-r--r--src/RegisterPage.h10
-rw-r--r--src/WelcomePage.cpp3
-rw-r--r--src/main.cpp2
-rw-r--r--src/timeline/EventStore.cpp5
-rw-r--r--src/timeline/TimelineModel.cpp84
-rw-r--r--src/timeline/TimelineViewManager.cpp3
-rw-r--r--src/ui/TextField.cpp25
-rw-r--r--src/ui/TextField.h3
18 files changed, 535 insertions, 280 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 738f1152..97e99700 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -318,52 +318,67 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled)); txn.commit(); - - { - std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx); - session_storage.group_inbound_sessions[key] = std::move(session); - } } -OlmInboundGroupSession * +mtx::crypto::InboundGroupSessionPtr Cache::getInboundMegolmSession(const MegolmSessionIndex &index) { - std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx); - return session_storage.group_inbound_sessions[json(index).dump()].get(); + using namespace mtx::crypto; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::string key = json(index).dump(); + lmdb::val value; + + if (lmdb::dbi_get(txn, inboundMegolmSessionDb_, lmdb::val(key), value)) { + auto session = unpickle<InboundSessionObject>( + std::string(value.data(), value.size()), SECRET); + return session; + } + } catch (std::exception &e) { + nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); + } + + return nullptr; } bool Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) { - std::unique_lock<std::mutex> lock(session_storage.group_inbound_mtx); - return session_storage.group_inbound_sessions.find(json(index).dump()) != - session_storage.group_inbound_sessions.end(); + using namespace mtx::crypto; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::string key = json(index).dump(); + lmdb::val value; + + return lmdb::dbi_get(txn, inboundMegolmSessionDb_, lmdb::val(key), value); + } catch (std::exception &e) { + nhlog::db()->error("Failed to get inbound megolm session {}", e.what()); + } + + return false; } void -Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index) +Cache::updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data_, + mtx::crypto::OutboundGroupSessionPtr &ptr) { using namespace mtx::crypto; if (!outboundMegolmSessionExists(room_id)) return; - OutboundGroupSessionData data; - OlmOutboundGroupSession *session; - { - std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); - data = session_storage.group_outbound_session_data[room_id]; - session = session_storage.group_outbound_sessions[room_id].get(); - - // Update with the current message. - data.message_index = message_index; - session_storage.group_outbound_session_data[room_id] = data; - } + OutboundGroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(ptr.get()); + data.session_id = mtx::crypto::session_id(ptr.get()); + data.session_key = mtx::crypto::session_key(ptr.get()); // Save the updated pickled data for the session. json j; j["data"] = data; - j["session"] = pickle<OutboundSessionObject>(session, SECRET); + j["session"] = pickle<OutboundSessionObject>(ptr.get(), SECRET); auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); @@ -379,10 +394,6 @@ Cache::dropOutboundMegolmSession(const std::string &room_id) return; { - std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); - session_storage.group_outbound_session_data.erase(room_id); - session_storage.group_outbound_sessions.erase(room_id); - auto txn = lmdb::txn::begin(env_); lmdb::dbi_del(txn, outboundMegolmSessionDb_, lmdb::val(room_id), nullptr); txn.commit(); @@ -392,7 +403,7 @@ Cache::dropOutboundMegolmSession(const std::string &room_id) void Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session) + mtx::crypto::OutboundGroupSessionPtr &session) { using namespace mtx::crypto; const auto pickled = pickle<OutboundSessionObject>(session.get(), SECRET); @@ -404,30 +415,40 @@ Cache::saveOutboundMegolmSession(const std::string &room_id, auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); txn.commit(); - - { - std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); - session_storage.group_outbound_session_data[room_id] = data; - session_storage.group_outbound_sessions[room_id] = std::move(session); - } } bool Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept { - std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); - return (session_storage.group_outbound_sessions.find(room_id) != - session_storage.group_outbound_sessions.end()) && - (session_storage.group_outbound_session_data.find(room_id) != - session_storage.group_outbound_session_data.end()); + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val value; + return lmdb::dbi_get(txn, outboundMegolmSessionDb_, lmdb::val(room_id), value); + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); + return false; + } } OutboundGroupSessionDataRef Cache::getOutboundMegolmSession(const std::string &room_id) { - std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); - return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(), - session_storage.group_outbound_session_data[room_id]}; + try { + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val value; + lmdb::dbi_get(txn, outboundMegolmSessionDb_, lmdb::val(room_id), value); + auto obj = json::parse(std::string_view(value.data(), value.size())); + + OutboundGroupSessionDataRef ref{}; + ref.data = obj.at("data").get<OutboundGroupSessionData>(); + ref.session = unpickle<OutboundSessionObject>(obj.at("session"), SECRET); + return ref; + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); + return {}; + } } // @@ -537,56 +558,6 @@ Cache::saveOlmAccount(const std::string &data) txn.commit(); } -void -Cache::restoreSessions() -{ - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - std::string key, value; - - // - // Inbound Megolm Sessions - // - { - auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); - while (cursor.get(key, value, MDB_NEXT)) { - auto session = unpickle<InboundSessionObject>(value, SECRET); - session_storage.group_inbound_sessions[key] = std::move(session); - } - cursor.close(); - } - - // - // Outbound Megolm Sessions - // - { - auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_); - while (cursor.get(key, value, MDB_NEXT)) { - json obj; - - try { - obj = json::parse(value); - - session_storage.group_outbound_session_data[key] = - obj.at("data").get<OutboundGroupSessionData>(); - - auto session = - unpickle<OutboundSessionObject>(obj.at("session"), SECRET); - session_storage.group_outbound_sessions[key] = std::move(session); - } catch (const nlohmann::json::exception &e) { - nhlog::db()->critical( - "failed to parse outbound megolm session data: {}", e.what()); - } - } - cursor.close(); - } - - txn.commit(); - - nhlog::db()->info("sessions restored"); -} - std::string Cache::restoreOlmAccount() { @@ -3125,6 +3096,39 @@ Cache::roomMembers(const std::string &room_id) return members; } +std::map<std::string, std::optional<UserKeyCache>> +Cache::getMembersWithKeys(const std::string &room_id) +{ + lmdb::val keys; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::map<std::string, std::optional<UserKeyCache>> members; + + auto db = getMembersDb(txn, room_id); + auto keysDb = getUserKeysDb(txn); + + std::string user_id, unused; + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) { + auto res = lmdb::dbi_get(txn, keysDb, lmdb::val(user_id), keys); + + if (res) { + members[user_id] = + json::parse(std::string_view(keys.data(), keys.size())) + .get<UserKeyCache>(); + } else { + members[user_id] = {}; + } + } + cursor.close(); + + return members; + } catch (std::exception &) { + return {}; + } +} + QString Cache::displayName(const QString &room_id, const QString &user_id) { @@ -3265,6 +3269,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query updates[user].self_signing_keys = keys; for (auto &[user, update] : updates) { + nhlog::db()->debug("Updated user keys: {}", user); + lmdb::val oldKeys; auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); @@ -3327,6 +3333,8 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn, query.token = sync_token; for (const auto &user : user_ids) { + nhlog::db()->debug("Marking user keys out of date: {}", user); + lmdb::val oldKeys; auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); @@ -3681,11 +3689,40 @@ from_json(const json &j, MemberInfo &info) } void +to_json(nlohmann::json &obj, const DeviceAndMasterKeys &msg) +{ + obj["devices"] = msg.devices; + obj["master_keys"] = msg.master_keys; +} + +void +from_json(const nlohmann::json &obj, DeviceAndMasterKeys &msg) +{ + msg.devices = obj.at("devices").get<decltype(msg.devices)>(); + msg.master_keys = obj.at("master_keys").get<decltype(msg.master_keys)>(); +} + +void +to_json(nlohmann::json &obj, const SharedWithUsers &msg) +{ + obj["keys"] = msg.keys; +} + +void +from_json(const nlohmann::json &obj, SharedWithUsers &msg) +{ + msg.keys = obj.at("keys").get<std::map<std::string, DeviceAndMasterKeys>>(); +} + +void to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) { obj["session_id"] = msg.session_id; obj["session_key"] = msg.session_key; obj["message_index"] = msg.message_index; + + obj["initially"] = msg.initially; + obj["currently"] = msg.currently; } void @@ -3694,6 +3731,9 @@ from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) msg.session_id = obj.at("session_id"); msg.session_key = obj.at("session_key"); msg.message_index = obj.at("message_index"); + + msg.initially = obj.value("initially", SharedWithUsers{}); + msg.currently = obj.value("currently", SharedWithUsers{}); } void @@ -4128,9 +4168,9 @@ isRoomMember(const std::string &user_id, const std::string &room_id) void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session) + mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->saveOutboundMegolmSession(room_id, data, std::move(session)); + instance_->saveOutboundMegolmSession(room_id, data, session); } OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id) @@ -4143,9 +4183,11 @@ outboundMegolmSessionExists(const std::string &room_id) noexcept return instance_->outboundMegolmSessionExists(room_id); } void -updateOutboundMegolmSession(const std::string &room_id, int message_index) +updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->updateOutboundMegolmSession(room_id, message_index); + instance_->updateOutboundMegolmSession(room_id, data, session); } void dropOutboundMegolmSession(const std::string &room_id) @@ -4173,7 +4215,7 @@ saveInboundMegolmSession(const MegolmSessionIndex &index, { instance_->saveInboundMegolmSession(index, std::move(session)); } -OlmInboundGroupSession * +mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index) { return instance_->getInboundMegolmSession(index); @@ -4220,10 +4262,4 @@ restoreOlmAccount() { return instance_->restoreOlmAccount(); } - -void -restoreSessions() -{ - return instance_->restoreSessions(); -} } // namespace cache diff --git a/src/Cache.h b/src/Cache.h
index 8cbb0006..f38f1960 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -235,13 +235,15 @@ isRoomMember(const std::string &user_id, const std::string &room_id); void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); + mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void -updateOutboundMegolmSession(const std::string &room_id, int message_index); +updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); @@ -256,7 +258,7 @@ exportSessionKeys(); void saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session); -OlmInboundGroupSession * +mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); @@ -277,9 +279,7 @@ getLatestOlmSession(const std::string &curve25519); void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); - -void -restoreSessions(); } diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 6256dcf9..eb2cc445 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h
@@ -6,12 +6,28 @@ #include <mtx/responses/crypto.hpp> #include <mtxclient/crypto/objects.hpp> +struct DeviceAndMasterKeys +{ + // map from device id or master key id to message_index + std::map<std::string, uint64_t> devices, master_keys; +}; + +struct SharedWithUsers +{ + // userid to keys + std::map<std::string, DeviceAndMasterKeys> keys; +}; + // Extra information associated with an outbound megolm session. struct OutboundGroupSessionData { std::string session_id; std::string session_key; uint64_t message_index = 0; + + // who has access to this session. + // Rotate, when a user leaves the room and share, when a user gets added. + SharedWithUsers initially, currently; }; void @@ -21,7 +37,7 @@ from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg); struct OutboundGroupSessionDataRef { - OlmOutboundGroupSession *session; + mtx::crypto::OutboundGroupSessionPtr session; OutboundGroupSessionData data; }; @@ -52,18 +68,6 @@ to_json(nlohmann::json &obj, const MegolmSessionIndex &msg); void from_json(const nlohmann::json &obj, MegolmSessionIndex &msg); -struct OlmSessionStorage -{ - // Megolm sessions - std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; - std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; - std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; - - // Guards for accessing megolm sessions. - std::mutex group_outbound_mtx; - std::mutex group_inbound_mtx; -}; - struct StoredOlmSession { std::uint64_t last_message_ts = 0; diff --git a/src/Cache_p.h b/src/Cache_p.h
index 9c919fb5..fab2d964 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -59,6 +59,8 @@ public: // user cache stores user keys std::optional<UserKeyCache> userKeys(const std::string &user_id); + std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys( + const std::string &room_id); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, @@ -232,10 +234,12 @@ public: // void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); + mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; - void updateOutboundMegolmSession(const std::string &room_id, int message_index); + void updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); @@ -246,7 +250,8 @@ public: // void saveInboundMegolmSession(const MegolmSessionIndex &index, mtx::crypto::InboundGroupSessionPtr session); - OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( + const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); // @@ -264,8 +269,6 @@ public: void saveOlmAccount(const std::string &pickled); std::string restoreOlmAccount(); - void restoreSessions(); - signals: void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void roomReadStatus(const std::map<QString, bool> &status); @@ -577,7 +580,6 @@ private: QString localUserId_; QString cacheDirectory_; - OlmSessionStorage session_storage; VerificationStorage verification_storage; }; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index cb5f242f..dab414a9 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -526,7 +526,6 @@ ChatPage::loadStateFromCache() nhlog::db()->info("restoring state from cache"); try { - cache::restoreSessions(); olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); emit initializeEmptyViews(cache::client()->roomIds()); diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index ac625db1..8076d6d6 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp
@@ -90,6 +90,7 @@ LoginPage::LoginPage(QWidget *parent) matrixid_input_ = new TextField(this); matrixid_input_->setLabel(tr("Matrix ID")); + matrixid_input_->setRegexp(QRegularExpression("@.+?:.{3,}")); matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); matrixid_input_->setToolTip( tr("Your login name. A mxid should start with @ followed by the user id. After the user " @@ -175,7 +176,6 @@ LoginPage::LoginPage(QWidget *parent) connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk); connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError); - connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError); connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); @@ -186,32 +186,24 @@ LoginPage::LoginPage(QWidget *parent) connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); } - void -LoginPage::loginError(const QString &msg) +LoginPage::showError(const QString &msg) { auto rect = QFontMetrics(font()).boundingRect(msg); int width = rect.width(); int height = rect.height(); - error_label_->setFixedHeight(qCeil(width / 200) * height); + error_label_->setFixedHeight((int)qCeil(width / 200.0) * height); error_label_->setText(msg); } void -LoginPage::matrixIdError(const QString &msg) -{ - error_matrixid_label_->show(); - error_matrixid_label_->setText(msg); - matrixid_input_->setValid(false); -} - -bool -LoginPage::isMatrixIdValid() +LoginPage::showError(QLabel *label, const QString &msg) { - QRegularExpressionValidator v(QRegularExpression("@.+?:.{3,}"), this); - QString s = matrixid_input_->text(); - int pos = 0; - return v.validate(s, pos) == QValidator::Acceptable; + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); } void @@ -221,19 +213,21 @@ LoginPage::onMatrixIdEntered() User user; - if (!isMatrixIdValid()) { - matrixIdError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + "You have entered an invalid Matrix ID e.g @joe:matrix.org"); return; } else { error_matrixid_label_->setText(""); error_matrixid_label_->hide(); - matrixid_input_->setValid(true); } try { user = parse<User>(matrixid_input_->text().toStdString()); } catch (const std::exception &e) { - matrixIdError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + showError(error_matrixid_label_, + "You have entered an invalid Matrix ID e.g @joe:matrix.org"); return; } @@ -345,7 +339,7 @@ LoginPage::onServerAddressEntered() void LoginPage::versionError(const QString &error) { - loginError(error); + showError(error_label_, error); serverInput_->show(); spinner_->stop(); @@ -383,25 +377,27 @@ LoginPage::onLoginButtonClicked() User user; - if (!isMatrixIdValid()) { - matrixIdError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + if (!matrixid_input_->isValid()) { + error_matrixid_label_->show(); + showError(error_matrixid_label_, + "You have entered an invalid Matrix ID e.g @joe:matrix.org"); return; } else { error_matrixid_label_->setText(""); error_matrixid_label_->hide(); - matrixid_input_->setValid(true); } try { user = parse<User>(matrixid_input_->text().toStdString()); } catch (const std::exception &e) { - matrixIdError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + showError(error_matrixid_label_, + "You have entered an invalid Matrix ID e.g @joe:matrix.org"); return; } if (loginMethod == LoginMethod::Password) { if (password_input_->text().isEmpty()) - return loginError(tr("Empty password")); + return showError(error_label_, tr("Empty password")); http::client()->login( user.localpart(), @@ -410,7 +406,8 @@ LoginPage::onLoginButtonClicked() : deviceName_->text().toStdString(), [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { if (err) { - emit loginError(QString::fromStdString(err->matrix_error.error)); + showError(error_label_, + QString::fromStdString(err->matrix_error.error)); emit errorOccurred(); return; } @@ -435,7 +432,8 @@ LoginPage::onLoginButtonClicked() http::client()->login( req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { if (err) { - emit loginError( + showError( + error_label_, QString::fromStdString(err->matrix_error.error)); emit errorOccurred(); return; @@ -453,7 +451,7 @@ LoginPage::onLoginButtonClicked() sso->deleteLater(); }); connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() { - emit loginError(tr("SSO login failed")); + showError(error_label_, tr("SSO login failed")); emit errorOccurred(); sso->deleteLater(); }); diff --git a/src/LoginPage.h b/src/LoginPage.h
index 92b60afe..5ed21dec 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h
@@ -56,7 +56,6 @@ signals: //! Used to trigger the corresponding slot outside of the main thread. void versionErrorCb(const QString &err); - void loginErrorCb(const QString &err); void versionOkCb(LoginPage::LoginMethod method); void loginOk(const mtx::responses::Login &res); @@ -66,8 +65,8 @@ protected: public slots: // Displays errors produced during the login. - void loginError(const QString &msg); - void matrixIdError(const QString &msg); + void showError(const QString &msg); + void showError(QLabel *label, const QString &msg); private slots: // Callback for the back button. @@ -88,7 +87,6 @@ private slots: void versionOk(LoginPage::LoginMethod method); private: - bool isMatrixIdValid(); void checkHomeserverVersion(); std::string initialDeviceName() { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 37b54151..60b5168b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -108,7 +108,7 @@ MainWindow::MainWindow(const QString profile, QWidget *parent) 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); + login_page_->showError(msg); showLoginPage(); }); diff --git a/src/Olm.cpp b/src/Olm.cpp
index cdafabf3..808279a3 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -278,11 +278,168 @@ mtx::events::msg::Encrypted encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) { using namespace mtx::events; + using namespace mtx::identifiers; + + auto own_user_id = http::client()->user_id().to_string(); + + auto members = cache::client()->getMembersWithKeys(room_id); + + std::map<std::string, std::vector<std::string>> sendSessionTo; + mtx::crypto::OutboundGroupSessionPtr session = nullptr; + OutboundGroupSessionData group_session_data; + + if (cache::outboundMegolmSessionExists(room_id)) { + auto res = cache::getOutboundMegolmSession(room_id); + + auto member_it = members.begin(); + auto session_member_it = res.data.currently.keys.begin(); + auto session_member_it_end = res.data.currently.keys.end(); + + while (member_it != members.end() || session_member_it != session_member_it_end) { + if (member_it == members.end()) { + // a member left, purge session! + nhlog::crypto()->debug( + "Rotating megolm session because of left member"); + break; + } + + if (session_member_it == session_member_it_end) { + // share with all remaining members + while (member_it != members.end()) { + sendSessionTo[member_it->first] = {}; + + if (member_it->second) + for (const auto &dev : + member_it->second->device_keys) + if (member_it->first != own_user_id || + dev.first != device_id) + sendSessionTo[member_it->first] + .push_back(dev.first); + + ++member_it; + } + + session = std::move(res.session); + break; + } + + if (member_it->first > session_member_it->first) { + // a member left, purge session + nhlog::crypto()->debug( + "Rotating megolm session because of left member"); + break; + } else if (member_it->first < session_member_it->first) { + // new member, send them the session at this index + sendSessionTo[member_it->first] = {}; + + for (const auto &dev : member_it->second->device_keys) + if (member_it->first != own_user_id || + dev.first != device_id) + sendSessionTo[member_it->first].push_back( + dev.first); + + ++member_it; + } else { + // compare devices + bool device_removed = false; + for (const auto &dev : session_member_it->second.devices) { + if (!member_it->second || + !member_it->second->device_keys.count(dev.first)) { + device_removed = true; + break; + } + } + + if (device_removed) { + // device removed, rotate session! + nhlog::crypto()->debug( + "Rotating megolm session because of removed device of {}", + member_it->first); + break; + } + + // check for new devices to share with + if (member_it->second) + for (const auto &dev : member_it->second->device_keys) + if (!session_member_it->second.devices.count( + dev.first) && + (member_it->first != own_user_id || + dev.first != device_id)) + sendSessionTo[member_it->first].push_back( + dev.first); + + ++member_it; + ++session_member_it; + if (member_it == members.end() && + session_member_it == session_member_it_end) { + // all devices match or are newly added + session = std::move(res.session); + } + } + } + + group_session_data = std::move(res.data); + } + + if (!session) { + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(session.get()); + const auto session_key = mtx::crypto::session_key(session.get()); + + // Saving the new megolm session. + OutboundGroupSessionData session_data{}; + session_data.session_id = mtx::crypto::session_id(session.get()); + session_data.session_key = mtx::crypto::session_key(session.get()); + session_data.message_index = 0; + + sendSessionTo.clear(); + + for (const auto &[user, devices] : members) { + sendSessionTo[user] = {}; + session_data.initially.keys[user] = {}; + if (devices) { + for (const auto &[device_id_, key] : devices->device_keys) { + (void)key; + if (device_id != device_id_ || user != own_user_id) { + sendSessionTo[user].push_back(device_id_); + session_data.initially.keys[user] + .devices[device_id_] = 0; + } + } + } + } + + cache::saveOutboundMegolmSession(room_id, session_data, session); + group_session_data = std::move(session_data); + + { + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = session_id; + index.sender_key = olm::client()->identity_keys().curve25519; + auto megolm_session = + olm::client()->init_inbound_group_session(session_key); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); + } + } + + mtx::events::DeviceEvent<mtx::events::msg::RoomKey> megolm_payload{}; + megolm_payload.content.algorithm = MEGOLM_ALGO; + megolm_payload.content.room_id = room_id; + megolm_payload.content.session_id = mtx::crypto::session_id(session.get()); + megolm_payload.content.session_key = mtx::crypto::session_key(session.get()); + megolm_payload.type = mtx::events::EventType::RoomKey; + + if (!sendSessionTo.empty()) + olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - // relations shouldn't be encrypted... mtx::common::ReplyRelatesTo relation; mtx::common::RelatesTo r_relation; + // relations shouldn't be encrypted... if (body["content"].contains("m.relates_to") && body["content"]["m.relates_to"].contains("m.in_reply_to")) { relation = body["content"]["m.relates_to"]; @@ -292,25 +449,35 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, body["content"].erase("m.relates_to"); } - // Always check before for existence. - auto res = cache::getOutboundMegolmSession(room_id); - auto payload = olm::client()->encrypt_group_message(res.session, body.dump()); + auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); // Prepare the m.room.encrypted event. msg::Encrypted data; data.ciphertext = std::string((char *)payload.data(), payload.size()); data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = res.data.session_id; + data.session_id = mtx::crypto::session_id(session.get()); data.device_id = device_id; data.algorithm = MEGOLM_ALGO; data.relates_to = relation; data.r_relates_to = r_relation; - auto message_index = olm_outbound_group_session_message_index(res.session); - nhlog::crypto()->debug("next message_index {}", message_index); + group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); + nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); + + // update current set of members for the session with the new members and that message_index + for (const auto &[user, devices] : sendSessionTo) { + if (!group_session_data.currently.keys.count(user)) + group_session_data.currently.keys[user] = {}; + + for (const auto &device_id_ : devices) { + if (!group_session_data.currently.keys[user].devices.count(device_id_)) + group_session_data.currently.keys[user].devices[device_id_] = + group_session_data.message_index; + } + } // We need to re-pickle the session after we send a message to save the new message_index. - cache::updateOutboundMegolmSession(room_id, message_index); + cache::updateOutboundMegolmSession(room_id, group_session_data, session); return data; } @@ -534,7 +701,7 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR return; } - auto session_key = mtx::crypto::export_session(session); + auto session_key = mtx::crypto::export_session(session.get()); // // Prepare the m.room_key event. // @@ -584,8 +751,9 @@ decryptEvent(const MegolmSessionIndex &index, std::string msg_str; try { auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, event.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); + auto res = + olm::client()->decrypt_group_message(session.get(), event.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); } catch (const lmdb::error &e) { return {DecryptionErrorCode::DbError, e.what(), std::nullopt}; } catch (const mtx::crypto::olm_exception &e) { diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index b8fe93b5..26a66ab7 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp
@@ -20,6 +20,7 @@ #include <QPainter> #include <QStyleOption> #include <QTimer> +#include <QtMath> #include <mtx/responses/register.hpp> @@ -86,13 +87,13 @@ RegisterPage::RegisterPage(QWidget *parent) username_input_ = new TextField(); username_input_->setLabel(tr("Username")); - username_input_->setValidator( - new QRegularExpressionValidator(QRegularExpression("[a-z0-9._=/-]+"), this)); + username_input_->setRegexp(QRegularExpression("[a-z0-9._=/-]+")); username_input_->setToolTip(tr("The username must not be empty, and must contain only the " "characters a-z, 0-9, ., _, =, -, and /.")); password_input_ = new TextField(); password_input_->setLabel(tr("Password")); + password_input_->setRegexp(QRegularExpression("^.{8,}$")); password_input_->setEchoMode(QLineEdit::Password); password_input_->setToolTip(tr("Please choose a secure password. The exact requirements " "for password strength may depend on your server.")); @@ -107,19 +108,32 @@ RegisterPage::RegisterPage(QWidget *parent) tr("A server that allows registration. Since matrix is decentralized, you need to first " "find a server you can register on or host your own.")); + error_username_label_ = new QLabel(this); + error_username_label_->setWordWrap(true); + error_username_label_->hide(); + + error_password_label_ = new QLabel(this); + error_password_label_->setWordWrap(true); + error_password_label_->hide(); + + error_password_confirmation_label_ = new QLabel(this); + error_password_confirmation_label_->setWordWrap(true); + error_password_confirmation_label_->hide(); + form_layout_->addWidget(username_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_username_label_, Qt::AlignHCenter); form_layout_->addWidget(password_input_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_label_, Qt::AlignHCenter); form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter); + form_layout_->addWidget(error_password_confirmation_label_, Qt::AlignHCenter); form_layout_->addWidget(server_input_, Qt::AlignHCenter); button_layout_ = new QHBoxLayout(); button_layout_->setSpacing(0); button_layout_->setMargin(0); - QFont font; - error_label_ = new QLabel(this); - error_label_->setFont(font); + error_label_->setWordWrap(true); register_button_ = new RaisedButton(tr("REGISTER"), this); register_button_->setMinimumSize(350, 65); @@ -135,17 +149,24 @@ RegisterPage::RegisterPage(QWidget *parent) top_layout_->addLayout(form_wrapper_); top_layout_->addStretch(1); top_layout_->addLayout(button_layout_); - top_layout_->addStretch(1); top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect( + password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields); connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError); + connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields); + connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) { + showError(msg); + }); connect( this, &RegisterPage::registrationFlow, @@ -299,25 +320,93 @@ RegisterPage::onBackButtonClicked() } void -RegisterPage::registerError(const QString &msg) +RegisterPage::showError(const QString &msg) { emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + error_label_->setFixedHeight(qCeil(width / 200.0) * height); error_label_->setText(msg); } void -RegisterPage::onRegisterButtonClicked() +RegisterPage::showError(QLabel *label, const QString &msg) +{ + emit errorOccurred(); + auto rect = QFontMetrics(font()).boundingRect(msg); + int width = rect.width(); + int height = rect.height(); + label->setFixedHeight((int)qCeil(width / 200.0) * height); + label->setText(msg); +} + +bool +RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg) +{ + if (t_field->isValid()) { + label->setText(""); + label->hide(); + return true; + } else { + label->show(); + showError(label, msg); + return false; + } +} + +bool +RegisterPage::checkFields() { error_label_->setText(""); + error_username_label_->setText(""); + error_password_label_->setText(""); + error_password_confirmation_label_->setText(""); + + error_username_label_->hide(); + error_password_label_->hide(); + error_password_confirmation_label_->hide(); + + password_confirmation_->setValid(true); + server_input_->setValid(true); + + bool all_fields_good = true; + if (username_input_->isModified() && + !checkOneField(error_username_label_, + username_input_, + tr("The username must not be empty, and must contain only the " + "characters a-z, 0-9, ., _, =, -, and /."))) { + all_fields_good = false; + } else if (password_input_->isModified() && + !checkOneField(error_password_label_, + password_input_, + tr("Password is not long enough (min 8 chars)"))) { + all_fields_good = false; + } else if (password_confirmation_->isModified() && + password_input_->text() != password_confirmation_->text()) { + error_password_confirmation_label_->show(); + showError(error_password_confirmation_label_, tr("Passwords don't match")); + password_confirmation_->setValid(false); + all_fields_good = false; + } else if (server_input_->isModified() && + (!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) { + showError(tr("Invalid server name")); + server_input_->setValid(false); + all_fields_good = false; + } + if (!username_input_->isModified() || !password_input_->isModified() || + !password_confirmation_->isModified() || !server_input_->isModified()) { + all_fields_good = false; + } + return all_fields_good; +} - if (!username_input_->hasAcceptableInput()) { - registerError(tr("Invalid username")); - } else if (!password_input_->hasAcceptableInput()) { - registerError(tr("Password is not long enough (min 8 chars)")); - } else if (password_input_->text() != password_confirmation_->text()) { - registerError(tr("Passwords don't match")); - } else if (!server_input_->hasAcceptableInput()) { - registerError(tr("Invalid server name")); +void +RegisterPage::onRegisterButtonClicked() +{ + if (!checkFields()) { + showError(error_label_, tr("One or more fields have invalid inputs. Please correct those issues and try again.")); + return; } else { auto username = username_input_->text().toStdString(); auto password = password_input_->text().toStdString(); diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index 59ba3d1d..6d212955 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h
@@ -57,10 +57,13 @@ private slots: void onBackButtonClicked(); void onRegisterButtonClicked(); - // Display registration specific errors to the user. - void registerError(const QString &msg); + // function for showing different errors + void showError(const QString &msg); private: + bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg); + bool checkFields(); + void showError(QLabel *label, const QString &msg); QVBoxLayout *top_layout_; QHBoxLayout *back_layout_; @@ -69,6 +72,9 @@ private: QLabel *logo_; QLabel *error_label_; + QLabel *error_username_label_; + QLabel *error_password_label_; + QLabel *error_password_confirmation_label_; FlatButton *back_button_; RaisedButton *register_button_; diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp
index e4b0e1c6..22b73ac7 100644 --- a/src/WelcomePage.cpp +++ b/src/WelcomePage.cpp
@@ -37,8 +37,7 @@ WelcomePage::WelcomePage(QWidget *parent) QFont subTitleFont; subTitleFont.setPointSizeF(subTitleFont.pointSizeF() * 1.5); - QIcon icon; - icon.addFile(":/logos/splash.png"); + QIcon icon{QIcon::fromTheme("nheko", QIcon{":/logos/splash.png"})}; auto logo_ = new QLabel(this); logo_->setPixmap(icon.pixmap(256)); diff --git a/src/main.cpp b/src/main.cpp
index 6fbccf5c..5eeebb82 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -174,7 +174,7 @@ main(int argc, char *argv[]) parser.process(app); - app.setWindowIcon(QIcon(":/logos/nheko.png")); + app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"})); http::init(); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 1cb729d3..e561d099 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -604,8 +604,9 @@ EventStore::decryptEvent(const IdIndex &idx, std::string msg_str; try { auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); + auto res = + olm::client()->decrypt_group_message(session.get(), e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); } catch (const lmdb::error &e) { nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", index.room_id, diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 53791c98..11fa60c0 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -910,80 +910,16 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events:: {"room_id", room_id}}; try { - // Check if we have already an outbound megolm session then we can use. - if (cache::outboundMegolmSessionExists(room_id)) { - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - event.event_id = msg.event_id; - event.room_id = room_id; - event.sender = http::client()->user_id().to_string(); - event.type = mtx::events::EventType::RoomEncrypted; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - - emit this->addPendingMessageToStore(event); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - 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. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; - cache::saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - { - MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = session_id; - index.sender_key = olm::client()->identity_keys().curve25519; - auto megolm_session = - olm::client()->init_inbound_group_session(session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); - } - - const auto members = cache::roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - std::map<std::string, std::vector<std::string>> targets; - for (const auto &member : members) - targets[member] = {}; - - olm::send_encrypted_to_device_messages(targets, megolm_payload); - - try { - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - event.event_id = msg.event_id; - event.room_id = room_id; - event.sender = http::client()->user_id().to_string(); - event.type = mtx::events::EventType::RoomEncrypted; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - - emit this->addPendingMessageToStore(event); - } 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::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = + olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + event.event_id = msg.event_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + + emit this->addPendingMessageToStore(event); // TODO: Let the user know about the errors. } catch (const lmdb::error &e) { diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index d0356e15..03eb53fc 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -363,6 +363,9 @@ TimelineViewManager::toggleCameraView() void TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { + if (mxcUrl.isEmpty()) { + return; + } QQuickImageResponse *imgResponse = imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
index 941d00a3..055fe73b 100644 --- a/src/ui/TextField.cpp +++ b/src/ui/TextField.cpp
@@ -6,6 +6,7 @@ #include <QPaintEvent> #include <QPainter> #include <QPropertyAnimation> +#include <QRegularExpressionValidator> TextField::TextField(QWidget *parent) : QLineEdit(parent) @@ -70,18 +71,24 @@ TextField::hasLabel() const return show_label_; } -bool -TextField::isValid() const -{ - return is_valid_; -} - void TextField::setValid(bool valid) { is_valid_ = valid; } +bool +TextField::isValid() const +{ + QString s = text(); + int pos = 0; + if (regexp_.pattern().isEmpty()) { + return is_valid_; + } + QRegularExpressionValidator v(QRegularExpression(regexp_), 0); + return v.validate(s, pos) == QValidator::Acceptable; +} + void TextField::setLabelFontSize(qreal size) { @@ -156,6 +163,12 @@ TextField::setUnderlineColor(const QColor &color) update(); } +void +TextField::setRegexp(const QRegularExpression &regexp) +{ + regexp_ = regexp; +} + QColor TextField::underlineColor() const { diff --git a/src/ui/TextField.h b/src/ui/TextField.h
index 966155f4..01fd5782 100644 --- a/src/ui/TextField.h +++ b/src/ui/TextField.h
@@ -4,6 +4,7 @@ #include <QLineEdit> #include <QPaintEvent> #include <QPropertyAnimation> +#include <QRegularExpression> #include <QStateMachine> #include <QtGlobal> @@ -30,6 +31,7 @@ public: void setLabelFontSize(qreal size); void setShowLabel(bool value); void setUnderlineColor(const QColor &color); + void setRegexp(const QRegularExpression &regexp); void setValid(bool valid); QColor inkColor() const; @@ -56,6 +58,7 @@ private: TextFieldLabel *label_; TextFieldStateMachine *state_machine_; bool show_label_; + QRegularExpression regexp_; bool is_valid_; qreal label_font_size_; };