Merge pull request #420 from Nheko-Reborn/render-edits

Switch to new relations format and show edits
This commit is contained in:
DeepBlueV7.X 2021-02-10 14:12:49 +01:00 committed by GitHub
commit 4a5b5f992d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 554 additions and 168 deletions

View File

@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG v0.4.1 GIT_TAG fee5298f068394958c2de935836a2c145f273906
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")

View File

@ -220,8 +220,7 @@
"name": "mtxclient", "name": "mtxclient",
"sources": [ "sources": [
{ {
"commit": "4951190c938740defa0988d98d5e861038622936", "commit": "fee5298f068394958c2de935836a2c145f273906",
"tag": "v0.4.1",
"type": "git", "type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git" "url": "https://github.com/Nheko-Reborn/mtxclient.git"
} }

View File

@ -139,6 +139,7 @@ Rectangle {
if (TimelineManager.timeline) if (TimelineManager.timeline)
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
forceActiveFocus();
} }
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle) onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
onCursorPositionChanged: { onCursorPositionChanged: {
@ -260,13 +261,20 @@ Rectangle {
Connections { Connections {
ignoreUnknownSignals: true ignoreUnknownSignals: true
onInsertText: messageInput.insert(messageInput.cursorPosition, text) onInsertText: {
messageInput.insert(messageInput.cursorPosition, text);
}
onTextChanged: {
messageInput.text = newText;
messageInput.cursorPosition = newText.length;
}
target: TimelineManager.timeline ? TimelineManager.timeline.input : null target: TimelineManager.timeline ? TimelineManager.timeline.input : null
} }
Connections { Connections {
ignoreUnknownSignals: true ignoreUnknownSignals: true
onReplyChanged: messageInput.forceActiveFocus() onReplyChanged: messageInput.forceActiveFocus()
onEditChanged: messageInput.forceActiveFocus()
target: TimelineManager.timeline target: TimelineManager.timeline
} }

View File

@ -50,7 +50,12 @@ ListView {
Shortcut { Shortcut {
sequence: StandardKey.Cancel sequence: StandardKey.Cancel
onActivated: chat.model.reply = undefined onActivated: {
if (chat.model.edit)
chat.model.edit = undefined;
else
chat.model.reply = undefined;
}
} }
Shortcut { Shortcut {
@ -66,6 +71,11 @@ ListView {
} }
} }
Shortcut {
sequence: "Ctrl+E"
onActivated: chat.model.edit = chat.model.reply
}
Component { Component {
id: sectionHeader id: sectionHeader

View File

@ -10,15 +10,16 @@ Rectangle {
property var room: TimelineManager.timeline property var room: TimelineManager.timeline
Layout.fillWidth: true Layout.fillWidth: true
visible: room && room.reply visible: room && (room.reply || room.edit)
// Height of child, plus margins, plus border // Height of child, plus margins, plus border
implicitHeight: replyPreview.height + 10 implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10
color: colors.window color: colors.window
z: 3 z: 3
Reply { Reply {
id: replyPreview id: replyPreview
visible: room && room.reply
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 2 * 22 + 3 * 16 anchors.leftMargin: 2 * 22 + 3 * 16
anchors.right: closeReplyButton.left anchors.right: closeReplyButton.left
@ -32,8 +33,9 @@ Rectangle {
ImageButton { ImageButton {
id: closeReplyButton id: closeReplyButton
visible: room && room.reply
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 15 anchors.rightMargin: 16
anchors.top: replyPreview.top anchors.top: replyPreview.top
hoverEnabled: true hoverEnabled: true
width: 16 width: 16
@ -44,4 +46,17 @@ Rectangle {
onClicked: room.reply = undefined 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("Cancel edit")
onClicked: room.edit = undefined
}
} }

View File

@ -85,6 +85,25 @@ Item {
width: 16 width: 16
} }
ImageButton {
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
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 { EmojiButton {
id: reactButton id: reactButton

View File

@ -91,6 +91,11 @@ Page {
onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId) onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
} }
MenuItem {
text: qsTr("Edit")
onClicked: TimelineManager.timeline.editAction(messageContextMenu.eventId)
}
MenuItem { MenuItem {
text: qsTr("Read receipts") text: qsTr("Read receipts")
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)

View File

@ -108,6 +108,11 @@ Cache::isHiddenEvent(lmdb::txn &txn,
const std::string &room_id) const std::string &room_id)
{ {
using namespace mtx::events; using namespace mtx::events;
// Always hide edits
if (mtx::accessors::relations(e).replaces())
return true;
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
MegolmSessionIndex index; MegolmSessionIndex index;
index.room_id = room_id; index.room_id = room_id;
@ -1197,25 +1202,24 @@ Cache::calculateRoomReadStatus(const std::string &room_id)
const auto last_event_id = getLastEventId(txn, room_id); const auto last_event_id = getLastEventId(txn, room_id);
const auto localUser = utils::localUser().toStdString(); const auto localUser = utils::localUser().toStdString();
std::string fullyReadEventId;
if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) {
if (auto fr = std::get_if<
mtx::events::AccountDataEvent<mtx::events::account_data::FullyRead>>(
&ev.value())) {
fullyReadEventId = fr->content.event_id;
}
}
txn.commit(); txn.commit();
if (last_event_id.empty()) if (last_event_id.empty() || fullyReadEventId.empty())
return true;
if (last_event_id == fullyReadEventId)
return false; return false;
// Retrieve all read receipts for that event. // Retrieve all read receipts for that event.
const auto receipts = return getEventIndex(room_id, last_event_id) > getEventIndex(room_id, fullyReadEventId);
readReceipts(QString::fromStdString(last_event_id), QString::fromStdString(room_id));
if (receipts.size() == 0)
return true;
// Check if the local user has a read receipt for it.
for (auto it = receipts.cbegin(); it != receipts.cend(); it++) {
if (it->second == localUser)
return false;
}
return true;
} }
void void
@ -1891,6 +1895,108 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
return *val.data<uint64_t>(); return *val.data<uint64_t>();
} }
std::optional<uint64_t>
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) {
return {};
}
return *val.data<uint64_t>();
}
std::optional<std::pair<uint64_t, std::string>>
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<uint64_t>();
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<std::string>();
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<uint64_t>();
prevId = std::move(evId);
}
}
return std::pair{prevIdx, std::string(prevId)};
}
std::optional<uint64_t>
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<uint64_t>();
}
std::optional<std::string> std::optional<std::string>
Cache::getTimelineEventId(const std::string &room_id, uint64_t index) Cache::getTimelineEventId(const std::string &room_id, uint64_t index)
{ {
@ -2713,23 +2819,19 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
lmdb::dbi_put(txn, evToOrderDb, event_id, txn_order); lmdb::dbi_put(txn, evToOrderDb, event_id, txn_order);
lmdb::dbi_del(txn, evToOrderDb, lmdb::val(txn_id)); lmdb::dbi_del(txn, evToOrderDb, lmdb::val(txn_id));
if (event.contains("content") && auto relations = mtx::accessors::relations(e);
event["content"].contains("m.relates_to")) { if (!relations.relations.empty()) {
auto temp = event["content"]["m.relates_to"]; for (const auto &r : relations.relations) {
json relates_to_j = temp.contains("m.in_reply_to") && if (!r.event_id.empty()) {
temp["m.in_reply_to"].is_object() lmdb::dbi_del(txn,
? temp["m.in_reply_to"]["event_id"] relationsDb,
: temp["event_id"]; lmdb::val(r.event_id),
std::string relates_to = lmdb::val(txn_id));
relates_to_j.is_string() ? relates_to_j.get<std::string>() : ""; lmdb::dbi_put(txn,
relationsDb,
if (!relates_to.empty()) { lmdb::val(r.event_id),
lmdb::dbi_del(txn, event_id);
relationsDb, }
lmdb::val(relates_to),
lmdb::val(txn_id));
lmdb::dbi_put(
txn, relationsDb, lmdb::val(relates_to), event_id);
} }
} }
@ -2808,19 +2910,16 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
lmdb::val(&msgIndex, sizeof(msgIndex))); lmdb::val(&msgIndex, sizeof(msgIndex)));
} }
if (event.contains("content") && auto relations = mtx::accessors::relations(e);
event["content"].contains("m.relates_to")) { if (!relations.relations.empty()) {
auto temp = event["content"]["m.relates_to"]; for (const auto &r : relations.relations) {
json relates_to_j = temp.contains("m.in_reply_to") && if (!r.event_id.empty()) {
temp["m.in_reply_to"].is_object() lmdb::dbi_put(txn,
? temp["m.in_reply_to"]["event_id"] relationsDb,
: temp["event_id"]; lmdb::val(r.event_id),
std::string relates_to = event_id);
relates_to_j.is_string() ? relates_to_j.get<std::string>() : ""; }
}
if (!relates_to.empty())
lmdb::dbi_put(
txn, relationsDb, lmdb::val(relates_to), event_id);
} }
} }
} }
@ -2901,17 +3000,14 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex))); txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex)));
} }
if (event.contains("content") && event["content"].contains("m.relates_to")) { auto relations = mtx::accessors::relations(e);
auto temp = event["content"]["m.relates_to"]; if (!relations.relations.empty()) {
json relates_to_j = for (const auto &r : relations.relations) {
temp.contains("m.in_reply_to") && temp["m.in_reply_to"].is_object() if (!r.event_id.empty()) {
? temp["m.in_reply_to"]["event_id"] lmdb::dbi_put(
: temp["event_id"]; txn, relationsDb, lmdb::val(r.event_id), event_id);
std::string relates_to = }
relates_to_j.is_string() ? relates_to_j.get<std::string>() : ""; }
if (!relates_to.empty())
lmdb::dbi_put(txn, relationsDb, lmdb::val(relates_to), event_id);
} }
} }
@ -3222,9 +3318,12 @@ Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::st
lmdb::val data; lmdb::val data;
if (lmdb::dbi_get(txn, db, lmdb::val(to_string(type)), data)) { if (lmdb::dbi_get(txn, db, lmdb::val(to_string(type)), data)) {
mtx::responses::utils::RoomAccountDataEvents events; mtx::responses::utils::RoomAccountDataEvents events;
mtx::responses::utils::parse_room_account_data_events( json j = json::array({
std::string_view(data.data(), data.size()), events); json::parse(std::string_view(data.data(), data.size())),
return events.front(); });
mtx::responses::utils::parse_room_account_data_events(j, events);
if (events.size() == 1)
return events.front();
} }
} catch (...) { } catch (...) {
} }
@ -4233,6 +4332,18 @@ readReceipts(const QString &event_id, const QString &room_id)
return instance_->readReceipts(event_id, room_id); return instance_->readReceipts(event_id, room_id);
} }
std::optional<uint64_t>
getEventIndex(const std::string &room_id, std::string_view event_id)
{
return instance_->getEventIndex(room_id, event_id);
}
std::optional<std::pair<uint64_t, std::string>>
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id)
{
return instance_->lastInvisibleEventAfter(room_id, event_id);
}
QByteArray QByteArray
image(const QString &url) image(const QString &url)
{ {

View File

@ -168,6 +168,12 @@ using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>
UserReceipts UserReceipts
readReceipts(const QString &event_id, const QString &room_id); 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<uint64_t>
getEventIndex(const std::string &room_id, std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>>
lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id);
QByteArray QByteArray
image(const QString &url); image(const QString &url);
QByteArray QByteArray

View File

@ -204,7 +204,14 @@ public:
std::optional<TimelineRange> getTimelineRange(const std::string &room_id); std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
std::optional<uint64_t> getTimelineIndex(const std::string &room_id, std::optional<uint64_t> getTimelineIndex(const std::string &room_id,
std::string_view event_id); std::string_view event_id);
std::optional<uint64_t> getEventIndex(const std::string &room_id,
std::string_view event_id);
std::optional<std::pair<uint64_t, std::string>> lastInvisibleEventAfter(
const std::string &room_id,
std::string_view event_id);
std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index); std::optional<std::string> getTimelineEventId(const std::string &room_id, uint64_t index);
std::optional<uint64_t> getArrivalIndex(const std::string &room_id,
std::string_view event_id);
std::string previousBatchToken(const std::string &room_id); std::string previousBatchToken(const std::string &room_id);
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);

