From 4aefac08a4b57e6fb344cd8cffb0b6e32698e0ba Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Fri, 5 Feb 2021 22:42:08 +0530 Subject: focus message input on adding emoji/reacting to a message --- src/timeline/TimelineViewManager.cpp | 6 ++++++ src/timeline/TimelineViewManager.h | 2 ++ 2 files changed, 8 insertions(+) (limited to 'src/timeline') diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 93451976..9ca08e06 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -545,3 +545,9 @@ TimelineViewManager::queueCallMessage(const QString &roomid, { models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } + +void +TimelineViewManager::focusMessageInput() +{ + emit focusInput(); +} \ No newline at end of file diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 74128865..7c994a14 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -66,6 +66,7 @@ public: Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); Q_INVOKABLE void openMemberListDialog() const; Q_INVOKABLE void openLeaveRoomDialog() const; @@ -87,6 +88,7 @@ signals: void showRoomList(); void narrowViewChanged(); void focusChanged(); + void focusInput(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From 375e20462ba3641db3b71c12a66533c63ac2d597 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 7 Feb 2021 02:01:25 +0100 Subject: Native rendering breaks kerning --- src/timeline/TimelineViewManager.cpp | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src/timeline') diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 93451976..dab735db 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -170,10 +170,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->setResizeMode(QQuickWidget::SizeRootObjectToView); container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) - view->quickWindow()->setTextRenderType(QQuickWindow::NativeTextRendering); -#endif - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { nhlog::ui()->debug("Status changed to {}", status); }); -- cgit 1.5.1 From 2e77a1554f1572b7c7e59f8177a48e5dffa16c23 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 26 Jan 2021 22:36:35 +0100 Subject: Switch to new relations format --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 3 +- src/Cache.cpp | 72 ++++++++++++++++-------------------- src/DeviceVerificationFlow.cpp | 34 +++++++++-------- src/DeviceVerificationFlow.h | 10 +++-- src/EventAccessors.cpp | 35 ++++-------------- src/EventAccessors.h | 6 +-- src/Olm.cpp | 27 ++++---------- src/timeline/EventStore.cpp | 37 +++++++++--------- src/timeline/InputBar.cpp | 18 ++++++--- src/timeline/TimelineModel.cpp | 4 +- src/timeline/TimelineViewManager.cpp | 8 ++-- 12 files changed, 112 insertions(+), 144 deletions(-) (limited to 'src/timeline') diff --git a/CMakeLists.txt b/CMakeLists.txt index d5245ef8..cf2b5959 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG v0.4.1 + GIT_TAG 70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index e6eeb123..98ab9629 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,8 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "4951190c938740defa0988d98d5e861038622936", - "tag": "v0.4.1", + "commit": "70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/src/Cache.cpp b/src/Cache.cpp index 3f2bf73a..94b9a6a6 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2713,23 +2713,19 @@ Cache::saveTimelineMessages(lmdb::txn &txn, lmdb::dbi_put(txn, evToOrderDb, event_id, txn_order); lmdb::dbi_del(txn, evToOrderDb, lmdb::val(txn_id)); - if (event.contains("content") && - event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = temp.contains("m.in_reply_to") && - temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) { - lmdb::dbi_del(txn, - relationsDb, - lmdb::val(relates_to), - lmdb::val(txn_id)); - lmdb::dbi_put( - txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_del(txn, + relationsDb, + lmdb::val(r.event_id), + lmdb::val(txn_id)); + lmdb::dbi_put(txn, + relationsDb, + lmdb::val(r.event_id), + event_id); + } } } @@ -2808,19 +2804,16 @@ Cache::saveTimelineMessages(lmdb::txn &txn, lmdb::val(&msgIndex, sizeof(msgIndex))); } - if (event.contains("content") && - event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = temp.contains("m.in_reply_to") && - temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) - lmdb::dbi_put( - txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_put(txn, + relationsDb, + lmdb::val(r.event_id), + event_id); + } + } } } } @@ -2901,17 +2894,14 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex))); } - if (event.contains("content") && event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = - temp.contains("m.in_reply_to") && temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) - lmdb::dbi_put(txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_put( + txn, relationsDb, lmdb::val(r.event_id), event_id); + } + } } } diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp index 51ef79fd..c6277a9d 100644 --- a/src/DeviceVerificationFlow.cpp +++ b/src/DeviceVerificationFlow.cpp @@ -105,8 +105,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && @@ -136,8 +136,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } error_ = User; @@ -152,8 +152,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } @@ -217,8 +217,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } @@ -385,8 +385,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if ((msg.relates_to.has_value() && sender)) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; else { this->deviceId = QString::fromStdString(msg.from_device); @@ -402,8 +402,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } nhlog::ui()->info("Flow done on other side"); @@ -526,8 +526,8 @@ DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificati if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } if ((std::find(msg.key_agreement_protocols.begin(), @@ -625,8 +625,10 @@ DeviceVerificationFlow::startVerificationRequest() req.transaction_id = this->transaction_id; this->canonical_json = nlohmann::json(req); } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - req.relates_to = this->relation; - this->canonical_json = nlohmann::json(req); + req.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + req.relations.synthesized = true; + this->canonical_json = nlohmann::json(req); } send(req); setState(WaitingForOtherToAccept); diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h index 34b78962..6c613545 100644 --- a/src/DeviceVerificationFlow.h +++ b/src/DeviceVerificationFlow.h @@ -206,7 +206,7 @@ private: std::vector sasList; UserKeyCache their_keys; TimelineModel *model_; - mtx::common::RelatesTo relation; + mtx::common::Relation relation; State state_ = PromptStartVerification; Error error_ = UnknownMethod; @@ -230,8 +230,12 @@ private: static_cast(err->status_code)); }); } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - if constexpr (!std::is_same_v) - msg.relates_to = this->relation; + if constexpr (!std::is_same_v) { + msg.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + msg.relations.synthesized = true; + } (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type); } diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 3ae781f0..4218f491 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -250,31 +250,17 @@ struct EventFilesize } }; -struct EventInReplyTo +struct EventRelations { template - using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id); + using related_ev_id_t = decltype(Content::relations); template - std::string operator()(const mtx::events::Event &e) + mtx::common::Relations operator()(const mtx::events::Event &e) { if constexpr (is_detected::value) { - return e.content.relates_to.in_reply_to.event_id; + return e.content.relations; } - return ""; - } -}; - -struct EventRelatesTo -{ - template - using related_ev_id_t = decltype(Content::relates_to.event_id); - template - std::string operator()(const mtx::events::Event &e) - { - if constexpr (is_detected::value) { - return e.content.relates_to.event_id; - } - return ""; + return {}; } }; @@ -434,15 +420,10 @@ mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) { return std::visit(EventMimeType{}, event); } -std::string -mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event) -{ - return std::visit(EventInReplyTo{}, event); -} -std::string -mtx::accessors::relates_to_event_id(const mtx::events::collections::TimelineEvents &event) +mtx::common::Relations +mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventRelatesTo{}, event); + return std::visit(EventRelations{}, event); } std::string diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 0cdc5f89..60912497 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -53,10 +53,8 @@ std::string blurhash(const mtx::events::collections::TimelineEvents &event); std::string mimetype(const mtx::events::collections::TimelineEvents &event); -std::string -in_reply_to_event(const mtx::events::collections::TimelineEvents &event); -std::string -relates_to_event_id(const mtx::events::collections::TimelineEvents &event); +mtx::common::Relations +relations(const mtx::events::collections::TimelineEvents &event); std::string transaction_id(const mtx::events::collections::TimelineEvents &event); diff --git a/src/Olm.cpp b/src/Olm.cpp index 4ccf8ab9..54be4751 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -575,29 +575,19 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, if (!sendSessionTo.empty()) olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - mtx::common::ReplyRelatesTo relation; - mtx::common::RelatesTo r_relation; - // relations shouldn't be encrypted... - if (body["content"].contains("m.relates_to")) { - if (body["content"]["m.relates_to"].contains("m.in_reply_to")) { - relation = body["content"]["m.relates_to"]; - } else if (body["content"]["m.relates_to"].contains("event_id")) { - r_relation = body["content"]["m.relates_to"]; - } - } + mtx::common::Relations relations = mtx::common::parse_relations(body["content"]); auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); // Prepare the m.room.encrypted event. msg::Encrypted data; - data.ciphertext = std::string((char *)payload.data(), payload.size()); - data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = mtx::crypto::session_id(session.get()); - data.device_id = device_id; - data.algorithm = MEGOLM_ALGO; - data.relates_to = relation; - data.r_relates_to = r_relation; + data.ciphertext = std::string((char *)payload.data(), payload.size()); + data.sender_key = olm::client()->identity_keys().curve25519; + data.session_id = mtx::crypto::session_id(session.get()); + data.device_id = device_id; + data.algorithm = MEGOLM_ALGO; + data.relations = relations; group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); @@ -910,8 +900,7 @@ decryptEvent(const MegolmSessionIndex &index, body["unsigned"] = event.unsigned_data; // relations are unencrypted in content... - if (json old_ev = event; old_ev["content"].count("m.relates_to") != 0) - body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + mtx::common::add_relations(body["content"], event.content.relations); mtx::events::collections::TimelineEvent te; try { diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index be4bc09e..4a90222f 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -293,16 +293,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events) } for (const auto &event : events.events) { - std::string relates_to; + std::set relates_to; if (auto redaction = std::get_if>( &event)) { // fixup reactions auto redacted = events_by_id_.object({room_id_, redaction->redacts}); if (redacted) { - auto id = mtx::accessors::relates_to_event_id(*redacted); - if (!id.empty()) { - auto idx = idToIndex(id); + auto id = mtx::accessors::relations(*redacted); + if (id.annotates()) { + auto idx = idToIndex(id.annotates()->event_id); if (idx) { events_by_id_.remove( {room_id_, redaction->redacts}); @@ -312,20 +312,17 @@ EventStore::handleSync(const mtx::responses::Timeline &events) } } - relates_to = redaction->redacts; - } else if (auto reaction = - std::get_if>( - &event)) { - relates_to = reaction->content.relates_to.event_id; + relates_to.insert(redaction->redacts); } else { - relates_to = mtx::accessors::in_reply_to_event(event); + for (const auto &r : mtx::accessors::relations(event).relations) + relates_to.insert(r.event_id); } - if (!relates_to.empty()) { - auto idx = cache::client()->getTimelineIndex(room_id_, relates_to); + for (const auto &relates_to_id : relates_to) { + auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id); if (idx) { - events_by_id_.remove({room_id_, relates_to}); - decryptedEvents_.remove({room_id_, relates_to}); + events_by_id_.remove({room_id_, relates_to_id}); + decryptedEvents_.remove({room_id_, relates_to_id}); events_.remove({room_id_, *idx}); emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); } @@ -430,13 +427,14 @@ EventStore::reactions(const std::string &event_id) if (auto reaction = std::get_if>( related_event); - reaction && reaction->content.relates_to.key) { - auto &agg = aggregation[reaction->content.relates_to.key.value()]; + reaction && reaction->content.relations.annotates() && + reaction->content.relations.annotates()->key) { + auto key = reaction->content.relations.annotates()->key.value(); + auto &agg = aggregation[key]; if (agg.count == 0) { Reaction temp{}; - temp.key_ = - QString::fromStdString(reaction->content.relates_to.key.value()); + temp.key_ = QString::fromStdString(key); reactions.push_back(temp); } @@ -691,8 +689,7 @@ EventStore::decryptEvent(const IdIndex &idx, body["unsigned"] = e.unsigned_data; // relations are unencrypted in content... - if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) - body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + mtx::common::add_relations(body["content"], e.content.relations); json event_array = json::array(); event_array.push_back(body); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b31c1f76..738fb37c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -294,7 +294,8 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) text.formatted_body = utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); - text.relates_to.in_reply_to.event_id = related.related_event; + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, related.related_event}); room->resetReply(); } @@ -316,7 +317,8 @@ InputBar::emote(QString msg) } if (!room->reply().isEmpty()) { - emote.relates_to.in_reply_to.event_id = room->reply().toStdString(); + emote.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -346,7 +348,8 @@ InputBar::image(const QString &filename, image.url = url.toStdString(); if (!room->reply().isEmpty()) { - image.relates_to.in_reply_to.event_id = room->reply().toStdString(); + image.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -371,7 +374,8 @@ InputBar::file(const QString &filename, file.url = url.toStdString(); if (!room->reply().isEmpty()) { - file.relates_to.in_reply_to.event_id = room->reply().toStdString(); + file.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -397,7 +401,8 @@ InputBar::audio(const QString &filename, audio.url = url.toStdString(); if (!room->reply().isEmpty()) { - audio.relates_to.in_reply_to.event_id = room->reply().toStdString(); + audio.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -422,7 +427,8 @@ InputBar::video(const QString &filename, video.url = url.toStdString(); if (!room->reply().isEmpty()) { - video.relates_to.in_reply_to.event_id = room->reply().toStdString(); + video.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 968ec3c7..c47194f5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -360,7 +360,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r const static QRegularExpression replyFallback( ".*", QRegularExpression::DotMatchesEverythingOption); - bool isReply = !in_reply_to_event(event).empty(); + bool isReply = relations(event).reply_to().has_value(); auto formattedBody_ = QString::fromStdString(formatted_body(event)); if (formattedBody_.isEmpty()) { @@ -442,7 +442,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return cache::isRoomEncrypted(room_id_.toStdString()); } case ReplyTo: - return QVariant(QString::fromStdString(in_reply_to_event(event))); + return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { auto id = event_id(event); return QVariant::fromValue(events.reactions(id)); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 9e045e83..e1e2b681 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -503,9 +503,11 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt // If selfReactedEvent is empty, that means we haven't previously reacted if (selfReactedEvent.isEmpty()) { mtx::events::msg::Reaction reaction; - reaction.relates_to.rel_type = mtx::common::RelationType::Annotation; - reaction.relates_to.event_id = reactedEvent.toStdString(); - reaction.relates_to.key = reactionKey.toStdString(); + mtx::common::Relation rel; + rel.rel_type = mtx::common::RelationType::Annotation; + rel.event_id = reactedEvent.toStdString(); + rel.key = reactionKey.toStdString(); + reaction.relations.relations.push_back(rel); timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); // Otherwise, we have previously reacted and the reaction should be redacted -- cgit 1.5.1 From d6504812c71ff7251a5319113c580ab322469eb3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 02:45:33 +0100 Subject: Render edits --- src/Cache.cpp | 30 +++++++++++++++++++++++++ src/Cache_p.h | 2 ++ src/timeline/EventStore.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++--- src/timeline/EventStore.h | 4 +++- 4 files changed, 86 insertions(+), 4 deletions(-) (limited to 'src/timeline') diff --git a/src/Cache.cpp b/src/Cache.cpp index 94b9a6a6..49861a9a 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -108,6 +108,11 @@ Cache::isHiddenEvent(lmdb::txn &txn, const std::string &room_id) { using namespace mtx::events; + + // Always hide edits + if (mtx::accessors::relations(e).replaces()) + return true; + if (auto encryptedEvent = std::get_if>(&e)) { MegolmSessionIndex index; index.room_id = room_id; @@ -1891,6 +1896,31 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) return *val.data(); } +std::optional +Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val indexVal{event_id.data(), event_id.size()}, val; + + bool success = lmdb::dbi_get(txn, orderDb, indexVal, val); + if (!success) { + return {}; + } + + return *val.data(); +} + std::optional Cache::getTimelineEventId(const std::string &room_id, uint64_t index) { diff --git a/src/Cache_p.h b/src/Cache_p.h index e2ce1668..c96a3f30 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -205,6 +205,8 @@ public: std::optional getTimelineIndex(const std::string &room_id, std::string_view event_id); std::optional getTimelineEventId(const std::string &room_id, uint64_t index); + std::optional getArrivalIndex(const std::string &room_id, + std::string_view event_id); std::string previousBatchToken(const std::string &room_id); uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 4a90222f..ebf2f024 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -405,6 +405,41 @@ EventStore::handle_room_verification(mtx::events::collections::TimelineEvents ev event); } +std::vector +EventStore::edits(const std::string &event_id) +{ + auto event_ids = cache::client()->relatedEvents(room_id_, event_id); + + auto original_event = get(event_id, "", false, false); + if (!original_event) + return {}; + + auto original_sender = mtx::accessors::sender(*original_event); + + std::vector edits; + for (const auto &id : event_ids) { + auto related_event = get(id, event_id, false, false); + if (!related_event) + continue; + + auto edit_rel = mtx::accessors::relations(*related_event); + if (edit_rel.replaces() == event_id && + original_sender == mtx::accessors::sender(*related_event)) + edits.push_back(*related_event); + } + + auto c = cache::client(); + std::sort(edits.begin(), + edits.end(), + [this, c](const mtx::events::collections::TimelineEvents &a, + const mtx::events::collections::TimelineEvents &b) { + return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) < + c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b)); + }); + + return edits; +} + QVariantList EventStore::reactions(const std::string &event_id) { @@ -487,7 +522,13 @@ EventStore::get(int idx, bool decrypt) if (!event_id) return nullptr; - auto event = cache::client()->getEvent(room_id_, *event_id); + std::optional event; + auto edits_ = edits(*event_id); + if (edits_.empty()) + event = cache::client()->getEvent(room_id_, *event_id); + else + event = {edits_.back()}; + if (!event) return nullptr; else @@ -714,7 +755,7 @@ EventStore::decryptEvent(const IdIndex &idx, } mtx::events::collections::TimelineEvents * -EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) +EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); @@ -722,7 +763,14 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) if (id.empty()) return nullptr; - IdIndex index{room_id_, std::string(id.data(), id.size())}; + std::string id_ = std::string(id); + if (resolve_edits) { + auto edits_ = edits(id_); + if (!edits_.empty()) + id_ = mtx::accessors::event_id(edits_.back()); + } + + IdIndex index{room_id_, id_}; auto event_ptr = events_by_id_.object(index); if (!event_ptr) { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index f8eff9a9..ced7bdc0 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -66,7 +66,8 @@ public: // relatedFetched event mtx::events::collections::TimelineEvents *get(std::string_view id, std::string_view related_to, - bool decrypt = true); + bool decrypt = true, + bool resolve_edits = true); // always returns a proper event as long as the idx is valid mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); @@ -110,6 +111,7 @@ public slots: void clearTimeline(); private: + std::vector edits(const std::string &event_id); mtx::events::collections::TimelineEvents *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent &e); -- cgit 1.5.1 From faeaf9dc6bfae5bf56f6edee4e21bb54db08b2e1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 16:14:03 +0100 Subject: Fix edited replies --- src/EventAccessors.cpp | 21 +++++++++++++++++++++ src/EventAccessors.h | 2 ++ src/timeline/EventStore.cpp | 19 +++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) (limited to 'src/timeline') diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 4218f491..212c2970 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -264,6 +264,20 @@ struct EventRelations } }; +struct SetEventRelations +{ + mtx::common::Relations new_relations; + template + using related_ev_id_t = decltype(Content::relations); + template + void operator()(mtx::events::Event &e) + { + if constexpr (is_detected::value) { + e.content.relations = std::move(new_relations); + } + } +}; + struct EventTransactionId { template @@ -426,6 +440,13 @@ mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) return std::visit(EventRelations{}, event); } +void +mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event, + mtx::common::Relations relations) +{ + std::visit(SetEventRelations{std::move(relations)}, event); +} + std::string mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) { diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 60912497..95e5df24 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -55,6 +55,8 @@ std::string mimetype(const mtx::events::collections::TimelineEvents &event); mtx::common::Relations relations(const mtx::events::collections::TimelineEvents &event); +void +set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations); std::string transaction_id(const mtx::events::collections::TimelineEvents &event); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index ebf2f024..e5a66e19 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -414,7 +414,8 @@ EventStore::edits(const std::string &event_id) if (!original_event) return {}; - auto original_sender = mtx::accessors::sender(*original_event); + auto original_sender = mtx::accessors::sender(*original_event); + auto original_relations = mtx::accessors::relations(*original_event); std::vector edits; for (const auto &id : event_ids) { @@ -422,10 +423,20 @@ EventStore::edits(const std::string &event_id) if (!related_event) continue; - auto edit_rel = mtx::accessors::relations(*related_event); + auto related_ev = *related_event; + + auto edit_rel = mtx::accessors::relations(related_ev); if (edit_rel.replaces() == event_id && - original_sender == mtx::accessors::sender(*related_event)) - edits.push_back(*related_event); + original_sender == mtx::accessors::sender(related_ev)) { + if (edit_rel.synthesized && original_relations.reply_to() && + !edit_rel.reply_to()) { + edit_rel.relations.push_back( + {mtx::common::RelationType::InReplyTo, + original_relations.reply_to().value()}); + mtx::accessors::set_relations(related_ev, std::move(edit_rel)); + } + edits.push_back(std::move(related_ev)); + } } auto c = cache::client(); -- cgit 1.5.1 From 00fd4eecec1af3a38bc69f3849f9e49f826cef26 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 31 Jan 2021 22:41:43 +0100 Subject: Display edits correctly --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 2 +- resources/qml/TimelineRow.qml | 14 ++++++++++++++ src/EventAccessors.cpp | 20 ++++++++++++++++++++ src/EventAccessors.h | 3 +++ src/timeline/EventStore.cpp | 14 ++++++++------ src/timeline/TimelineModel.cpp | 39 ++++++++++++++++++++++++++++++++++++--- src/timeline/TimelineModel.h | 17 ++++++++++++++++- 8 files changed, 99 insertions(+), 12 deletions(-) (limited to 'src/timeline') diff --git a/CMakeLists.txt b/CMakeLists.txt index cf2b5959..577cbffc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b + GIT_TAG 31e300546eb63ea25b0b879fb255beee6022da03 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index 98ab9629..f498dd5a 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,7 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b", + "commit": "31e300546eb63ea25b0b879fb255beee6022da03", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 95a025cf..e4dc267b 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -85,6 +85,20 @@ Item { width: 16 } + ImageButton { + id: editButton + + visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/edit.png" + ToolTip.visible: hovered + ToolTip.text: model.isEditable ? qsTr("Edit") : qsTr("Edited") + onClicked: if (model.isEditable) chat.model.editAction(model.id) + } + EmojiButton { id: reactButton diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 212c2970..e6bc61b0 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -34,6 +34,20 @@ struct detector>, Op, Args...> template class Op, class... Args> using is_detected = typename detail::detector::value_t; +struct IsStateEvent +{ + template + bool operator()(const mtx::events::StateEvent &) + { + return true; + } + template + bool operator()(const mtx::events::Event &) + { + return false; + } +}; + struct EventMsgType { template @@ -476,3 +490,9 @@ mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents & { return std::visit([](const auto &e) { return nlohmann::json(e); }, event); } + +bool +mtx::accessors::is_state_event(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(IsStateEvent{}, event); +} diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 95e5df24..7bf695fc 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -17,6 +17,9 @@ room_id(const mtx::events::collections::TimelineEvents &event); std::string sender(const mtx::events::collections::TimelineEvents &event); +bool +is_state_event(const mtx::events::collections::TimelineEvents &event); + QDateTime origin_server_ts(const mtx::events::collections::TimelineEvents &event); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index e5a66e19..94d43a83 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -774,15 +774,17 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, if (id.empty()) return nullptr; - std::string id_ = std::string(id); + IdIndex index{room_id_, std::string(id)}; if (resolve_edits) { - auto edits_ = edits(id_); - if (!edits_.empty()) - id_ = mtx::accessors::event_id(edits_.back()); + auto edits_ = edits(index.id); + if (!edits_.empty()) { + index.id = mtx::accessors::event_id(edits_.back()); + auto event_ptr = + new mtx::events::collections::TimelineEvents(std::move(edits_.back())); + events_by_id_.insert(index, event_ptr); + } } - IdIndex index{room_id_, id_}; - auto event_ptr = events_by_id_.object(index); if (!event_ptr) { auto event = cache::client()->getEvent(room_id_, index.id); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index c47194f5..dd4f8696 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -288,6 +288,8 @@ TimelineModel::roleNames() const {ProportionalHeight, "proportionalHeight"}, {Id, "id"}, {State, "state"}, + {IsEdited, "isEdited"}, + {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, {IsRoomEncrypted, "isRoomEncrypted"}, {ReplyTo, "replyTo"}, @@ -409,8 +411,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(prop > 0 ? prop : 1.); } - case Id: - return QVariant(QString::fromStdString(event_id(event))); + case Id: { + if (auto replaces = relations(event).replaces()) + return QVariant(QString::fromStdString(replaces.value())); + else + return QVariant(QString::fromStdString(event_id(event))); + } case State: { auto id = QString::fromStdString(event_id(event)); auto containsOthers = [](const auto &vec) { @@ -430,6 +436,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r else return qml_mtx_events::Received; } + case IsEdited: + return QVariant(relations(event).replaces().has_value()); + case IsEditable: + return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == + http::client()->user_id().to_string()); case IsEncrypted: { auto id = event_id(event); auto encrypted_event = events.get(id, id, false); @@ -444,7 +455,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case ReplyTo: return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { - auto id = event_id(event); + auto id = relations(event).replaces().value_or(event_id(event)); return QVariant::fromValue(events.reactions(id)); } case RoomId: @@ -813,6 +824,12 @@ TimelineModel::replyAction(QString id) setReply(id); } +void +TimelineModel::editAction(QString id) +{ + setEdit(id); +} + RelatedInfo TimelineModel::relatedInfo(QString id) { @@ -1501,6 +1518,22 @@ TimelineModel::formatMemberEvent(QString id) return rendered; } +void +TimelineModel::setEdit(QString newEdit) +{ + if (edit_ != newEdit) { + edit_ = newEdit; + emit editChanged(edit_); + + auto ev = events.get(newEdit.toStdString(), ""); + if (ev) { + setReply(QString::fromStdString( + mtx::accessors::relations(*ev).reply_to().value_or(""))); + // input()->setText(mtx::accessors::body(*ev)); + } + } +} + QString TimelineModel::roomName() const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 51b8049e..463d8705 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -145,6 +145,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(std::vector typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged) Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) + Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) Q_PROPERTY( bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) @@ -181,6 +182,8 @@ public: ProportionalHeight, Id, State, + IsEdited, + IsEditable, IsEncrypted, IsRoomEncrypted, ReplyTo, @@ -213,6 +216,7 @@ public: Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid, bool global = false); + Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; Q_INVOKABLE void redactEvent(QString id); @@ -268,6 +272,16 @@ public slots: emit replyChanged(reply_); } } + QString edit() const { return edit_; } + void setEdit(QString newEdit); + void resetEdit() + { + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + resetReply(); + } + } void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } void clearTimeline() { events.clearTimeline(); } void receivedSessionKey(const std::string &session_key) @@ -292,6 +306,7 @@ signals: void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void typingUsersChanged(std::vector users); void replyChanged(QString reply); + void editChanged(QString reply); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); @@ -322,7 +337,7 @@ private: bool m_paginationInProgress = false; QString currentId; - QString reply_; + QString reply_, edit_; std::vector typingUsers_; TimelineViewManager *manager_; -- cgit 1.5.1 From 9b7d33e847b02031fdc153716614f125992b3734 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 1 Feb 2021 02:22:53 +0100 Subject: Implement message editing The UI still looks ugly, but I have no good idea atm. fixes #134 --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 2 +- resources/qml/MessageInput.qml | 4 ++++ resources/qml/ReplyPopup.qml | 21 ++++++++++++++++++--- resources/qml/TimelineRow.qml | 1 + src/timeline/InputBar.cpp | 40 +++++++++++++++++++++++++++++++++++++++- src/timeline/InputBar.h | 2 ++ src/timeline/TimelineModel.cpp | 24 +++++++++++++++++++++++- src/timeline/TimelineModel.h | 9 +-------- 9 files changed, 90 insertions(+), 15 deletions(-) (limited to 'src/timeline') diff --git a/CMakeLists.txt b/CMakeLists.txt index 577cbffc..2d3c189f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 31e300546eb63ea25b0b879fb255beee6022da03 + GIT_TAG fee5298f068394958c2de935836a2c145f273906 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index f498dd5a..453d6c8a 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,7 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "31e300546eb63ea25b0b879fb255beee6022da03", + "commit": "fee5298f068394958c2de935836a2c145f273906", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 9a83b52b..d665566c 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -261,6 +261,10 @@ Rectangle { Connections { ignoreUnknownSignals: true onInsertText: messageInput.insert(messageInput.cursorPosition, text) + onTextChanged: { + messageInput.text = newText; + messageInput.cursorPosition = newText.length; + } target: TimelineManager.timeline ? TimelineManager.timeline.input : null } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 4659e075..85b64114 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -10,14 +10,15 @@ Rectangle { property var room: TimelineManager.timeline Layout.fillWidth: true - visible: room && room.reply + visible: room && (room.reply || room.edit) // Height of child, plus margins, plus border - implicitHeight: replyPreview.height + 10 + implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10 color: colors.window z: 3 Reply { id: replyPreview + visible: room && room.reply anchors.left: parent.left anchors.leftMargin: 2 * 22 + 3 * 16 @@ -31,9 +32,10 @@ Rectangle { ImageButton { id: closeReplyButton + visible: room && room.reply anchors.right: parent.right - anchors.rightMargin: 15 + anchors.rightMargin: 16 anchors.top: replyPreview.top hoverEnabled: true width: 16 @@ -44,4 +46,17 @@ Rectangle { onClicked: room.reply = undefined } + Button { + id: closeEditButton + visible: room && room.edit + + anchors.left: parent.left + anchors.rightMargin: 16 + anchors.topMargin: 10 + anchors.top: parent.top + //height: 16 + text: qsTr("Abort edit") + onClicked: room.edit = undefined + } + } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index e4dc267b..d4f058e5 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -89,6 +89,7 @@ Item { id: editButton visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited + buttonTextColor: chat.model.edit == model.id ? colors.highlight : colors.buttonText Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 738fb37c..08cbd15b 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -268,7 +268,18 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) text.format = "org.matrix.custom.html"; } - if (!room->reply().isEmpty()) { + if (!room->edit().isEmpty()) { + if (!room->reply().isEmpty()) { + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + room->resetReply(); + } + + text.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + + } else if (!room->reply().isEmpty()) { auto related = room->relatedInfo(room->reply()); QString body; @@ -321,6 +332,11 @@ InputBar::emote(QString msg) {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + emote.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); } @@ -352,6 +368,11 @@ InputBar::image(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + image.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); } @@ -378,6 +399,11 @@ InputBar::file(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + file.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); } @@ -405,6 +431,11 @@ InputBar::audio(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + audio.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); } @@ -431,6 +462,11 @@ InputBar::video(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + video.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); } @@ -524,6 +560,8 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & [this](const QByteArray data, const QString &mime, const QString &fn) { setUploading(true); + setText(""); + auto payload = std::string(data.data(), data.size()); std::optional encryptedFile; if (cache::isRoomEncrypted(room->roomId().toStdString())) { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index f173bbc0..696a0dd9 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -41,6 +41,7 @@ public slots: QString text() const; QString previousText(); QString nextText(); + void setText(QString newText) { emit textChanged(newText); } void send(); void paste(bool fromMouse); @@ -58,6 +59,7 @@ private slots: signals: void insertText(QString text); + void textChanged(QString newText); void uploadingChanged(bool value); private: diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index dd4f8696..de43d5ea 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1529,11 +1529,33 @@ TimelineModel::setEdit(QString newEdit) if (ev) { setReply(QString::fromStdString( mtx::accessors::relations(*ev).reply_to().value_or(""))); - // input()->setText(mtx::accessors::body(*ev)); + + auto msgType = mtx::accessors::msg_type(*ev); + if (msgType == mtx::events::MessageType::Text || + msgType == mtx::events::MessageType::Notice) { + input()->setText(relatedInfo(newEdit).quoted_body); + } else if (msgType == mtx::events::MessageType::Emote) { + input()->setText("/me " + relatedInfo(newEdit).quoted_body); + } else { + input()->setText(""); + } + } else { + input()->setText(""); } } } +void +TimelineModel::resetEdit() +{ + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + input()->setText(""); + resetReply(); + } +} + QString TimelineModel::roomName() const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 463d8705..0aec27a1 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -274,14 +274,7 @@ public slots: } QString edit() const { return edit_; } void setEdit(QString newEdit); - void resetEdit() - { - if (!edit_.isEmpty()) { - edit_ = ""; - emit editChanged(edit_); - resetReply(); - } - } + void resetEdit(); void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } void clearTimeline() { events.clearTimeline(); } void receivedSessionKey(const std::string &session_key) -- cgit 1.5.1 From bdb6e6b79e9beeaabfbde99cd760de77247d11a4 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 01:03:20 +0100 Subject: Fix stuck notifications because of edits Does not fix the read status yet, for that we need to compare read receipts for all events after the last visible event. --- src/Cache.cpp | 90 ++++++++++++++++++++++++++++++++++++++++++ src/Cache.h | 6 +++ src/Cache_p.h | 5 +++ src/timeline/TimelineModel.cpp | 23 +++++++++-- src/timeline/TimelineModel.h | 2 +- 5 files changed, 121 insertions(+), 5 deletions(-) (limited to 'src/timeline') diff --git a/src/Cache.cpp b/src/Cache.cpp index 49861a9a..109fc60d 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1896,6 +1896,84 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) return *val.data(); } +std::optional +Cache::getEventIndex(const std::string &room_id, std::string_view event_id) +{ + if (event_id.empty()) + return {}; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val indexVal{event_id.data(), event_id.size()}, val; + + bool success = lmdb::dbi_get(txn, orderDb, indexVal, val); + if (!success) { + nhlog::db()->critical("Could not find event id: {}", event_id); + return {}; + } + + return *val.data(); +} + +std::optional> +Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) +{ + if (event_id.empty()) + return {}; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + lmdb::dbi eventOrderDb{0}; + lmdb::dbi timelineDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + eventOrderDb = getEventOrderDb(txn, room_id); + timelineDb = getMessageToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val eventIdVal{event_id.data(), event_id.size()}, indexVal; + + bool success = lmdb::dbi_get(txn, orderDb, eventIdVal, indexVal); + if (!success) { + return {}; + } + uint64_t prevIdx = *indexVal.data(); + std::string prevId{eventIdVal.data(), eventIdVal.size()}; + + auto cursor = lmdb::cursor::open(txn, eventOrderDb); + cursor.get(indexVal, MDB_SET); + while (cursor.get(indexVal, eventIdVal, MDB_NEXT)) { + std::string evId = + json::parse(std::string_view(eventIdVal.data(), eventIdVal.size()))["event_id"] + .get(); + lmdb::val temp; + if (lmdb::dbi_get(txn, timelineDb, lmdb::val(evId.data(), evId.size()), temp)) { + return std::pair{prevIdx, std::string(prevId)}; + } else { + prevIdx = *indexVal.data(); + prevId = std::move(evId); + } + } + + return std::pair{prevIdx, std::string(prevId)}; +} + std::optional Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) { @@ -4253,6 +4331,18 @@ readReceipts(const QString &event_id, const QString &room_id) return instance_->readReceipts(event_id, room_id); } +std::optional +getEventIndex(const std::string &room_id, std::string_view event_id) +{ + return instance_->getEventIndex(room_id, event_id); +} + +std::optional> +lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) +{ + return instance_->lastInvisibleEventAfter(room_id, event_id); +} + QByteArray image(const QString &url) { diff --git a/src/Cache.h b/src/Cache.h index 91956725..e60fc970 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -168,6 +168,12 @@ using UserReceipts = std::multimap UserReceipts readReceipts(const QString &event_id, const QString &room_id); +//! get index of the event in the event db, not representing the visual index +std::optional +getEventIndex(const std::string &room_id, std::string_view event_id); +std::optional> +lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); + QByteArray image(const QString &url); QByteArray diff --git a/src/Cache_p.h b/src/Cache_p.h index c96a3f30..431e7bc3 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -204,6 +204,11 @@ public: std::optional getTimelineRange(const std::string &room_id); std::optional getTimelineIndex(const std::string &room_id, std::string_view event_id); + std::optional getEventIndex(const std::string &room_id, + std::string_view event_id); + std::optional> lastInvisibleEventAfter( + const std::string &room_id, + std::string_view event_id); std::optional getTimelineEventId(const std::string &room_id, uint64_t index); std::optional getArrivalIndex(const std::string &room_id, std::string_view event_id); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index de43d5ea..1163d931 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -740,10 +740,25 @@ TimelineModel::setCurrentIndex(int index) auto oldIndex = idToIndex(currentId); currentId = indexToId(index); - emit currentIndexChanged(index); - - if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) { - readEvent(currentId.toStdString()); + if (index != oldIndex) + emit currentIndexChanged(index); + + if (!currentId.startsWith("m")) { + auto oldReadIndex = + cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); + auto nextEventIndexAndId = + cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); + + if (nextEventIndexAndId && + (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { + readEvent(nextEventIndexAndId->second); + currentReadId = QString::fromStdString(nextEventIndexAndId->second); + + nhlog::net()->info("Marked as read {}, index {}, oldReadIndex {}", + nextEventIndexAndId->second, + nextEventIndexAndId->first, + *oldReadIndex); + } } } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0aec27a1..017b6589 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -329,7 +329,7 @@ private: bool decryptDescription = true; bool m_paginationInProgress = false; - QString currentId; + QString currentId, currentReadId; QString reply_, edit_; std::vector typingUsers_; -- cgit 1.5.1 From a62276c28933986907022662cea965cf6269eb5e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 14:32:16 +0100 Subject: Fix UI allowing edits of foreign messages in some cases --- resources/qml/MessageInput.qml | 4 ++-- resources/qml/MessageView.qml | 4 +++- resources/qml/TimelineRow.qml | 6 +++--- resources/qml/TimelineView.qml | 6 +++++- resources/qml/emoji/EmojiButton.qml | 2 +- src/timeline/TimelineModel.cpp | 10 ++++++---- 6 files changed, 20 insertions(+), 12 deletions(-) (limited to 'src/timeline') diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 1b40931f..b5c96660 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -165,7 +165,7 @@ Rectangle { event.accepted = true; } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon - if(cursorPosition == completerTriggeredAt + 1) + if (cursorPosition == completerTriggeredAt + 1) popup.close(); if (popup.opened && popup.count <= 0) @@ -310,7 +310,7 @@ Rectangle { ToolTip.text: qsTr("Emoji") onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { messageInput.insert(messageInput.cursorPosition, emoji); - TimelineManager.focusMessageInput() + TimelineManager.focusMessageInput(); }) } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 0f058830..50e051ab 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -73,7 +73,9 @@ ListView { Shortcut { sequence: "Ctrl+E" - onActivated: chat.model.edit = chat.model.reply + onActivated: { + chat.model.edit = chat.model.reply; + } } Component { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 9f054b9b..5ec23d62 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -26,12 +26,12 @@ Item { acceptedButtons: Qt.AllButtons onClicked: { if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, row); + messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row); else mouse.accepted = false; } onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)); + messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, mouse.x, mouse.y)); } } @@ -143,7 +143,7 @@ Item { image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip.visible: hovered ToolTip.text: qsTr("Options") - onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) + onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, optionsButton) } Label { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4b3c006a..b0880493 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -66,11 +66,13 @@ Page { property string eventId property int eventType property bool isEncrypted + property bool isEditable - function show(eventId_, eventType_, isEncrypted_, showAt_, position) { + function show(eventId_, eventType_, isEncrypted_, isEditable_, showAt_, position) { eventId = eventId_; eventType = eventType_; isEncrypted = isEncrypted_; + isEditable = isEditable_; if (position) popup(position, showAt_); else @@ -92,6 +94,8 @@ Page { } MenuItem { + visible: messageContextMenu.isEditable + height: visible ? implicitHeight : 0 text: qsTr("Edit") onClicked: TimelineManager.timeline.editAction(messageContextMenu.eventId) } diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml index 622f8aa2..dd7530a6 100644 --- a/resources/qml/emoji/EmojiButton.qml +++ b/resources/qml/emoji/EmojiButton.qml @@ -14,6 +14,6 @@ ImageButton { image: ":/icons/icons/ui/smile.png" onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { TimelineManager.queueReactionMessage(event_id, emoji); - TimelineManager.focusMessageInput() + TimelineManager.focusMessageInput(); }) } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 1163d931..493f755b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1537,11 +1537,11 @@ void TimelineModel::setEdit(QString newEdit) { if (edit_ != newEdit) { - edit_ = newEdit; - emit editChanged(edit_); - auto ev = events.get(newEdit.toStdString(), ""); - if (ev) { + if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { + edit_ = newEdit; + emit editChanged(edit_); + setReply(QString::fromStdString( mtx::accessors::relations(*ev).reply_to().value_or(""))); @@ -1555,6 +1555,8 @@ TimelineModel::setEdit(QString newEdit) input()->setText(""); } } else { + edit_ = ""; + emit editChanged(edit_); input()->setText(""); } } -- cgit 1.5.1 From 2606568376e032a5b66275397a52267f2e4c4578 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 15:24:00 +0100 Subject: Fix messages sometimes not being rendered, when they are too large --- resources/qml/MessageView.qml | 1 - src/timeline/DelegateChooser.cpp | 4 ---- 2 files changed, 5 deletions(-) (limited to 'src/timeline') diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index b0498c4e..09dc4e36 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -13,7 +13,6 @@ ListView { Layout.fillWidth: true Layout.fillHeight: true - cacheBuffer: 400 model: TimelineManager.timeline boundsBehavior: Flickable.StopAtBounds pixelAligned: true diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp index 1f5fae7e..8598fa77 100644 --- a/src/timeline/DelegateChooser.cpp +++ b/src/timeline/DelegateChooser.cpp @@ -123,10 +123,6 @@ DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) } chooser.child_->setParentItem(&chooser); - connect(chooser.child_, &QQuickItem::heightChanged, &chooser, [this]() { - chooser.setHeight(chooser.child_->height()); - }); - chooser.setHeight(chooser.child_->height()); QQmlEngine::setObjectOwnership(chooser.child_, QQmlEngine::ObjectOwnership::JavaScriptOwnership); emit chooser.childChanged(); -- cgit 1.5.1 From 0db4d71ec2483c7ac5a7b536737fee8fc53a76d7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 18:07:55 +0100 Subject: Prevent edits of unsent messages --- src/timeline/TimelineModel.cpp | 3 +++ src/timeline/TimelineModel.h | 3 +++ 2 files changed, 6 insertions(+) (limited to 'src/timeline') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 493f755b..b6ebeb84 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1536,6 +1536,9 @@ TimelineModel::formatMemberEvent(QString id) void TimelineModel::setEdit(QString newEdit) { + if (edit_.startsWith('m')) + return; + if (edit_ != newEdit) { auto ev = events.get(newEdit.toStdString(), ""); if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 017b6589..83012cd8 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -260,6 +260,9 @@ public slots: QString reply() const { return reply_; } void setReply(QString newReply) { + if (edit_.startsWith('m')) + return; + if (reply_ != newReply) { reply_ = newReply; emit replyChanged(reply_); -- cgit 1.5.1 From 3c91b5b47befb588a7c8005745a1e8dadf47df03 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 12 Feb 2021 16:10:48 +0100 Subject: Fix crash when editing an edited message pointing to itself --- src/timeline/TimelineModel.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'src/timeline') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b6ebeb84..0f35a290 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1542,13 +1542,11 @@ TimelineModel::setEdit(QString newEdit) if (edit_ != newEdit) { auto ev = events.get(newEdit.toStdString(), ""); if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { - edit_ = newEdit; - emit editChanged(edit_); - + auto e = *ev; setReply(QString::fromStdString( - mtx::accessors::relations(*ev).reply_to().value_or(""))); + mtx::accessors::relations(e).reply_to().value_or(""))); - auto msgType = mtx::accessors::msg_type(*ev); + auto msgType = mtx::accessors::msg_type(e); if (msgType == mtx::events::MessageType::Text || msgType == mtx::events::MessageType::Notice) { input()->setText(relatedInfo(newEdit).quoted_body); @@ -1557,11 +1555,15 @@ TimelineModel::setEdit(QString newEdit) } else { input()->setText(""); } + + edit_ = newEdit; } else { - edit_ = ""; - emit editChanged(edit_); + resetReply(); + input()->setText(""); + edit_ = ""; } + emit editChanged(edit_); } } -- cgit 1.5.1 From 7ddcab3902a6b39c3ed8328c245f58a495b4c43f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 13 Feb 2021 01:41:09 +0100 Subject: Mark messages as read, when Nheko gets focused fixes #235 --- resources/qml/MessageView.qml | 15 ++++++++++++++- src/timeline/TimelineModel.cpp | 6 +++--- 2 files changed, 17 insertions(+), 4 deletions(-) (limited to 'src/timeline') diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 13b4c82c..35b5cac4 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -77,6 +77,19 @@ ListView { } } + Connections { + target: TimelineManager + onFocusChanged: readTimer.running = TimelineManager.isWindowFocused + } + + Timer { + id: readTimer + + // force current read index to update + onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex) + interval: 1000 + } + Component { id: sectionHeader @@ -193,7 +206,7 @@ ListView { Connections { target: chat onMovementEnded: { - if (y + height + 2 * chat.spacing > chat.contentY + timelineRoot.height && y < chat.contentY + timelineRoot.height) + if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) chat.model.currentIndex = index; } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 0f35a290..5c904932 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -735,14 +735,14 @@ TimelineModel::updateLastMessage() void TimelineModel::setCurrentIndex(int index) { - if (!ChatPage::instance()->isActiveWindow()) - return; - auto oldIndex = idToIndex(currentId); currentId = indexToId(index); if (index != oldIndex) emit currentIndexChanged(index); + if (!ChatPage::instance()->isActiveWindow()) + return; + if (!currentId.startsWith("m")) { auto oldReadIndex = cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); -- cgit 1.5.1 From d43607d01c63e003c54c6ba56bb1108cb38cace1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 14 Feb 2021 01:28:28 +0100 Subject: Fix hover handling in the timeline --- CMakeLists.txt | 2 ++ README.md | 4 +-- resources/qml/Avatar.qml | 5 ++++ resources/qml/EncryptionIndicator.qml | 9 ++---- resources/qml/ImageButton.qml | 4 +-- resources/qml/MatrixText.qml | 8 ++--- resources/qml/MessageView.qml | 13 ++++---- resources/qml/TimelineRow.qml | 38 ++++++++++-------------- resources/qml/delegates/FileMessage.qml | 12 ++++++-- resources/qml/delegates/ImageMessage.qml | 16 +++++----- resources/qml/delegates/PlayableMediaMessage.qml | 11 ++++--- resources/qml/delegates/Reply.qml | 16 +++++----- resources/qml/delegates/TextMessage.qml | 1 + src/timeline/TimelineViewManager.cpp | 4 ++- src/ui/NhekoCursorShape.cpp | 25 ++++++++++++++++ src/ui/NhekoCursorShape.h | 26 ++++++++++++++++ 16 files changed, 127 insertions(+), 67 deletions(-) create mode 100644 src/ui/NhekoCursorShape.cpp create mode 100644 src/ui/NhekoCursorShape.h (limited to 'src/timeline') diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d3c189f..72190947 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,6 +281,7 @@ set(SRC_FILES src/ui/InfoMessage.cpp src/ui/Label.cpp src/ui/LoadingIndicator.cpp + src/ui/NhekoCursorShape.cpp src/ui/NhekoDropArea.cpp src/ui/OverlayModal.cpp src/ui/OverlayWidget.cpp @@ -495,6 +496,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ui/Label.h src/ui/FloatingButton.h src/ui/Menu.h + src/ui/NhekoCursorShape.h src/ui/NhekoDropArea.h src/ui/OverlayWidget.h src/ui/SnackBar.h diff --git a/README.md b/README.md index 2ee06940..b9690fc5 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,7 @@ brew install --cask nheko ### Build Requirements -- Qt5 (5.10 or greater). Qt 5.7 adds support for color font rendering with - Freetype, which is essential to properly support emoji, 5.8 adds some features - to make interopability with Qml easier, 5.10 makes sliders actually visible with different palettes. +- Qt5 (5.12 or greater). Required for overlapping hover handlers in Qml. - CMake 3.15 or greater. (Lower version may work, but may break boost linking) - [mtxclient](https://github.com/Nheko-Reborn/mtxclient) - [LMDB](https://symas.com/lightning-memory-mapped-database/) diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index a459fe5a..f01911bb 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -90,4 +90,9 @@ Rectangle { } } + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 46ca62c5..00efe9e4 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -1,4 +1,4 @@ -import QtQuick 2.5 +import QtQuick 2.12 import QtQuick.Controls 2.1 import im.nheko 1.0 @@ -24,14 +24,11 @@ Rectangle { color: "transparent" width: 16 height: 16 - ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.visible: ma.hovered && indicator.visible ToolTip.text: getEncryptionTooltip() - MouseArea { + HoverHandler { id: ma - - anchors.fill: parent - hoverEnabled: true } Image { diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 9c0faef3..159c750f 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -1,6 +1,7 @@ import "./ui" import QtQuick 2.3 import QtQuick.Controls 2.3 +import im.nheko 1.0 // for cursor shape AbstractButton { id: button @@ -23,11 +24,10 @@ AbstractButton { source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" } - MouseArea { + CursorShape { id: mouseArea anchors.fill: parent - onPressed: mouse.accepted = false cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index bb3b4296..4ea15518 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -8,6 +8,7 @@ TextEdit { focus: false wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode + enabled: selectByMouse color: colors.text onLinkActivated: { if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { @@ -25,12 +26,9 @@ TextEdit { ToolTip.visible: hoveredLink ToolTip.text: hoveredLink - MouseArea { - id: ma - + CursorShape { anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 179f6f54..e1641a36 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -1,6 +1,6 @@ import "./delegates" import QtGraphicalEffects 1.0 -import QtQuick 2.9 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -153,12 +153,15 @@ ScrollView { color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window) textFormat: Text.RichText - MouseArea { + TapHandler { + //cursorShape: Qt.PointingHandCursor + + onSingleTapped: chat.model.openUserProfile(modelData.userId) + } + + CursorShape { anchors.fill: parent - Layout.alignment: Qt.AlignHCenter - onClicked: chat.model.openUserProfile(modelData.userId) cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 5ec23d62..3a2ed627 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -1,6 +1,6 @@ import "./delegates" import "./emoji" -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -12,27 +12,23 @@ Item { height: row.height Rectangle { - color: (Settings.messageHoverHighlight && hoverHandler.containsMouse) ? colors.alternateBase : "transparent" + color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent" anchors.fill: row } - MouseArea { + HoverHandler { id: hoverHandler - anchors.fill: parent - propagateComposedEvents: true - preventStealing: false - hoverEnabled: true - acceptedButtons: Qt.AllButtons - onClicked: { - if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row); - else - mouse.accepted = false; - } - onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, mouse.x, mouse.y)); - } + acceptedDevices: PointerDevice.GenericPointer + } + + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, eventPoint.position.x, eventPoint.position.y)) + } + + TapHandler { + onLongPressed: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, point.position.x, point.position.y)) } RowLayout { @@ -151,15 +147,11 @@ Item { text: model.timestamp.toLocaleTimeString("HH:mm") width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) color: inactiveColors.text - ToolTip.visible: ma.containsMouse + ToolTip.visible: ma.hovered ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - MouseArea { + HoverHandler { id: ma - - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index ffd1e82b..4bc202eb 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Layouts 1.2 import im.nheko 1.0 @@ -31,7 +31,15 @@ Item { MouseArea { anchors.fill: parent - onClicked: TimelineManager.timeline.saveMedia(model.data.id) + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) + } + + CursorShape { + anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index e8e325f0..3bb9eb05 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import im.nheko 1.0 Item { @@ -32,20 +32,20 @@ Item { smooth: true mipmap: true - MouseArea { - id: mouseArea - + TapHandler { enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready - hoverEnabled: true - anchors.fill: parent - onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) + onSingleTapped: TimelineManager.openImageOverlay(model.data.url, model.data.id) + } + + HoverHandler { + id: mouseArea } Item { id: overlay anchors.fill: parent - visible: mouseArea.containsMouse + visible: mouseArea.hovered Rectangle { id: container diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 1534da2e..70f39e43 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,5 +1,5 @@ import QtMultimedia 5.6 -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 import im.nheko 1.0 @@ -140,9 +140,8 @@ Rectangle { fillMode: Image.Pad } - MouseArea { - anchors.fill: parent - onClicked: { + TapHandler { + onSingleTapped: { switch (button.state) { case "": TimelineManager.timeline.cacheMedia(model.data.id); @@ -159,6 +158,10 @@ Rectangle { break; } } + } + + CursorShape { + anchors.fill: parent cursorShape: Qt.PointingHandCursor } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index ff1fa657..28c4bf6e 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.12 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 @@ -13,10 +13,12 @@ Item { width: parent.width height: replyContainer.height - MouseArea { + TapHandler { + onSingleTapped: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) + } + + CursorShape { anchors.fill: parent - preventStealing: false - onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) cursorShape: Qt.PointingHandCursor } @@ -43,10 +45,8 @@ Item { color: replyComponent.userColor textFormat: Text.RichText - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(reply.modelData.userId) - cursorShape: Qt.PointingHandCursor + TapHandler { + onSingleTapped: chat.model.openUserProfile(reply.modelData.userId) } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 2c449ca2..82d0d0e4 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -8,5 +8,6 @@ MatrixText { width: parent ? parent.width : undefined height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined clip: isReply + selectByMouse: !Settings.mobileMode && !isReply font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index e1e2b681..b7d2bfb1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -21,6 +21,7 @@ #include "dialogs/ImageOverlay.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" #include //only for debugging @@ -118,6 +119,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par qmlRegisterType("im.nheko", 1, 0, "DelegateChoice"); qmlRegisterType("im.nheko", 1, 0, "DelegateChooser"); qmlRegisterType("im.nheko", 1, 0, "NhekoDropArea"); + qmlRegisterType("im.nheko", 1, 0, "CursorShape"); qmlRegisterUncreatableType( "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); qmlRegisterUncreatableType( @@ -548,4 +550,4 @@ void TimelineViewManager::focusMessageInput() { emit focusInput(); -} \ No newline at end of file +} diff --git a/src/ui/NhekoCursorShape.cpp b/src/ui/NhekoCursorShape.cpp new file mode 100644 index 00000000..06b0a321 --- /dev/null +++ b/src/ui/NhekoCursorShape.cpp @@ -0,0 +1,25 @@ +#include "NhekoCursorShape.h" + +#include + +NhekoCursorShape::NhekoCursorShape(QQuickItem *parent) + : QQuickItem(parent) + , currentShape_(Qt::CursorShape::ArrowCursor) +{} + +Qt::CursorShape +NhekoCursorShape::cursorShape() const +{ + return cursor().shape(); +} + +void +NhekoCursorShape::setCursorShape(Qt::CursorShape cursorShape) +{ + if (currentShape_ == cursorShape) + return; + + currentShape_ = cursorShape; + setCursor(cursorShape); + emit cursorShapeChanged(); +} diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h new file mode 100644 index 00000000..2eab5e42 --- /dev/null +++ b/src/ui/NhekoCursorShape.h @@ -0,0 +1,26 @@ +#pragma once + +// see +// https://stackoverflow.com/questions/27821054/how-to-change-cursor-shape-in-qml-when-mousearea-is-covered-with-another-mousear/29382092#29382092 + +#include + +class NhekoCursorShape : public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY + cursorShapeChanged) + +public: + explicit NhekoCursorShape(QQuickItem *parent = 0); + +private: + Qt::CursorShape cursorShape() const; + void setCursorShape(Qt::CursorShape cursorShape); + + Qt::CursorShape currentShape_; + +signals: + void cursorShapeChanged(); +}; -- cgit 1.5.1