From b453b6578732a1d71d64ab0e08c924d31cb6b898 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 15 Jul 2021 17:56:32 +0200 Subject: [PATCH 01/14] Try to make scrolling emoji picker a bit smoother --- resources/qml/emoji/EmojiPicker.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml index 6f10a230..354e340c 100644 --- a/resources/qml/emoji/EmojiPicker.qml +++ b/resources/qml/emoji/EmojiPicker.qml @@ -130,6 +130,7 @@ Menu { boundsBehavior: Flickable.StopAtBounds clip: true currentIndex: -1 // prevent sorting from stealing focus + cacheBuffer: 500 // Individual emoji delegate: AbstractButton { From 0b864d948586d6cc3ef3968376a5b932e06b793e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 16 Jul 2021 11:47:49 +0200 Subject: [PATCH 02/14] Fix replies not reloading after fetching them --- resources/qml/TimelineRow.qml | 38 +++++++++++++++++----------------- src/timeline/TimelineModel.cpp | 8 +++++++ src/timeline/TimelineModel.h | 5 +---- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 58e367a0..70db08e7 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -86,29 +86,29 @@ Item { // fancy reply, if this is a reply Reply { function fromModel(role) { - return replyTo != "" ? room.dataById(replyTo, role) : null; + return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; } visible: replyTo - userColor: TimelineManager.userColor(userId, Nheko.colors.base) - blurhash: fromModel(Room.Blurhash) ?? "" - body: fromModel(Room.Body) ?? "" - formattedBody: fromModel(Room.FormattedBody) ?? "" + userColor: replyTo, TimelineManager.userColor(userId, Nheko.colors.base) + blurhash: replyTo, fromModel(Room.Blurhash) ?? "" + body: replyTo, fromModel(Room.Body) ?? "" + formattedBody: replyTo, fromModel(Room.FormattedBody) ?? "" eventId: fromModel(Room.EventId) ?? "" - filename: fromModel(Room.Filename) ?? "" - filesize: fromModel(Room.Filesize) ?? "" - proportionalHeight: fromModel(Room.ProportionalHeight) ?? 1 - type: fromModel(Room.Type) ?? MtxEvent.UnknownMessage - typeString: fromModel(Room.TypeString) ?? "" - url: fromModel(Room.Url) ?? "" - originalWidth: fromModel(Room.OriginalWidth) ?? 0 - isOnlyEmoji: fromModel(Room.IsOnlyEmoji) ?? false - userId: fromModel(Room.UserId) ?? "" - userName: fromModel(Room.UserName) ?? "" - thumbnailUrl: fromModel(Room.ThumbnailUrl) ?? "" - roomTopic: fromModel(Room.RoomTopic) ?? "" - roomName: fromModel(Room.RoomName) ?? "" - callType: fromModel(Room.CallType) ?? "" + 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) ?? "" } // actual message content 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; From 9fadd148715790743cb4e87bfe1854923e59c06b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 01:27:37 +0200 Subject: [PATCH 03/14] 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(-) 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); From 3f0aa13cb6f83c8768dc30b1a1872b9696992fa7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 02:14:44 +0200 Subject: [PATCH 04/14] 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 | 52 ++++++++++++++++++++++++++++----------------------- 4 files changed, 56 insertions(+), 23 deletions(-) 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,21 +887,16 @@ 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); - return; - } - - // Check if we have the keys for the requested session. - auto outboundSession = cache::getOutboundMegolmSession(req.content.room_id); - if (!outboundSession.session) { - nhlog::crypto()->warn("requested session not found in room: {}", - req.content.room_id); + // 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; } @@ -908,7 +904,15 @@ handle_key_request_message(const mtx::events::DeviceEventidentity_keys().curve25519; + index.sender_key = req.content.sender_key; + + // Check if we have the keys for the requested session. + auto sessionData = cache::getMegolmSessionData(index); + if (!sessionData) { + nhlog::crypto()->warn("requested session not found in room: {}", + req.content.room_id); + return; + } const auto session = cache::getInboundMegolmSession(index); if (!session) { @@ -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 From aa84cf24f6ad7008d481f1741ee6f04e851d1298 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 17 Jul 2021 21:43:25 +0200 Subject: [PATCH 05/14] Disable unchecked key sharing to trusted users by default --- src/UserSettingsPage.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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_); From 8a1666bc889d963693b5dff8f0b4c7612319644a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 15 Jul 2021 20:37:52 +0200 Subject: [PATCH 06/14] 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 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; } From 9d5ba4f681901a0ee241e542fa9be5e2b73f58db Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 03:02:30 +0200 Subject: [PATCH 07/14] 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(-) 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; From 56b44a85b572df2565dfdb8269146830d7ba02d0 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 03:29:48 +0200 Subject: [PATCH 08/14] Fix unused state key when iterating room stickers --- src/Cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } } From 9f416f1fc9cc3a973159b2a5a84ee668ffbc5063 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 12:43:16 +0200 Subject: [PATCH 09/14] 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(-) 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; } From 60be0e8c09ab671cb50dfb8adeb48b15389aba3a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 14:57:10 +0200 Subject: [PATCH 10/14] Make scrolling sticker picker bearable --- resources/qml/emoji/StickerPicker.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index ae1695df..eca302eb 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -130,6 +130,12 @@ Menu { currentIndex: -1 // prevent sorting from stealing focus cacheBuffer: 500 + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + // Individual emoji delegate: AbstractButton { width: stickerDim From 3b56ff2d85e607b2620ced0f45bdc2c6ef100feb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 16:14:09 +0200 Subject: [PATCH 11/14] Fix replying and editing stickers --- src/timeline/InputBar.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) 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); } From 11c96664089e1437be1eb0ae8bca6c051da5e950 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 17:45:55 +0200 Subject: [PATCH 12/14] Cache sticker picker --- resources/qml/MessageInput.qml | 2 +- resources/qml/emoji/StickerPicker.qml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index d4f7ca62..415d67a7 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -331,7 +331,7 @@ Rectangle { 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) { + onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId(), function(row) { room.input.sticker(stickerPopup.model.sourceModel, row); TimelineManager.focusMessageInput(); }) diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index eca302eb..a3d01d7a 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -15,7 +15,7 @@ Menu { property var callback property var colors - property alias model: gridView.model + property string roomid property var textArea property real highlightHue: Nheko.colors.highlight.hslHue property real highlightSat: Nheko.colors.highlight.hslSaturation @@ -24,9 +24,9 @@ Menu { readonly property int stickerDimPad: 128 + Nheko.paddingSmall readonly property int stickersPerRow: 3 - function show(showAt, model_, callback) { + function show(showAt, roomid_, callback) { console.debug("Showing sticker picker"); - model = model_; + roomid = roomid_; stickerPopup.callback = callback; popup(showAt ? showAt : null); } @@ -120,6 +120,8 @@ Menu { GridView { id: gridView + model: roomid ? TimelineManager.completerFor("stickers", roomid) : null + Layout.preferredHeight: cellHeight * 3.5 Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 Layout.leftMargin: 4 From 752ffa5c5187ae4e55c6cfb77ba90edd7e3848c8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 17:49:57 +0200 Subject: [PATCH 13/14] Model alias is still needed --- resources/qml/emoji/StickerPicker.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml index a3d01d7a..813c0b12 100644 --- a/resources/qml/emoji/StickerPicker.qml +++ b/resources/qml/emoji/StickerPicker.qml @@ -16,6 +16,7 @@ Menu { property var callback property var colors property string roomid + property alias model: gridView.model property var textArea property real highlightHue: Nheko.colors.highlight.hslHue property real highlightSat: Nheko.colors.highlight.hslSaturation From 6d169cea7dfb4554ef09aceada33ba715b1df09c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 19 Jul 2021 17:59:38 +0200 Subject: [PATCH 14/14] Fix reaction button again --- resources/qml/MessageView.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 4e605ad7..f56af237 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -101,7 +101,7 @@ ScrollView { image: ":/icons/icons/ui/smile.png" ToolTip.visible: hovered ToolTip.text: qsTr("React") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, function(emoji) { var event_id = row.model ? row.model.eventId : ""; room.input.reaction(event_id, emoji); TimelineManager.focusMessageInput();