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 --- src/timeline/TimelineModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') 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)); -- 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/TimelineModel.cpp') 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/TimelineModel.cpp') 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/TimelineModel.cpp') 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/TimelineModel.cpp') 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 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/TimelineModel.cpp') 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/TimelineModel.cpp') 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/TimelineModel.cpp') 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