Merge pull request #420 from Nheko-Reborn/render-edits
Switch to new relations format and show edits
This commit is contained in:
commit
4a5b5f992d
@ -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 fee5298f068394958c2de935836a2c145f273906
|
||||
)
|
||||
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
||||
|
@ -220,8 +220,7 @@
|
||||
"name": "mtxclient",
|
||||
"sources": [
|
||||
{
|
||||
"commit": "4951190c938740defa0988d98d5e861038622936",
|
||||
"tag": "v0.4.1",
|
||||
"commit": "fee5298f068394958c2de935836a2c145f273906",
|
||||
"type": "git",
|
||||
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
|
||||
}
|
||||
|
@ -139,6 +139,7 @@ Rectangle {
|
||||
if (TimelineManager.timeline)
|
||||
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
|
||||
|
||||
forceActiveFocus();
|
||||
}
|
||||
onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle)
|
||||
onCursorPositionChanged: {
|
||||
@ -260,13 +261,20 @@ Rectangle {
|
||||
|
||||
Connections {
|
||||
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
|
||||
}
|
||||
|
||||
Connections {
|
||||
ignoreUnknownSignals: true
|
||||
onReplyChanged: messageInput.forceActiveFocus()
|
||||
onEditChanged: messageInput.forceActiveFocus()
|
||||
target: TimelineManager.timeline
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,12 @@ ListView {
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: chat.model.reply = undefined
|
||||
onActivated: {
|
||||
if (chat.model.edit)
|
||||
chat.model.edit = undefined;
|
||||
else
|
||||
chat.model.reply = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
@ -66,6 +71,11 @@ ListView {
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+E"
|
||||
onActivated: chat.model.edit = chat.model.reply
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sectionHeader
|
||||
|
||||
|
@ -10,15 +10,16 @@ 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
|
||||
anchors.right: closeReplyButton.left
|
||||
@ -32,8 +33,9 @@ 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("Cancel edit")
|
||||
onClicked: room.edit = undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -85,6 +85,25 @@ Item {
|
||||
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 {
|
||||
id: reactButton
|
||||
|
||||
|
@ -91,6 +91,11 @@ Page {
|
||||
onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Edit")
|
||||
onClicked: TimelineManager.timeline.editAction(messageContextMenu.eventId)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Read receipts")
|
||||
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
|
||||
|
219
src/Cache.cpp
219
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<EncryptedEvent<msg::Encrypted>>(&e)) {
|
||||
MegolmSessionIndex index;
|
||||
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 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();
|
||||
|
||||
if (last_event_id.empty())
|
||||
if (last_event_id.empty() || fullyReadEventId.empty())
|
||||
return true;
|
||||
|
||||
if (last_event_id == fullyReadEventId)
|
||||
return false;
|
||||
|
||||
// Retrieve all read receipts for that event.
|
||||
const auto receipts =
|
||||
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;
|
||||
return getEventIndex(room_id, last_event_id) > getEventIndex(room_id, fullyReadEventId);
|
||||
}
|
||||
|
||||
void
|
||||
@ -1891,6 +1895,108 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
|
||||
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>
|
||||
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_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<std::string>() : "";
|
||||
|
||||
if (!relates_to.empty()) {
|
||||
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(relates_to),
|
||||
lmdb::val(r.event_id),
|
||||
lmdb::val(txn_id));
|
||||
lmdb::dbi_put(
|
||||
txn, relationsDb, lmdb::val(relates_to), event_id);
|
||||
lmdb::dbi_put(txn,
|
||||
relationsDb,
|
||||
lmdb::val(r.event_id),
|
||||
event_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2808,19 +2910,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<std::string>() : "";
|
||||
|
||||
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 +3000,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<std::string>() : "";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3222,8 +3318,11 @@ Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::st
|
||||
lmdb::val data;
|
||||
if (lmdb::dbi_get(txn, db, lmdb::val(to_string(type)), data)) {
|
||||
mtx::responses::utils::RoomAccountDataEvents events;
|
||||
mtx::responses::utils::parse_room_account_data_events(
|
||||
std::string_view(data.data(), data.size()), events);
|
||||
json j = json::array({
|
||||
json::parse(std::string_view(data.data(), data.size())),
|
||||
});
|
||||
mtx::responses::utils::parse_room_account_data_events(j, events);
|
||||
if (events.size() == 1)
|
||||
return events.front();
|
||||
}
|
||||
} catch (...) {
|
||||
@ -4233,6 +4332,18 @@ readReceipts(const QString &event_id, const QString &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
|
||||
image(const QString &url)
|
||||
{
|
||||
|
@ -168,6 +168,12 @@ using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>
|
||||
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<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
|
||||
image(const QString &url);
|
||||
QByteArray
|
||||
|
@ -204,7 +204,14 @@ public:
|
||||
std::optional<TimelineRange> getTimelineRange(const std::string &room_id);
|
||||
std::optional<uint64_t> getTimelineIndex(const std::string &room_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<uint64_t> 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);
|
||||
|
@ -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,7 +625,9 @@ 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;
|
||||
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);
|
||||
|
@ -206,7 +206,7 @@ private:
|
||||
std::vector<int> 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<int>(err->status_code));
|
||||
});
|
||||
} else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
|
||||
if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>)
|
||||
msg.relates_to = this->relation;
|
||||
if constexpr (!std::is_same_v<T,
|
||||
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>);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,20 @@ struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
|
||||
template<template<class...> class Op, class... Args>
|
||||
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
|
||||
{
|
||||
template<class E>
|
||||
@ -250,31 +264,31 @@ struct EventFilesize
|
||||
}
|
||||
};
|
||||
|
||||
struct EventInReplyTo
|
||||
struct EventRelations
|
||||
{
|
||||
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>
|
||||
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) {
|
||||
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>
|
||||
using related_ev_id_t = decltype(Content::relates_to.event_id);
|
||||
using related_ev_id_t = decltype(Content::relations);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
std::string
|
||||
mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event)
|
||||
mtx::common::Relations
|
||||
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
|
||||
@ -474,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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
@ -53,10 +56,10 @@ 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);
|
||||
void
|
||||
set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations);
|
||||
std::string
|
||||
transaction_id(const mtx::events::collections::TimelineEvents &event);
|
||||
|
||||
|
17
src/Olm.cpp
17
src/Olm.cpp
@ -575,17 +575,8 @@ 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());
|
||||
|
||||
@ -596,8 +587,7 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
|
||||
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.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 {
|
||||
|
@ -293,16 +293,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
|
||||
}
|
||||
|
||||
for (const auto &event : events.events) {
|
||||
std::string relates_to;
|
||||
std::set<std::string> relates_to;
|
||||
if (auto redaction =
|
||||
std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(
|
||||
&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<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
|
||||
&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));
|
||||
}
|
||||
@ -408,6 +405,52 @@ EventStore::handle_room_verification(mtx::events::collections::TimelineEvents ev
|
||||
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
|
||||
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>>(
|
||||
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);
|
||||
}
|
||||
|
||||
@ -489,7 +533,13 @@ EventStore::get(int idx, bool decrypt)
|
||||
if (!event_id)
|
||||
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)
|
||||
return nullptr;
|
||||
else
|
||||
@ -691,8 +741,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);
|
||||
@ -717,7 +766,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__);
|
||||
@ -725,7 +774,16 @@ 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())};
|
||||
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);
|
||||
if (!event_ptr) {
|
||||
|
@ -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<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
|
||||
mtx::events::collections::TimelineEvents *decryptEvent(
|
||||
const IdIndex &idx,
|
||||
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
|
||||
|
@ -268,7 +268,18 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
|
||||
text.format = "org.matrix.custom.html";
|
||||
}
|
||||
|
||||
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;
|
||||
@ -294,7 +305,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,9 +328,15 @@ 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -346,9 +364,15 @@ 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -371,9 +395,15 @@ 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -397,9 +427,15 @@ 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -422,9 +458,15 @@ 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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -518,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<mtx::crypto::EncryptedFile> encryptedFile;
|
||||
if (cache::isRoomEncrypted(room->roomId().toStdString())) {
|
||||
|
@ -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:
|
||||
|
@ -288,6 +288,8 @@ TimelineModel::roleNames() const
|
||||
{ProportionalHeight, "proportionalHeight"},
|
||||
{Id, "id"},
|
||||
{State, "state"},
|
||||
{IsEdited, "isEdited"},
|
||||
{IsEditable, "isEditable"},
|
||||
{IsEncrypted, "isEncrypted"},
|
||||
{IsRoomEncrypted, "isRoomEncrypted"},
|
||||
{ReplyTo, "replyTo"},
|
||||
@ -360,7 +362,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
||||
const static QRegularExpression replyFallback(
|
||||
"<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));
|
||||
if (formattedBody_.isEmpty()) {
|
||||
@ -409,8 +411,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
||||
|
||||
return QVariant(prop > 0 ? prop : 1.);
|
||||
}
|
||||
case Id:
|
||||
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);
|
||||
@ -442,9 +453,9 @@ 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);
|
||||
auto id = relations(event).replaces().value_or(event_id(event));
|
||||
return QVariant::fromValue(events.reactions(id));
|
||||
}
|
||||
case RoomId:
|
||||
@ -729,10 +740,25 @@ TimelineModel::setCurrentIndex(int index)
|
||||
|
||||
auto oldIndex = idToIndex(currentId);
|
||||
currentId = indexToId(index);
|
||||
if (index != oldIndex)
|
||||
emit currentIndexChanged(index);
|
||||
|
||||
if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) {
|
||||
readEvent(currentId.toStdString());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -813,6 +839,12 @@ TimelineModel::replyAction(QString id)
|
||||
setReply(id);
|
||||
}
|
||||
|
||||
void
|
||||
TimelineModel::editAction(QString id)
|
||||
{
|
||||
setEdit(id);
|
||||
}
|
||||
|
||||
RelatedInfo
|
||||
TimelineModel::relatedInfo(QString id)
|
||||
{
|
||||
@ -1501,6 +1533,44 @@ 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("")));
|
||||
|
||||
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
|
||||
{
|
||||
|
@ -145,6 +145,7 @@ class TimelineModel : public QAbstractListModel
|
||||
Q_PROPERTY(std::vector<QString> 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,9 @@ public slots:
|
||||
emit replyChanged(reply_);
|
||||
}
|
||||
}
|
||||
QString edit() const { return edit_; }
|
||||
void setEdit(QString newEdit);
|
||||
void resetEdit();
|
||||
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
|
||||
void clearTimeline() { events.clearTimeline(); }
|
||||
void receivedSessionKey(const std::string &session_key)
|
||||
@ -292,6 +299,7 @@ signals:
|
||||
void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
|
||||
void typingUsersChanged(std::vector<QString> users);
|
||||
void replyChanged(QString reply);
|
||||
void editChanged(QString reply);
|
||||
void paginationInProgressChanged(const bool);
|
||||
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
|
||||
|
||||
@ -321,8 +329,8 @@ private:
|
||||
bool decryptDescription = true;
|
||||
bool m_paginationInProgress = false;
|
||||
|
||||
QString currentId;
|
||||
QString reply_;
|
||||
QString currentId, currentReadId;
|
||||
QString reply_, edit_;
|
||||
std::vector<QString> typingUsers_;
|
||||
|
||||
TimelineViewManager *manager_;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user