View File

@ -105,8 +105,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && 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.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
error_ = User; error_ = User;
@ -152,8 +152,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
@ -217,8 +217,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
@ -385,8 +385,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if ((msg.relates_to.has_value() && sender)) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
else { else {
this->deviceId = QString::fromStdString(msg.from_device); this->deviceId = QString::fromStdString(msg.from_device);
@ -402,8 +402,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
if (msg.transaction_id.has_value()) { if (msg.transaction_id.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
nhlog::ui()->info("Flow done on other side"); 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.has_value()) {
if (msg.transaction_id.value() != this->transaction_id) if (msg.transaction_id.value() != this->transaction_id)
return; return;
} else if (msg.relates_to.has_value()) { } else if (msg.relations.references()) {
if (msg.relates_to.value().event_id != this->relation.event_id) if (msg.relations.references() != this->relation.event_id)
return; return;
} }
if ((std::find(msg.key_agreement_protocols.begin(), if ((std::find(msg.key_agreement_protocols.begin(),
@ -625,8 +625,10 @@ DeviceVerificationFlow::startVerificationRequest()
req.transaction_id = this->transaction_id; req.transaction_id = this->transaction_id;
this->canonical_json = nlohmann::json(req); this->canonical_json = nlohmann::json(req);
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
req.relates_to = this->relation; req.relations.relations.push_back(this->relation);
this->canonical_json = nlohmann::json(req); // Set synthesized to surpress the nheko relation extensions
req.relations.synthesized = true;
this->canonical_json = nlohmann::json(req);
} }
send(req); send(req);
setState(WaitingForOtherToAccept); setState(WaitingForOtherToAccept);

View File

@ -206,7 +206,7 @@ private:
std::vector<int> sasList; std::vector<int> sasList;
UserKeyCache their_keys; UserKeyCache their_keys;
TimelineModel *model_; TimelineModel *model_;
mtx::common::RelatesTo relation; mtx::common::Relation relation;
State state_ = PromptStartVerification; State state_ = PromptStartVerification;
Error error_ = UnknownMethod; Error error_ = UnknownMethod;
@ -230,8 +230,12 @@ private:
static_cast<int>(err->status_code)); static_cast<int>(err->status_code));
}); });
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>) if constexpr (!std::is_same_v<T,
msg.relates_to = this->relation; mtx::events::msg::KeyVerificationRequest>) {
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<T>); (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>);
} }

