diff --git a/CMakeLists.txt b/CMakeLists.txt
index ba90835c..a1e0a41f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -581,7 +581,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
- GIT_TAG b3740e26ad07a534b6092959c0da4d91b77a8528
+ GIT_TAG 5ef4460c26acb02f24530db1c6058534b87014f6
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index 1c358f88..eb2feb96 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -173,7 +173,7 @@ modules:
buildsystem: cmake-ninja
name: mtxclient
sources:
- - commit: b3740e26ad07a534b6092959c0da4d91b77a8528
+ - commit: 5ef4460c26acb02f24530db1c6058534b87014f6
#tag: v0.8.2
type: git
url: https://github.com/Nheko-Reborn/mtxclient.git
diff --git a/resources/icons/ui/bookmark-disabled.svg b/resources/icons/ui/bookmark-disabled.svg
new file mode 100644
index 00000000..00d47430
--- /dev/null
+++ b/resources/icons/ui/bookmark-disabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/icons/ui/bookmark.svg b/resources/icons/ui/bookmark.svg
new file mode 100644
index 00000000..59fec384
--- /dev/null
+++ b/resources/icons/ui/bookmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/icons/ui/dismiss_thread.svg b/resources/icons/ui/dismiss_thread.svg
new file mode 100644
index 00000000..3285d31c
--- /dev/null
+++ b/resources/icons/ui/dismiss_thread.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/icons/ui/needle.svg b/resources/icons/ui/needle.svg
new file mode 100644
index 00000000..a210c958
--- /dev/null
+++ b/resources/icons/ui/needle.svg
@@ -0,0 +1,6 @@
+
diff --git a/resources/icons/ui/new-thread.svg b/resources/icons/ui/new-thread.svg
new file mode 100644
index 00000000..f79f2d99
--- /dev/null
+++ b/resources/icons/ui/new-thread.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/icons/ui/thread.svg b/resources/icons/ui/thread.svg
new file mode 100644
index 00000000..81eab2a0
--- /dev/null
+++ b/resources/icons/ui/thread.svg
@@ -0,0 +1 @@
+
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 6848f85c..6174a6c0 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -378,6 +378,10 @@ Rectangle {
messageInput.forceActiveFocus();
}
+ function onThreadChanged() {
+ messageInput.forceActiveFocus();
+ }
+
ignoreUnknownSignals: true
target: room
}
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 2e1d9398..29e5ccf5 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -116,9 +116,7 @@ Item {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
onClicked: {
- if (row.model.isEditable)
- chat.model.editAction(row.model.eventId);
-
+ if (row.model.isEditable) chat.model.edit = row.model.eventId;
}
}
@@ -139,6 +137,19 @@ Item {
})
}
+ ImageButton {
+ id: threadButton
+
+ visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false
+ width: 16
+ hoverEnabled: true
+ image: row.model.threadId ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
+ ToolTip.visible: hovered
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: row.model.threadId ? qsTr("Reply in thread") : qsTr("New thread")
+ onClicked: chat.model.thread = (row.model.threadId || row.model.eventId)
+ }
+
ImageButton {
id: replyButton
@@ -149,7 +160,7 @@ Item {
ToolTip.visible: hovered
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Reply")
- onClicked: chat.model.replyAction(row.model.eventId)
+ onClicked: chat.model.reply = row.model.eventId
}
ImageButton {
@@ -161,7 +172,7 @@ Item {
ToolTip.visible: hovered
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Options")
- onClicked: messageContextMenu.show(row.model.eventId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
+ onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
}
}
@@ -196,8 +207,10 @@ Item {
room.input.declineUploads();
else if(chat.model.reply)
chat.model.reply = undefined;
- else
+ else if (chat.model.edit)
chat.model.edit = undefined;
+ else
+ chat.model.thread = undefined
TimelineManager.focusMessageInput();
}
}
@@ -383,6 +396,7 @@ Item {
required property bool isStateEvent
required property bool previousMessageIsStateEvent
required property string replyTo
+ required property string threadId
required property string userId
required property string roomTopic
required property string roomName
@@ -448,6 +462,7 @@ Item {
isEdited: wrapper.isEdited
isStateEvent: wrapper.isStateEvent
replyTo: wrapper.replyTo
+ threadId: wrapper.threadId
userId: wrapper.userId
userName: wrapper.userName
roomTopic: wrapper.roomTopic
@@ -554,6 +569,7 @@ Item {
id: messageContextMenu
property string eventId
+ property string threadId
property string link
property string text
property int eventType
@@ -561,8 +577,9 @@ Item {
property bool isEditable
property bool isSender
- function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
+ function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
+ threadId = threadId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
@@ -623,14 +640,21 @@ Item {
Platform.MenuItem {
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Repl&y")
- onTriggered: room.replyAction(messageContextMenu.eventId)
+ onTriggered: room.reply = (messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("&Edit")
- onTriggered: room.editAction(messageContextMenu.eventId)
+ onTriggered: room.edit = (messageContextMenu.eventId)
+ }
+
+ Platform.MenuItem {
+ visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
+ enabled: visible
+ text: qsTr("&Thread")
+ onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId)
}
Platform.MenuItem {
@@ -641,7 +665,7 @@ Item {
}
Platform.MenuItem {
- text: qsTr("Read receip&ts")
+ text: qsTr("&Read receipts")
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
}
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 914f5d33..ec1de346 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -13,9 +13,9 @@ Rectangle {
id: replyPopup
Layout.fillWidth: true
- visible: room && (room.reply || room.edit)
+ visible: room && (room.reply || room.edit || room.thread)
// Height of child, plus margins, plus border
- implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + Nheko.paddingSmall
+ implicitHeight: (room && room.reply ? replyPreview.height : Math.max(closeEditButton.height, closeThreadButton.height)) + Nheko.paddingSmall
color: Nheko.colors.window
z: 3
@@ -71,7 +71,7 @@ Rectangle {
id: closeEditButton
visible: room && room.edit
- anchors.right: parent.right
+ anchors.right: closeThreadButton.left
anchors.margins: 8
anchors.top: parent.top
hoverEnabled: true
@@ -83,4 +83,21 @@ Rectangle {
onClicked: room.edit = undefined
}
+ ImageButton {
+ id: closeThreadButton
+
+ visible: room && room.thread
+ anchors.right: parent.right
+ anchors.margins: 8
+ anchors.top: parent.top
+ hoverEnabled: true
+ buttonTextColor: TimelineManager.userColor(room.thread, Nheko.colors.base)
+ image: ":/icons/icons/ui/dismiss_thread.svg"
+ width: 22
+ height: 22
+ ToolTip.visible: closeThreadButton.hovered
+ ToolTip.text: qsTr("Cancel Thread")
+ onClicked: room.thread = undefined
+ }
+
}
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index dc640099..173c3fb5 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -33,6 +33,7 @@ AbstractButton {
required property bool isEdited
required property bool isStateEvent
required property string replyTo
+ required property string threadId
required property string userId
required property string userName
required property string roomTopic
@@ -58,14 +59,14 @@ AbstractButton {
// this looks better without margins
TapHandler {
acceptedButtons: Qt.RightButton
- onSingleTapped: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+ onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
}
- onPressAndHold: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+ onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
onDoubleClicked: chat.model.reply = eventId
DragHandler {
@@ -229,6 +230,20 @@ AbstractButton {
anchors.verticalCenter: ts.verticalCenter
}
+ ImageButton {
+ visible: threadId
+ Layout.alignment: Qt.AlignRight | Qt.AlignTop
+ height: parent.iconSize
+ width: parent.iconSize
+ image: ":/icons/icons/ui/thread.svg"
+ buttonTextColor: TimelineManager.userColor(threadId, Nheko.colors.base)
+ ToolTip.visible: hovered
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: qsTr("Part of a thread")
+ anchors.verticalCenter: ts.verticalCenter
+ onClicked: room.thread = threadId
+ }
+
Image {
visible: isEdited || eventId == chat.model.edit
Layout.alignment: Qt.AlignRight | Qt.AlignTop
diff --git a/resources/res.qrc b/resources/res.qrc
index 7affe702..595dd5a7 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -23,6 +23,9 @@
icons/ui/microphone-mute.svg
icons/ui/microphone-unmute.svg
icons/ui/music.svg
+ icons/ui/thread.svg
+ icons/ui/new-thread.svg
+ icons/ui/dismiss_thread.svg
icons/ui/options.svg
icons/ui/pause-symbol.svg
icons/ui/people.svg
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 02b456db..b14f7414 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2229,7 +2229,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
}
std::optional
-Cache::getEvent(const std::string &room_id, const std::string &event_id)
+Cache::getEvent(const std::string &room_id, std::string_view event_id)
{
auto txn = ro_txn(env_);
auto eventsDb = getEventsDb(txn, room_id);
@@ -2555,30 +2555,6 @@ Cache::lastVisibleEvent(const std::string &room_id, std::string_view event_id)
}
}
-std::optional
-Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
-{
- auto txn = ro_txn(env_);
-
- lmdb::dbi orderDb;
- 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 {};
- }
-
- std::string_view val;
-
- bool success = orderDb.get(txn, event_id, val);
- if (!success) {
- return {};
- }
-
- return lmdb::from_sv(val);
-}
-
std::optional
Cache::getTimelineEventId(const std::string &room_id, uint64_t index)
{
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 839688f1..2d6df140 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -195,7 +195,7 @@ public:
bool forward = false);
std::optional
- getEvent(const std::string &room_id, const std::string &event_id);
+ getEvent(const std::string &room_id, std::string_view event_id);
void storeEvent(const std::string &room_id,
const std::string &event_id,
const mtx::events::collections::TimelineEvent &event);
@@ -216,7 +216,6 @@ public:
std::optional>
lastVisibleEvent(const std::string &room_id, std::string_view event_id);
std::optional getTimelineEventId(const std::string &room_id, uint64_t index);
- std::optional getArrivalIndex(const std::string &room_id, std::string_view event_id);
std::string previousBatchToken(const std::string &room_id);
uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res);
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 019cf78b..81f2eb9c 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -547,8 +547,8 @@ EventStore::edits(const std::string &event_id)
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 c->getEventIndex(this->room_id_, mtx::accessors::event_id(a)) <
+ c->getEventIndex(this->room_id_, mtx::accessors::event_id(b));
});
return edits;
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 4ac2708e..6854fce3 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -23,9 +23,11 @@
#include
#include "Cache.h"
+#include "Cache_p.h"
#include "ChatPage.h"
#include "CombinedImagePackModel.h"
#include "Config.h"
+#include "EventAccessors.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "TimelineModel.h"
@@ -37,6 +39,28 @@
static constexpr size_t INPUT_HISTORY_SIZE = 10;
+std::string
+threadFallbackEventId(const std::string &room_id, const std::string &thread_id)
+{
+ auto event_ids = cache::client()->relatedEvents(room_id, thread_id);
+
+ std::map> orderedEvents;
+
+ for (const auto &e : event_ids) {
+ if (auto index = cache::client()->getTimelineIndex(room_id, e))
+ orderedEvents.emplace(*index, e);
+ }
+
+ for (const auto &[index, event_id] : orderedEvents) {
+ (void)index;
+ if (auto event = cache::client()->getEvent(room_id, event_id)) {
+ if (mtx::accessors::relations(event->data).thread() == thread_id)
+ return std::string(event_id);
+ }
+ }
+ return thread_id;
+}
+
QUrl
MediaUpload::thumbnailDataUrl() const
{
@@ -384,6 +408,31 @@ replaceMatrixToMarkdownLink(QString input)
return input;
}
+mtx::common::Relations
+InputBar::generateRelations() const
+{
+ mtx::common::Relations relations;
+ if (!room->thread().isEmpty()) {
+ relations.relations.push_back(
+ {mtx::common::RelationType::Thread, room->thread().toStdString()});
+ if (room->reply().isEmpty())
+ relations.relations.push_back(
+ {mtx::common::RelationType::InReplyTo,
+ threadFallbackEventId(room->roomId().toStdString(), room->thread().toStdString()),
+ std::nullopt,
+ true});
+ }
+ if (!room->reply().isEmpty()) {
+ relations.relations.push_back(
+ {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
+ }
+ if (!room->edit().isEmpty()) {
+ relations.relations.push_back(
+ {mtx::common::RelationType::Replace, room->edit().toStdString()});
+ }
+ return relations;
+}
+
void
InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
{
@@ -404,16 +453,8 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
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()});
- }
-
- text.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
-
- } else if (!room->reply().isEmpty()) {
+ text.relations = generateRelations();
+ if (!room->reply().isEmpty() && room->thread().isEmpty() && room->edit().isEmpty()) {
auto related = room->relatedInfo(room->reply());
// Skip reply fallbacks to users who would cause a room ping with the fallback.
@@ -448,9 +489,6 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
text.formatted_body =
utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
}
-
- text.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, related.related_event});
}
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
@@ -471,14 +509,7 @@ InputBar::emote(const QString &msg, bool rainbowify)
emote.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
}
- if (!room->reply().isEmpty()) {
- emote.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- emote.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ emote.relations = generateRelations();
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
}
@@ -498,14 +529,7 @@ InputBar::notice(const QString &msg, bool rainbowify)
notice.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
}
- if (!room->reply().isEmpty()) {
- notice.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- notice.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ notice.relations = generateRelations();
room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
}
@@ -548,14 +572,7 @@ InputBar::image(const QString &filename,
image.info.thumbnail_info.mimetype = "image/png";
}
- if (!room->reply().isEmpty()) {
- image.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- image.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ image.relations = generateRelations();
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
}
@@ -577,14 +594,7 @@ InputBar::file(const QString &filename,
else
file.url = url.toStdString();
- if (!room->reply().isEmpty()) {
- file.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- file.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ file.relations = generateRelations();
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
}
@@ -611,14 +621,7 @@ InputBar::audio(const QString &filename,
else
audio.url = url.toStdString();
- if (!room->reply().isEmpty()) {
- audio.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- audio.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ audio.relations = generateRelations();
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
}
@@ -667,14 +670,7 @@ InputBar::video(const QString &filename,
video.info.thumbnail_info.mimetype = "image/png";
}
- if (!room->reply().isEmpty()) {
- video.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- video.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ video.relations = generateRelations();
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
}
@@ -699,14 +695,7 @@ InputBar::sticker(CombinedImagePackModel *model, int row)
sticker.info.thumbnail_info.h = sticker.info.h;
sticker.info.thumbnail_info.w = sticker.info.w;
- if (!room->reply().isEmpty()) {
- sticker.relations.relations.push_back(
- {mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- }
- if (!room->edit().isEmpty()) {
- sticker.relations.relations.push_back(
- {mtx::common::RelationType::Replace, room->edit().toStdString()});
- }
+ sticker.relations = generateRelations();
room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 7df556e8..d130fb8b 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -266,6 +266,8 @@ private:
const QSize &thumbnailDimensions,
const QString &blurhash);
+ mtx::common::Relations generateRelations() const;
+
void startUploadFromPath(const QString &path);
void startUploadFromMimeData(const QMimeData &source, const QString &format);
void startUpload(std::unique_ptr dev, const QString &orgPath, const QString &format);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 34568385..01856a4d 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -508,6 +508,7 @@ TimelineModel::roleNames() const
{Trustlevel, "trustlevel"},
{EncryptionError, "encryptionError"},
{ReplyTo, "replyTo"},
+ {ThreadId, "threadId"},
{Reactions, "reactions"},
{RoomId, "roomId"},
{RoomName, "roomName"},
@@ -725,8 +726,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
case EncryptionError:
return events.decryptionError(event_id(event));
- case ReplyTo:
- return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
+ case ReplyTo: {
+ const auto &rels = relations(event);
+ return QVariant(QString::fromStdString(rels.reply_to(!rels.thread()).value_or("")));
+ }
+ case ThreadId:
+ return QVariant(QString::fromStdString(relations(event).thread().value_or("")));
case Reactions: {
auto id = relations(event).replaces().value_or(event_id(event));
return QVariant::fromValue(events.reactions(id));
@@ -1205,12 +1210,6 @@ TimelineModel::openUserProfile(QString userid)
emit manager_->openProfile(userProfile);
}
-void
-TimelineModel::replyAction(const QString &id)
-{
- setReply(id);
-}
-
void
TimelineModel::unpin(const QString &id)
{
@@ -1265,12 +1264,6 @@ TimelineModel::pin(const QString &id)
});
}
-void
-TimelineModel::editAction(QString id)
-{
- setEdit(id);
-}
-
RelatedInfo
TimelineModel::relatedInfo(const QString &id)
{
@@ -2672,6 +2665,26 @@ TimelineModel::formatMemberEvent(const QString &id)
return rendered;
}
+void
+TimelineModel::setThread(const QString &id)
+{
+ if (id.isEmpty()) {
+ resetThread();
+ return;
+ } else if (id != thread_) {
+ thread_ = id;
+ emit threadChanged(thread_);
+ }
+}
+void
+TimelineModel::resetThread()
+{
+ if (!thread_.isEmpty()) {
+ thread_.clear();
+ emit threadChanged(thread_);
+ }
+}
+
void
TimelineModel::setEdit(const QString &newEdit)
{
@@ -2693,6 +2706,7 @@ TimelineModel::setEdit(const QString &newEdit)
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
auto e = *ev;
setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or("")));
+ setThread(QString::fromStdString(mtx::accessors::relations(e).thread().value_or("")));
auto msgType = mtx::accessors::msg_type(e);
if (msgType == mtx::events::MessageType::Text ||
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index e16e79b0..43ea0b24 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -180,6 +180,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
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(QString thread READ thread WRITE setThread NOTIFY threadChanged RESET resetThread)
Q_PROPERTY(
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
Q_PROPERTY(QString roomId READ roomId CONSTANT)
@@ -240,6 +241,7 @@ public:
Trustlevel,
EncryptionError,
ReplyTo,
+ ThreadId,
Reactions,
RoomId,
RoomName,
@@ -281,8 +283,6 @@ public:
Q_INVOKABLE void forwardMessage(const QString &eventId, QString roomId);
Q_INVOKABLE void viewDecryptedRawMessage(const QString &id);
Q_INVOKABLE void openUserProfile(QString userid);
- Q_INVOKABLE void editAction(QString id);
- Q_INVOKABLE void replyAction(const QString &id);
Q_INVOKABLE void unpin(const QString &id);
Q_INVOKABLE void pin(const QString &id);
Q_INVOKABLE void showReadReceipts(QString id);
@@ -383,6 +383,9 @@ public slots:
QString edit() const { return edit_; }
void setEdit(const QString &newEdit);
void resetEdit();
+ QString thread() const { return thread_; }
+ void setThread(const QString &newThread);
+ void resetThread();
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
void clearTimeline() { events.clearTimeline(); }
void resetState();
@@ -420,6 +423,7 @@ signals:
void typingUsersChanged(std::vector users);
void replyChanged(QString reply);
void editChanged(QString reply);
+ void threadChanged(QString id);
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
void showRawMessageDialog(QString rawMessage);
void paginationInProgressChanged(const bool);
@@ -466,7 +470,7 @@ private:
mutable EventStore events;
QString currentId, currentReadId;
- QString reply_, edit_;
+ QString reply_, edit_, thread_;
QString textBeforeEdit, replyBeforeEdit;
std::vector typingUsers_;
diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp
index 8ecea7d9..90066fa0 100644
--- a/src/ui/MxcAnimatedImage.cpp
+++ b/src/ui/MxcAnimatedImage.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -34,8 +35,12 @@ MxcAnimatedImage::startDownload()
QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
- static const auto formats = QMovie::supportedFormats();
- animatable_ = formats.contains(mimeType.split('/').back());
+ static const auto movieFormats = QMovie::supportedFormats();
+ QByteArray imageFormat;
+ const auto imageFormats = QImageReader::imageFormatsForMimeType(mimeType);
+ if (!imageFormats.isEmpty())
+ imageFormat = imageFormats.front();
+ animatable_ = movieFormats.contains(imageFormat);
animatableChanged();
if (!animatable_)
@@ -66,14 +71,14 @@ MxcAnimatedImage::startDownload()
QPointer self = this;
- auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) {
+ auto processBuffer = [this, imageFormat, encryptionInfo, self](QIODevice &device) {
if (!self)
return;
try {
if (buffer.isOpen()) {
- movie.stop();
- movie.setDevice(nullptr);
+ frameTimer.stop();
+ movie->setDevice(nullptr);
buffer.close();
}
@@ -92,21 +97,22 @@ MxcAnimatedImage::startDownload()
nhlog::net()->error("Failed to setup animated image buffer: {}", e.what());
}
- QTimer::singleShot(0, this, [this, mimeType] {
+ QTimer::singleShot(0, this, [this, imageFormat] {
nhlog::ui()->info(
"Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
- movie.setFormat(mimeType);
- movie.setDevice(&buffer);
+ movie->setFormat(imageFormat);
+ movie->setDevice(&buffer);
if (height() != 0 && width() != 0)
- movie.setScaledSize(this->size().toSize());
- if (buffer.bytesAvailable() <
- 4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM
- movie.setCacheMode(QMovie::CacheAll);
+ movie->setScaledSize(this->size().toSize());
+
+ if (movie->supportsAnimation())
+ frameTimer.setInterval(movie->nextImageDelay());
+
if (play_)
- movie.start();
+ frameTimer.start();
else
- movie.jumpToFrame(0);
+ movie->jumpToImage(0);
emit loadedChanged();
update();
});
@@ -159,9 +165,9 @@ MxcAnimatedImage::geometryChanged(const QRectF &newGeometry, const QRectF &oldGe
if (newGeometry.size() != oldGeometry.size()) {
if (height() != 0 && width() != 0) {
- QSizeF r = movie.scaledSize();
+ QSizeF r = movie->scaledSize();
r.scale(newGeometry.size(), Qt::KeepAspectRatio);
- movie.setScaledSize(r.toSize());
+ movie->setScaledSize(r.toSize());
imageDirty = true;
update();
}
@@ -184,16 +190,15 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD
n->setFlags(QSGNode::OwnedByParent);
}
- auto img = movie.currentImage();
- n->setSourceRect(img.rect());
- if (!img.isNull())
- n->setTexture(window()->createTextureFromImage(std::move(img)));
+ n->setSourceRect(currentFrame.rect());
+ if (!currentFrame.isNull())
+ n->setTexture(window()->createTextureFromImage(currentFrame));
else {
delete n;
return nullptr;
}
- QSizeF r = img.size();
+ QSizeF r = currentFrame.size();
r.scale(size(), Qt::KeepAspectRatio);
n->setRect((width() - r.width()) / 2, (height() - r.height()) / 2, r.width(), r.height());
diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h
index 8891e57e..2b067166 100644
--- a/src/ui/MxcAnimatedImage.h
+++ b/src/ui/MxcAnimatedImage.h
@@ -6,7 +6,7 @@
#pragma once
#include
-#include
+#include
#include
#include
@@ -24,10 +24,11 @@ class MxcAnimatedImage : public QQuickItem
public:
MxcAnimatedImage(QQuickItem *parent = nullptr)
: QQuickItem(parent)
+ , movie(new QImageReader())
{
connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload);
- connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame);
+ connect(&frameTimer, &QTimer::timeout, this, &MxcAnimatedImage::newFrame);
setFlag(QQuickItem::ItemHasContents);
// setAcceptHoverEvents(true);
}
@@ -55,7 +56,10 @@ public:
{
if (play_ != newPlay) {
play_ = newPlay;
- movie.setPaused(!play_);
+ if (play_)
+ frameTimer.start();
+ else
+ frameTimer.stop();
emit playChanged();
}
}
@@ -73,10 +77,16 @@ signals:
private slots:
void startDownload();
- void newFrame(int frame)
+ void newFrame()
{
- currentFrame = frame;
- imageDirty = true;
+ if (movie->currentImageNumber() > 0 && !movie->canRead() && movie->imageCount() > 1) {
+ buffer.seek(0);
+ movie.reset(new QImageReader(movie->device(), movie->format()));
+ if (height() != 0 && width() != 0)
+ movie->setScaledSize(this->size().toSize());
+ }
+ movie->read(¤tFrame);
+ imageDirty = true;
update();
}
@@ -86,8 +96,9 @@ private:
QString filename_;
bool animatable_ = false;
QBuffer buffer;
- QMovie movie;
- int currentFrame = 0;
- bool imageDirty = true;
- bool play_ = true;
+ std::unique_ptr movie;
+ bool imageDirty = true;
+ bool play_ = true;
+ QTimer frameTimer;
+ QImage currentFrame;
};