From 0b864d948586d6cc3ef3968376a5b932e06b793e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 16 Jul 2021 11:47:49 +0200 Subject: Fix replies not reloading after fetching them --- src/timeline/TimelineModel.cpp | 8 ++++++++ src/timeline/TimelineModel.h | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ab11f99b..5832f56e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -710,6 +710,14 @@ TimelineModel::data(const QModelIndex &index, int role) const return data(*event, role); } +QVariant +TimelineModel::dataById(QString id, int role, QString relatedTo) +{ + if (auto event = events.get(id.toStdString(), relatedTo.toStdString())) + return data(*event, role); + return QVariant(); +} + bool TimelineModel::canFetchMore(const QModelIndex &) const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index a3c973d6..b67234f2 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -215,10 +215,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; - Q_INVOKABLE QVariant dataById(QString id, int role) - { - return data(index(idToIndex(id)), role); - } + Q_INVOKABLE QVariant dataById(QString id, int role, QString relatedTo); bool canFetchMore(const QModelIndex &) const override; void fetchMore(const QModelIndex &) override; -- cgit 1.5.1 From 9fadd148715790743cb4e87bfe1854923e59c06b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 01:27:37 +0200 Subject: Store megolm session data in separate database --- src/Cache.cpp | 102 ++++++++++++++++++++++++----------- src/Cache.h | 7 +-- src/CacheCryptoStructs.h | 27 ++++++---- src/Cache_p.h | 8 +-- src/ChatPage.cpp | 10 ++-- src/Olm.cpp | 137 +++++++++++++++++++++++++++++++++++------------ src/Olm.h | 5 +- 7 files changed, 210 insertions(+), 86 deletions(-) (limited to 'src') diff --git a/src/Cache.cpp b/src/Cache.cpp index 9304db0e..1c156104 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -78,6 +78,8 @@ constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); //! MegolmSessionIndex -> pickled OlmOutboundGroupSession constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); +//! MegolmSessionIndex -> session data about which devices have access to this +constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db"); using CachedReceipts = std::multimap>; using Receipts = std::map>; @@ -284,6 +286,7 @@ Cache::setup() // Session management inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE); txn.commit(); @@ -387,9 +390,14 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) index.session_id = s.session_id; index.sender_key = s.sender_key; + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain; + if (s.sender_claimed_keys.count("ed25519")) + data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519"); + auto exported_session = mtx::crypto::import_session(s.session_key); - saveInboundMegolmSession(index, std::move(exported_session)); + saveInboundMegolmSession(index, std::move(exported_session), data); ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id); } } @@ -400,7 +408,8 @@ Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) void Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session) + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data) { using namespace mtx::crypto; const auto key = json(index).dump(); @@ -420,6 +429,7 @@ Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, } inboundMegolmSessionDb_.put(txn, key, pickled); + megolmSessionDataDb_.put(txn, key, json(data).dump()); txn.commit(); } @@ -464,7 +474,7 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) void Cache::updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data_, + const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &ptr) { using namespace mtx::crypto; @@ -472,18 +482,20 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, if (!outboundMegolmSessionExists(room_id)) return; - 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()); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(ptr.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ptr.get()); // Save the updated pickled data for the session. json j; - j["data"] = data; j["session"] = pickle(ptr.get(), SECRET); auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); txn.commit(); } @@ -498,24 +510,32 @@ Cache::dropOutboundMegolmSession(const std::string &room_id) { auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.del(txn, room_id); + // don't delete session data, so that we can still share the session. txn.commit(); } } void Cache::saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &session) { using namespace mtx::crypto; const auto pickled = pickle(session.get(), SECRET); + GroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(session.get()); + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(session.get()); + json j; - j["data"] = data; j["session"] = pickled; auto txn = lmdb::txn::begin(env_); outboundMegolmSessionDb_.put(txn, room_id, j.dump()); + megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump()); txn.commit(); } @@ -544,8 +564,17 @@ Cache::getOutboundMegolmSession(const std::string &room_id) auto obj = json::parse(value); OutboundGroupSessionDataRef ref{}; - ref.data = obj.at("data").get(); ref.session = unpickle(obj.at("session"), SECRET); + + MegolmSessionIndex index; + index.room_id = room_id; + index.sender_key = olm::client()->identity_keys().ed25519; + index.session_id = mtx::crypto::session_id(ref.session.get()); + + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + ref.data = nlohmann::json::parse(value).get(); + } + return ref; } catch (std::exception &e) { nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what()); @@ -829,6 +858,7 @@ Cache::deleteData() lmdb::dbi_close(env_, inboundMegolmSessionDb_); lmdb::dbi_close(env_, outboundMegolmSessionDb_); + lmdb::dbi_close(env_, megolmSessionDataDb_); env_.close(); @@ -3525,6 +3555,7 @@ to_json(json &j, const UserKeyCache &info) { j["device_keys"] = info.device_keys; j["seen_device_keys"] = info.seen_device_keys; + j["seen_device_ids"] = info.seen_device_ids; j["master_keys"] = info.master_keys; j["master_key_changed"] = info.master_key_changed; j["user_signing_keys"] = info.user_signing_keys; @@ -3538,6 +3569,7 @@ from_json(const json &j, UserKeyCache &info) { info.device_keys = j.value("device_keys", std::map{}); info.seen_device_keys = j.value("seen_device_keys", std::set{}); + info.seen_device_ids = j.value("seen_device_ids", std::set{}); info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); info.master_key_changed = j.value("master_key_changed", false); info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); @@ -3634,6 +3666,15 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query keyReused = true; break; } + if (updateToWrite.seen_device_ids.count( + device_id)) { + nhlog::crypto()->warn( + "device_id '{}' reused by ({})", + device_id, + user); + keyReused = true; + break; + } } if (!keyReused && !oldDeviceKeys.count(device_id)) @@ -3644,6 +3685,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query (void)key_id; updateToWrite.seen_device_keys.insert(key); } + updateToWrite.seen_device_ids.insert(device_id); } } db.put(txn, user, json(updateToWrite).dump()); @@ -4077,17 +4119,15 @@ from_json(const json &j, MemberInfo &info) } void -to_json(nlohmann::json &obj, const DeviceAndMasterKeys &msg) +to_json(nlohmann::json &obj, const DeviceKeysToMsgIndex &msg) { - obj["devices"] = msg.devices; - obj["master_keys"] = msg.master_keys; + obj["deviceids"] = msg.deviceids; } void -from_json(const nlohmann::json &obj, DeviceAndMasterKeys &msg) +from_json(const nlohmann::json &obj, DeviceKeysToMsgIndex &msg) { - msg.devices = obj.at("devices").get(); - msg.master_keys = obj.at("master_keys").get(); + msg.deviceids = obj.at("deviceids").get(); } void @@ -4099,30 +4139,31 @@ to_json(nlohmann::json &obj, const SharedWithUsers &msg) void from_json(const nlohmann::json &obj, SharedWithUsers &msg) { - msg.keys = obj.at("keys").get>(); + msg.keys = obj.at("keys").get>(); } void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +to_json(nlohmann::json &obj, const GroupSessionData &msg) { - obj["session_id"] = msg.session_id; - obj["session_key"] = msg.session_key; obj["message_index"] = msg.message_index; obj["ts"] = msg.timestamp; - obj["initially"] = msg.initially; + obj["sender_claimed_ed25519_key"] = msg.sender_claimed_ed25519_key; + obj["forwarding_curve25519_key_chain"] = msg.forwarding_curve25519_key_chain; + obj["currently"] = msg.currently; } void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +from_json(const nlohmann::json &obj, GroupSessionData &msg) { - msg.session_id = obj.at("session_id"); - msg.session_key = obj.at("session_key"); msg.message_index = obj.at("message_index"); msg.timestamp = obj.value("ts", 0ULL); - msg.initially = obj.value("initially", SharedWithUsers{}); + msg.sender_claimed_ed25519_key = obj.value("sender_claimed_ed25519_key", ""); + msg.forwarding_curve25519_key_chain = + obj.value("forwarding_curve25519_key_chain", std::vector{}); + msg.currently = obj.value("currently", SharedWithUsers{}); } @@ -4522,7 +4563,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id) // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->saveOutboundMegolmSession(room_id, data, session); @@ -4539,7 +4580,7 @@ outboundMegolmSessionExists(const std::string &room_id) noexcept } void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { instance_->updateOutboundMegolmSession(room_id, data, session); @@ -4566,9 +4607,10 @@ exportSessionKeys() // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session) + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data) { - instance_->saveInboundMegolmSession(index, std::move(session)); + instance_->saveInboundMegolmSession(index, std::move(session), data); } mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index) diff --git a/src/Cache.h b/src/Cache.h index b0520f6b..2b547876 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -200,7 +200,7 @@ isRoomMember(const std::string &user_id, const std::string &room_id); // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); @@ -208,7 +208,7 @@ bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); @@ -223,7 +223,8 @@ exportSessionKeys(); // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data); mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index); bool diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 07ca274e..409c9d67 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -27,40 +27,43 @@ enum Trust Q_ENUM_NS(Trust) } -struct DeviceAndMasterKeys +struct DeviceKeysToMsgIndex { - // map from device id or master key id to message_index - std::map devices, master_keys; + // map from device key to message_index + // Using the device id is safe because we check for reuse on device list updates + // Using the device id makes our logic much easier to read. + std::map deviceids; }; struct SharedWithUsers { // userid to keys - std::map keys; + std::map keys; }; // Extra information associated with an outbound megolm session. -struct OutboundGroupSessionData +struct GroupSessionData { - std::string session_id; - std::string session_key; uint64_t message_index = 0; uint64_t timestamp = 0; + std::string sender_claimed_ed25519_key; + std::vector forwarding_curve25519_key_chain; + // who has access to this session. // Rotate, when a user leaves the room and share, when a user gets added. - SharedWithUsers initially, currently; + SharedWithUsers currently; }; void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg); +to_json(nlohmann::json &obj, const GroupSessionData &msg); void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg); +from_json(const nlohmann::json &obj, GroupSessionData &msg); struct OutboundGroupSessionDataRef { mtx::crypto::OutboundGroupSessionPtr session; - OutboundGroupSessionData data; + GroupSessionData data; }; struct DevicePublicKeys @@ -134,6 +137,8 @@ struct UserKeyCache bool master_key_changed = false; //! Device keys that were already used at least once std::set seen_device_keys; + //! Device ids that were already used at least once + std::set seen_device_ids; }; void diff --git a/src/Cache_p.h b/src/Cache_p.h index c76cc717..6f208925 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -238,12 +238,12 @@ public: // Outbound Megolm Sessions // void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, + const GroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); @@ -254,7 +254,8 @@ public: // Inbound Megolm Sessions // void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); + mtx::crypto::InboundGroupSessionPtr session, + const GroupSessionData &data); mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); @@ -676,6 +677,7 @@ private: lmdb::dbi inboundMegolmSessionDb_; lmdb::dbi outboundMegolmSessionDb_; + lmdb::dbi megolmSessionDataDb_; QString localUserId_; QString cacheDirectory_; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 6003eb85..10a91557 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -939,12 +939,16 @@ ChatPage::ensureOneTimeKeyCount(const std::map &counts) [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { if (err) { nhlog::crypto()->warn( - "failed to update one-time keys: {} {}", + "failed to update one-time keys: {} {} {}", err->matrix_error.error, - static_cast(err->status_code)); - return; + static_cast(err->status_code), + static_cast(err->error_code)); + + if (err->status_code < 400 || err->status_code >= 500) + return; } + // mark as published anyway, otherwise we may end up in a loop. olm::mark_keys_as_published(); }); } diff --git a/src/Olm.cpp b/src/Olm.cpp index ff4c883b..d9447031 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -123,7 +123,17 @@ handle_to_device_messages(const std::vectorquery_keys( + olm_msg.sender, + [olm_msg](const UserKeyCache &userKeys, mtx::http::RequestErr e) { + if (e) { + nhlog::crypto()->error( + "Failed to query user keys, dropping olm " + "message"); + return; + } + handle_olm_message(std::move(olm_msg), userKeys); + }); } catch (const nlohmann::json::exception &e) { nhlog::crypto()->warn( "parsing error for olm message: {} {}", e.what(), j_msg.dump(2)); @@ -197,7 +207,7 @@ handle_to_device_messages(const std::vectorinfo("sender : {}", msg.sender); nhlog::crypto()->info("sender_key: {}", msg.sender_key); @@ -209,7 +219,7 @@ handle_olm_message(const OlmMessage &msg) if (cipher.first != my_key) { nhlog::crypto()->debug( "Skipping message for {} since we are {}.", cipher.first, my_key); - continue; + return; } const auto type = cipher.second.type; @@ -231,6 +241,57 @@ handle_olm_message(const OlmMessage &msg) if (!payload.is_null()) { mtx::events::collections::DeviceEvents device_event; + // Other properties are included in order to prevent an attacker from + // publishing someone else's curve25519 keys as their own and subsequently + // claiming to have sent messages which they didn't. sender must correspond + // to the user who sent the event, recipient to the local user, and + // recipient_keys to the local ed25519 key. + std::string receiver_ed25519 = payload["recipient_keys"]["ed25519"]; + if (receiver_ed25519.empty() || + receiver_ed25519 != olm::client()->identity_keys().ed25519) { + nhlog::crypto()->warn( + "Decrypted event doesn't include our ed25519: {}", + payload.dump()); + return; + } + std::string receiver = payload["recipient"]; + if (receiver.empty() || receiver != http::client()->user_id().to_string()) { + nhlog::crypto()->warn( + "Decrypted event doesn't include our user_id: {}", + payload.dump()); + return; + } + + // Clients must confirm that the sender_key and the ed25519 field value + // under the keys property match the keys returned by /keys/query for the + // given user, and must also verify the signature of the payload. Without + // this check, a client cannot be sure that the sender device owns the + // private part of the ed25519 key it claims to have in the Olm payload. + // This is crucial when the ed25519 key corresponds to a verified device. + std::string sender_ed25519 = payload["keys"]["ed25519"]; + if (sender_ed25519.empty()) { + nhlog::crypto()->warn( + "Decrypted event doesn't include sender ed25519: {}", + payload.dump()); + return; + } + + bool from_their_device = false; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + if (key.keys.at("curve25519:" + device_id) == msg.sender_key) { + if (key.keys.at("ed25519:" + device_id) == sender_ed25519) { + from_their_device = true; + break; + } + } + } + if (!from_their_device) { + nhlog::crypto()->warn("Decrypted event isn't sent from a device " + "listed by that user! {}", + payload.dump()); + return; + } + { std::string msg_type = payload["type"]; json event_array = json::array(); @@ -242,7 +303,7 @@ handle_olm_message(const OlmMessage &msg) if (temp_events.empty()) { nhlog::crypto()->warn("Decrypted unknown event: {}", payload.dump()); - continue; + return; } device_event = temp_events.at(0); } @@ -276,17 +337,20 @@ handle_olm_message(const OlmMessage &msg) ChatPage::instance()->receivedDeviceVerificationDone(e8->content); } else if (auto roomKey = std::get_if>(&device_event)) { - create_inbound_megolm_session(*roomKey, msg.sender_key); + create_inbound_megolm_session( + *roomKey, msg.sender_key, sender_ed25519); } else if (auto forwardedRoomKey = std::get_if>( &device_event)) { + forwardedRoomKey->content.forwarding_curve25519_key_chain.push_back( + msg.sender_key); import_inbound_megolm_session(*forwardedRoomKey); } else if (auto e = std::get_if>(&device_event)) { auto local_user = http::client()->user_id(); if (msg.sender != local_user.to_string()) - continue; + return; auto secret_name = request_id_to_secret_name.find(e->content.request_id); @@ -306,7 +370,7 @@ handle_olm_message(const OlmMessage &msg) cache::verificationStatus(local_user.to_string()); if (!verificationStatus) - continue; + return; auto deviceKeys = cache::userKeys(local_user.to_string()); std::string sender_device_id; @@ -344,7 +408,6 @@ handle_olm_message(const OlmMessage &msg) "for secrect " "'{}'", name); - return; } }); @@ -364,13 +427,8 @@ handle_olm_message(const OlmMessage &msg) } try { - auto otherUserDeviceKeys = cache::userKeys(msg.sender); - - if (!otherUserDeviceKeys) - return; - std::map> targets; - for (auto [device_id, key] : otherUserDeviceKeys->device_keys) { + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { if (key.keys.at("curve25519:" + device_id) == msg.sender_key) targets[msg.sender].push_back(device_id); } @@ -450,7 +508,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, std::map> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; - OutboundGroupSessionData group_session_data; + GroupSessionData group_session_data; if (cache::outboundMegolmSessionExists(room_id)) { auto res = cache::getOutboundMegolmSession(room_id); @@ -519,7 +577,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, } else { // compare devices bool device_removed = false; - for (const auto &dev : session_member_it->second.devices) { + for (const auto &dev : + session_member_it->second.deviceids) { if (!member_it->second || !member_it->second->device_keys.count( dev.first)) { @@ -541,7 +600,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, if (member_it->second) for (const auto &dev : member_it->second->device_keys) - if (!session_member_it->second.devices + if (!session_member_it->second.deviceids .count(dev.first) && (member_it->first != own_user_id || dev.first != device_id)) @@ -571,9 +630,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, 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()); + GroupSessionData session_data{}; session_data.message_index = 0; session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); @@ -581,22 +638,19 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, for (const auto &[user, devices] : members) { sendSessionTo[user] = {}; - session_data.initially.keys[user] = {}; + session_data.currently.keys[user] = {}; if (devices) { for (const auto &[device_id_, key] : devices->device_keys) { (void)key; if (device_id != device_id_ || user != own_user_id) { sendSessionTo[user].push_back(device_id_); - session_data.initially.keys[user] - .devices[device_id_] = 0; + session_data.currently.keys[user] + .deviceids[device_id_] = 0; } } } } - cache::saveOutboundMegolmSession(room_id, session_data, session); - group_session_data = std::move(session_data); - { MegolmSessionIndex index; index.room_id = room_id; @@ -604,8 +658,12 @@ encrypt_group_message(const std::string &room_id, const std::string &device_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)); + cache::saveInboundMegolmSession( + index, std::move(megolm_session), session_data); } + + cache::saveOutboundMegolmSession(room_id, session_data, session); + group_session_data = std::move(session_data); } mtx::events::DeviceEvent megolm_payload{}; @@ -641,8 +699,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, 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_] = + if (!group_session_data.currently.keys[user].deviceids.count(device_id_)) + group_session_data.currently.keys[user].deviceids[device_id_] = group_session_data.message_index; } } @@ -704,7 +762,8 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip void create_inbound_megolm_session(const mtx::events::DeviceEvent &roomKey, - const std::string &sender_key) + const std::string &sender_key, + const std::string &sender_ed25519) { MegolmSessionIndex index; index.room_id = roomKey.content.room_id; @@ -712,9 +771,13 @@ create_inbound_megolm_session(const mtx::events::DeviceEventinit_inbound_group_session(roomKey.content.session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -741,7 +804,13 @@ import_inbound_megolm_session( try { auto megolm_session = olm::client()->import_inbound_group_session(roomKey.content.session_key); - cache::saveInboundMegolmSession(index, std::move(megolm_session)); + + GroupSessionData data{}; + data.forwarding_curve25519_key_chain = + roomKey.content.forwarding_curve25519_key_chain; + data.sender_claimed_ed25519_key = roomKey.content.sender_claimed_ed25519_key; + + cache::saveInboundMegolmSession(index, std::move(megolm_session), data); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -875,10 +944,10 @@ handle_key_request_message(const mtx::events::DeviceEvent &roomKey, - const std::string &sender_key); + const std::string &sender_key, + const std::string &sender_ed25519); void import_inbound_megolm_session( const mtx::events::DeviceEvent &roomKey); -- cgit 1.5.1 From 3f0aa13cb6f83c8768dc30b1a1872b9696992fa7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 02:14:44 +0200 Subject: Share historical keys We share all keys with our devices and ones created by us to other users. --- src/Cache.cpp | 24 ++++++++++++++++++++++++ src/Cache.h | 2 ++ src/Cache_p.h | 1 + src/Olm.cpp | 50 ++++++++++++++++++++++++++++---------------------- 4 files changed, 55 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/Cache.cpp b/src/Cache.cpp index 1c156104..7b6a6135 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -582,6 +582,25 @@ Cache::getOutboundMegolmSession(const std::string &room_id) } } +std::optional +Cache::getMegolmSessionData(const MegolmSessionIndex &index) +{ + try { + using namespace mtx::crypto; + + auto txn = ro_txn(env_); + + std::string_view value; + if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) { + return nlohmann::json::parse(value).get(); + } + + return std::nullopt; + } catch (std::exception &e) { + nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what()); + return std::nullopt; + } +} // // OLM sessions. // @@ -4622,6 +4641,11 @@ inboundMegolmSessionExists(const MegolmSessionIndex &index) { return instance_->inboundMegolmSessionExists(index); } +std::optional +getMegolmSessionData(const MegolmSessionIndex &index) +{ + return instance_->getMegolmSessionData(index); +} // // Olm Sessions diff --git a/src/Cache.h b/src/Cache.h index 2b547876..57a36d73 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -229,6 +229,8 @@ mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession(const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); +std::optional +getMegolmSessionData(const MegolmSessionIndex &index); // // Olm Sessions diff --git a/src/Cache_p.h b/src/Cache_p.h index 6f208925..d1f6307d 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -259,6 +259,7 @@ public: mtx::crypto::InboundGroupSessionPtr getInboundMegolmSession( const MegolmSessionIndex &index); bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + std::optional getMegolmSessionData(const MegolmSessionIndex &index); // // Olm Sessions diff --git a/src/Olm.cpp b/src/Olm.cpp index d9447031..18e2ddcf 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -631,8 +631,9 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, // Saving the new megolm session. GroupSessionData session_data{}; - session_data.message_index = 0; - session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); + session_data.message_index = 0; + session_data.timestamp = QDateTime::currentMSecsSinceEpoch(); + session_data.sender_claimed_ed25519_key = olm::client()->identity_keys().ed25519; sendSessionTo.clear(); @@ -886,30 +887,33 @@ handle_key_request_message(const mtx::events::DeviceEventidentity_keys().curve25519) { - nhlog::crypto()->debug("ignoring key request {} because we were not the sender: " - "\nrequested({}) ours({})", - req.content.request_id, - req.content.sender_key, - olm::client()->identity_keys().curve25519); + // Check if we were the sender of the session being requested (unless it is actually us + // requesting the session). + if (req.sender != http::client()->user_id().to_string() && + req.content.sender_key != olm::client()->identity_keys().curve25519) { + nhlog::crypto()->debug( + "ignoring key request {} because we did not create the requested session: " + "\nrequested({}) ours({})", + req.content.request_id, + req.content.sender_key, + olm::client()->identity_keys().curve25519); return; } + // Check that the requested session_id and the one we have saved match. + MegolmSessionIndex index{}; + index.room_id = req.content.room_id; + index.session_id = req.content.session_id; + index.sender_key = req.content.sender_key; + // Check if we have the keys for the requested session. - auto outboundSession = cache::getOutboundMegolmSession(req.content.room_id); - if (!outboundSession.session) { + auto sessionData = cache::getMegolmSessionData(index); + if (!sessionData) { 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. - 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); @@ -942,11 +946,11 @@ handle_key_request_message(const mtx::events::DeviceEventcurrently.keys.count(req.sender)) { + if (sessionData->currently.keys.at(req.sender) .deviceids.count(req.content.requesting_device_id)) { shouldSeeKeys = true; - minimumIndex = outboundSession.data.currently.keys.at(req.sender) + minimumIndex = sessionData->currently.keys.at(req.sender) .deviceids.at(req.content.requesting_device_id); } } @@ -976,8 +980,9 @@ handle_key_request_message(const mtx::events::DeviceEventidentity_keys().ed25519; - forward_key.forwarding_curve25519_key_chain = {}; + forward_key.sender_claimed_ed25519_key = sessionData->sender_claimed_ed25519_key; + forward_key.forwarding_curve25519_key_chain = + sessionData->forwarding_curve25519_key_chain; send_megolm_key_to_device( req.sender, req.content.requesting_device_id, forward_key); @@ -998,6 +1003,7 @@ send_megolm_key_to_device(const std::string &user_id, std::map> targets; targets[user_id] = {device_id}; send_encrypted_to_device_messages(targets, room_key); + nhlog::crypto()->debug("Forwarded key to {}:{}", user_id, device_id); } DecryptionResult -- cgit 1.5.1 From aa84cf24f6ad7008d481f1741ee6f04e851d1298 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 21:43:25 +0200 Subject: Disable unchecked key sharing to trusted users by default --- src/UserSettingsPage.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 740b8979..ffaebe61 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -91,7 +91,7 @@ UserSettings::load(std::optional profile) privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); shareKeysWithTrustedUsers_ = - settings.value("user/share_keys_with_trusted_users", true).toBool(); + settings.value("user/automatically_share_keys_with_trusted_users", false).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(); @@ -610,7 +610,8 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("share_keys_with_trusted_users", shareKeysWithTrustedUsers_); + settings.setValue("automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); -- cgit 1.5.1 From 8a1666bc889d963693b5dff8f0b4c7612319644a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 15 Jul 2021 20:37:52 +0200 Subject: Basic sticker support --- CMakeLists.txt | 2 + resources/icons/ui/sticky-note-solid.svg | 1 + resources/qml/MatrixText.qml | 8 +- resources/qml/MessageInput.qml | 29 +++++- resources/qml/MessageView.qml | 10 +- resources/qml/emoji/EmojiButton.qml | 23 ---- resources/qml/emoji/StickerPicker.qml | 174 +++++++++++++++++++++++++++++++ resources/res.qrc | 3 +- src/Cache.cpp | 7 ++ src/Cache_p.h | 6 ++ src/ImagePackModel.cpp | 91 ++++++++++++++++ src/ImagePackModel.h | 52 +++++++++ src/timeline/InputBar.cpp | 17 +++ src/timeline/InputBar.h | 2 + src/timeline/TimelineModel.cpp | 9 ++ src/timeline/TimelineModel.h | 15 ++- src/timeline/TimelineViewManager.cpp | 7 ++ 17 files changed, 419 insertions(+), 37 deletions(-) create mode 100644 resources/icons/ui/sticky-note-solid.svg delete mode 100644 resources/qml/emoji/EmojiButton.qml create mode 100644 resources/qml/emoji/StickerPicker.qml create mode 100644 src/ImagePackModel.cpp create mode 100644 src/ImagePackModel.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index 78900535..6b26b2e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -355,6 +355,7 @@ set(SRC_FILES src/Olm.cpp src/RegisterPage.cpp src/SSOHandler.cpp + src/ImagePackModel.cpp src/TrayIcon.cpp src/UserSettingsPage.cpp src/UsersModel.cpp @@ -559,6 +560,7 @@ qt5_wrap_cpp(MOC_HEADERS src/MxcImageProvider.h src/RegisterPage.h src/SSOHandler.h + src/ImagePackModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h diff --git a/resources/icons/ui/sticky-note-solid.svg b/resources/icons/ui/sticky-note-solid.svg new file mode 100644 index 00000000..bc36d474 --- /dev/null +++ b/resources/icons/ui/sticky-note-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 9129b154..35e5f7e7 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -8,6 +8,7 @@ import im.nheko 1.0 TextEdit { id: r + textFormat: TextEdit.RichText readOnly: true focus: false @@ -19,14 +20,13 @@ TextEdit { onLinkActivated: Nheko.openLink(link) ToolTip.visible: hoveredLink ToolTip.text: hoveredLink + Component.onCompleted: { + TimelineManager.fixImageRendering(r.textDocument, r); + } CursorShape { anchors.fill: parent cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } - Component.onCompleted: { - TimelineManager.fixImageRendering(r.textDocument, r) - } - } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 24f9b0e8..d4f7ca62 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "./emoji" import "./voip" import QtQuick 2.12 import QtQuick.Controls 2.3 @@ -87,7 +88,7 @@ Rectangle { Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter Layout.maximumHeight: Window.height / 4 Layout.minimumHeight: Settings.fontSize - implicitWidth: inputBar.width - 4 * (22 + 16) - 24 + implicitWidth: inputBar.width - 5 * (22 + 16) - 24 TextArea { id: messageInput @@ -319,6 +320,30 @@ Rectangle { } + ImageButton { + id: stickerButton + + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.margins: 8 + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/sticky-note-solid.svg" + ToolTip.visible: hovered + ToolTip.text: qsTr("Stickers") + onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, TimelineManager.completerFor("stickers", room.roomId()), function(row) { + room.input.sticker(stickerPopup.model.sourceModel, row); + TimelineManager.focusMessageInput(); + }) + + StickerPicker { + id: stickerPopup + + colors: Nheko.colors + } + + } + ImageButton { id: emojiButton @@ -330,7 +355,7 @@ Rectangle { image: ":/icons/icons/ui/smile.png" ToolTip.visible: hovered ToolTip.text: qsTr("Emoji") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(function(emoji) { messageInput.insert(messageInput.cursorPosition, emoji); TimelineManager.focusMessageInput(); }) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 33dff122..4e605ad7 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -92,16 +92,20 @@ ScrollView { } } - EmojiButton { + ImageButton { id: reactButton visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false width: 16 hoverEnabled: true + image: ":/icons/icons/ui/smile.png" ToolTip.visible: hovered ToolTip.text: qsTr("React") - emojiPicker: emojiPopup - event_id: row.model ? row.model.eventId : "" + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { + var event_id = row.model ? row.model.eventId : ""; + room.input.reaction(event_id, emoji); + TimelineManager.focusMessageInput(); + }) } ImageButton { diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml deleted file mode 100644 index 5f4d23d3..00000000 --- a/resources/qml/emoji/EmojiButton.qml +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -import "../" -import QtQuick 2.10 -import QtQuick.Controls 2.1 -import im.nheko 1.0 -import im.nheko.EmojiModel 1.0 - -ImageButton { - id: emojiButton - - property var colors: currentActivePalette - property var emojiPicker - property string event_id - - image: ":/icons/icons/ui/smile.png" - onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { - room.input.reaction(event_id, emoji); - TimelineManager.focusMessageInput(); - }) -} diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml new file mode 100644 index 00000000..3fe17ef2 --- /dev/null +++ b/resources/qml/emoji/StickerPicker.qml @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "../" +import QtGraphicalEffects 1.0 +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 +import im.nheko.EmojiModel 1.0 + +Menu { + id: stickerPopup + + property var callback + property var colors + property alias model: gridView.model + property var textArea + property real highlightHue: Nheko.colors.highlight.hslHue + property real highlightSat: Nheko.colors.highlight.hslSaturation + property real highlightLight: Nheko.colors.highlight.hslLightness + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + readonly property int stickersPerRow: 3 + + function show(showAt, model_, callback) { + console.debug("Showing sticker picker"); + model = model_; + stickerPopup.callback = callback; + popup(showAt ? showAt : null); + } + + margins: 0 + bottomPadding: 1 + leftPadding: 1 + rightPadding: 1 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + //height: columnView.implicitHeight + 4 + //width: columnView.implicitWidth + width: stickersPerRow * stickerDimPad + 20 + + Rectangle { + color: Nheko.colors.window + height: columnView.implicitHeight + 4 + width: stickersPerRow * stickerDimPad + 20 + + ColumnLayout { + id: columnView + + spacing: 0 + anchors.leftMargin: 3 + anchors.rightMargin: 3 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 2 + + // Search field + TextField { + id: emojiSearch + + Layout.topMargin: 3 + Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - 6 + palette: Nheko.colors + background: null + placeholderTextColor: Nheko.colors.buttonText + color: Nheko.colors.text + placeholderText: qsTr("Search") + selectByMouse: true + rightPadding: clearSearch.width + onTextChanged: searchTimer.restart() + onVisibleChanged: { + if (visible) + forceActiveFocus(); + + } + + Timer { + id: searchTimer + + interval: 350 // tweak as needed? + onTriggered: stickerPopup.model.searchString = emojiSearch.text + } + + ToolButton { + id: clearSearch + + visible: emojiSearch.text !== '' + icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText) + focusPolicy: Qt.NoFocus + onClicked: emojiSearch.clear() + hoverEnabled: true + background: null + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + // clear the default hover effects. + + Image { + height: parent.height - 2 * Nheko.paddingSmall + width: height + source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText) + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + margins: Nheko.paddingSmall + } + + } + + } + + } + + // emoji grid + GridView { + id: gridView + + Layout.preferredHeight: cellHeight * 3.5 + Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 + Layout.leftMargin: 4 + cellWidth: stickerDimPad + cellHeight: stickerDimPad + boundsBehavior: Flickable.StopAtBounds + clip: true + currentIndex: -1 // prevent sorting from stealing focus + cacheBuffer: 500 + + // Individual emoji + delegate: AbstractButton { + width: stickerDim + height: stickerDim + hoverEnabled: true + ToolTip.text: ":" + model.shortcode + ": - " + model.body + ToolTip.visible: hovered + // TODO: maybe add favorites at some point? + onClicked: { + console.debug("Picked " + model.shortcode); + stickerPopup.close(); + callback(model.originalRow); + } + + contentItem: Image { + height: stickerDim + width: stickerDim + source: model.url.replace("mxc://", "image://MxcImage/") + fillMode: Image.PreserveAspectFit + } + + background: Rectangle { + anchors.fill: parent + color: hovered ? Nheko.colors.highlight : 'transparent' + radius: 5 + } + + } + + ScrollBar.vertical: ScrollBar { + id: emojiScroll + } + + } + + } + + } + +} diff --git a/resources/res.qrc b/resources/res.qrc index f41835f9..e9479e57 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -26,6 +26,7 @@ icons/ui/search@2x.png icons/ui/settings.png icons/ui/settings@2x.png + icons/ui/sticky-note-solid.svg icons/ui/smile.png icons/ui/smile@2x.png icons/ui/speech-bubbles-comment-option.png @@ -150,8 +151,8 @@ qml/ForwardCompleter.qml qml/TypingIndicator.qml qml/RoomSettings.qml - qml/emoji/EmojiButton.qml qml/emoji/EmojiPicker.qml + qml/emoji/StickerPicker.qml qml/UserProfile.qml qml/delegates/MessageDelegate.qml qml/delegates/TextMessage.qml diff --git a/src/Cache.cpp b/src/Cache.cpp index 7b6a6135..8c3d8c42 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3382,6 +3382,13 @@ Cache::getChildRoomIds(const std::string &room_id) return roomids; } +std::optional +Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) +{ + auto txn = ro_txn(env_); + return getAccountData(txn, type, room_id); +} + std::optional Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { diff --git a/src/Cache_p.h b/src/Cache_p.h index d1f6307d..3752f5e4 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -88,6 +88,12 @@ public: //! Retrieve if the room is a space bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb); + //! retrieve a specific event from account data + //! pass empty room_id for global account data + std::optional getAccountData( + mtx::events::EventType type, + const std::string &room_id = ""); + //! Get a specific state event template std::optional> getStateEvent(const std::string &room_id, diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp new file mode 100644 index 00000000..fb2599a5 --- /dev/null +++ b/src/ImagePackModel.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ImagePackModel.h" + +#include "Cache_p.h" +#include "CompletionModelRoles.h" + +ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + auto accountpackV = + cache::client()->getAccountData(mtx::events::EventType::ImagePackInAccountData); + auto enabledRoomPacksV = + cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms); + + std::optional accountPack; + if (accountpackV) { + auto tmp = + std::get_if>( + &*accountpackV); + if (tmp) + accountPack = tmp->content; + } + // mtx::events::msc2545::ImagePackRooms *enabledRoomPacks = nullptr; + // if (enabledRoomPacksV) + // enabledRoomPacks = + // std::get_if(&*enabledRoomPacksV); + + if (accountPack && (!accountPack->pack || (stickers ? accountPack->pack->is_sticker() + : accountPack->pack->is_emoji()))) { + QString packname; + if (accountPack->pack) + packname = QString::fromStdString(accountPack->pack->display_name); + + for (const auto &img : accountPack->images) { + if (img.second.overrides_usage() && + (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) + continue; + + ImageDesc i{}; + i.shortcode = QString::fromStdString(img.first); + i.packname = packname; + i.image = img.second; + images.push_back(std::move(i)); + } + } +} + +QHash +ImagePackModel::roleNames() const +{ + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::Url, "url"}, + {Roles::ShortCode, "shortcode"}, + {Roles::Body, "body"}, + {Roles::PackName, "packname"}, + {Roles::OriginalRow, "originalRow"}, + }; +} + +QVariant +ImagePackModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + return QString::fromStdString(images[index.row()].image.url); + case Roles::Url: + return QString::fromStdString(images[index.row()].image.url); + case CompletionModel::SearchRole: + case Roles::ShortCode: + return images[index.row()].shortcode; + case CompletionModel::SearchRole2: + case Roles::Body: + return QString::fromStdString(images[index.row()].image.body); + case Roles::PackName: + return images[index.row()].packname; + case Roles::OriginalRow: + return index.row(); + default: + return {}; + } + } + return {}; +} diff --git a/src/ImagePackModel.h b/src/ImagePackModel.h new file mode 100644 index 00000000..10e71b8f --- /dev/null +++ b/src/ImagePackModel.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +class ImagePackModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + PackName, + OriginalRow, + }; + + ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)images.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + + mtx::events::msc2545::PackImage imageAt(int row) + { + if (row < 0 || static_cast(row) >= images.size()) + return {}; + return images.at(static_cast(row)).image; + } + +private: + std::string room_id; + + struct ImageDesc + { + QString shortcode; + QString packname; + + mtx::events::msc2545::PackImage image; + }; + + std::vector images; +}; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b0747a7c..0f210722 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -21,6 +21,7 @@ #include "ChatPage.h" #include "CompletionProxyModel.h" #include "Config.h" +#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -501,6 +502,22 @@ InputBar::video(const QString &filename, room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); } +void +InputBar::sticker(ImagePackModel *model, int row) +{ + if (!model || row < 0) + return; + + auto img = model->imageAt(row); + + mtx::events::msg::StickerImage sticker{}; + sticker.info = img.info.value_or(mtx::common::ImageInfo{}); + sticker.url = img.url; + sticker.body = img.body; + + room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); +} + void InputBar::command(QString command, QString args) { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index c9728379..acedceb7 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -12,6 +12,7 @@ #include class TimelineModel; +class ImagePackModel; class QMimeData; class QDropEvent; class QStringList; @@ -57,6 +58,7 @@ public slots: MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); void reaction(const QString &reactedEvent, const QString &reactionKey); + void sticker(ImagePackModel *model, int row); private slots: void startTyping(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 5832f56e..abfe28a9 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1300,6 +1300,14 @@ struct SendMessageVisitor sendRoomEvent(msg); } + void operator()(mtx::events::Sticker msg) + { + msg.type = mtx::events::EventType::Sticker; + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(msg, mtx::events::EventType::Sticker); + } else + emit model_->addPendingMessageToStore(msg); + } TimelineModel *model_; }; @@ -1309,6 +1317,7 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) { std::visit( [](auto &msg) { + // gets overwritten for reactions and stickers in SendMessageVisitor msg.type = mtx::events::EventType::RoomMessage; msg.event_id = "m" + http::client()->generate_txn_id(); msg.sender = http::client()->user_id().to_string(); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b67234f2..0e2895d4 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -410,10 +410,17 @@ template void TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventType) { - mtx::events::RoomEvent msgCopy = {}; - msgCopy.content = content; - msgCopy.type = eventType; - emit newMessageToSend(msgCopy); + if constexpr (std::is_same_v) { + mtx::events::Sticker msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } else { + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = content; + msgCopy.type = eventType; + emit newMessageToSend(msgCopy); + } resetReply(); resetEdit(); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index b39ef615..ec1b3573 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -19,6 +19,7 @@ #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" #include "EventAccessors.h" +#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -144,6 +145,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", @@ -593,6 +595,11 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) auto proxy = new CompletionProxyModel(roomModel); roomModel->setParent(proxy); return proxy; + } else if (completerName == "stickers") { + auto stickerModel = new ImagePackModel(roomId.toStdString(), true); + auto proxy = new CompletionProxyModel(stickerModel); + stickerModel->setParent(proxy); + return proxy; } return nullptr; } -- cgit 1.5.1 From 9d5ba4f681901a0ee241e542fa9be5e2b73f58db Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 03:02:30 +0200 Subject: Move sticker parsing and enable room stickers --- resources/qml/emoji/StickerPicker.qml | 2 -- src/Cache.cpp | 68 +++++++++++++++++++++++++++++++++-- src/CacheStructs.h | 7 ++++ src/Cache_p.h | 8 ++--- src/ImagePackModel.cpp | 31 +++------------- 5 files changed, 78 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index 3fe17ef2..ae1695df 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -38,8 +38,6 @@ Menu { modal: true focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - //height: columnView.implicitHeight + 4 - //width: columnView.implicitWidth width: stickersPerRow * stickerDimPad + 20 Rectangle { diff --git a/src/Cache.cpp b/src/Cache.cpp index 8c3d8c42..4321393c 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3382,11 +3382,73 @@ Cache::getChildRoomIds(const std::string &room_id) return roomids; } -std::optional -Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) +std::vector +Cache::getImagePacks(const std::string &room_id, bool stickers) { auto txn = ro_txn(env_); - return getAccountData(txn, type, room_id); + std::vector infos; + + auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) { + if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) { + ImagePackInfo info; + if (pack.pack) + info.packname = pack.pack->display_name; + + for (const auto &img : pack.images) { + if (img.second.overrides_usage() && + (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) + continue; + + info.images.insert(img); + } + + if (!info.images.empty()) + infos.push_back(std::move(info)); + } + }; + + // packs from account data + if (auto accountpack = + getAccountData(txn, mtx::events::EventType::ImagePackInAccountData, "")) { + auto tmp = + std::get_if>( + &*accountpack); + if (tmp) + addPack(tmp->content); + } + + // packs from rooms, that were enabled globally + if (auto roomPacks = getAccountData(txn, mtx::events::EventType::ImagePackRooms, "")) { + auto tmp = + std::get_if>( + &*roomPacks); + if (tmp) { + for (const auto &[room_id2, state_to_d] : tmp->content.rooms) { + // don't add stickers from this room twice + if (room_id2 == room_id) + continue; + + for (const auto &[state_id, d] : state_to_d) { + (void)d; + if (auto pack = + getStateEvent( + txn, room_id2)) + addPack(pack->content); + } + } + } + } + + // packs from current room + if (auto pack = getStateEvent(txn, room_id)) { + addPack(pack->content); + } + for (const auto &pack : + getStateEventsWithType(txn, room_id)) { + addPack(pack.content); + } + + return infos; } std::optional diff --git a/src/CacheStructs.h b/src/CacheStructs.h index 28c70055..f274d70f 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -11,6 +11,7 @@ #include #include +#include namespace cache { enum class CacheVersion : int @@ -109,3 +110,9 @@ struct RoomSearchResult std::string room_id; RoomInfo info; }; + +struct ImagePackInfo +{ + std::string packname; + std::map images; +}; diff --git a/src/Cache_p.h b/src/Cache_p.h index 3752f5e4..13fbc371 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -88,12 +88,6 @@ public: //! Retrieve if the room is a space bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb); - //! retrieve a specific event from account data - //! pass empty room_id for global account data - std::optional getAccountData( - mtx::events::EventType type, - const std::string &room_id = ""); - //! Get a specific state event template std::optional> getStateEvent(const std::string &room_id, @@ -231,6 +225,8 @@ public: std::vector getParentRoomIds(const std::string &room_id); std::vector getChildRoomIds(const std::string &room_id); + std::vector getImagePacks(const std::string &room_id, bool stickers); + //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); bool isRoomEncrypted(const std::string &room_id); diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp index fb2599a5..4345e383 100644 --- a/src/ImagePackModel.cpp +++ b/src/ImagePackModel.cpp @@ -11,35 +11,12 @@ ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject : QAbstractListModel(parent) , room_id(roomId) { - auto accountpackV = - cache::client()->getAccountData(mtx::events::EventType::ImagePackInAccountData); - auto enabledRoomPacksV = - cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms); + auto packs = cache::client()->getImagePacks(room_id, stickers); - std::optional accountPack; - if (accountpackV) { - auto tmp = - std::get_if>( - &*accountpackV); - if (tmp) - accountPack = tmp->content; - } - // mtx::events::msc2545::ImagePackRooms *enabledRoomPacks = nullptr; - // if (enabledRoomPacksV) - // enabledRoomPacks = - // std::get_if(&*enabledRoomPacksV); - - if (accountPack && (!accountPack->pack || (stickers ? accountPack->pack->is_sticker() - : accountPack->pack->is_emoji()))) { - QString packname; - if (accountPack->pack) - packname = QString::fromStdString(accountPack->pack->display_name); - - for (const auto &img : accountPack->images) { - if (img.second.overrides_usage() && - (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) - continue; + for (const auto &pack : packs) { + QString packname = QString::fromStdString(pack.packname); + for (const auto &img : pack.images) { ImageDesc i{}; i.shortcode = QString::fromStdString(img.first); i.packname = packname; -- cgit 1.5.1 From 56b44a85b572df2565dfdb8269146830d7ba02d0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 03:29:48 +0200 Subject: Fix unused state key when iterating room stickers --- src/Cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/Cache.cpp b/src/Cache.cpp index 4321393c..0bcf9fbf 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3432,7 +3432,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) (void)d; if (auto pack = getStateEvent( - txn, room_id2)) + txn, room_id2, state_id)) addPack(pack->content); } } -- cgit 1.5.1 From 9f416f1fc9cc3a973159b2a5a84ee668ffbc5063 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 12:43:16 +0200 Subject: Fix only first 7 stickers showing up --- src/ImagePackModel.cpp | 6 ++++++ src/ImagePackModel.h | 6 +----- src/timeline/TimelineViewManager.cpp | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp index 4345e383..9b0dca8d 100644 --- a/src/ImagePackModel.cpp +++ b/src/ImagePackModel.cpp @@ -26,6 +26,12 @@ ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject } } +int +ImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)images.size(); +} + QHash ImagePackModel::roleNames() const { diff --git a/src/ImagePackModel.h b/src/ImagePackModel.h index 10e71b8f..937014ec 100644 --- a/src/ImagePackModel.h +++ b/src/ImagePackModel.h @@ -23,11 +23,7 @@ public: ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - (void)parent; - return (int)images.size(); - } + int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; mtx::events::msc2545::PackImage imageAt(int row) diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index ec1b3573..3e69f92b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -597,7 +597,7 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) return proxy; } else if (completerName == "stickers") { auto stickerModel = new ImagePackModel(roomId.toStdString(), true); - auto proxy = new CompletionProxyModel(stickerModel); + auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast(-1) / 4); stickerModel->setParent(proxy); return proxy; } -- cgit 1.5.1 From 3b56ff2d85e607b2620ced0f45bdc2c6ef100feb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 16:14:09 +0200 Subject: Fix replying and editing stickers --- src/timeline/InputBar.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 0f210722..56d0d1ce 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -515,6 +515,15 @@ InputBar::sticker(ImagePackModel *model, int row) sticker.url = img.url; sticker.body = img.body; + if (!room->reply().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + } + if (!room->edit().isEmpty()) { + sticker.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + } + room->sendMessageEvent(sticker, mtx::events::EventType::Sticker); } -- cgit 1.5.1 From 88ed0fade74915e83df3a8e335d7cc49ee068d5c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 20 Jul 2021 14:09:19 +0200 Subject: Explicitly reload data in delegates, if related events got loaded --- resources/qml/MessageView.qml | 2 ++ resources/qml/TimelineRow.qml | 39 ++++++++++++++++------------- resources/qml/delegates/MessageDelegate.qml | 11 ++++---- resources/qml/delegates/Reply.qml | 2 ++ resources/qml/emoji/StickerPicker.qml | 1 - src/timeline/TimelineModel.cpp | 4 +++ src/timeline/TimelineModel.h | 3 +++ 7 files changed, 38 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index f56af237..564c8dc7 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -341,6 +341,7 @@ ScrollView { required property var timestamp required property int status required property int index + required property int relatedEventCacheBuster required property string previousMessageUserId required property string day required property string previousMessageDay @@ -446,6 +447,7 @@ ScrollView { trustlevel: wrapper.trustlevel timestamp: wrapper.timestamp status: wrapper.status + relatedEventCacheBuster: wrapper.relatedEventCacheBuster y: section.visible && section.active ? section.y + section.height : 0 HoverHandler { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 70db08e7..755ab503 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -40,6 +40,7 @@ Item { required property int trustlevel required property var timestamp required property int status + required property int relatedEventCacheBuster anchors.left: parent.left anchors.right: parent.right @@ -90,25 +91,26 @@ Item { } visible: replyTo - userColor: replyTo, TimelineManager.userColor(userId, Nheko.colors.base) - blurhash: replyTo, fromModel(Room.Blurhash) ?? "" - body: replyTo, fromModel(Room.Body) ?? "" - formattedBody: replyTo, fromModel(Room.FormattedBody) ?? "" + userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, Nheko.colors.base) + blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" + body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" + formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" eventId: fromModel(Room.EventId) ?? "" - filename: replyTo, fromModel(Room.Filename) ?? "" - filesize: replyTo, fromModel(Room.Filesize) ?? "" - proportionalHeight: replyTo, fromModel(Room.ProportionalHeight) ?? 1 - type: replyTo, fromModel(Room.Type) ?? MtxEvent.UnknownMessage - typeString: replyTo, fromModel(Room.TypeString) ?? "" - url: replyTo, fromModel(Room.Url) ?? "" - originalWidth: replyTo, fromModel(Room.OriginalWidth) ?? 0 - isOnlyEmoji: replyTo, fromModel(Room.IsOnlyEmoji) ?? false - userId: replyTo, fromModel(Room.UserId) ?? "" - userName: replyTo, fromModel(Room.UserName) ?? "" - thumbnailUrl: replyTo, fromModel(Room.ThumbnailUrl) ?? "" - roomTopic: replyTo, fromModel(Room.RoomTopic) ?? "" - roomName: replyTo, fromModel(Room.RoomName) ?? "" - callType: replyTo, fromModel(Room.CallType) ?? "" + filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" + filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" + proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 + type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage + typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" + url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" + originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 + isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false + userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" + userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" + thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" + roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" + roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" + callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" + relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 } // actual message content @@ -134,6 +136,7 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + relatedEventCacheBuster: r.relatedEventCacheBuster isReply: false } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 0b060629..c64ae887 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -29,6 +29,7 @@ Item { required property string roomTopic required property string roomName required property string callType + required property int relatedEventCacheBuster height: chooser.childrenRect.height @@ -301,7 +302,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatPowerLevelEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId) } } @@ -313,7 +314,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatJoinRuleEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId) } } @@ -325,7 +326,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatHistoryVisibilityEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId) } } @@ -337,7 +338,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatGuestAccessEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId) } } @@ -349,7 +350,7 @@ Item { body: formatted isOnlyEmoji: false isReply: d.isReply - formatted: room.formatMemberEvent(d.eventId) + formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) } } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 3a188d78..75e3d617 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -30,6 +30,7 @@ Item { property string roomTopic property string roomName property string callType + property int relatedEventCacheBuster width: parent.width height: replyContainer.height @@ -95,6 +96,7 @@ Item { roomTopic: r.roomTopic roomName: r.roomName callType: r.callType + relatedEventCacheBuster: r.relatedEventCacheBuster enabled: false width: parent.width isReply: true diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index 813c0b12..3731a948 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -122,7 +122,6 @@ Menu { id: gridView model: roomid ? TimelineManager.completerFor("stickers", roomid) : null - Layout.preferredHeight: cellHeight * 3.5 Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 Layout.leftMargin: 4 diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index abfe28a9..7b3f0729 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -344,6 +344,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj &EventStore::dataChanged, this, [this](int from, int to) { + relatedEventCacheBuster++; nhlog::ui()->debug( "data changed {} to {}", events.size() - to - 1, events.size() - from - 1); emit dataChanged(index(events.size() - to - 1, 0), @@ -443,6 +444,7 @@ TimelineModel::roleNames() const {RoomTopic, "roomTopic"}, {CallType, "callType"}, {Dump, "dump"}, + {RelatedEventCacheBuster, "relatedEventCacheBuster"}, }; } int @@ -676,6 +678,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(m); } + case RelatedEventCacheBuster: + return relatedEventCacheBuster; default: return QVariant(); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0e2895d4..3c80ade8 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -208,6 +208,7 @@ public: RoomTopic, CallType, Dump, + RelatedEventCacheBuster, }; Q_ENUM(Roles); @@ -400,6 +401,8 @@ private: int notification_count = 0, highlight_count = 0; + unsigned int relatedEventCacheBuster = 0; + bool decryptDescription = true; bool m_paginationInProgress = false; bool isSpace_ = false; -- cgit 1.5.1 From 77a0c574bfc962b6c37426fb16a70ca16c08a3f5 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 29 May 2021 21:09:21 -0400 Subject: QML the room member list --- CMakeLists.txt | 4 +- resources/qml/RoomMembers.qml | 111 ++++++++++++++++++++++++++ resources/qml/TopBar.qml | 2 +- resources/res.qrc | 1 + src/MainWindow.cpp | 10 +-- src/MainWindow.h | 1 - src/MemberList.cpp | 91 ++++++++++++++++++++++ src/MemberList.h | 58 ++++++++++++++ src/dialogs/MemberList.cpp | 146 ----------------------------------- src/dialogs/MemberList.h | 57 -------------- src/main.cpp | 3 + src/timeline/TimelineModel.cpp | 12 ++- src/timeline/TimelineModel.h | 5 +- src/timeline/TimelineViewManager.cpp | 7 +- src/timeline/TimelineViewManager.h | 1 - 15 files changed, 284 insertions(+), 225 deletions(-) create mode 100644 resources/qml/RoomMembers.qml create mode 100644 src/MemberList.cpp create mode 100644 src/MemberList.h delete mode 100644 src/dialogs/MemberList.cpp delete mode 100644 src/dialogs/MemberList.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b26b2e5..84f52766 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -285,7 +285,6 @@ set(SRC_FILES src/dialogs/JoinRoom.cpp src/dialogs/LeaveRoom.cpp src/dialogs/Logout.cpp - src/dialogs/MemberList.cpp src/dialogs/PreviewUploadOverlay.cpp src/dialogs/ReCaptcha.cpp src/dialogs/ReadReceipts.cpp @@ -351,6 +350,7 @@ set(SRC_FILES src/LoginPage.cpp src/MainWindow.cpp src/MatrixClient.cpp + src/MemberList.cpp src/MxcImageProvider.cpp src/Olm.cpp src/RegisterPage.cpp @@ -496,7 +496,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/JoinRoom.h src/dialogs/LeaveRoom.h src/dialogs/Logout.h - src/dialogs/MemberList.h src/dialogs/PreviewUploadOverlay.h src/dialogs/RawMessage.h src/dialogs/ReCaptcha.h @@ -557,6 +556,7 @@ qt5_wrap_cpp(MOC_HEADERS src/InviteeItem.h src/LoginPage.h src/MainWindow.h + src/MemberList.h src/MxcImageProvider.h src/RegisterPage.h src/SSOHandler.h diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml new file mode 100644 index 00000000..4406c1b0 --- /dev/null +++ b/resources/qml/RoomMembers.qml @@ -0,0 +1,111 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 +import im.nheko 1.0 + +ApplicationWindow { + id: roomMembersRoot + + property string roomName: Rooms.currentRoom.roomName + property MemberList members + + title: qsTr("Members of ") + roomName + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 650 + width: 420 + minimumHeight: 420 + + Shortcut { + sequence: StandardKey.Cancel + onActivated: roomMembersRoot.close() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Avatar { + id: roomAvatar + + width: 130 + height: width + displayName: members.roomName + Layout.alignment: Qt.AlignHCenter + url: members.avatarUrl.replace("mxc://", "image://MxcImage/") + onClicked: TimelineManager.timeline.openRoomSettings(members.roomId) + } + + Label { + font.pixelSize: 24 + text: members.memberCount + (members.memberCount === 1 ? qsTr(" person in ") : qsTr(" people in ")) + roomName + Layout.alignment: Qt.AlignHCenter + } + + ScrollView { + clip: false + palette: colors + padding: 10 + ScrollBar.horizontal.visible: false + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + ListView { + id: memberList + + clip: true + spacing: 8 + boundsBehavior: Flickable.StopAtBounds + model: members + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + delegate: RowLayout { + spacing: 10 + + Avatar { + width: avatarSize + height: avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: TimelineManager.timeline.openUserProfile(model.mxid) + } + + ColumnLayout { + spacing: 5 + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", colors.window) + font.pointSize: 12 + } + + Label { + text: model.mxid + color: colors.buttonText + font.pointSize: 10 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } + } + } + } + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: roomMembersRoot.close() + } +} diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 58aba0c7..50c2447c 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -116,7 +116,7 @@ Rectangle { Platform.MenuItem { text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog(room.roomId()) + onTriggered: Rooms.currentRoom.openRoomMembers(room.roomId()) } Platform.MenuItem { diff --git a/resources/res.qrc b/resources/res.qrc index e9479e57..da5288c8 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -185,6 +185,7 @@ qml/components/AdaptiveLayout.qml qml/components/AdaptiveLayoutElement.qml qml/components/FlatButton.qml + qml/RoomMembers.qml media/ring.ogg diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index ed337ca4..36bada83 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -21,6 +21,7 @@ #include "LoginPage.h" #include "MainWindow.h" #include "MatrixClient.h" +#include "MemberList.h" #include "RegisterPage.h" #include "TrayIcon.h" #include "UserSettingsPage.h" @@ -36,7 +37,6 @@ #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" -#include "dialogs/MemberList.h" #include "dialogs/ReadReceipts.h" MainWindow *MainWindow::instance_ = nullptr; @@ -310,14 +310,6 @@ MainWindow::hasActiveUser() settings.contains(prefix + "auth/user_id"); } -void -MainWindow::openMemberListDialog(const QString &room_id) -{ - auto dialog = new dialogs::MemberList(room_id, this); - - showDialog(dialog); -} - void MainWindow::openLeaveRoomDialog(const QString &room_id) { diff --git a/src/MainWindow.h b/src/MainWindow.h index 3571f079..6d62545c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -65,7 +65,6 @@ public: std::function callback); void openJoinRoomDialog(std::function callback); void openLogoutDialog(); - void openMemberListDialog(const QString &room_id); void openReadReceiptsDialog(const QString &event_id); void hideOverlay(); diff --git a/src/MemberList.cpp b/src/MemberList.cpp new file mode 100644 index 00000000..62488277 --- /dev/null +++ b/src/MemberList.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MemberList.h" + +#include "Cache.h" +#include "ChatPage.h" +#include "Config.h" +#include "Logging.h" +#include "Utils.h" +#include "timeline/TimelineViewManager.h" +#include "ui/Avatar.h" + +MemberList::MemberList(const QString &room_id, QWidget *parent) + : QAbstractListModel{parent} + , room_id_{room_id} +{ + try { + info_ = cache::singleRoomInfo(room_id_.toStdString()); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve room info from cache: {}", + room_id_.toStdString()); + } + + try { + addUsers(cache::getMembers(room_id_.toStdString())); + } catch (const lmdb::error &e) { + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); + } +} + +void +MemberList::addUsers(const std::vector &members) +{ + beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + + for (const auto &member : members) + m_memberList.push_back( + {member, + ChatPage::instance()->timelineManager()->rooms()->currentRoom()->avatarUrl( + member.user_id)}); + + endInsertRows(); +} + +QHash +MemberList::roleNames() const +{ + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; +} + +QVariant +MemberList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)m_memberList.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return m_memberList[index.row()].first.user_id; + case DisplayName: + return m_memberList[index.row()].first.display_name; + case AvatarUrl: + return m_memberList[index.row()].second; + default: + return {}; + } +} + +bool MemberList::canFetchMore(const QModelIndex &) const +{ + const size_t numMembers = rowCount(); + return (numMembers > 1 && numMembers < info_.member_count); +} + +void +MemberList::fetchMore(const QModelIndex &) +{ + addUsers(cache::getMembers(room_id_.toStdString(), rowCount())); +} diff --git a/src/MemberList.h b/src/MemberList.h new file mode 100644 index 00000000..dbe69f4b --- /dev/null +++ b/src/MemberList.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "CacheStructs.h" +#include + +class MemberList : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(size_t memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; + MemberList(const QString &room_id, QWidget *parent = nullptr); + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return static_cast(m_memberList.size()); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QString roomName() const { return QString::fromStdString(info_.name); } + size_t memberCount() const { return info_.member_count; } + QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } + QString roomId() const { return room_id_; } + +signals: + void roomNameChanged(); + void memberCountChanged(); + void avatarUrlChanged(); + void roomIdChanged(); + +public slots: + void addUsers(const std::vector &users); + +protected: + bool canFetchMore(const QModelIndex &) const; + void fetchMore(const QModelIndex &); + +private: + QVector> m_memberList; + QString room_id_; + RoomInfo info_; +}; diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp deleted file mode 100644 index 21eb72b0..00000000 --- a/src/dialogs/MemberList.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/MemberList.h" - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Utils.h" -#include "ui/Avatar.h" - -using namespace dialogs; - -MemberItem::MemberItem(const RoomMember &member, QWidget *parent) - : QWidget(parent) -{ - topLayout_ = new QHBoxLayout(this); - topLayout_->setMargin(0); - - textLayout_ = new QVBoxLayout; - textLayout_->setMargin(0); - textLayout_->setSpacing(0); - - avatar_ = new Avatar(this, 44); - avatar_->setLetter(utils::firstChar(member.display_name)); - - avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - - userId_ = new QLabel(member.user_id, this); - userName_ = new QLabel(member.display_name, this); - userName_->setFont(nameFont); - - textLayout_->addWidget(userName_); - textLayout_->addWidget(userId_); - - topLayout_->addWidget(avatar_); - topLayout_->addLayout(textLayout_, 1); -} - -void -MemberItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -MemberList::MemberList(const QString &room_id, QWidget *parent) - : QFrame(parent) - , room_id_{room_id} -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - list_ = new QListWidget; - list_->setFrameStyle(QFrame::NoFrame); - list_->setSelectionMode(QAbstractItemView::NoSelection); - list_->setSpacing(5); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(list_->sizeHint().height() * 2); - setMinimumWidth(std::max(list_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN, - QFontMetrics(largeFont).averageCharWidth() * 30 - - 2 * conf::modals::WIDGET_MARGIN)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - topLabel_ = new QLabel(tr("Room members"), this); - topLabel_->setAlignment(Qt::AlignCenter); - topLabel_->setFont(font); - - auto okBtn = new QPushButton(tr("OK"), this); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->addStretch(1); - buttonLayout->addWidget(okBtn); - - layout->addWidget(topLabel_); - layout->addWidget(list_); - layout->addLayout(buttonLayout); - - list_->clear(); - - connect(list_->verticalScrollBar(), &QAbstractSlider::valueChanged, this, [this](int pos) { - if (pos != list_->verticalScrollBar()->maximum()) - return; - - const size_t numMembers = list_->count() - 1; - - if (numMembers > 0) - addUsers(cache::getMembers(room_id_.toStdString(), numMembers)); - }); - - try { - addUsers(cache::getMembers(room_id_.toStdString())); - } catch (const lmdb::error &e) { - nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); - } - - auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); - connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); - connect(okBtn, &QPushButton::clicked, this, &MemberList::close); -} - -void -MemberList::addUsers(const std::vector &members) -{ - for (const auto &member : members) { - auto user = new MemberItem(member, this); - auto item = new QListWidgetItem; - - item->setSizeHint(user->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - list_->insertItem(list_->count() - 1, item); - list_->setItemWidget(item, user); - } -} diff --git a/src/dialogs/MemberList.h b/src/dialogs/MemberList.h deleted file mode 100644 index b822eec8..00000000 --- a/src/dialogs/MemberList.h +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class Avatar; -class QPushButton; -class QHBoxLayout; -class QLabel; -class QVBoxLayout; - -struct RoomMember; - -template -class QSharedPointer; - -namespace dialogs { - -class MemberItem : public QWidget -{ - Q_OBJECT - -public: - MemberItem(const RoomMember &member, QWidget *parent); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *userId_; -}; - -class MemberList : public QFrame -{ - Q_OBJECT -public: - MemberList(const QString &room_id, QWidget *parent = nullptr); - -public slots: - void addUsers(const std::vector &users); - -private: - QString room_id_; - QLabel *topLabel_; - QListWidget *list_; -}; -} // dialogs diff --git a/src/main.cpp b/src/main.cpp index 29e93d49..376fc4f5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -205,6 +205,9 @@ main(int argc, char *argv[]) parser.process(app); + // make sure that size_t properties will work + qRegisterMetaType("size_t"); + app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"})); http::init(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 7b3f0729..48d69493 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -25,6 +25,7 @@ #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" +#include "MemberList.h" #include "MxcImageProvider.h" #include "Olm.h" #include "TimelineViewManager.h" @@ -1061,9 +1062,16 @@ TimelineModel::openUserProfile(QString userid) } void -TimelineModel::openRoomSettings() +TimelineModel::openRoomMembers() { - RoomSettings *settings = new RoomSettings(roomId(), this); + MemberList *memberList = new MemberList(roomId()); + emit openRoomMembersDialog(memberList); +} + +void +TimelineModel::openRoomSettings(QString room_id) +{ + RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this); connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged); openRoomSettingsDialog(settings); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 3c80ade8..feb7b5f5 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -17,6 +17,7 @@ #include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" +#include "MemberList.h" #include "Permissions.h" #include "ui/RoomSettings.h" #include "ui/UserProfile.h" @@ -235,7 +236,8 @@ public: Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid); - Q_INVOKABLE void openRoomSettings(); + Q_INVOKABLE void openRoomMembers(); + Q_INVOKABLE void openRoomSettings(QString room_id = QString()); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; @@ -352,6 +354,7 @@ signals: void lastMessageChanged(); void notificationsChanged(); + void openRoomMembersDialog(MemberList *members); void openRoomSettingsDialog(RoomSettings *settings); void newMessageToSend(mtx::events::collections::TimelineEvents event); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 3e69f92b..011ff61c 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -174,6 +174,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "UserProfileModel", "UserProfile needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", 1, 0, "MemberList", "MemberList needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, @@ -428,11 +430,6 @@ TimelineViewManager::openInviteUsersDialog() [this](const QStringList &invitees) { emit inviteUsers(invitees); }); } void -TimelineViewManager::openMemberListDialog(QString roomid) const -{ - MainWindow::instance()->openMemberListDialog(roomid); -} -void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { MainWindow::instance()->openLeaveRoomDialog(roomid); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 15b4f523..39d0d31c 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -66,7 +66,6 @@ public: Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); - Q_INVOKABLE void openMemberListDialog(QString roomid) const; Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); -- cgit 1.5.1 From 6c57fa6c5b491e981958e417458edac40e9000b4 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 10 Jun 2021 20:11:49 -0400 Subject: QML the invite dialog This also adds a property `roomId` to TimelineModel. --- CMakeLists.txt | 4 - resources/qml/InviteDialog.qml | 112 +++++++++++++++++++++++++ resources/qml/TopBar.qml | 14 +++- resources/qml/types/Invitee.qml | 5 ++ resources/res.qrc | 13 +-- src/ChatPage.cpp | 28 +++++++ src/InviteeItem.cpp | 28 ------- src/InviteeItem.h | 31 ------- src/MainWindow.cpp | 13 --- src/dialogs/InviteUsers.cpp | 158 ----------------------------------- src/dialogs/InviteUsers.h | 45 ---------- src/timeline/TimelineModel.h | 1 + src/timeline/TimelineViewManager.cpp | 51 +++++++++++ src/timeline/TimelineViewManager.h | 5 +- 14 files changed, 215 insertions(+), 293 deletions(-) create mode 100644 resources/qml/InviteDialog.qml create mode 100644 resources/qml/types/Invitee.qml delete mode 100644 src/InviteeItem.cpp delete mode 100644 src/InviteeItem.h delete mode 100644 src/dialogs/InviteUsers.cpp delete mode 100644 src/dialogs/InviteUsers.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index 84f52766..56592950 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,7 +281,6 @@ set(SRC_FILES src/dialogs/CreateRoom.cpp src/dialogs/FallbackAuth.cpp src/dialogs/ImageOverlay.cpp - src/dialogs/InviteUsers.cpp src/dialogs/JoinRoom.cpp src/dialogs/LeaveRoom.cpp src/dialogs/Logout.cpp @@ -345,7 +344,6 @@ set(SRC_FILES src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp - src/InviteeItem.cpp src/Logging.cpp src/LoginPage.cpp src/MainWindow.cpp @@ -492,7 +490,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/CreateRoom.h src/dialogs/FallbackAuth.h src/dialogs/ImageOverlay.h - src/dialogs/InviteUsers.h src/dialogs/JoinRoom.h src/dialogs/LeaveRoom.h src/dialogs/Logout.h @@ -553,7 +550,6 @@ qt5_wrap_cpp(MOC_HEADERS src/Clipboard.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h - src/InviteeItem.h src/LoginPage.h src/MainWindow.h src/MemberList.h diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml new file mode 100644 index 00000000..5d3a8f1e --- /dev/null +++ b/resources/qml/InviteDialog.qml @@ -0,0 +1,112 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 +import "./types" + +ApplicationWindow { + id: inviteDialogRoot + + property string roomId + property string roomName + property list invitees + + function addInvite() { + if (inviteeEntry.text.match("@.+?:.{3,}")) + { + invitees.push(inviteeComponent.createObject( + inviteDialogRoot, { + "invitee": inviteeEntry.text + })); + inviteeEntry.clear(); + } + } + + function accept() { + if (inviteeEntry.text !== "") + addInvite(); + + var inviteeStringList = ["temp"]; // the "temp" element exists to declare this as a string array + for (var i = 0; i < invitees.length; ++i) + inviteeStringList.push(invitees[i].invitee); + inviteeStringList.shift(); // remove the first item + + TimelineManager.inviteUsers(inviteDialogRoot.roomId, inviteeStringList); + } + + title: qsTr("Invite users to ") + roomName + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 380 + width: 340 + + Component { + id: inviteeComponent + + Invitee {} + } + + // TODO: make this work in the TextField + Shortcut { + sequence: "Ctrl+Enter" + onActivated: inviteDialogRoot.accept() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Label { + text: qsTr("User ID to invite") + Layout.fillWidth: true + } + + RowLayout { + spacing: 10 + + TextField { + id: inviteeEntry + + placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") + Layout.fillWidth: true + onAccepted: if (text !== "") addInvite() + } + + Button { + text: qsTr("Invite") + onClicked: if (inviteeEntry.text !== "") addInvite() + } + } + + ListView { + id: inviteesList + + Layout.fillWidth: true + Layout.fillHeight: true + model: invitees + delegate: Label { + text: model.invitee + } + } + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Invite") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + onClicked: { + inviteDialogRoot.accept(); + inviteDialogRoot.close(); + } + } + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: inviteDialogRoot.close(); + } + } +} diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 50c2447c..72dbe604 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -21,6 +21,12 @@ Rectangle { z: 3 color: Nheko.colors.window + Component { + id: inviteDialog + + InviteDialog {} + } + TapHandler { onSingleTapped: { if (room) @@ -111,7 +117,13 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: TimelineManager.openInviteUsersDialog() + onTriggered: { + var dialog = inviteDialog.createObject(topBar, { + "roomId": room.roomId, + "roomName": room.roomName + }); + dialog.show(); + } } Platform.MenuItem { diff --git a/resources/qml/types/Invitee.qml b/resources/qml/types/Invitee.qml new file mode 100644 index 00000000..fbc0b781 --- /dev/null +++ b/resources/qml/types/Invitee.qml @@ -0,0 +1,5 @@ +import QtQuick 2.12 + +Item { + property string invitee +} diff --git a/resources/res.qrc b/resources/res.qrc index da5288c8..ad7b6665 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -9,7 +9,6 @@ icons/ui/do-not-disturb-rounded-sign@2x.png icons/ui/round-remove-button.png icons/ui/round-remove-button@2x.png - icons/ui/double-tick-indicator.png icons/ui/double-tick-indicator@2x.png icons/ui/lock.png @@ -55,22 +54,17 @@ icons/ui/pause-symbol@2x.png icons/ui/remove-symbol.png icons/ui/remove-symbol@2x.png - icons/ui/world.png icons/ui/world@2x.png - icons/ui/tag.png icons/ui/tag@2x.png icons/ui/star.png icons/ui/star@2x.png icons/ui/lowprio.png icons/ui/lowprio@2x.png - icons/ui/edit.png icons/ui/edit@2x.png - icons/ui/mail-reply.png - icons/ui/place-call.png icons/ui/end-call.png icons/ui/microphone-mute.png @@ -78,7 +72,6 @@ icons/ui/screen-share.png icons/ui/toggle-camera-view.png icons/ui/video-call.png - icons/emoji-categories/people.png icons/emoji-categories/people@2x.png icons/emoji-categories/nature.png @@ -99,16 +92,12 @@ nheko.png nheko.svg - splash.png splash@2x.png - register.png register@2x.png - login.png login@2x.png - nheko-512.png nheko-256.png nheko-128.png @@ -186,6 +175,8 @@ qml/components/AdaptiveLayoutElement.qml qml/components/FlatButton.qml qml/RoomMembers.qml + qml/InviteDialog.qml + qml/types/Invitee.qml media/ring.ogg diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 10a91557..f6ea4539 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -140,6 +140,34 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) } }); + connect( + view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); + connect( + view_manager_, + &TimelineViewManager::inviteUsers, + this, + [this](QString roomId, QStringList users) { + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 500, this, [this, roomId, ii, users]() { + const auto user = users.at(ii); + + http::client()->invite_user( + roomId.toStdString(), + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to invite user: %1").arg(user)); + return; + } + + emit showNotification(tr("Invited user: %1").arg(user)); + }); + }); + } + }); + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); diff --git a/src/InviteeItem.cpp b/src/InviteeItem.cpp deleted file mode 100644 index 27f02560..00000000 --- a/src/InviteeItem.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include - -#include "InviteeItem.h" - -constexpr int SidePadding = 10; - -InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent) - : QWidget{parent} - , user_{QString::fromStdString(user.to_string())} -{ - auto topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0); - - name_ = new QLabel(user_, this); - removeUserBtn_ = new QPushButton(tr("Remove"), this); - - topLayout_->addWidget(name_); - topLayout_->addWidget(removeUserBtn_, 0, Qt::AlignRight); - - connect(removeUserBtn_, &QPushButton::clicked, this, &InviteeItem::removeItem); -} diff --git a/src/InviteeItem.h b/src/InviteeItem.h deleted file mode 100644 index 014541ea..00000000 --- a/src/InviteeItem.h +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include - -class QPushButton; -class QLabel; - -class InviteeItem : public QWidget -{ - Q_OBJECT - -public: - InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr); - - QString userID() { return user_; } - -signals: - void removeItem(); - -private: - QString user_; - - QLabel *name_; - QPushButton *removeUserBtn_; -}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 36bada83..c0486d01 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -33,7 +33,6 @@ #include "ui/SnackBar.h" #include "dialogs/CreateRoom.h" -#include "dialogs/InviteUsers.h" #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" @@ -333,18 +332,6 @@ MainWindow::showOverlayProgressBar() showSolidOverlayModal(spinner_); } -void -MainWindow::openInviteUsersDialog(std::function callback) -{ - auto dialog = new dialogs::InviteUsers(this); - connect(dialog, &dialogs::InviteUsers::sendInvites, this, [callback](QStringList invitees) { - if (!invitees.isEmpty()) - callback(invitees); - }); - - showDialog(dialog); -} - void MainWindow::openJoinRoomDialog(std::function callback) { diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp deleted file mode 100644 index 9dd6085f..00000000 --- a/src/dialogs/InviteUsers.cpp +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dialogs/InviteUsers.h" - -#include "Config.h" -#include "InviteeItem.h" -#include "ui/TextField.h" - -#include - -using namespace dialogs; - -InviteUsers::InviteUsers(QWidget *parent) - : QFrame(parent) -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - setMinimumWidth(conf::window::minModalWidth); - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(0); - buttonLayout->setMargin(0); - - confirmBtn_ = new QPushButton("Invite", this); - confirmBtn_->setDefault(true); - cancelBtn_ = new QPushButton(tr("Cancel"), this); - - buttonLayout->addStretch(1); - buttonLayout->setSpacing(15); - buttonLayout->addWidget(cancelBtn_); - buttonLayout->addWidget(confirmBtn_); - - inviteeInput_ = new TextField(this); - inviteeInput_->setLabel(tr("User ID to invite")); - - inviteeList_ = new QListWidget; - inviteeList_->setFrameStyle(QFrame::NoFrame); - inviteeList_->setSelectionMode(QAbstractItemView::NoSelection); - inviteeList_->setAttribute(Qt::WA_MacShowFocusRect, 0); - inviteeList_->setSpacing(5); - - errorLabel_ = new QLabel(this); - errorLabel_->setAlignment(Qt::AlignCenter); - - layout->addWidget(inviteeInput_); - layout->addWidget(errorLabel_); - layout->addWidget(inviteeList_); - layout->addLayout(buttonLayout); - - connect(inviteeInput_, &TextField::returnPressed, this, &InviteUsers::addUser); - connect(confirmBtn_, &QPushButton::clicked, [this]() { - if (!inviteeInput_->text().trimmed().isEmpty()) { - addUser(); - } - - emit sendInvites(invitedUsers()); - - inviteeInput_->clear(); - inviteeList_->clear(); - errorLabel_->hide(); - - emit close(); - }); - - connect(cancelBtn_, &QPushButton::clicked, [this]() { - inviteeInput_->clear(); - inviteeList_->clear(); - errorLabel_->hide(); - - emit close(); - }); -} - -void -InviteUsers::addUser() -{ - auto user_id = inviteeInput_->text(); - - try { - namespace ids = mtx::identifiers; - auto user = ids::parse(user_id.toStdString()); - - auto item = new QListWidgetItem(inviteeList_); - auto invitee = new InviteeItem(user, this); - - item->setSizeHint(invitee->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - inviteeList_->setItemWidget(item, invitee); - - connect(invitee, &InviteeItem::removeItem, this, [this, item]() { - emit removeInvitee(item); - }); - - errorLabel_->hide(); - inviteeInput_->clear(); - } catch (std::exception &e) { - errorLabel_->setText(e.what()); - errorLabel_->show(); - } -} - -void -InviteUsers::removeInvitee(QListWidgetItem *item) -{ - int row = inviteeList_->row(item); - auto widget = inviteeList_->takeItem(row); - - inviteeList_->removeItemWidget(widget); -} - -QStringList -InviteUsers::invitedUsers() const -{ - QStringList users; - - for (int ii = 0; ii < inviteeList_->count(); ++ii) { - auto item = inviteeList_->item(ii); - auto widget = inviteeList_->itemWidget(item); - auto invitee = qobject_cast(widget); - - if (invitee) - users << invitee->userID(); - else - qDebug() << "Cast InviteeItem failed"; - } - - return users; -} - -void -InviteUsers::showEvent(QShowEvent *event) -{ - inviteeInput_->setFocus(); - - QFrame::showEvent(event); -} diff --git a/src/dialogs/InviteUsers.h b/src/dialogs/InviteUsers.h deleted file mode 100644 index e40183c1..00000000 --- a/src/dialogs/InviteUsers.h +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -class QPushButton; -class QLabel; -class TextField; -class QListWidget; -class QListWidgetItem; - -namespace dialogs { - -class InviteUsers : public QFrame -{ - Q_OBJECT -public: - explicit InviteUsers(QWidget *parent = nullptr); - -protected: - void showEvent(QShowEvent *event) override; - -signals: - void sendInvites(QStringList invitees); - -private slots: - void removeInvitee(QListWidgetItem *item); - -private: - void addUser(); - QStringList invitedUsers() const; - - QPushButton *confirmBtn_; - QPushButton *cancelBtn_; - - TextField *inviteeInput_; - QLabel *errorLabel_; - - QListWidget *inviteeList_; -}; -} // dialogs diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index feb7b5f5..5730fbab 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -159,6 +159,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) Q_PROPERTY( bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) + Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 011ff61c..43b9a646 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -429,6 +429,57 @@ TimelineViewManager::openInviteUsersDialog() MainWindow::instance()->openInviteUsersDialog( [this](const QStringList &invitees) { emit inviteUsers(invitees); }); } + +void +TimelineViewManager::openLink(QString link) const +{ + QUrl url(link); + if (url.scheme() == "https" && url.host() == "matrix.to") { + // handle matrix.to links internally + QString p = url.fragment(QUrl::FullyEncoded); + if (p.startsWith("/")) + p.remove(0, 1); + + auto temp = p.split("?"); + QString query; + if (temp.size() >= 2) + query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); + + temp = temp.first().split("/"); + auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); + QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); + if (!identifier.isEmpty()) { + if (identifier.startsWith("@")) { + QByteArray uri = + "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("#")) { + QByteArray uri = + "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("!")) { + QByteArray uri = "matrix:roomid/" + + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } + } + } else { + QDesktopServices::openUrl(url); + } +} + void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 39d0d31c..945ba2d5 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -65,7 +65,6 @@ public: Q_INVOKABLE QString userStatus(QString id) const; Q_INVOKABLE void focusMessageInput(); - Q_INVOKABLE void openInviteUsersDialog(); Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); @@ -80,7 +79,9 @@ signals: void replyingEventChanged(QString replyingEvent); void replyClosed(); void newDeviceVerificationRequest(DeviceVerificationFlow *flow); - void inviteUsers(QStringList users); + void inviteUsers(QString roomId, QStringList users); + void showRoomList(); + void narrowViewChanged(); void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); -- cgit 1.5.1 From e1acf5d324615e8c61c469883a6a42933c8f76bc Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 10 Jun 2021 20:13:12 -0400 Subject: make lint --- CMakeLists.txt | 2 + resources/qml/InviteDialog.qml | 78 +++++++++++++++++++++++------------- resources/qml/RoomMembers.qml | 9 +++++ resources/qml/Root.qml | 38 ++++++++++++++++++ resources/qml/TimelineView.qml | 1 - resources/qml/TopBar.qml | 14 +------ resources/qml/types/Invitee.qml | 5 --- resources/res.qrc | 1 - src/ChatPage.cpp | 49 +++++++++++----------- src/InviteesModel.cpp | 77 +++++++++++++++++++++++++++++++++++ src/InviteesModel.h | 56 ++++++++++++++++++++++++++ src/MemberList.cpp | 10 +++-- src/timeline/TimelineModel.cpp | 10 +++++ src/timeline/TimelineModel.h | 3 ++ src/timeline/TimelineViewManager.cpp | 70 +++++++------------------------- 15 files changed, 290 insertions(+), 133 deletions(-) delete mode 100644 resources/qml/types/Invitee.qml create mode 100644 src/InviteesModel.cpp create mode 100644 src/InviteesModel.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index 56592950..f77d9978 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -344,6 +344,7 @@ set(SRC_FILES src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp + src/InviteesModel.cpp src/Logging.cpp src/LoginPage.cpp src/MainWindow.cpp @@ -550,6 +551,7 @@ qt5_wrap_cpp(MOC_HEADERS src/Clipboard.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h + src/InviteesModel.h src/LoginPage.h src/MainWindow.h src/MemberList.h diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 5d3a8f1e..d5cc4c6d 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -2,50 +2,28 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import im.nheko 1.0 -import "./types" ApplicationWindow { id: inviteDialogRoot property string roomId property string roomName - property list invitees + property InviteesModel invitees function addInvite() { if (inviteeEntry.text.match("@.+?:.{3,}")) { - invitees.push(inviteeComponent.createObject( - inviteDialogRoot, { - "invitee": inviteeEntry.text - })); + invitees.addUser(inviteeEntry.text); inviteeEntry.clear(); } } - function accept() { - if (inviteeEntry.text !== "") - addInvite(); - - var inviteeStringList = ["temp"]; // the "temp" element exists to declare this as a string array - for (var i = 0; i < invitees.length; ++i) - inviteeStringList.push(invitees[i].invitee); - inviteeStringList.shift(); // remove the first item - - TimelineManager.inviteUsers(inviteDialogRoot.roomId, inviteeStringList); - } - title: qsTr("Invite users to ") + roomName x: MainWindow.x + (MainWindow.width / 2) - (width / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 380 width: 340 - Component { - id: inviteeComponent - - Invitee {} - } - // TODO: make this work in the TextField Shortcut { sequence: "Ctrl+Enter" @@ -74,7 +52,7 @@ ApplicationWindow { } Button { - text: qsTr("Invite") + text: qsTr("Add") onClicked: if (inviteeEntry.text !== "") addInvite() } } @@ -85,9 +63,53 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true model: invitees - delegate: Label { - text: model.invitee + + delegate: RowLayout { + spacing: 10 + + Avatar { + width: avatarSize + height: avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: TimelineManager.timeline.openUserProfile(model.mxid) + } + + ColumnLayout { + spacing: 5 + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", colors.window) + font.pointSize: 12 + } + + Label { + text: model.mxid + color: colors.buttonText + font.pointSize: 10 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } } +// delegate: RowLayout { +// spacing: 10 + +// Avatar { +// url: model.avatarUrl +// width: 20 +// height: width +// } + +// Label { +// text: model.displayName + " (" + model.mxid + ")" +// } +// } } } @@ -98,7 +120,7 @@ ApplicationWindow { text: qsTr("Invite") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole onClicked: { - inviteDialogRoot.accept(); + invitees.accept(); inviteDialogRoot.close(); } } diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 4406c1b0..d31fe319 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -44,6 +44,15 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter } + ImageButton { + Layout.alignment: Qt.AlignHCenter + image: ":/icons/icons/ui/add-square-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Invite more people") + onClicked: TimelineManager.timeline.openInviteUsersDialog() + } + ScrollView { clip: false palette: colors diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 5316e20d..ecd0bdb7 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -89,6 +89,12 @@ Page { } } + Component { + id: inviteDialog + + InviteDialog { + } + } Connections { target: TimelineManager @@ -116,6 +122,38 @@ Page { } } + Connections { + target: TimelineManager.timeline + onOpenRoomMembersDialog: { + var membersDialog = roomMembersComponent.createObject(timelineRoot, { + "members": members, + "roomName": TimelineManager.timeline.roomName + }); + membersDialog.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenInviteUsersDialog: { + var dialog = inviteDialog.createObject(timelineRoot, { + "roomId": TimelineManager.timeline.roomId, + "roomName": TimelineManager.timeline.roomName + }); + dialog.show(); + } + } + ChatPage { anchors.fill: parent } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 148a5817..d515b9b4 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -157,7 +157,6 @@ Item { Layout.alignment: Qt.AlignHCenter enabled: false } - MatrixText { text: parent.roomName font.pixelSize: 24 diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 72dbe604..6cf747c5 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -21,12 +21,6 @@ Rectangle { z: 3 color: Nheko.colors.window - Component { - id: inviteDialog - - InviteDialog {} - } - TapHandler { onSingleTapped: { if (room) @@ -117,13 +111,7 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: { - var dialog = inviteDialog.createObject(topBar, { - "roomId": room.roomId, - "roomName": room.roomName - }); - dialog.show(); - } + onTriggered: TimelineManager.timeline.openInviteUsers() } Platform.MenuItem { diff --git a/resources/qml/types/Invitee.qml b/resources/qml/types/Invitee.qml deleted file mode 100644 index fbc0b781..00000000 --- a/resources/qml/types/Invitee.qml +++ /dev/null @@ -1,5 +0,0 @@ -import QtQuick 2.12 - -Item { - property string invitee -} diff --git a/resources/res.qrc b/resources/res.qrc index ad7b6665..f8c040e4 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -176,7 +176,6 @@ qml/components/FlatButton.qml qml/RoomMembers.qml qml/InviteDialog.qml - qml/types/Invitee.qml media/ring.ogg diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index f6ea4539..8b4cfeef 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -116,32 +116,31 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { - const auto room_id = currentRoom().toStdString(); - - for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - const auto user = users.at(ii); - - http::client()->invite_user( - room_id, - user.toStdString(), - [this, user](const mtx::responses::RoomInvite &, - mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to invite user: %1").arg(user)); - return; - } + // TODO: once this signal is moved, reenable this +// connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { +// const auto room_id = currentRoom().toStdString(); + +// for (int ii = 0; ii < users.size(); ++ii) { +// QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { +// const auto user = users.at(ii); + +// http::client()->invite_user( +// room_id, +// user.toStdString(), +// [this, user](const mtx::responses::RoomInvite &, +// mtx::http::RequestErr err) { +// if (err) { +// emit showNotification( +// tr("Failed to invite user: %1").arg(user)); +// return; +// } + +// emit showNotification(tr("Invited user: %1").arg(user)); +// }); +// }); +// } +// }); - emit showNotification(tr("Invited user: %1").arg(user)); - }); - }); - } - }); - - connect( - view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); connect( view_manager_, &TimelineViewManager::inviteUsers, diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp new file mode 100644 index 00000000..849c5281 --- /dev/null +++ b/src/InviteesModel.cpp @@ -0,0 +1,77 @@ +#include "InviteesModel.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "mtx/responses/profile.hpp" + +InviteesModel::InviteesModel(QObject *parent) + : QAbstractListModel{parent} +{} + +void +InviteesModel::addUser(QString mxid) +{ + beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); + + auto invitee = new Invitee{mxid, this}; + connect(invitee, &Invitee::userInfoLoaded, this, [this]() { + emit dataChanged(QModelIndex{}, QModelIndex{}); + }); + + invitees_.push_back(invitee); + + endInsertRows(); +} + +QHash +InviteesModel::roleNames() const +{ + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; +} + +QVariant +InviteesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)invitees_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return invitees_[index.row()]->mxid_; + case DisplayName: + return invitees_[index.row()]->displayName_; + case AvatarUrl: + return invitees_[index.row()]->avatarUrl_; + default: + return {}; + } +} + +QStringList +InviteesModel::mxids() +{ + QStringList mxidList; + for (int i = 0; i < invitees_.length(); ++i) + mxidList.push_back(invitees_[i]->mxid_); + return mxidList; +} + +Invitee::Invitee(const QString &mxid, QObject *parent) + : QObject{parent} + , mxid_{mxid} +{ + http::client()->get_profile( + mxid_.toStdString(), + [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve own profile info"); + return; + } + + displayName_ = QString::fromStdString(res.display_name); + avatarUrl_ = QString::fromStdString(res.avatar_url); + + emit userInfoLoaded(); + }); +} diff --git a/src/InviteesModel.h b/src/InviteesModel.h new file mode 100644 index 00000000..4bcc4e9d --- /dev/null +++ b/src/InviteesModel.h @@ -0,0 +1,56 @@ +#ifndef INVITEESMODEL_H +#define INVITEESMODEL_H + +#include +#include + +class Invitee : public QObject +{ + Q_OBJECT + +public: + Invitee(const QString &mxid, QObject *parent = nullptr); + +signals: + void userInfoLoaded(); + +private: + const QString mxid_; + QString displayName_; + QString avatarUrl_; + + friend class InviteesModel; +}; + +class InviteesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + }; + + InviteesModel(QObject *parent = nullptr); + + Q_INVOKABLE void addUser(QString mxid); + + QHash roleNames() const override; + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return (int)invitees_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QStringList mxids(); + +signals: + void accept(); + +private: + QVector invitees_; +}; + +#endif // INVITEESMODEL_H diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 62488277..2a9c3fbc 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -43,7 +43,8 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) void MemberList::addUsers(const std::vector &members) { - beginInsertRows(QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); + beginInsertRows( + QModelIndex{}, m_memberList.count(), m_memberList.count() + members.size() - 1); for (const auto &member : members) m_memberList.push_back( @@ -78,10 +79,11 @@ MemberList::data(const QModelIndex &index, int role) const } } -bool MemberList::canFetchMore(const QModelIndex &) const +bool +MemberList::canFetchMore(const QModelIndex &) const { - const size_t numMembers = rowCount(); - return (numMembers > 1 && numMembers < info_.member_count); + const size_t numMembers = rowCount(); + return (numMembers > 1 && numMembers < info_.member_count); } void diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 48d69493..2127801c 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1076,6 +1076,16 @@ TimelineModel::openRoomSettings(QString room_id) openRoomSettingsDialog(settings); } +void +TimelineModel::openInviteUsers(QString room_id) +{ + InviteesModel *model = new InviteesModel{this}; + connect(model, &InviteesModel::accept, this, [this, model, room_id]() { + manager_->inviteUsers(room_id, model->mxids()); + }); + openInviteUsersDialog(model); +} + void TimelineModel::replyAction(QString id) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 5730fbab..e5189e61 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -17,6 +17,7 @@ #include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" +#include "InviteesModel.h" #include "MemberList.h" #include "Permissions.h" #include "ui/RoomSettings.h" @@ -239,6 +240,7 @@ public: Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void openRoomMembers(); Q_INVOKABLE void openRoomSettings(QString room_id = QString()); + Q_INVOKABLE void openInviteUsers(QString room_id = QString()); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; @@ -357,6 +359,7 @@ signals: void openRoomMembersDialog(MemberList *members); void openRoomSettingsDialog(RoomSettings *settings); + void openInviteUsersDialog(InviteesModel *invitees); void newMessageToSend(mtx::events::collections::TimelineEvents event); void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 43b9a646..08b88efd 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -20,6 +20,7 @@ #include "DeviceVerificationFlow.h" #include "EventAccessors.h" #include "ImagePackModel.h" +#include "InviteesModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -184,6 +185,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Room Settings needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "InviteesModel", + "InviteesModel needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType( @@ -423,62 +430,13 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -void -TimelineViewManager::openInviteUsersDialog() -{ - MainWindow::instance()->openInviteUsersDialog( - [this](const QStringList &invitees) { emit inviteUsers(invitees); }); -} - -void -TimelineViewManager::openLink(QString link) const -{ - QUrl url(link); - if (url.scheme() == "https" && url.host() == "matrix.to") { - // handle matrix.to links internally - QString p = url.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray uri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("#")) { - QByteArray uri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("!")) { - QByteArray uri = "matrix:roomid/" + - QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } - } - } else { - QDesktopServices::openUrl(url); - } -} +//void +//TimelineViewManager::openInviteUsersDialog() +//{ + // TODO: move this somewhere where it will actually work (probably Rooms) +// MainWindow::instance()->openInviteUsersDialog( +// [this](const QStringList &invitees) { emit inviteUsers(invitees); }); +//} void TimelineViewManager::openLeaveRoomDialog(QString roomid) const -- cgit 1.5.1 From a76fc7d20058fa10e1345e0de0a1431a7161cd57 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 11 Jun 2021 18:53:32 -0400 Subject: Add a fancy loading spinner to the member list --- resources/qml/RoomMembers.qml | 7 +++++++ src/MemberList.cpp | 15 ++++++++++++--- src/MemberList.h | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index d31fe319..8060a6cc 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -109,6 +109,13 @@ ApplicationWindow { } } } + + footer: BusyIndicator { + // This is not a wonderful solution, but it is the best way to calculate whether + // all users are loaded while keeping canFetchMore() const + running: members.numUsersLoaded < members.memberCount + anchors.centerIn: parent + } } } } diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 2a9c3fbc..da4412d2 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -34,7 +34,10 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) } try { - addUsers(cache::getMembers(room_id_.toStdString())); + auto members = cache::getMembers(room_id_.toStdString()); + addUsers(members); + numUsersLoaded_ = members.size(); + emit numUsersLoadedChanged(); } catch (const lmdb::error &e) { nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); } @@ -83,11 +86,17 @@ bool MemberList::canFetchMore(const QModelIndex &) const { const size_t numMembers = rowCount(); - return (numMembers > 1 && numMembers < info_.member_count); + if (numMembers > 1 && numMembers < info_.member_count) + return true; + else + return false; } void MemberList::fetchMore(const QModelIndex &) { - addUsers(cache::getMembers(room_id_.toStdString(), rowCount())); + auto members = cache::getMembers(room_id_.toStdString(), rowCount()); + addUsers(members); + numUsersLoaded_ = members.size(); + emit numUsersLoadedChanged(); } diff --git a/src/MemberList.h b/src/MemberList.h index dbe69f4b..afc1a6e5 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -15,6 +15,7 @@ class MemberList : public QAbstractListModel Q_PROPERTY(size_t memberCount READ memberCount NOTIFY memberCountChanged) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) + Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) public: enum Roles @@ -37,12 +38,14 @@ public: size_t memberCount() const { return info_.member_count; } QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } QString roomId() const { return room_id_; } + int numUsersLoaded() const { return numUsersLoaded_; } signals: void roomNameChanged(); void memberCountChanged(); void avatarUrlChanged(); void roomIdChanged(); + void numUsersLoadedChanged(); public slots: void addUsers(const std::vector &users); @@ -55,4 +58,5 @@ private: QVector> m_memberList; QString room_id_; RoomInfo info_; + int numUsersLoaded_; }; -- cgit 1.5.1 From f0c88fc47435bda93a55271eb830bf80dc88443f Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 11 Jun 2021 19:18:04 -0400 Subject: Get member info loading working --- src/InviteesModel.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 849c5281..0081c7df 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -16,12 +16,10 @@ InviteesModel::addUser(QString mxid) auto invitee = new Invitee{mxid, this}; connect(invitee, &Invitee::userInfoLoaded, this, [this]() { - emit dataChanged(QModelIndex{}, QModelIndex{}); + endInsertRows(); }); invitees_.push_back(invitee); - - endInsertRows(); } QHash -- cgit 1.5.1 From a176de5f11f11bb3dad6cb8ea71a3b5edd6f27f4 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 11 Jun 2021 20:46:57 -0400 Subject: Make sure to use the default room id if none is specified --- resources/qml/InviteDialog.qml | 3 ++- src/timeline/TimelineModel.cpp | 6 +++--- src/timeline/TimelineModel.h | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index d5cc4c6d..278f772f 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -43,10 +43,11 @@ ApplicationWindow { RowLayout { spacing: 10 - TextField { + MatrixTextField { id: inviteeEntry placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") + backgroundColor: colors.window Layout.fillWidth: true onAccepted: if (text !== "") addInvite() } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 2127801c..ebbca6f4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1077,11 +1077,11 @@ TimelineModel::openRoomSettings(QString room_id) } void -TimelineModel::openInviteUsers(QString room_id) +TimelineModel::openInviteUsers(QString roomId) { InviteesModel *model = new InviteesModel{this}; - connect(model, &InviteesModel::accept, this, [this, model, room_id]() { - manager_->inviteUsers(room_id, model->mxids()); + connect(model, &InviteesModel::accept, this, [this, model, roomId]() { + manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); }); openInviteUsersDialog(model); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index e5189e61..b5144308 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -240,7 +240,7 @@ public: Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void openRoomMembers(); Q_INVOKABLE void openRoomSettings(QString room_id = QString()); - Q_INVOKABLE void openInviteUsers(QString room_id = QString()); + Q_INVOKABLE void openInviteUsers(QString roomId = QString()); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; -- cgit 1.5.1 From c566a6254138037d8916bedf9660a66f2d65187f Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 11 Jun 2021 20:48:28 -0400 Subject: Clean up code --- resources/qml/InviteDialog.qml | 13 ------------- src/InviteesModel.cpp | 4 +--- 2 files changed, 1 insertion(+), 16 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 9ace3246..d8e176e6 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -172,19 +172,6 @@ ApplicationWindow { } } } -// delegate: RowLayout { -// spacing: 10 - -// Avatar { -// url: model.avatarUrl -// width: 20 -// height: width -// } - -// Label { -// text: model.displayName + " (" + model.mxid + ")" -// } -// } } } diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 0081c7df..1da7baf4 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -15,9 +15,7 @@ InviteesModel::addUser(QString mxid) beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); auto invitee = new Invitee{mxid, this}; - connect(invitee, &Invitee::userInfoLoaded, this, [this]() { - endInsertRows(); - }); + connect(invitee, &Invitee::userInfoLoaded, this, [this]() { endInsertRows(); }); invitees_.push_back(invitee); } -- cgit 1.5.1 From 60b3c34d78120d10114fc14600b2b142bbc80362 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 12 Jun 2021 13:17:31 -0400 Subject: Permissions only needs a roomid to function --- src/timeline/Permissions.cpp | 6 +++--- src/timeline/Permissions.h | 4 ++-- src/timeline/TimelineModel.cpp | 1 + src/timeline/TimelineModel.h | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/timeline/Permissions.cpp b/src/timeline/Permissions.cpp index 1eaab468..e4957045 100644 --- a/src/timeline/Permissions.cpp +++ b/src/timeline/Permissions.cpp @@ -8,9 +8,9 @@ #include "MatrixClient.h" #include "TimelineModel.h" -Permissions::Permissions(TimelineModel *parent) +Permissions::Permissions(QString roomId, QObject *parent) : QObject(parent) - , room(parent) + , roomId_(roomId) { invalidate(); } @@ -19,7 +19,7 @@ void Permissions::invalidate() { pl = cache::client() - ->getStateEvent(room->roomId().toStdString()) + ->getStateEvent(roomId_.toStdString()) .value_or(mtx::events::StateEvent{}) .content; } diff --git a/src/timeline/Permissions.h b/src/timeline/Permissions.h index f7e6f389..7aab1ddb 100644 --- a/src/timeline/Permissions.h +++ b/src/timeline/Permissions.h @@ -15,7 +15,7 @@ class Permissions : public QObject Q_OBJECT public: - Permissions(TimelineModel *parent); + Permissions(QString roomId, QObject *parent = nullptr); Q_INVOKABLE bool canInvite(); Q_INVOKABLE bool canBan(); @@ -28,6 +28,6 @@ public: void invalidate(); private: - TimelineModel *room; + QString roomId_; mtx::events::state::PowerLevels pl; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ebbca6f4..516a499b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -318,6 +318,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , events(room_id.toStdString(), this) , room_id_(room_id) , manager_(manager) + , permissions_{room_id} { lastMessage_.timestamp = 0; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b5144308..ebf24bec 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -396,7 +396,7 @@ private: TimelineViewManager *manager_; InputBar input_{this}; - Permissions permissions_{this}; + Permissions permissions_; QTimer showEventTimer{this}; QString eventIdToShow; -- cgit 1.5.1 From db8af24bea5b46b76c0afd90813a4a29e13eea28 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 12 Jun 2021 22:00:14 -0400 Subject: Don't emit signal in constructor --- src/MemberList.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/MemberList.cpp b/src/MemberList.cpp index da4412d2..7f4dfa99 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -37,7 +37,6 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) auto members = cache::getMembers(room_id_.toStdString()); addUsers(members); numUsersLoaded_ = members.size(); - emit numUsersLoadedChanged(); } catch (const lmdb::error &e) { nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); } -- cgit 1.5.1 From d2d5229ede5124ba6cf9e85790dcd564faad00db Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 13:31:38 -0400 Subject: make lint --- resources/qml/InviteDialog.qml | 44 ++++++++++++++++++++++++-------- resources/qml/RoomMembers.qml | 4 +++ resources/qml/TimelineView.qml | 1 + src/ChatPage.cpp | 49 +++++++++++++++++++----------------- src/InviteesModel.cpp | 4 +++ src/InviteesModel.h | 4 +++ src/timeline/TimelineViewManager.cpp | 6 ++--- 7 files changed, 75 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 2932e398..ae74d3da 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 @@ -11,14 +15,11 @@ ApplicationWindow { property InviteesModel invitees function addInvite() { - if (inviteeEntry.text.match("@.+?:.{3,}")) - { + if (inviteeEntry.text.match("@.+?:.{3,}")) { invitees.addUser(inviteeEntry.text); inviteeEntry.clear(); - } - else - { - warningLabel.show() + } else { + warningLabel.show(); } } @@ -57,19 +58,29 @@ ApplicationWindow { placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") Layout.fillWidth: true - onAccepted: if (text !== "") addInvite() + onAccepted: { + if (text !== "") { + addInvite(); + } + } Component.onCompleted: forceActiveFocus() Shortcut { sequence: "Ctrl+Enter" onActivated: invitees.accept() } + } Button { text: qsTr("Add") - onClicked: if (inviteeEntry.text !== "") addInvite() + onClicked: { + if (inviteeEntry.text !== "") { + addInvite(); + } + } } + } Label { @@ -85,26 +96,28 @@ ApplicationWindow { visible: false opacity: 0 state: "hidden" - states: [ State { name: "shown" + PropertyChanges { target: warningLabel opacity: 1 visible: true } + }, State { name: "hidden" + PropertyChanges { target: warningLabel opacity: 0 visible: false } + } ] - transitions: [ Transition { from: "shown" @@ -122,7 +135,9 @@ ApplicationWindow { target: warningLabel property: "visible" } + } + } ] @@ -134,6 +149,7 @@ ApplicationWindow { running: false onTriggered: warningLabel.state = "hidden" } + } ListView { @@ -174,9 +190,13 @@ ApplicationWindow { Layout.fillHeight: true Layout.fillWidth: true } + } + } + } + } footer: DialogButtonBox { @@ -194,7 +214,9 @@ ApplicationWindow { Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole - onClicked: inviteDialogRoot.close(); + onClicked: inviteDialogRoot.close() } + } + } diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 8addd704..44b917b1 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index d515b9b4..148a5817 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -157,6 +157,7 @@ Item { Layout.alignment: Qt.AlignHCenter enabled: false } + MatrixText { text: parent.roomName font.pixelSize: 24 diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 8b4cfeef..70fd32fd 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -117,29 +117,32 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); // TODO: once this signal is moved, reenable this -// connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { -// const auto room_id = currentRoom().toStdString(); - -// for (int ii = 0; ii < users.size(); ++ii) { -// QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { -// const auto user = users.at(ii); - -// http::client()->invite_user( -// room_id, -// user.toStdString(), -// [this, user](const mtx::responses::RoomInvite &, -// mtx::http::RequestErr err) { -// if (err) { -// emit showNotification( -// tr("Failed to invite user: %1").arg(user)); -// return; -// } - -// emit showNotification(tr("Invited user: %1").arg(user)); -// }); -// }); -// } -// }); + // connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList + // users) { + // const auto room_id = currentRoom().toStdString(); + + // for (int ii = 0; ii < users.size(); ++ii) { + // QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { + // const auto user = users.at(ii); + + // http::client()->invite_user( + // room_id, + // user.toStdString(), + // [this, user](const mtx::responses::RoomInvite &, + // mtx::http::RequestErr err) { + // if (err) { + // emit showNotification( + // tr("Failed to invite user: + // %1").arg(user)); + // return; + // } + + // emit showNotification(tr("Invited user: + // %1").arg(user)); + // }); + // }); + // } + // }); connect( view_manager_, diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 1da7baf4..59054690 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #include "InviteesModel.h" #include "Cache.h" diff --git a/src/InviteesModel.h b/src/InviteesModel.h index 4bcc4e9d..ac9208a0 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #ifndef INVITEESMODEL_H #define INVITEESMODEL_H diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 08b88efd..8daa2124 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -430,10 +430,10 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -//void -//TimelineViewManager::openInviteUsersDialog() +// void +// TimelineViewManager::openInviteUsersDialog() //{ - // TODO: move this somewhere where it will actually work (probably Rooms) +// TODO: move this somewhere where it will actually work (probably Rooms) // MainWindow::instance()->openInviteUsersDialog( // [this](const QStringList &invitees) { emit inviteUsers(invitees); }); //} -- cgit 1.5.1 From 913d0fd13985712f48ee50dcdd46618606364f91 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 15:52:10 -0400 Subject: Make macOS CI happy (again) --- src/MemberList.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/MemberList.h b/src/MemberList.h index afc1a6e5..80ab834c 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -51,8 +51,8 @@ public slots: void addUsers(const std::vector &users); protected: - bool canFetchMore(const QModelIndex &) const; - void fetchMore(const QModelIndex &); + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; private: QVector> m_memberList; -- cgit 1.5.1 From baa9dfe110698a741eedc6209e33a4db687dffbe Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 16:49:34 -0400 Subject: Clean up code --- src/ChatPage.cpp | 28 ---------------------------- src/timeline/TimelineModel.cpp | 6 +++--- src/timeline/TimelineViewManager.cpp | 8 -------- 3 files changed, 3 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 70fd32fd..6b8c1e10 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -116,34 +116,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - // TODO: once this signal is moved, reenable this - // connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList - // users) { - // const auto room_id = currentRoom().toStdString(); - - // for (int ii = 0; ii < users.size(); ++ii) { - // QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - // const auto user = users.at(ii); - - // http::client()->invite_user( - // room_id, - // user.toStdString(), - // [this, user](const mtx::responses::RoomInvite &, - // mtx::http::RequestErr err) { - // if (err) { - // emit showNotification( - // tr("Failed to invite user: - // %1").arg(user)); - // return; - // } - - // emit showNotification(tr("Invited user: - // %1").arg(user)); - // }); - // }); - // } - // }); - connect( view_manager_, &TimelineViewManager::inviteUsers, diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 516a499b..7ce0e98a 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1074,7 +1074,7 @@ TimelineModel::openRoomSettings(QString room_id) { RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this); connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged); - openRoomSettingsDialog(settings); + emit openRoomSettingsDialog(settings); } void @@ -1082,9 +1082,9 @@ TimelineModel::openInviteUsers(QString roomId) { InviteesModel *model = new InviteesModel{this}; connect(model, &InviteesModel::accept, this, [this, model, roomId]() { - manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); + emit manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); }); - openInviteUsersDialog(model); + emit openInviteUsersDialog(model); } void diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 8daa2124..64493e5b 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -430,14 +430,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) }); } -// void -// TimelineViewManager::openInviteUsersDialog() -//{ -// TODO: move this somewhere where it will actually work (probably Rooms) -// MainWindow::instance()->openInviteUsersDialog( -// [this](const QStringList &invitees) { emit inviteUsers(invitees); }); -//} - void TimelineViewManager::openLeaveRoomDialog(QString roomid) const { -- cgit 1.5.1 From f30c3db86058f2700a1e2331bb1167d2ebf88f4d Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 17 Jul 2021 18:24:04 -0400 Subject: Fix loaded member count --- src/MemberList.cpp | 2 +- src/MemberList.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 7f4dfa99..b2ed82bd 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -96,6 +96,6 @@ MemberList::fetchMore(const QModelIndex &) { auto members = cache::getMembers(room_id_.toStdString(), rowCount()); addUsers(members); - numUsersLoaded_ = members.size(); + numUsersLoaded_ += members.size(); emit numUsersLoadedChanged(); } diff --git a/src/MemberList.h b/src/MemberList.h index 80ab834c..aa240b7a 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -58,5 +58,5 @@ private: QVector> m_memberList; QString room_id_; RoomInfo info_; - int numUsersLoaded_; + int numUsersLoaded_{0}; }; -- cgit 1.5.1 From 4384554587c3e9327382f2f9cbc36e893fbe4dab Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 19 Jul 2021 12:31:20 -0400 Subject: Only invite if there is something/someone to invite --- resources/qml/InviteDialog.qml | 81 +++--------------------------------------- src/InviteesModel.cpp | 2 ++ src/InviteesModel.h | 3 ++ 3 files changed, 9 insertions(+), 77 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 02cb5e07..94a95861 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -18,13 +18,11 @@ ApplicationWindow { if (inviteeEntry.text.match("@.+?:.{3,}")) { invitees.addUser(inviteeEntry.text); inviteeEntry.clear(); - } else { - warningLabel.show(); } } function cleanUpAndClose() { - if (inviteeEntry.text !== "") + if (inviteeEntry.text.match("@.+?:.{3,}")) addInvite(); invitees.accept(); close(); @@ -79,80 +77,8 @@ ApplicationWindow { Button { text: qsTr("Add") - onClicked: { - if (inviteeEntry.text !== "") { - addInvite(); - } - } - } - - } - - Label { - id: warningLabel - - function show() { - state = "shown"; - warningLabelTimer.start(); - } - - text: qsTr("Please enter a valid username (e.g. @joe:matrix.org).") - color: "red" - visible: false - opacity: 0 - state: "hidden" - states: [ - State { - name: "shown" - - PropertyChanges { - target: warningLabel - opacity: 1 - visible: true - } - - }, - State { - name: "hidden" - - PropertyChanges { - target: warningLabel - opacity: 0 - visible: false - } - - } - ] - transitions: [ - Transition { - from: "shown" - to: "hidden" - reversible: true - - SequentialAnimation { - NumberAnimation { - target: warningLabel - property: "opacity" - duration: 500 - } - - PropertyAction { - target: warningLabel - property: "visible" - } - - } - - } - ] - - Timer { - id: warningLabelTimer - - interval: 2000 - repeat: false - running: false - onTriggered: warningLabel.state = "hidden" + enabled: inviteeEntry.text.match("@.+?:.{3,}") + onClicked: addInvite() } } @@ -210,6 +136,7 @@ ApplicationWindow { Button { text: qsTr("Invite") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + enabled: invitees.count > 0 onClicked: cleanUpAndClose() } diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 59054690..9b64f57c 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -22,6 +22,8 @@ InviteesModel::addUser(QString mxid) connect(invitee, &Invitee::userInfoLoaded, this, [this]() { endInsertRows(); }); invitees_.push_back(invitee); + + emit countChanged(); } QHash diff --git a/src/InviteesModel.h b/src/InviteesModel.h index ac9208a0..a4e19ebb 100644 --- a/src/InviteesModel.h +++ b/src/InviteesModel.h @@ -30,6 +30,8 @@ class InviteesModel : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + public: enum Roles { @@ -52,6 +54,7 @@ public: signals: void accept(); + void countChanged(); private: QVector invitees_; -- cgit 1.5.1 From 10c6f2b43fa88830e4a693e8a71734ab90c053c2 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 19 Jul 2021 12:33:54 -0400 Subject: Use Nico's favored formatting (*shrugs*) --- src/MemberList.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/MemberList.cpp b/src/MemberList.cpp index b2ed82bd..dd5997f5 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -60,7 +60,7 @@ MemberList::addUsers(const std::vector &members) QHash MemberList::roleNames() const { - return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"}}; + return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"},}; } QVariant -- cgit 1.5.1 From 48669302ece255010f31150543d8e0ef4830fb74 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 19 Jul 2021 14:31:08 -0400 Subject: make lint --- resources/qml/InviteDialog.qml | 13 ++++++++----- resources/qml/RoomMembers.qml | 8 +++++++- resources/qml/Root.qml | 3 +++ src/MemberList.cpp | 6 +++++- 4 files changed, 23 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 94a95861..b1b1bb39 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -24,6 +24,7 @@ ApplicationWindow { function cleanUpAndClose() { if (inviteeEntry.text.match("@.+?:.{3,}")) addInvite(); + invitees.accept(); close(); } @@ -64,15 +65,17 @@ ApplicationWindow { placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.") Layout.fillWidth: true onAccepted: { - if (text !== "") { + if (text !== "") addInvite(); - } + } Component.onCompleted: forceActiveFocus() - Keys.onShortcutOverride: event.accepted = ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers & Qt.ControlModifier)) - Keys.onPressed: if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers === Qt.ControlModifier)) cleanUpAndClose() - + Keys.onPressed: { + if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers === Qt.ControlModifier)) { + cleanUpAndClose(); + } + } } Button { diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 0d957165..57353132 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -2,12 +2,12 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import "./ui" import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Window 2.12 import im.nheko 1.0 -import "./ui" ApplicationWindow { id: roomMembersRoot @@ -110,7 +110,9 @@ ApplicationWindow { Layout.fillHeight: true Layout.fillWidth: true } + } + } footer: Spinner { @@ -119,12 +121,16 @@ ApplicationWindow { height: visible ? undefined : 0 anchors.centerIn: parent } + } + } + } footer: DialogButtonBox { standardButtons: DialogButtonBox.Ok onAccepted: roomMembersRoot.close() } + } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 31d00585..102d0411 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -52,6 +52,7 @@ Page { RoomMembers { } + } Component { @@ -77,11 +78,13 @@ Page { } } + Component { id: inviteDialog InviteDialog { } + } Shortcut { diff --git a/src/MemberList.cpp b/src/MemberList.cpp index dd5997f5..04377a0f 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -60,7 +60,11 @@ MemberList::addUsers(const std::vector &members) QHash MemberList::roleNames() const { - return {{Mxid, "mxid"}, {DisplayName, "displayName"}, {AvatarUrl, "avatarUrl"},}; + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + }; } QVariant -- cgit 1.5.1 From a9ed83a1cef0b2ac156e784276ccd01dc2fbf7e8 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 19 Jul 2021 14:34:04 -0400 Subject: Remove size_t property stuff --- src/MemberList.h | 4 ++-- src/main.cpp | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/MemberList.h b/src/MemberList.h index aa240b7a..cc3b75f7 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -12,7 +12,7 @@ class MemberList : public QAbstractListModel Q_OBJECT Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) - Q_PROPERTY(size_t memberCount READ memberCount NOTIFY memberCountChanged) + Q_PROPERTY(int memberCount READ memberCount NOTIFY memberCountChanged) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) @@ -35,7 +35,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QString roomName() const { return QString::fromStdString(info_.name); } - size_t memberCount() const { return info_.member_count; } + int memberCount() const { return info_.member_count; } QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } QString roomId() const { return room_id_; } int numUsersLoaded() const { return numUsersLoaded_; } diff --git a/src/main.cpp b/src/main.cpp index 376fc4f5..29e93d49 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -205,9 +205,6 @@ main(int argc, char *argv[]) parser.process(app); - // make sure that size_t properties will work - qRegisterMetaType("size_t"); - app.setWindowIcon(QIcon::fromTheme("nheko", QIcon{":/logos/nheko.png"})); http::init(); -- cgit 1.5.1 From 21eb312f69111b8141e2ac9da5bb3871287045bd Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Mon, 19 Jul 2021 14:49:57 -0400 Subject: Only run spinner while loading members --- resources/qml/RoomMembers.qml | 2 +- src/MemberList.cpp | 6 ++++++ src/MemberList.h | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 57353132..b190be07 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -116,7 +116,7 @@ ApplicationWindow { } footer: Spinner { - visible: members.numUsersLoaded < members.memberCount + visible: members.numUsersLoaded < members.memberCount && members.loadingMoreMembers // use the default height if it's visible, otherwise no height at all height: visible ? undefined : 0 anchors.centerIn: parent diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 04377a0f..415e3b57 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -98,8 +98,14 @@ MemberList::canFetchMore(const QModelIndex &) const void MemberList::fetchMore(const QModelIndex &) { + loadingMoreMembers_ = true; + emit loadingMoreMembersChanged(); + auto members = cache::getMembers(room_id_.toStdString(), rowCount()); addUsers(members); numUsersLoaded_ += members.size(); emit numUsersLoadedChanged(); + + loadingMoreMembers_ = false; + emit loadingMoreMembersChanged(); } diff --git a/src/MemberList.h b/src/MemberList.h index cc3b75f7..070666a2 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -16,6 +16,7 @@ class MemberList : public QAbstractListModel Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(QString roomId READ roomId NOTIFY roomIdChanged) Q_PROPERTY(int numUsersLoaded READ numUsersLoaded NOTIFY numUsersLoadedChanged) + Q_PROPERTY(bool loadingMoreMembers READ loadingMoreMembers NOTIFY loadingMoreMembersChanged) public: enum Roles @@ -39,6 +40,7 @@ public: QString avatarUrl() const { return QString::fromStdString(info_.avatar_url); } QString roomId() const { return room_id_; } int numUsersLoaded() const { return numUsersLoaded_; } + bool loadingMoreMembers() const { return loadingMoreMembers_; } signals: void roomNameChanged(); @@ -46,6 +48,7 @@ signals: void avatarUrlChanged(); void roomIdChanged(); void numUsersLoadedChanged(); + void loadingMoreMembersChanged(); public slots: void addUsers(const std::vector &users); @@ -59,4 +62,5 @@ private: QString room_id_; RoomInfo info_; int numUsersLoaded_{0}; + bool loadingMoreMembers_{false}; }; -- cgit 1.5.1 From 44d2818e0cb2bceb25e29edf7ef32d02ede42432 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 20 Jul 2021 19:17:20 -0400 Subject: Add property for plain room name --- resources/qml/InviteDialog.qml | 4 ++-- resources/qml/Root.qml | 2 +- src/timeline/TimelineModel.cpp | 3 +++ src/timeline/TimelineModel.h | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 026e3297..e9ff475d 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -11,7 +11,7 @@ ApplicationWindow { id: inviteDialogRoot property string roomId - property string roomName + property string plainRoomName property InviteesModel invitees function addInvite() { @@ -29,7 +29,7 @@ ApplicationWindow { close(); } - title: qsTr("Invite users to ") + roomName + title: qsTr("Invite users to ") + plainRoomName x: MainWindow.x + (MainWindow.width / 2) - (width / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2) height: 380 diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index b5395232..f71c18e2 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -150,7 +150,7 @@ Page { onOpenInviteUsersDialog: { var dialog = inviteDialog.createObject(timelineRoot, { "roomId": Rooms.currentRoom.roomId, - "roomName": Rooms.currentRoom.roomName, + "plainRoomName": Rooms.currentRoom.plainRoomName, "invitees": invitees }); dialog.show(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 7ce0e98a..e431e1ac 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -327,6 +327,9 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj this->isSpace_ = create->content.type == mtx::events::state::room_type::space; this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); + // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it needs to be + connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged); + connect( this, &TimelineModel::redactionFailed, diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index ebf24bec..0d1eb1f9 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -162,6 +162,7 @@ class TimelineModel : public QAbstractListModel bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) + Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) @@ -367,6 +368,7 @@ signals: void encryptionChanged(); void roomNameChanged(); + void plainRoomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); void roomMemberCountChanged(); -- cgit 1.5.1 From 77c636f3d3f0bc11c2fc5e6cc90ee58bd42ab5f9 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 20 Jul 2021 19:20:55 -0400 Subject: Insert user before loading avatar/display name --- src/InviteesModel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 9b64f57c..7bc2b2d4 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -19,10 +19,12 @@ InviteesModel::addUser(QString mxid) beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); auto invitee = new Invitee{mxid, this}; - connect(invitee, &Invitee::userInfoLoaded, this, [this]() { endInsertRows(); }); + auto indexOfInvitee = invitees_.count(); + connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); }); invitees_.push_back(invitee); + endInsertRows(); emit countChanged(); } -- cgit 1.5.1 From efda94ca50c3942d0eb3165ce30490edd4823462 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 20 Jul 2021 19:21:04 -0400 Subject: Modify message to be more accurate --- src/InviteesModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index 7bc2b2d4..f73fddd9 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -69,7 +69,8 @@ Invitee::Invitee(const QString &mxid, QObject *parent) mxid_.toStdString(), [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { if (err) { - nhlog::net()->warn("failed to retrieve own profile info"); + nhlog::net()->warn("failed to retrieve profile info"); + emit userInfoLoaded(); return; } -- cgit 1.5.1 From 6458614ea11d6ce25936e7ca1ac550eedfa9942a Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 20 Jul 2021 19:41:12 -0400 Subject: make lint --- src/InviteesModel.cpp | 6 ++++-- src/timeline/TimelineModel.cpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/InviteesModel.cpp b/src/InviteesModel.cpp index f73fddd9..27b2116f 100644 --- a/src/InviteesModel.cpp +++ b/src/InviteesModel.cpp @@ -18,9 +18,11 @@ InviteesModel::addUser(QString mxid) { beginInsertRows(QModelIndex(), invitees_.count(), invitees_.count()); - auto invitee = new Invitee{mxid, this}; + auto invitee = new Invitee{mxid, this}; auto indexOfInvitee = invitees_.count(); - connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); }); + connect(invitee, &Invitee::userInfoLoaded, this, [this, indexOfInvitee]() { + emit dataChanged(index(indexOfInvitee), index(indexOfInvitee)); + }); invitees_.push_back(invitee); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index e431e1ac..66d931fd 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -327,7 +327,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj this->isSpace_ = create->content.type == mtx::events::state::room_type::space; this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it needs to be + // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it + // needs to be connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged); connect( -- cgit 1.5.1 From 8a300f733303bac95c3c41839bc553d1cfd2786f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 21 Jul 2021 16:55:28 +0200 Subject: Fix some media not being cached properly Most noticeable in the goose chooser. --- src/MxcImageProvider.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index ab6540a4..ab0f8152 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -196,7 +196,6 @@ MxcImageProvider::download(const QString &id, image.setText("original filename", QString::fromStdString(originalFilename)); image.setText("mxc url", "mxc://" + id); - image.save(fileInfo.absoluteFilePath()); then(id, requestedSize, image, fileInfo.absoluteFilePath()); }); } catch (std::exception &e) { -- cgit 1.5.1 From f401ed16ace26dbe397fa2dce59fbb2f455a35f7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 21 Jul 2021 17:44:29 +0200 Subject: Tweak rainbows slightly --- src/Utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/Utils.cpp b/src/Utils.cpp index 8d5ae4a9..484b45cf 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -556,7 +556,7 @@ utils::markdownToHtml(const QString &text, bool rainbowify) // Use colors as described here: // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html auto color = - QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 1.0, 0.5); + QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5); // format color for HTML auto colorString = color.name(QColor::NameFormat::HexRgb); // create HTML element for current char -- cgit 1.5.1 From b17002929c2e968835b510bd47c02d9df2461bc3 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 21 Jul 2021 10:08:04 -0400 Subject: Open room members when member info label clicked --- resources/qml/RoomSettings.qml | 12 +++++++++++- src/MemberList.cpp | 2 +- src/MemberList.h | 2 +- src/timeline/TimelineModel.cpp | 4 ++-- src/timeline/TimelineModel.h | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 2701edf9..8746d4d3 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -4,7 +4,7 @@ import "./ui" import Qt.labs.platform 1.1 as Platform -import QtQuick 2.9 +import QtQuick 2.15 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 @@ -105,6 +105,16 @@ ApplicationWindow { MatrixText { text: qsTr("%1 member(s)").arg(roomSettings.memberCount) Layout.alignment: Qt.AlignHCenter + + TapHandler { + onTapped: Rooms.currentRoom.openRoomMembers(roomSettings.roomId) + } + + CursorShape { + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + } + } } diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 415e3b57..0ef3b696 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -22,7 +22,7 @@ #include "timeline/TimelineViewManager.h" #include "ui/Avatar.h" -MemberList::MemberList(const QString &room_id, QWidget *parent) +MemberList::MemberList(const QString &room_id, QObject *parent) : QAbstractListModel{parent} , room_id_{room_id} { diff --git a/src/MemberList.h b/src/MemberList.h index 070666a2..9932f6a4 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -25,7 +25,7 @@ public: DisplayName, AvatarUrl, }; - MemberList(const QString &room_id, QWidget *parent = nullptr); + MemberList(const QString &room_id, QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 66d931fd..e9fa4a05 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1067,9 +1067,9 @@ TimelineModel::openUserProfile(QString userid) } void -TimelineModel::openRoomMembers() +TimelineModel::openRoomMembers(QString room_id) { - MemberList *memberList = new MemberList(roomId()); + MemberList *memberList = new MemberList(room_id == QString() ? roomId() : room_id, this); emit openRoomMembersDialog(memberList); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0d1eb1f9..077245cb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -239,7 +239,7 @@ public: Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid); - Q_INVOKABLE void openRoomMembers(); + Q_INVOKABLE void openRoomMembers(QString room_id = QString()); Q_INVOKABLE void openRoomSettings(QString room_id = QString()); Q_INVOKABLE void openInviteUsers(QString roomId = QString()); Q_INVOKABLE void editAction(QString id); -- cgit 1.5.1 From 44be4c1f4a4e1ef60fbb6c1f51adc93e5a555f14 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 21 Jul 2021 18:56:20 -0400 Subject: Move various room auxiliary functions to TimelineManager --- resources/qml/RoomMembers.qml | 5 ++--- resources/qml/RoomSettings.qml | 2 +- resources/qml/Root.qml | 2 +- resources/qml/TopBar.qml | 10 +++++----- src/timeline/TimelineModel.cpp | 25 ------------------------- src/timeline/TimelineModel.h | 7 ------- src/timeline/TimelineViewManager.cpp | 28 ++++++++++++++++++++++++++++ src/timeline/TimelineViewManager.h | 7 +++++++ 8 files changed, 44 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 3758cb0b..5bf847ca 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -41,7 +41,7 @@ ApplicationWindow { displayName: members.roomName Layout.alignment: Qt.AlignHCenter url: members.avatarUrl.replace("mxc://", "image://MxcImage/") - onClicked: Rooms.currentRoom.openRoomSettings(members.roomId) + onClicked: TimelineManager.openRoomSettings(members.roomId) } ElidedLabel { @@ -57,7 +57,7 @@ ApplicationWindow { hoverEnabled: true ToolTip.visible: hovered ToolTip.text: qsTr("Invite more people") - onClicked: Rooms.currentRoom.openInviteUsers() + onClicked: TimelineManager.openInviteUsers(members.roomId) } ScrollView { @@ -121,7 +121,6 @@ ApplicationWindow { footer: Item { width: parent.width visible: (members.numUsersLoaded < members.memberCount) && members.loadingMoreMembers - // use the default height if it's visible, otherwise no height at all height: membersLoadingSpinner.height anchors.margins: Nheko.paddingMedium diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 8746d4d3..b4936f3e 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -107,7 +107,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter TapHandler { - onTapped: Rooms.currentRoom.openRoomMembers(roomSettings.roomId) + onTapped: TimelineManager.openRoomMembers(roomSettings.roomId) } CursorShape { diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index f71c18e2..8e226639 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -133,7 +133,7 @@ Page { } Connections { - target: Rooms.currentRoom + target: TimelineManager onOpenRoomMembersDialog: { var membersDialog = roomMembersComponent.createObject(timelineRoot, { "members": members, diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 48491f84..8543d02a 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -24,7 +24,7 @@ Rectangle { TapHandler { onSingleTapped: { if (room) - room.openRoomSettings(); + TimelineManager.openRoomSettings(room.roomId); eventPoint.accepted = true; } @@ -66,7 +66,7 @@ Rectangle { displayName: roomName onClicked: { if (room) - room.openRoomSettings(); + TimelineManager.openRoomSettings(room.roomId); } } @@ -111,12 +111,12 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: Rooms.currentRoom.openInviteUsers() + onTriggered: TimelineManager.openInviteUsers(room.roomId) } Platform.MenuItem { text: qsTr("Members") - onTriggered: Rooms.currentRoom.openRoomMembers() + onTriggered: TimelineManager.openRoomMembers(room.roomId) } Platform.MenuItem { @@ -126,7 +126,7 @@ Rectangle { Platform.MenuItem { text: qsTr("Settings") - onTriggered: room.openRoomSettings() + onTriggered: TimelineManager.openRoomSettings(room.roomId) } } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index e9fa4a05..ee5564a5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1066,31 +1066,6 @@ TimelineModel::openUserProfile(QString userid) emit manager_->openProfile(userProfile); } -void -TimelineModel::openRoomMembers(QString room_id) -{ - MemberList *memberList = new MemberList(room_id == QString() ? roomId() : room_id, this); - emit openRoomMembersDialog(memberList); -} - -void -TimelineModel::openRoomSettings(QString room_id) -{ - RoomSettings *settings = new RoomSettings(room_id == QString() ? roomId() : room_id, this); - connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged); - emit openRoomSettingsDialog(settings); -} - -void -TimelineModel::openInviteUsers(QString roomId) -{ - InviteesModel *model = new InviteesModel{this}; - connect(model, &InviteesModel::accept, this, [this, model, roomId]() { - emit manager_->inviteUsers(roomId == QString() ? room_id_ : roomId, model->mxids()); - }); - emit openInviteUsersDialog(model); -} - void TimelineModel::replyAction(QString id) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 077245cb..0e2ce153 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -239,9 +239,6 @@ public: Q_INVOKABLE void forwardMessage(QString eventId, QString roomId); Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid); - Q_INVOKABLE void openRoomMembers(QString room_id = QString()); - Q_INVOKABLE void openRoomSettings(QString room_id = QString()); - Q_INVOKABLE void openInviteUsers(QString roomId = QString()); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; @@ -358,10 +355,6 @@ signals: void lastMessageChanged(); void notificationsChanged(); - void openRoomMembersDialog(MemberList *members); - void openRoomSettingsDialog(RoomSettings *settings); - void openInviteUsersDialog(InviteesModel *invitees); - void newMessageToSend(mtx::events::collections::TimelineEvents event); void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); void updateFlowEventId(std::string event_id); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 64493e5b..b1643798 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -351,6 +351,34 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par &TimelineViewManager::openImageOverlayInternal); } +void +TimelineViewManager::openRoomMembers(QString room_id) +{ + MemberList *memberList = new MemberList(room_id, this); + emit openRoomMembersDialog(memberList); +} + +void +TimelineViewManager::openRoomSettings(QString room_id) +{ + RoomSettings *settings = new RoomSettings(room_id, this); + connect(rooms_->getRoomById(room_id).data(), + &TimelineModel::roomAvatarUrlChanged, + settings, + &RoomSettings::avatarChanged); + emit openRoomSettingsDialog(settings); +} + +void +TimelineViewManager::openInviteUsers(QString roomId) +{ + InviteesModel *model = new InviteesModel{this}; + connect(model, &InviteesModel::accept, this, [this, model, roomId]() { + emit inviteUsers(roomId, model->mxids()); + }); + emit openInviteUsersDialog(model); +} + void TimelineViewManager::setVideoCallItem() { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 945ba2d5..374685e3 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -64,6 +64,10 @@ public: Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; + Q_INVOKABLE void openRoomMembers(QString room_id); + Q_INVOKABLE void openRoomSettings(QString room_id); + Q_INVOKABLE void openInviteUsers(QString roomId); + Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); @@ -85,6 +89,9 @@ signals: void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); + void openRoomMembersDialog(MemberList *members); + void openRoomSettingsDialog(RoomSettings *settings); + void openInviteUsersDialog(InviteesModel *invitees); void openProfile(UserProfile *profile); public slots: -- cgit 1.5.1 From bbecadf1a8c3d78609a6112a879288f841b592f4 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Wed, 21 Jul 2021 20:38:18 -0400 Subject: Add functionality for loading global user profile where needed This is so viewing profiles from the invite dialog will work as expected. --- resources/qml/InviteDialog.qml | 2 +- src/timeline/TimelineViewManager.cpp | 6 ++++++ src/timeline/TimelineViewManager.h | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml index 5aaf0d6d..50287ad5 100644 --- a/resources/qml/InviteDialog.qml +++ b/resources/qml/InviteDialog.qml @@ -107,7 +107,7 @@ ApplicationWindow { userid: model.mxid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: model.displayName - onClicked: Rooms.currentRoom.openUserProfile(model.mxid) + onClicked: TimelineManager.openGlobalUserProfile(model.mxid) } ColumnLayout { diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index b1643798..da3ba282 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -379,6 +379,12 @@ TimelineViewManager::openInviteUsers(QString roomId) emit openInviteUsersDialog(model); } +void TimelineViewManager::openGlobalUserProfile(QString userId) +{ + UserProfile *profile = new UserProfile{QString{}, userId, this}; + emit openProfile(profile); +} + void TimelineViewManager::setVideoCallItem() { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 374685e3..bfc116b1 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -67,6 +67,7 @@ public: Q_INVOKABLE void openRoomMembers(QString room_id); Q_INVOKABLE void openRoomSettings(QString room_id); Q_INVOKABLE void openInviteUsers(QString roomId); + Q_INVOKABLE void openGlobalUserProfile(QString userId); Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; -- cgit 1.5.1 From 5ae73bf8fd65c9d5897b7b87bb41513dc37171a3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 22 Jul 2021 12:00:14 +0200 Subject: Add trailing newline to session export to make gomuks happy --- src/UserSettingsPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index ffaebe61..a062780a 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -1399,7 +1399,7 @@ UserSettingsPage::exportSessionKeys() QString suffix("-----END MEGOLM SESSION DATA-----"); QString newline("\n"); QTextStream out(&file); - out << prefix << newline << b64 << newline << suffix; + out << prefix << newline << b64 << newline << suffix << newline; file.close(); } catch (const std::exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); -- cgit 1.5.1 From 0ce7d02abed2f687a1a9795da8ab8e2ec96b0d65 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 22 Jul 2021 07:55:12 -0400 Subject: make lint --- resources/qml/MessageView.qml | 1 + src/timeline/TimelineViewManager.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index d2c97d57..50cbd371 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -493,6 +493,7 @@ ScrollView { foreground: Nheko.colors.mid z: 3 } + } } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index da3ba282..c08cfd53 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -379,7 +379,8 @@ TimelineViewManager::openInviteUsers(QString roomId) emit openInviteUsersDialog(model); } -void TimelineViewManager::openGlobalUserProfile(QString userId) +void +TimelineViewManager::openGlobalUserProfile(QString userId) { UserProfile *profile = new UserProfile{QString{}, userId, this}; emit openProfile(profile); -- cgit 1.5.1 From 0c798554b54e25e03cd7a211f278362bd3dc2630 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 21 Jul 2021 01:03:38 +0200 Subject: Rename image pack model --- CMakeLists.txt | 4 +- src/CombinedImagePackModel.cpp | 76 ++++++++++++++++++++++++++++++++++++ src/CombinedImagePackModel.h | 48 +++++++++++++++++++++++ src/ImagePackModel.cpp | 74 ----------------------------------- src/ImagePackModel.h | 48 ----------------------- src/timeline/InputBar.cpp | 4 +- src/timeline/InputBar.h | 4 +- src/timeline/TimelineViewManager.cpp | 6 +-- 8 files changed, 133 insertions(+), 131 deletions(-) create mode 100644 src/CombinedImagePackModel.cpp create mode 100644 src/CombinedImagePackModel.h delete mode 100644 src/ImagePackModel.cpp delete mode 100644 src/ImagePackModel.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index f77d9978..b802d37c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -354,7 +354,7 @@ set(SRC_FILES src/Olm.cpp src/RegisterPage.cpp src/SSOHandler.cpp - src/ImagePackModel.cpp + src/CombinedImagePackModel.cpp src/TrayIcon.cpp src/UserSettingsPage.cpp src/UsersModel.cpp @@ -558,7 +558,7 @@ qt5_wrap_cpp(MOC_HEADERS src/MxcImageProvider.h src/RegisterPage.h src/SSOHandler.h - src/ImagePackModel.h + src/CombinedImagePackModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp new file mode 100644 index 00000000..c5b5b886 --- /dev/null +++ b/src/CombinedImagePackModel.cpp @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CombinedImagePackModel.h" + +#include "Cache_p.h" +#include "CompletionModelRoles.h" + +CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, + bool stickers, + QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + auto packs = cache::client()->getImagePacks(room_id, stickers); + + for (const auto &pack : packs) { + QString packname = QString::fromStdString(pack.packname); + + for (const auto &img : pack.images) { + ImageDesc i{}; + i.shortcode = QString::fromStdString(img.first); + i.packname = packname; + i.image = img.second; + images.push_back(std::move(i)); + } + } +} + +int +CombinedImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)images.size(); +} + +QHash +CombinedImagePackModel::roleNames() const +{ + return { + {CompletionModel::CompletionRole, "completionRole"}, + {CompletionModel::SearchRole, "searchRole"}, + {CompletionModel::SearchRole2, "searchRole2"}, + {Roles::Url, "url"}, + {Roles::ShortCode, "shortcode"}, + {Roles::Body, "body"}, + {Roles::PackName, "packname"}, + {Roles::OriginalRow, "originalRow"}, + }; +} + +QVariant +CombinedImagePackModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + switch (role) { + case CompletionModel::CompletionRole: + return QString::fromStdString(images[index.row()].image.url); + case Roles::Url: + return QString::fromStdString(images[index.row()].image.url); + case CompletionModel::SearchRole: + case Roles::ShortCode: + return images[index.row()].shortcode; + case CompletionModel::SearchRole2: + case Roles::Body: + return QString::fromStdString(images[index.row()].image.body); + case Roles::PackName: + return images[index.row()].packname; + case Roles::OriginalRow: + return index.row(); + default: + return {}; + } + } + return {}; +} diff --git a/src/CombinedImagePackModel.h b/src/CombinedImagePackModel.h new file mode 100644 index 00000000..f0f69799 --- /dev/null +++ b/src/CombinedImagePackModel.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +class CombinedImagePackModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + PackName, + OriginalRow, + }; + + CombinedImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + mtx::events::msc2545::PackImage imageAt(int row) + { + if (row < 0 || static_cast(row) >= images.size()) + return {}; + return images.at(static_cast(row)).image; + } + +private: + std::string room_id; + + struct ImageDesc + { + QString shortcode; + QString packname; + + mtx::events::msc2545::PackImage image; + }; + + std::vector images; +}; diff --git a/src/ImagePackModel.cpp b/src/ImagePackModel.cpp deleted file mode 100644 index 9b0dca8d..00000000 --- a/src/ImagePackModel.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "ImagePackModel.h" - -#include "Cache_p.h" -#include "CompletionModelRoles.h" - -ImagePackModel::ImagePackModel(const std::string &roomId, bool stickers, QObject *parent) - : QAbstractListModel(parent) - , room_id(roomId) -{ - auto packs = cache::client()->getImagePacks(room_id, stickers); - - for (const auto &pack : packs) { - QString packname = QString::fromStdString(pack.packname); - - for (const auto &img : pack.images) { - ImageDesc i{}; - i.shortcode = QString::fromStdString(img.first); - i.packname = packname; - i.image = img.second; - images.push_back(std::move(i)); - } - } -} - -int -ImagePackModel::rowCount(const QModelIndex &) const -{ - return (int)images.size(); -} - -QHash -ImagePackModel::roleNames() const -{ - return { - {CompletionModel::CompletionRole, "completionRole"}, - {CompletionModel::SearchRole, "searchRole"}, - {CompletionModel::SearchRole2, "searchRole2"}, - {Roles::Url, "url"}, - {Roles::ShortCode, "shortcode"}, - {Roles::Body, "body"}, - {Roles::PackName, "packname"}, - {Roles::OriginalRow, "originalRow"}, - }; -} - -QVariant -ImagePackModel::data(const QModelIndex &index, int role) const -{ - if (hasIndex(index.row(), index.column(), index.parent())) { - switch (role) { - case CompletionModel::CompletionRole: - return QString::fromStdString(images[index.row()].image.url); - case Roles::Url: - return QString::fromStdString(images[index.row()].image.url); - case CompletionModel::SearchRole: - case Roles::ShortCode: - return images[index.row()].shortcode; - case CompletionModel::SearchRole2: - case Roles::Body: - return QString::fromStdString(images[index.row()].image.body); - case Roles::PackName: - return images[index.row()].packname; - case Roles::OriginalRow: - return index.row(); - default: - return {}; - } - } - return {}; -} diff --git a/src/ImagePackModel.h b/src/ImagePackModel.h deleted file mode 100644 index 937014ec..00000000 --- a/src/ImagePackModel.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include - -class ImagePackModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum Roles - { - Url = Qt::UserRole, - ShortCode, - Body, - PackName, - OriginalRow, - }; - - ImagePackModel(const std::string &roomId, bool stickers, QObject *parent = nullptr); - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - - mtx::events::msc2545::PackImage imageAt(int row) - { - if (row < 0 || static_cast(row) >= images.size()) - return {}; - return images.at(static_cast(row)).image; - } - -private: - std::string room_id; - - struct ImageDesc - { - QString shortcode; - QString packname; - - mtx::events::msc2545::PackImage image; - }; - - std::vector images; -}; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 56d0d1ce..f17081e5 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -19,9 +19,9 @@ #include "Cache.h" #include "ChatPage.h" +#include "CombinedImagePackModel.h" #include "CompletionProxyModel.h" #include "Config.h" -#include "ImagePackModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -503,7 +503,7 @@ InputBar::video(const QString &filename, } void -InputBar::sticker(ImagePackModel *model, int row) +InputBar::sticker(CombinedImagePackModel *model, int row) { if (!model || row < 0) return; diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index acedceb7..2e6fb5c0 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -12,7 +12,7 @@ #include class TimelineModel; -class ImagePackModel; +class CombinedImagePackModel; class QMimeData; class QDropEvent; class QStringList; @@ -58,7 +58,7 @@ public slots: MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); void reaction(const QString &reactedEvent, const QString &reactionKey); - void sticker(ImagePackModel *model, int row); + void sticker(CombinedImagePackModel *model, int row); private slots: void startTyping(); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index b1643798..2da7d789 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -15,11 +15,11 @@ #include "ChatPage.h" #include "Clipboard.h" #include "ColorImageProvider.h" +#include "CombinedImagePackModel.h" #include "CompletionProxyModel.h" #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" #include "EventAccessors.h" -#include "ImagePackModel.h" #include "InviteesModel.h" #include "Logging.h" #include "MainWindow.h" @@ -146,7 +146,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", @@ -622,7 +622,7 @@ TimelineViewManager::completerFor(QString completerName, QString roomId) roomModel->setParent(proxy); return proxy; } else if (completerName == "stickers") { - auto stickerModel = new ImagePackModel(roomId.toStdString(), true); + auto stickerModel = new CombinedImagePackModel(roomId.toStdString(), true); auto proxy = new CompletionProxyModel(stickerModel, 1, static_cast(-1) / 4); stickerModel->setParent(proxy); return proxy; -- cgit 1.5.1 From eafbab6ae128dd5b14f145ba9214a6673e982951 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 21 Jul 2021 13:37:57 +0200 Subject: Add menu to enable or disable stickers globally --- CMakeLists.txt | 4 + resources/qml/RoomSettings.qml | 11 + resources/qml/Root.qml | 15 ++ resources/qml/dialogs/ImagePackSettingsDialog.qml | 309 ++++++++++++++++++++++ resources/res.qrc | 1 + src/Cache.cpp | 33 ++- src/CacheStructs.h | 5 +- src/Cache_p.h | 9 +- src/CombinedImagePackModel.cpp | 5 +- src/ImagePackListModel.cpp | 76 ++++++ src/ImagePackListModel.h | 37 +++ src/SingleImagePackModel.cpp | 100 +++++++ src/SingleImagePackModel.h | 61 +++++ src/timeline/TimelineViewManager.cpp | 20 ++ src/timeline/TimelineViewManager.h | 3 + src/ui/RoomSettings.h | 2 +- 16 files changed, 674 insertions(+), 17 deletions(-) create mode 100644 resources/qml/dialogs/ImagePackSettingsDialog.qml create mode 100644 src/ImagePackListModel.cpp create mode 100644 src/ImagePackListModel.h create mode 100644 src/SingleImagePackModel.cpp create mode 100644 src/SingleImagePackModel.h (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index b802d37c..90ad1276 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -355,6 +355,8 @@ set(SRC_FILES src/RegisterPage.cpp src/SSOHandler.cpp src/CombinedImagePackModel.cpp + src/SingleImagePackModel.cpp + src/ImagePackListModel.cpp src/TrayIcon.cpp src/UserSettingsPage.cpp src/UsersModel.cpp @@ -559,6 +561,8 @@ qt5_wrap_cpp(MOC_HEADERS src/RegisterPage.h src/SSOHandler.h src/CombinedImagePackModel.h + src/SingleImagePackModel.h + src/ImagePackListModel.h src/TrayIcon.h src/UserSettingsPage.h src/UsersModel.h diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 11b9fa2a..accb5637 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -249,6 +249,17 @@ ApplicationWindow { Layout.alignment: Qt.AlignRight } + MatrixText { + text: qsTr("Sticker & Emote Settings") + } + + Button { + text: qsTr("Change") + ToolTip.text: qsTr("Change what packs are enabled, remove packs or create new ones") + onClicked: TimelineManager.openImagePackSettings(roomSettings.roomId) + Layout.alignment: Qt.AlignRight + } + Item { // for adding extra space between sections Layout.fillWidth: true diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 8e226639..1793d9bc 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -4,6 +4,7 @@ import "./delegates" import "./device-verification" +import "./dialogs" import "./emoji" import "./voip" import Qt.labs.platform 1.1 as Platform @@ -87,6 +88,14 @@ Page { } + Component { + id: packSettingsComponent + + ImagePackSettingsDialog { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -120,6 +129,12 @@ Page { }); userProfile.show(); } + onShowImagePackSettings: { + var packSet = packSettingsComponent.createObject(timelineRoot, { + "packlist": packlist + }); + packSet.show(); + } } Connections { diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml new file mode 100644 index 00000000..c4b4a885 --- /dev/null +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -0,0 +1,309 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../components" +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + id: win + + property ImagePackListModel packlist + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property SingleImagePackModel currentPack: packlist.packAt(currentPackIndex) + property int currentPackIndex: 0 + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + + title: qsTr("Image pack settings") + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 400 + width: 600 + palette: Nheko.colors + color: Nheko.colors.base + modality: Qt.NonModal + flags: Qt.Dialog + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: false + pageIndex: 0 + + AdaptiveLayoutElement { + id: packlistC + + visible: Settings.groupView + minimumWidth: 200 + collapsedWidth: 200 + preferredWidth: 300 + maximumWidth: 300 + + ListView { + model: packlist + clip: true + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + delegate: Rectangle { + id: packItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + required property string displayName + required property string avatarUrl + required property bool fromAccountData + required property bool fromCurrentRoom + required property int index + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered && !(index == currentPackIndex) + + PropertyChanges { + target: packItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: index == currentPackIndex + + PropertyChanges { + target: packItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: currentPackIndex = index + } + + HoverHandler { + id: hovered + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + // In the future we could show an online indicator by setting the userid for the avatar + //userid: Nheko.currentUser.userid + + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: packItem.displayName + } + + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + width: parent.width - avatar.width + Layout.preferredWidth: parent.width - avatar.width + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: packItem.importantText + elideWidth: textContent.width - Nheko.paddingMedium + fullText: displayName + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + color: packItem.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - Nheko.paddingSmall + fullText: { + if (fromAccountData) + return qsTr("Private pack"); + else if (fromCurrentRoom) + return qsTr("Pack from this room"); + else + return qsTr("Globally enabled pack"); + } + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + + } + + } + + } + + AdaptiveLayoutElement { + id: packinfoC + + Rectangle { + color: Nheko.colors.window + + ColumnLayout { + id: packinfo + + property string packName: currentPack ? currentPack.packname : "" + property string avatarUrl: currentPack ? currentPack.avatarUrl : "" + + anchors.fill: parent + anchors.margins: Nheko.paddingLarge + spacing: Nheko.paddingLarge + + Avatar { + url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: packinfo.packName + height: 100 + width: 100 + Layout.alignment: Qt.AlignHCenter + enabled: false + } + + MatrixText { + text: packinfo.packName + font.pixelSize: 24 + Layout.alignment: Qt.AlignHCenter + } + + GridLayout { + Layout.alignment: Qt.AlignHCenter + visible: currentPack && currentPack.roomid != "" + columns: 2 + rowSpacing: Nheko.paddingMedium + + MatrixText { + text: qsTr("Enable globally") + } + + ToggleButton { + ToolTip.text: qsTr("Enables this pack to be used in all rooms") + checked: currentPack ? currentPack.isGloballyEnabled : false + onClicked: currentPack.isGloballyEnabled = !currentPack.isGloballyEnabled + Layout.alignment: Qt.AlignRight + } + + } + + GridView { + Layout.fillHeight: true + Layout.fillWidth: true + model: currentPack + cellWidth: stickerDimPad + cellHeight: stickerDimPad + boundsBehavior: Flickable.StopAtBounds + clip: true + currentIndex: -1 // prevent sorting from stealing focus + cacheBuffer: 500 + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + // Individual emoji + delegate: AbstractButton { + width: stickerDim + height: stickerDim + hoverEnabled: true + ToolTip.text: ":" + model.shortcode + ": - " + model.body + ToolTip.visible: hovered + + contentItem: Image { + height: stickerDim + width: stickerDim + source: model.url.replace("mxc://", "image://MxcImage/") + fillMode: Image.PreserveAspectFit + } + + background: Rectangle { + anchors.fill: parent + color: hovered ? Nheko.colors.highlight : 'transparent' + radius: 5 + } + + } + + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Close") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + onClicked: win.close() + } + + } + +} diff --git a/resources/res.qrc b/resources/res.qrc index f8c040e4..5d37c397 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -160,6 +160,7 @@ qml/device-verification/Failed.qml qml/device-verification/Success.qml qml/dialogs/InputDialog.qml + qml/dialogs/ImagePackSettingsDialog.qml qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml diff --git a/src/Cache.cpp b/src/Cache.cpp index 0bcf9fbf..d651b182 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3383,26 +3383,30 @@ Cache::getChildRoomIds(const std::string &room_id) } std::vector -Cache::getImagePacks(const std::string &room_id, bool stickers) +Cache::getImagePacks(const std::string &room_id, std::optional stickers) { auto txn = ro_txn(env_); std::vector infos; - auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) { - if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) { + auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack, + const std::string &source_room, + const std::string &state_key) { + if (!pack.pack || !stickers.has_value() || + (stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) { ImagePackInfo info; - if (pack.pack) - info.packname = pack.pack->display_name; + info.source_room = source_room; + info.state_key = state_key; + info.pack.pack = pack.pack; for (const auto &img : pack.images) { if (img.second.overrides_usage() && (stickers ? !img.second.is_sticker() : !img.second.is_emoji())) continue; - info.images.insert(img); + info.pack.images.insert(img); } - if (!info.images.empty()) + if (!info.pack.images.empty()) infos.push_back(std::move(info)); } }; @@ -3414,7 +3418,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) std::get_if>( &*accountpack); if (tmp) - addPack(tmp->content); + addPack(tmp->content, "", ""); } // packs from rooms, that were enabled globally @@ -3433,7 +3437,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) if (auto pack = getStateEvent( txn, room_id2, state_id)) - addPack(pack->content); + addPack(pack->content, room_id2, state_id); } } } @@ -3441,16 +3445,23 @@ Cache::getImagePacks(const std::string &room_id, bool stickers) // packs from current room if (auto pack = getStateEvent(txn, room_id)) { - addPack(pack->content); + addPack(pack->content, room_id, ""); } for (const auto &pack : getStateEventsWithType(txn, room_id)) { - addPack(pack.content); + addPack(pack.content, room_id, pack.state_key); } return infos; } +std::optional +Cache::getAccountData(mtx::events::EventType type, const std::string &room_id) +{ + auto txn = ro_txn(env_); + return getAccountData(txn, type, room_id); +} + std::optional Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id) { diff --git a/src/CacheStructs.h b/src/CacheStructs.h index f274d70f..4a5c5c76 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -113,6 +113,7 @@ struct RoomSearchResult struct ImagePackInfo { - std::string packname; - std::map images; + mtx::events::msc2545::ImagePack pack; + std::string source_room; + std::string state_key; }; diff --git a/src/Cache_p.h b/src/Cache_p.h index 13fbc371..c9d42202 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -97,6 +97,12 @@ public: return getStateEvent(txn, room_id, state_key); } + //! retrieve a specific event from account data + //! pass empty room_id for global account data + std::optional getAccountData( + mtx::events::EventType type, + const std::string &room_id = ""); + //! Retrieve member info from a room. std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, @@ -225,7 +231,8 @@ public: std::vector getParentRoomIds(const std::string &room_id); std::vector getChildRoomIds(const std::string &room_id); - std::vector getImagePacks(const std::string &room_id, bool stickers); + std::vector getImagePacks(const std::string &room_id, + std::optional stickers); //! Mark a room that uses e2e encryption. void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); diff --git a/src/CombinedImagePackModel.cpp b/src/CombinedImagePackModel.cpp index c5b5b886..341a34ec 100644 --- a/src/CombinedImagePackModel.cpp +++ b/src/CombinedImagePackModel.cpp @@ -16,9 +16,10 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId, auto packs = cache::client()->getImagePacks(room_id, stickers); for (const auto &pack : packs) { - QString packname = QString::fromStdString(pack.packname); + QString packname = + pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : ""; - for (const auto &img : pack.images) { + for (const auto &img : pack.pack.images) { ImageDesc i{}; i.shortcode = QString::fromStdString(img.first); i.packname = packname; diff --git a/src/ImagePackListModel.cpp b/src/ImagePackListModel.cpp new file mode 100644 index 00000000..89f1f68e --- /dev/null +++ b/src/ImagePackListModel.cpp @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ImagePackListModel.h" + +#include + +#include "Cache_p.h" +#include "SingleImagePackModel.h" + +ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *parent) + : QAbstractListModel(parent) + , room_id(roomId) +{ + auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt); + + for (const auto &pack : packs_) { + packs.push_back( + QSharedPointer(new SingleImagePackModel(pack))); + } +} + +int +ImagePackListModel::rowCount(const QModelIndex &) const +{ + return (int)packs.size(); +} + +QHash +ImagePackListModel::roleNames() const +{ + return { + {Roles::DisplayName, "displayName"}, + {Roles::AvatarUrl, "avatarUrl"}, + {Roles::FromAccountData, "fromAccountData"}, + {Roles::FromCurrentRoom, "fromCurrentRoom"}, + {Roles::StateKey, "statekey"}, + {Roles::RoomId, "roomid"}, + }; +} + +QVariant +ImagePackListModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &pack = packs.at(index.row()); + switch (role) { + case Roles::DisplayName: + return pack->packname(); + case Roles::AvatarUrl: + return pack->avatarUrl(); + case Roles::FromAccountData: + return pack->roomid().isEmpty(); + case Roles::FromCurrentRoom: + return pack->roomid().toStdString() == this->room_id; + case Roles::StateKey: + return pack->statekey(); + case Roles::RoomId: + return pack->roomid(); + default: + return {}; + } + } + return {}; +} + +SingleImagePackModel * +ImagePackListModel::packAt(int row) +{ + if (row < 0 || static_cast(row) >= packs.size()) + return {}; + auto e = packs.at(row).get(); + QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership); + return e; +} diff --git a/src/ImagePackListModel.h b/src/ImagePackListModel.h new file mode 100644 index 00000000..0a044690 --- /dev/null +++ b/src/ImagePackListModel.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +class SingleImagePackModel; +class ImagePackListModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + DisplayName = Qt::UserRole, + AvatarUrl, + FromAccountData, + FromCurrentRoom, + StateKey, + RoomId, + }; + + ImagePackListModel(const std::string &roomId, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + Q_INVOKABLE SingleImagePackModel *packAt(int row); + +private: + std::string room_id; + + std::vector> packs; +}; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp new file mode 100644 index 00000000..ba873c19 --- /dev/null +++ b/src/SingleImagePackModel.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "SingleImagePackModel.h" + +#include "Cache_p.h" +#include "MatrixClient.h" + +SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) + : QAbstractListModel(parent) + , roomid_(std::move(pack_.source_room)) + , statekey_(std::move(pack_.state_key)) + , pack(std::move(pack_.pack)) +{ + if (!pack.pack) + pack.pack = mtx::events::msc2545::ImagePack::PackDescription{}; + + for (const auto &e : pack.images) + shortcodes.push_back(e.first); +} + +int +SingleImagePackModel::rowCount(const QModelIndex &) const +{ + return (int)shortcodes.size(); +} + +QHash +SingleImagePackModel::roleNames() const +{ + return { + {Roles::Url, "url"}, + {Roles::ShortCode, "shortCode"}, + {Roles::Body, "body"}, + {Roles::IsEmote, "isEmote"}, + {Roles::IsSticker, "isSticker"}, + }; +} + +QVariant +SingleImagePackModel::data(const QModelIndex &index, int role) const +{ + if (hasIndex(index.row(), index.column(), index.parent())) { + const auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case Url: + return QString::fromStdString(img.url); + case ShortCode: + return QString::fromStdString(shortcodes.at(index.row())); + case Body: + return QString::fromStdString(img.body); + case IsEmote: + return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + case IsSticker: + return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + default: + return {}; + } + } + return {}; +} + +bool +SingleImagePackModel::isGloballyEnabled() const +{ + if (auto roomPacks = + cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = std::get_if< + mtx::events::EphemeralEvent>( + &*roomPacks)) { + if (tmp->content.rooms.count(roomid_) && + tmp->content.rooms.at(roomid_).count(statekey_)) + return true; + } + } + return false; +} +void +SingleImagePackModel::setGloballyEnabled(bool enabled) +{ + mtx::events::msc2545::ImagePackRooms content{}; + if (auto roomPacks = + cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) { + if (auto tmp = std::get_if< + mtx::events::EphemeralEvent>( + &*roomPacks)) { + content = tmp->content; + } + } + + if (enabled) + content.rooms[roomid_][statekey_] = {}; + else + content.rooms[roomid_].erase(statekey_); + + http::client()->put_account_data(content, [this](mtx::http::RequestErr) { + // emit this->globallyEnabledChanged(); + }); +} diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h new file mode 100644 index 00000000..e0c791ba --- /dev/null +++ b/src/SingleImagePackModel.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +#include "CacheStructs.h" + +class SingleImagePackModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString roomid READ roomid CONSTANT) + Q_PROPERTY(QString statekey READ statekey CONSTANT) + Q_PROPERTY(QString attribution READ statekey CONSTANT) + Q_PROPERTY(QString packname READ packname CONSTANT) + Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT) + Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT) + Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT) + Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY + globallyEnabledChanged) +public: + enum Roles + { + Url = Qt::UserRole, + ShortCode, + Body, + IsEmote, + IsSticker, + }; + + SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + QString roomid() const { return QString::fromStdString(roomid_); } + QString statekey() const { return QString::fromStdString(statekey_); } + QString packname() const { return QString::fromStdString(pack.pack->display_name); } + QString attribution() const { return QString::fromStdString(pack.pack->attribution); } + QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); } + bool isStickerPack() const { return pack.pack->is_sticker(); } + bool isEmotePack() const { return pack.pack->is_emoji(); } + + bool isGloballyEnabled() const; + void setGloballyEnabled(bool enabled); + +signals: + void globallyEnabledChanged(); + +private: + std::string roomid_; + std::string statekey_; + + mtx::events::msc2545::ImagePack pack; + std::vector shortcodes; +}; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 2da7d789..4353ef62 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -20,12 +20,14 @@ #include "DelegateChooser.h" #include "DeviceVerificationFlow.h" #include "EventAccessors.h" +#include "ImagePackListModel.h" #include "InviteesModel.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" #include "MxcImageProvider.h" #include "RoomsModel.h" +#include "SingleImagePackModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" #include "dialogs/ImageOverlay.h" @@ -185,6 +187,18 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Room Settings needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "ImagePackListModel", + "ImagePackListModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "SingleImagePackModel", + "SingleImagePackModel needs to be instantiated on the C++ side"); qmlRegisterUncreatableType( "im.nheko", 1, @@ -436,6 +450,12 @@ TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) }); } +void +TimelineViewManager::openImagePackSettings(QString roomid) +{ + emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this)); +} + void TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 374685e3..bdec405a 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -33,6 +33,7 @@ class ColorImageProvider; class UserSettings; class ChatPage; class DeviceVerificationFlow; +class ImagePackListModel; class TimelineViewManager : public QObject { @@ -57,6 +58,7 @@ public: Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } bool isWindowFocused() const { return isWindowFocused_; } Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); + Q_INVOKABLE void openImagePackSettings(QString roomid); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } @@ -93,6 +95,7 @@ signals: void openRoomSettingsDialog(RoomSettings *settings); void openInviteUsersDialog(InviteesModel *invitees); void openProfile(UserProfile *profile); + void showImagePackSettings(ImagePackListModel *packlist); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h index 367f3111..cf36f795 100644 --- a/src/ui/RoomSettings.h +++ b/src/ui/RoomSettings.h @@ -136,4 +136,4 @@ private: RoomInfo info_; int notifications_ = 0; int accessRules_ = 0; -}; \ No newline at end of file +}; -- cgit 1.5.1 From dba2fb525a4c7a58f189995c1b323be23e7aab41 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 23 Jul 2021 18:21:55 +0200 Subject: Remove unused this capture --- src/SingleImagePackModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index ba873c19..6c508da0 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -94,7 +94,7 @@ SingleImagePackModel::setGloballyEnabled(bool enabled) else content.rooms[roomid_].erase(statekey_); - http::client()->put_account_data(content, [this](mtx::http::RequestErr) { + http::client()->put_account_data(content, [](mtx::http::RequestErr) { // emit this->globallyEnabledChanged(); }); } -- cgit 1.5.1 From bef2cbbd7f0a66fb552d6b19a7ebc706f9e4b7ad Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 23 Jul 2021 19:08:44 +0200 Subject: Try to avoid more olm loops --- src/Olm.cpp | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/Olm.cpp b/src/Olm.cpp index 18e2ddcf..0c9962f9 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -214,12 +214,14 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey const auto my_key = olm::client()->identity_keys().curve25519; + bool failed_decryption = false; + for (const auto &cipher : msg.ciphertext) { // We skip messages not meant for the current device. if (cipher.first != my_key) { nhlog::crypto()->debug( "Skipping message for {} since we are {}.", cipher.first, my_key); - return; + continue; } const auto type = cipher.second.type; @@ -234,6 +236,7 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey msg.sender, msg.sender_key, cipher.second); } else { nhlog::crypto()->error("Undecryptable olm message!"); + failed_decryption = true; continue; } } @@ -423,22 +426,28 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey } return; + } else { + failed_decryption = true; } } - try { - std::map> targets; - for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { - if (key.keys.at("curve25519:" + device_id) == msg.sender_key) - targets[msg.sender].push_back(device_id); - } + if (failed_decryption) { + try { + std::map> targets; + for (auto [device_id, key] : otherUserDeviceKeys.device_keys) { + if (key.keys.at("curve25519:" + device_id) == msg.sender_key) + targets[msg.sender].push_back(device_id); + } - send_encrypted_to_device_messages( - targets, mtx::events::DeviceEvent{}, true); - nhlog::crypto()->info( - "Recovering from broken olm channel with {}:{}", msg.sender, msg.sender_key); - } catch (std::exception &e) { - nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", e.what()); + send_encrypted_to_device_messages( + targets, mtx::events::DeviceEvent{}, true); + nhlog::crypto()->info("Recovering from broken olm channel with {}:{}", + msg.sender, + msg.sender_key); + } catch (std::exception &e) { + nhlog::crypto()->error("Failed to recover from broken olm sessions: {}", + e.what()); + } } } -- cgit 1.5.1 From a61678242b91944c07bf4b82b4eb25baad0db00b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 24 Jul 2021 14:59:14 +0200 Subject: Fix edge case that could lead to no new one time keys being uploaded --- src/ChatPage.cpp | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 6b8c1e10..615a15b3 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -929,31 +929,33 @@ ChatPage::currentPresence() const void ChatPage::ensureOneTimeKeyCount(const std::map &counts) { - for (const auto &entry : counts) { - if (entry.second < MAX_ONETIME_KEYS) { - const int nkeys = MAX_ONETIME_KEYS - entry.second; + uint16_t count = 0; + if (auto c = counts.find(mtx::crypto::SIGNED_CURVE25519); c != counts.end()) + count = c->second; - nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); - olm::client()->generate_one_time_keys(nkeys); + if (count < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - count; - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn( - "failed to update one-time keys: {} {} {}", - err->matrix_error.error, - static_cast(err->status_code), - static_cast(err->error_code)); + nhlog::crypto()->info( + "uploading {} {} keys", nkeys, mtx::crypto::SIGNED_CURVE25519); + olm::client()->generate_one_time_keys(nkeys); - if (err->status_code < 400 || err->status_code >= 500) - return; - } + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn("failed to update one-time keys: {} {} {}", + err->matrix_error.error, + static_cast(err->status_code), + static_cast(err->error_code)); - // mark as published anyway, otherwise we may end up in a loop. - olm::mark_keys_as_published(); - }); - } + if (err->status_code < 400 || err->status_code >= 500) + return; + } + + // mark as published anyway, otherwise we may end up in a loop. + olm::mark_keys_as_published(); + }); } } -- cgit 1.5.1