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(
|
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 "")
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
227
src/Cache.cpp
227
src/Cache.cpp
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
27
src/Olm.cpp
27
src/Olm.cpp
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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())) {
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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_;
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user