View File

@ -34,6 +34,20 @@ struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
template<template<class...> class Op, class... Args> template<template<class...> class Op, class... Args>
using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t; using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t;
struct IsStateEvent
{
template<class T>
bool operator()(const mtx::events::StateEvent<T> &)
{
return true;
}
template<class T>
bool operator()(const mtx::events::Event<T> &)
{
return false;
}
};
struct EventMsgType struct EventMsgType
{ {
template<class E> template<class E>
@ -250,31 +264,31 @@ struct EventFilesize
} }
}; };
struct EventInReplyTo struct EventRelations
{ {
template<class Content> template<class Content>
using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id); using related_ev_id_t = decltype(Content::relations);
template<class T> template<class T>
std::string operator()(const mtx::events::Event<T> &e) mtx::common::Relations operator()(const mtx::events::Event<T> &e)
{ {
if constexpr (is_detected<related_ev_id_t, T>::value) { if constexpr (is_detected<related_ev_id_t, T>::value) {
return e.content.relates_to.in_reply_to.event_id; return e.content.relations;
} }
return ""; return {};
} }
}; };
struct EventRelatesTo struct SetEventRelations
{ {
mtx::common::Relations new_relations;
template<class Content> template<class Content>
using related_ev_id_t = decltype(Content::relates_to.event_id); using related_ev_id_t = decltype(Content::relations);
template<class T> template<class T>
std::string operator()(const mtx::events::Event<T> &e) void operator()(mtx::events::Event<T> &e)
{ {
if constexpr (is_detected<related_ev_id_t, T>::value) { if constexpr (is_detected<related_ev_id_t, T>::value) {
return e.content.relates_to.event_id; e.content.relations = std::move(new_relations);
} }
return "";
} }
}; };
@ -434,15 +448,17 @@ mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event)
{ {
return std::visit(EventMimeType{}, event); return std::visit(EventMimeType{}, event);
} }
std::string mtx::common::Relations
mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event) mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event)
{ {
return std::visit(EventInReplyTo{}, event); return std::visit(EventRelations{}, event);
} }
std::string
mtx::accessors::relates_to_event_id(const mtx::events::collections::TimelineEvents &event) void
mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event,
mtx::common::Relations relations)
{ {
return std::visit(EventRelatesTo{}, event); std::visit(SetEventRelations{std::move(relations)}, event);
} }
std::string std::string
@ -474,3 +490,9 @@ mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents &
{ {
return std::visit([](const auto &e) { return nlohmann::json(e); }, event); 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);
}

View File

@ -17,6 +17,9 @@ room_id(const mtx::events::collections::TimelineEvents &event);
std::string std::string
sender(const mtx::events::collections::TimelineEvents &event); sender(const mtx::events::collections::TimelineEvents &event);
bool
is_state_event(const mtx::events::collections::TimelineEvents &event);
QDateTime QDateTime
origin_server_ts(const mtx::events::collections::TimelineEvents &event); origin_server_ts(const mtx::events::collections::TimelineEvents &event);
@ -53,10 +56,10 @@ std::string
blurhash(const mtx::events::collections::TimelineEvents &event); blurhash(const mtx::events::collections::TimelineEvents &event);
std::string std::string
mimetype(const mtx::events::collections::TimelineEvents &event); mimetype(const mtx::events::collections::TimelineEvents &event);
std::string mtx::common::Relations
in_reply_to_event(const mtx::events::collections::TimelineEvents &event); relations(const mtx::events::collections::TimelineEvents &event);
std::string void
relates_to_event_id(const mtx::events::collections::TimelineEvents &event); set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations);
std::string std::string
transaction_id(const mtx::events::collections::TimelineEvents &event); transaction_id(const mtx::events::collections::TimelineEvents &event);

View File

@ -575,29 +575,19 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
if (!sendSessionTo.empty()) if (!sendSessionTo.empty())
olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload);
mtx::common::ReplyRelatesTo relation;
mtx::common::RelatesTo r_relation;
// relations shouldn't be encrypted... // relations shouldn't be encrypted...
if (body["content"].contains("m.relates_to")) { mtx::common::Relations relations = mtx::common::parse_relations(body["content"]);
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"];
}
}
auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); auto payload = olm::client()->encrypt_group_message(session.get(), body.dump());
// Prepare the m.room.encrypted event. // Prepare the m.room.encrypted event.
msg::Encrypted data; msg::Encrypted data;
data.ciphertext = std::string((char *)payload.data(), payload.size()); data.ciphertext = std::string((char *)payload.data(), payload.size());
data.sender_key = olm::client()->identity_keys().curve25519; data.sender_key = olm::client()->identity_keys().curve25519;
data.session_id = mtx::crypto::session_id(session.get()); data.session_id = mtx::crypto::session_id(session.get());
data.device_id = device_id; data.device_id = device_id;
data.algorithm = MEGOLM_ALGO; data.algorithm = MEGOLM_ALGO;
data.relates_to = relation; data.relations = relations;
data.r_relates_to = r_relation;
group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); group_session_data.message_index = olm_outbound_group_session_message_index(session.get());
nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); nhlog::crypto()->debug("next message_index {}", group_session_data.message_index);
@ -910,8 +900,7 @@ decryptEvent(const MegolmSessionIndex &index,
body["unsigned"] = event.unsigned_data; body["unsigned"] = event.unsigned_data;
// relations are unencrypted in content... // relations are unencrypted in content...
if (json old_ev = event; old_ev["content"].count("m.relates_to") != 0) mtx::common::add_relations(body["content"], event.content.relations);
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
mtx::events::collections::TimelineEvent te; mtx::events::collections::TimelineEvent te;
try { try {

View File

@ -293,16 +293,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
} }
for (const auto &event : events.events) { for (const auto &event : events.events) {
std::string relates_to; std::set<std::string> relates_to;
if (auto redaction = if (auto redaction =
std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>( std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
&event)) { &event)) {
// fixup reactions // fixup reactions
auto redacted = events_by_id_.object({room_id_, redaction->redacts}); auto redacted = events_by_id_.object({room_id_, redaction->redacts});
if (redacted) { if (redacted) {
auto id = mtx::accessors::relates_to_event_id(*redacted); auto id = mtx::accessors::relations(*redacted);
if (!id.empty()) { if (id.annotates()) {
auto idx = idToIndex(id); auto idx = idToIndex(id.annotates()->event_id);
if (idx) { if (idx) {
events_by_id_.remove( events_by_id_.remove(
{room_id_, redaction->redacts}); {room_id_, redaction->redacts});
@ -312,20 +312,17 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
} }
} }
relates_to = redaction->redacts; relates_to.insert(redaction->redacts);
} else if (auto reaction =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
&event)) {
relates_to = reaction->content.relates_to.event_id;
} else { } 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()) { for (const auto &relates_to_id : relates_to) {
auto idx = cache::client()->getTimelineIndex(room_id_, relates_to); auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id);
if (idx) { if (idx) {
events_by_id_.remove({room_id_, relates_to}); events_by_id_.remove({room_id_, relates_to_id});
decryptedEvents_.remove({room_id_, relates_to}); decryptedEvents_.remove({room_id_, relates_to_id});
events_.remove({room_id_, *idx}); events_.remove({room_id_, *idx});
emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
} }
@ -408,6 +405,52 @@ EventStore::handle_room_verification(mtx::events::collections::TimelineEvents ev
event); event);
} }
std::vector<mtx::events::collections::TimelineEvents>
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);
auto original_relations = mtx::accessors::relations(*original_event);
std::vector<mtx::events::collections::TimelineEvents> edits;
for (const auto &id : event_ids) {
auto related_event = get(id, event_id, false, false);
if (!related_event)
continue;
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_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();
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 QVariantList
EventStore::reactions(const std::string &event_id) EventStore::reactions(const std::string &event_id)
{ {
@ -430,13 +473,14 @@ EventStore::reactions(const std::string &event_id)
if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>( if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
related_event); related_event);
reaction && reaction->content.relates_to.key) { reaction && reaction->content.relations.annotates() &&
auto &agg = aggregation[reaction->content.relates_to.key.value()]; reaction->content.relations.annotates()->key) {
auto key = reaction->content.relations.annotates()->key.value();
auto &agg = aggregation[key];
if (agg.count == 0) { if (agg.count == 0) {
Reaction temp{}; Reaction temp{};
temp.key_ = temp.key_ = QString::fromStdString(key);
QString::fromStdString(reaction->content.relates_to.key.value());
reactions.push_back(temp); reactions.push_back(temp);
} }
@ -489,7 +533,13 @@ EventStore::get(int idx, bool decrypt)
if (!event_id) if (!event_id)
return nullptr; return nullptr;
auto event = cache::client()->getEvent(room_id_, *event_id); std::optional<mtx::events::collections::TimelineEvent> event;
auto edits_ = edits(*event_id);
if (edits_.empty())
event = cache::client()->getEvent(room_id_, *event_id);
else
event = {edits_.back()};
if (!event) if (!event)
return nullptr; return nullptr;
else else
@ -691,8 +741,7 @@ EventStore::decryptEvent(const IdIndex &idx,
body["unsigned"] = e.unsigned_data; body["unsigned"] = e.unsigned_data;
// relations are unencrypted in content... // relations are unencrypted in content...
if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) mtx::common::add_relations(body["content"], e.content.relations);
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
json event_array = json::array(); json event_array = json::array();
event_array.push_back(body); event_array.push_back(body);
@ -717,7 +766,7 @@ EventStore::decryptEvent(const IdIndex &idx,
} }
mtx::events::collections::TimelineEvents * 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()) if (this->thread() != QThread::currentThread())
nhlog::db()->warn("{} called from a different thread!", __func__); nhlog::db()->warn("{} called from a different thread!", __func__);
@ -725,7 +774,16 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
if (id.empty()) if (id.empty())
return nullptr; return nullptr;
IdIndex index{room_id_, std::string(id.data(), id.size())}; IdIndex index{room_id_, std::string(id)};
if (resolve_edits) {
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);
}
}
auto event_ptr = events_by_id_.object(index); auto event_ptr = events_by_id_.object(index);
if (!event_ptr) { if (!event_ptr) {

View File

@ -66,7 +66,8 @@ public:
// relatedFetched event // relatedFetched event
mtx::events::collections::TimelineEvents *get(std::string_view id, mtx::events::collections::TimelineEvents *get(std::string_view id,
std::string_view related_to, 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 // always returns a proper event as long as the idx is valid
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
@ -110,6 +111,7 @@ public slots:
void clearTimeline(); void clearTimeline();
private: private:
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
mtx::events::collections::TimelineEvents *decryptEvent( mtx::events::collections::TimelineEvents *decryptEvent(
const IdIndex &idx, const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);

View File

@ -268,7 +268,18 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
text.format = "org.matrix.custom.html"; 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()); auto related = room->relatedInfo(room->reply());
QString body; QString body;
@ -294,7 +305,8 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
text.formatted_body = text.formatted_body =
utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); 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(); room->resetReply();
} }
@ -316,9 +328,15 @@ InputBar::emote(QString msg)
} }
if (!room->reply().isEmpty()) { 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(); 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); room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
} }
@ -346,9 +364,15 @@ InputBar::image(const QString &filename,
image.url = url.toStdString(); image.url = url.toStdString();
if (!room->reply().isEmpty()) { 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(); 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); room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
} }
@ -371,9 +395,15 @@ InputBar::file(const QString &filename,
file.url = url.toStdString(); file.url = url.toStdString();
if (!room->reply().isEmpty()) { 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(); 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); room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
} }
@ -397,9 +427,15 @@ InputBar::audio(const QString &filename,
audio.url = url.toStdString(); audio.url = url.toStdString();
if (!room->reply().isEmpty()) { 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(); 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); room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
} }
@ -422,9 +458,15 @@ InputBar::video(const QString &filename,
video.url = url.toStdString(); video.url = url.toStdString();
if (!room->reply().isEmpty()) { 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(); 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); room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
} }
@ -518,6 +560,8 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
[this](const QByteArray data, const QString &mime, const QString &fn) { [this](const QByteArray data, const QString &mime, const QString &fn) {
setUploading(true); setUploading(true);
setText("");
auto payload = std::string(data.data(), data.size()); auto payload = std::string(data.data(), data.size());
std::optional<mtx::crypto::EncryptedFile> encryptedFile; std::optional<mtx::crypto::EncryptedFile> encryptedFile;
if (cache::isRoomEncrypted(room->roomId().toStdString())) { if (cache::isRoomEncrypted(room->roomId().toStdString())) {

View File

@ -41,6 +41,7 @@ public slots:
QString text() const; QString text() const;
QString previousText(); QString previousText();
QString nextText(); QString nextText();
void setText(QString newText) { emit textChanged(newText); }
void send(); void send();
void paste(bool fromMouse); void paste(bool fromMouse);
@ -58,6 +59,7 @@ private slots:
signals: signals:
void insertText(QString text); void insertText(QString text);
void textChanged(QString newText);
void uploadingChanged(bool value); void uploadingChanged(bool value);
private: private:

View File

@ -288,6 +288,8 @@ TimelineModel::roleNames() const
{ProportionalHeight, "proportionalHeight"}, {ProportionalHeight, "proportionalHeight"},
{Id, "id"}, {Id, "id"},
{State, "state"}, {State, "state"},
{IsEdited, "isEdited"},
{IsEditable, "isEditable"},
{IsEncrypted, "isEncrypted"}, {IsEncrypted, "isEncrypted"},
{IsRoomEncrypted, "isRoomEncrypted"}, {IsRoomEncrypted, "isRoomEncrypted"},
{ReplyTo, "replyTo"}, {ReplyTo, "replyTo"},
@ -360,7 +362,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
const static QRegularExpression replyFallback( const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); "<mx-reply>.*</mx-reply>", 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)); auto formattedBody_ = QString::fromStdString(formatted_body(event));
if (formattedBody_.isEmpty()) { if (formattedBody_.isEmpty()) {
@ -409,8 +411,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return QVariant(prop > 0 ? prop : 1.); return QVariant(prop > 0 ? prop : 1.);
} }
case Id: case Id: {
return QVariant(QString::fromStdString(event_id(event))); if (auto replaces = relations(event).replaces())
return QVariant(QString::fromStdString(replaces.value()));
else
return QVariant(QString::fromStdString(event_id(event)));
}
case State: { case State: {
auto id = QString::fromStdString(event_id(event)); auto id = QString::fromStdString(event_id(event));
auto containsOthers = [](const auto &vec) { auto containsOthers = [](const auto &vec) {
@ -430,6 +436,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
else else
return qml_mtx_events::Received; 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: { case IsEncrypted: {
auto id = event_id(event); auto id = event_id(event);
auto encrypted_event = events.get(id, id, false); auto encrypted_event = events.get(id, id, false);
@ -442,9 +453,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return cache::isRoomEncrypted(room_id_.toStdString()); return cache::isRoomEncrypted(room_id_.toStdString());
} }
case ReplyTo: case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event))); return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
case Reactions: { case Reactions: {
auto id = event_id(event); auto id = relations(event).replaces().value_or(event_id(event));
return QVariant::fromValue(events.reactions(id)); return QVariant::fromValue(events.reactions(id));
} }
case RoomId: case RoomId:
@ -729,10 +740,25 @@ TimelineModel::setCurrentIndex(int index)
auto oldIndex = idToIndex(currentId); auto oldIndex = idToIndex(currentId);
currentId = indexToId(index); currentId = indexToId(index);
emit currentIndexChanged(index); if (index != oldIndex)
emit currentIndexChanged(index);
if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) { if (!currentId.startsWith("m")) {
readEvent(currentId.toStdString()); 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);
}
} }
} }
@ -813,6 +839,12 @@ TimelineModel::replyAction(QString id)
setReply(id); setReply(id);
} }
void
TimelineModel::editAction(QString id)
{
setEdit(id);
}
RelatedInfo RelatedInfo
TimelineModel::relatedInfo(QString id) TimelineModel::relatedInfo(QString id)
{ {
@ -1501,6 +1533,44 @@ TimelineModel::formatMemberEvent(QString id)
return rendered; 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("")));
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 QString
TimelineModel::roomName() const TimelineModel::roomName() const
{ {

View File

@ -145,6 +145,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY
typingUsersChanged) typingUsersChanged)
Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) 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( Q_PROPERTY(
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
@ -181,6 +182,8 @@ public:
ProportionalHeight, ProportionalHeight,
Id, Id,
State, State,
IsEdited,
IsEditable,
IsEncrypted, IsEncrypted,
IsRoomEncrypted, IsRoomEncrypted,
ReplyTo, ReplyTo,
@ -213,6 +216,7 @@ public:
Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void viewRawMessage(QString id) const;
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void openUserProfile(QString userid, bool global = false); Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
Q_INVOKABLE void editAction(QString id);
Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void replyAction(QString id);
Q_INVOKABLE void readReceiptsAction(QString id) const; Q_INVOKABLE void readReceiptsAction(QString id) const;
Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE void redactEvent(QString id);
@ -268,6 +272,9 @@ public slots:
emit replyChanged(reply_); emit replyChanged(reply_);
} }
} }
QString edit() const { return edit_; }
void setEdit(QString newEdit);
void resetEdit();
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
void clearTimeline() { events.clearTimeline(); } void clearTimeline() { events.clearTimeline(); }
void receivedSessionKey(const std::string &session_key) void receivedSessionKey(const std::string &session_key)
@ -292,6 +299,7 @@ signals:
void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
void typingUsersChanged(std::vector<QString> users); void typingUsersChanged(std::vector<QString> users);
void replyChanged(QString reply); void replyChanged(QString reply);
void editChanged(QString reply);
void paginationInProgressChanged(const bool); void paginationInProgressChanged(const bool);
void newCallEvent(const mtx::events::collections::TimelineEvents &event); void newCallEvent(const mtx::events::collections::TimelineEvents &event);
@ -321,8 +329,8 @@ private:
bool decryptDescription = true; bool decryptDescription = true;
bool m_paginationInProgress = false; bool m_paginationInProgress = false;
QString currentId; QString currentId, currentReadId;
QString reply_; QString reply_, edit_;
std::vector<QString> typingUsers_; std::vector<QString> typingUsers_;
TimelineViewManager *manager_; TimelineViewManager *manager_;

View File

@ -503,9 +503,11 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt
// If selfReactedEvent is empty, that means we haven't previously reacted // If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) { if (selfReactedEvent.isEmpty()) {
mtx::events::msg::Reaction reaction; mtx::events::msg::Reaction reaction;
reaction.relates_to.rel_type = mtx::common::RelationType::Annotation; mtx::common::Relation rel;
reaction.relates_to.event_id = reactedEvent.toStdString(); rel.rel_type = mtx::common::RelationType::Annotation;
reaction.relates_to.key = reactionKey.toStdString(); rel.event_id = reactedEvent.toStdString();
rel.key = reactionKey.toStdString();
reaction.relations.relations.push_back(rel);
timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted // Otherwise, we have previously reacted and the reaction should be redacted