mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
Basic threading support
This commit is contained in:
parent
bffa0115d4
commit
88cbac1695
22 changed files with 240 additions and 163 deletions
|
@ -581,7 +581,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 b3740e26ad07a534b6092959c0da4d91b77a8528
|
GIT_TAG 5ef4460c26acb02f24530db1c6058534b87014f6
|
||||||
)
|
)
|
||||||
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 "")
|
||||||
|
|
|
@ -173,7 +173,7 @@ modules:
|
||||||
buildsystem: cmake-ninja
|
buildsystem: cmake-ninja
|
||||||
name: mtxclient
|
name: mtxclient
|
||||||
sources:
|
sources:
|
||||||
- commit: b3740e26ad07a534b6092959c0da4d91b77a8528
|
- commit: 5ef4460c26acb02f24530db1c6058534b87014f6
|
||||||
#tag: v0.8.2
|
#tag: v0.8.2
|
||||||
type: git
|
type: git
|
||||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||||
|
|
1
resources/icons/ui/bookmark-disabled.svg
Normal file
1
resources/icons/ui/bookmark-disabled.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3.28 2.22a.75.75 0 1 0-1.06 1.06l2.788 2.788a3.3 3.3 0 0 0-.005.181v14.996a.75.75 0 0 0 1.188.609L12 17.673l5.812 4.18A.75.75 0 0 0 19 21.246v-1.183l1.718 1.718a.75.75 0 0 0 1.061-1.06L3.28 2.22Zm14.221 16.342v1.22L12.44 16.14a.75.75 0 0 0-.876 0l-5.061 3.642V7.563L17.5 18.562ZM17.501 6.25v8.07l1.5 1.5V6.25A3.25 3.25 0 0 0 15.751 3H8.253c-.595 0-1.153.16-1.633.438l1.133 1.133a1.75 1.75 0 0 1 .5-.072h7.498c.967 0 1.75.784 1.75 1.75Z" fill="#212121"/></svg>
|
After Width: | Height: | Size: 564 B |
1
resources/icons/ui/bookmark.svg
Normal file
1
resources/icons/ui/bookmark.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.19 21.854a.75.75 0 0 1-1.188-.61V6.25a3.25 3.25 0 0 1 3.25-3.25h7.499A3.25 3.25 0 0 1 19 6.249v14.996a.75.75 0 0 1-1.188.609l-5.811-4.181-5.812 4.18ZM17.5 6.249a1.75 1.75 0 0 0-1.75-1.75H8.253a1.75 1.75 0 0 0-1.75 1.75v13.532l5.062-3.64a.75.75 0 0 1 .876 0l5.06 3.64V6.25Z" fill="#212121"/></svg>
|
After Width: | Height: | Size: 403 B |
4
resources/icons/ui/dismiss_thread.svg
Normal file
4
resources/icons/ui/dismiss_thread.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="96" height="96" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 18h13a.75.75 0 0 1 .102 1.493L20 19.5H7a.75.75 0 0 1-.102-1.493L7 18Zm10-3a.75.75 0 0 1 .102 1.493L17 16.5H4a.75.75 0 0 1-.102-1.493L4 15h13Zm3-3a.75.75 0 0 1 .102 1.493L20 13.5H7a.75.75 0 0 1-.102-1.493L7 12h13ZM6 5a2.75 2.75 0 0 1 2.55 1.717.75.75 0 0 1-1.346.655l-.045-.091A1.25 1.25 0 1 0 6 9h11.5a.75.75 0 0 1 .102 1.493l-.102.007H6A2.75 2.75 0 0 1 6 5Zm14 1a.75.75 0 0 1 .102 1.493L20 7.5h-9a.75.75 0 0 1-.102-1.493L11 6h9Z" fill="#212121"/>
|
||||||
|
<path d="m3.2035 3.2035c0.29289-0.2929 0.76777-0.2929 1.0607 0 16.532 16.532 0 0 16.532 16.532 0.29295 0.29288 0.29295 0.76778 0 1.0606-0.29288 0.29288-0.7677 0.29288-1.0606 0-16.532-16.532-0.11015-0.11011-16.532-16.532-0.2929-0.29289-0.2929-0.76777 1e-7 -1.0607z" fill="#212121" stroke-width=".75"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 868 B |
6
resources/icons/ui/needle.svg
Normal file
6
resources/icons/ui/needle.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="32" height="32" version="1.1" viewBox="0 0 8.4667 8.4667" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="rotate(17.066 .26297 6.1037)" stroke="#000" stroke-linejoin="round">
|
||||||
|
<path d="m1.0583 0.26458c-0.79375 0-0.79375 0.79375-0.79375 0.79375l0.79375 6.35 0.79375-6.35s-9e-7 -0.79375-0.79375-0.79375zm0 0.26458a0.52917 0.52917 0 0 1 0.52917 0.52917 0.52917 0.52917 0 0 1-0.52917 0.52917 0.52917 0.52917 0 0 1-0.52917-0.52917 0.52917 0.52917 0 0 1 0.52917-0.52917z" fill-rule="evenodd" stroke-width=".026458"/>
|
||||||
|
<path d="m1.0583 1.0583s4.4594-1.5552 5.2917 0c0.60018 1.1215-2.242 1.9092-2.1167 3.175 0.049161 0.49648 1.0583 0.55943 1.0583 1.0583s-1.0583 1.0583-1.0583 1.0583" fill="none" stroke-linecap="round" stroke-width=".52917"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 765 B |
1
resources/icons/ui/new-thread.svg
Normal file
1
resources/icons/ui/new-thread.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 2-.09.007a.5.5 0 0 0-.402.402L17 14.5V17L14.498 17l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402l.09.008H17v2.503l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402l.008-.09V18l2.504.001.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.403-.402l-.09-.008H18v-2.5l-.008-.09a.5.5 0 0 0-.402-.403L17.5 14Zm-6.481 4c.04.519.14 1.021.294 1.5H7a.75.75 0 0 1-.102-1.493L7 18h4.019Zm.479-3c-.198.475-.34.977-.422 1.5H4a.75.75 0 0 1-.102-1.493L4 15h7.498Zm2.537-3a6.534 6.534 0 0 0-1.659 1.5H7a.75.75 0 0 1-.102-1.493L7 12h7.035ZM6 5a2.75 2.75 0 0 1 2.55 1.717.75.75 0 0 1-1.346.655l-.045-.091A1.25 1.25 0 1 0 6 9h11.5a.75.75 0 0 1 .102 1.493l-.102.007H6A2.75 2.75 0 0 1 6 5Zm14 1a.75.75 0 0 1 .102 1.493L20 7.5h-9a.75.75 0 0 1-.102-1.493L11 6h9Z" fill="#212121"/></svg>
|
After Width: | Height: | Size: 958 B |
1
resources/icons/ui/thread.svg
Normal file
1
resources/icons/ui/thread.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="96" height="96" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 18h13a.75.75 0 0 1 .102 1.493L20 19.5H7a.75.75 0 0 1-.102-1.493L7 18Zm10-3a.75.75 0 0 1 .102 1.493L17 16.5H4a.75.75 0 0 1-.102-1.493L4 15h13Zm3-3a.75.75 0 0 1 .102 1.493L20 13.5H7a.75.75 0 0 1-.102-1.493L7 12h13ZM6 5a2.75 2.75 0 0 1 2.55 1.717.75.75 0 0 1-1.346.655l-.045-.091A1.25 1.25 0 1 0 6 9h11.5a.75.75 0 0 1 .102 1.493l-.102.007H6A2.75 2.75 0 0 1 6 5Zm14 1a.75.75 0 0 1 .102 1.493L20 7.5h-9a.75.75 0 0 1-.102-1.493L11 6h9Z" fill="#212121"/></svg>
|
After Width: | Height: | Size: 561 B |
|
@ -378,6 +378,10 @@ Rectangle {
|
||||||
messageInput.forceActiveFocus();
|
messageInput.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onThreadChanged() {
|
||||||
|
messageInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
ignoreUnknownSignals: true
|
ignoreUnknownSignals: true
|
||||||
target: room
|
target: room
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,9 +116,7 @@ Item {
|
||||||
ToolTip.delay: Nheko.tooltipDelay
|
ToolTip.delay: Nheko.tooltipDelay
|
||||||
ToolTip.text: qsTr("Edit")
|
ToolTip.text: qsTr("Edit")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (row.model.isEditable)
|
if (row.model.isEditable) chat.model.edit = row.model.eventId;
|
||||||
chat.model.editAction(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 {
|
ImageButton {
|
||||||
id: replyButton
|
id: replyButton
|
||||||
|
|
||||||
|
@ -149,7 +160,7 @@ Item {
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: Nheko.tooltipDelay
|
ToolTip.delay: Nheko.tooltipDelay
|
||||||
ToolTip.text: qsTr("Reply")
|
ToolTip.text: qsTr("Reply")
|
||||||
onClicked: chat.model.replyAction(row.model.eventId)
|
onClicked: chat.model.reply = row.model.eventId
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageButton {
|
ImageButton {
|
||||||
|
@ -161,7 +172,7 @@ Item {
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: Nheko.tooltipDelay
|
ToolTip.delay: Nheko.tooltipDelay
|
||||||
ToolTip.text: qsTr("Options")
|
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();
|
room.input.declineUploads();
|
||||||
else if(chat.model.reply)
|
else if(chat.model.reply)
|
||||||
chat.model.reply = undefined;
|
chat.model.reply = undefined;
|
||||||
else
|
else if (chat.model.edit)
|
||||||
chat.model.edit = undefined;
|
chat.model.edit = undefined;
|
||||||
|
else
|
||||||
|
chat.model.thread = undefined
|
||||||
TimelineManager.focusMessageInput();
|
TimelineManager.focusMessageInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -383,6 +396,7 @@ Item {
|
||||||
required property bool isStateEvent
|
required property bool isStateEvent
|
||||||
required property bool previousMessageIsStateEvent
|
required property bool previousMessageIsStateEvent
|
||||||
required property string replyTo
|
required property string replyTo
|
||||||
|
required property string threadId
|
||||||
required property string userId
|
required property string userId
|
||||||
required property string roomTopic
|
required property string roomTopic
|
||||||
required property string roomName
|
required property string roomName
|
||||||
|
@ -448,6 +462,7 @@ Item {
|
||||||
isEdited: wrapper.isEdited
|
isEdited: wrapper.isEdited
|
||||||
isStateEvent: wrapper.isStateEvent
|
isStateEvent: wrapper.isStateEvent
|
||||||
replyTo: wrapper.replyTo
|
replyTo: wrapper.replyTo
|
||||||
|
threadId: wrapper.threadId
|
||||||
userId: wrapper.userId
|
userId: wrapper.userId
|
||||||
userName: wrapper.userName
|
userName: wrapper.userName
|
||||||
roomTopic: wrapper.roomTopic
|
roomTopic: wrapper.roomTopic
|
||||||
|
@ -554,6 +569,7 @@ Item {
|
||||||
id: messageContextMenu
|
id: messageContextMenu
|
||||||
|
|
||||||
property string eventId
|
property string eventId
|
||||||
|
property string threadId
|
||||||
property string link
|
property string link
|
||||||
property string text
|
property string text
|
||||||
property int eventType
|
property int eventType
|
||||||
|
@ -561,8 +577,9 @@ Item {
|
||||||
property bool isEditable
|
property bool isEditable
|
||||||
property bool isSender
|
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_;
|
eventId = eventId_;
|
||||||
|
threadId = threadId_;
|
||||||
eventType = eventType_;
|
eventType = eventType_;
|
||||||
isEncrypted = isEncrypted_;
|
isEncrypted = isEncrypted_;
|
||||||
isEditable = isEditable_;
|
isEditable = isEditable_;
|
||||||
|
@ -623,14 +640,21 @@ Item {
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
||||||
text: qsTr("Repl&y")
|
text: qsTr("Repl&y")
|
||||||
onTriggered: room.replyAction(messageContextMenu.eventId)
|
onTriggered: room.reply = (messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
||||||
enabled: visible
|
enabled: visible
|
||||||
text: qsTr("&Edit")
|
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 {
|
Platform.MenuItem {
|
||||||
|
@ -641,7 +665,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
text: qsTr("Read receip&ts")
|
text: qsTr("&Read receipts")
|
||||||
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
|
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ Rectangle {
|
||||||
id: replyPopup
|
id: replyPopup
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: room && (room.reply || room.edit)
|
visible: room && (room.reply || room.edit || room.thread)
|
||||||
// Height of child, plus margins, plus border
|
// 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
|
color: Nheko.colors.window
|
||||||
z: 3
|
z: 3
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ Rectangle {
|
||||||
id: closeEditButton
|
id: closeEditButton
|
||||||
|
|
||||||
visible: room && room.edit
|
visible: room && room.edit
|
||||||
anchors.right: parent.right
|
anchors.right: closeThreadButton.left
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
@ -83,4 +83,21 @@ Rectangle {
|
||||||
onClicked: room.edit = undefined
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ AbstractButton {
|
||||||
required property bool isEdited
|
required property bool isEdited
|
||||||
required property bool isStateEvent
|
required property bool isStateEvent
|
||||||
required property string replyTo
|
required property string replyTo
|
||||||
|
required property string threadId
|
||||||
required property string userId
|
required property string userId
|
||||||
required property string userName
|
required property string userName
|
||||||
required property string roomTopic
|
required property string roomTopic
|
||||||
|
@ -58,14 +59,14 @@ AbstractButton {
|
||||||
// this looks better without margins
|
// this looks better without margins
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
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
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
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
|
onDoubleClicked: chat.model.reply = eventId
|
||||||
|
|
||||||
DragHandler {
|
DragHandler {
|
||||||
|
@ -229,6 +230,20 @@ AbstractButton {
|
||||||
anchors.verticalCenter: ts.verticalCenter
|
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 {
|
Image {
|
||||||
visible: isEdited || eventId == chat.model.edit
|
visible: isEdited || eventId == chat.model.edit
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
<file>icons/ui/microphone-mute.svg</file>
|
<file>icons/ui/microphone-mute.svg</file>
|
||||||
<file>icons/ui/microphone-unmute.svg</file>
|
<file>icons/ui/microphone-unmute.svg</file>
|
||||||
<file>icons/ui/music.svg</file>
|
<file>icons/ui/music.svg</file>
|
||||||
|
<file>icons/ui/thread.svg</file>
|
||||||
|
<file>icons/ui/new-thread.svg</file>
|
||||||
|
<file>icons/ui/dismiss_thread.svg</file>
|
||||||
<file>icons/ui/options.svg</file>
|
<file>icons/ui/options.svg</file>
|
||||||
<file>icons/ui/pause-symbol.svg</file>
|
<file>icons/ui/pause-symbol.svg</file>
|
||||||
<file>icons/ui/people.svg</file>
|
<file>icons/ui/people.svg</file>
|
||||||
|
|
|
@ -2229,7 +2229,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<mtx::events::collections::TimelineEvent>
|
std::optional<mtx::events::collections::TimelineEvent>
|
||||||
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 txn = ro_txn(env_);
|
||||||
auto eventsDb = getEventsDb(txn, room_id);
|
auto eventsDb = getEventsDb(txn, room_id);
|
||||||
|
@ -2555,30 +2555,6 @@ Cache::lastVisibleEvent(const std::string &room_id, std::string_view event_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<uint64_t>
|
|
||||||
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<uint64_t>(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -195,7 +195,7 @@ public:
|
||||||
bool forward = false);
|
bool forward = false);
|
||||||
|
|
||||||
std::optional<mtx::events::collections::TimelineEvent>
|
std::optional<mtx::events::collections::TimelineEvent>
|
||||||
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,
|
void storeEvent(const std::string &room_id,
|
||||||
const std::string &event_id,
|
const std::string &event_id,
|
||||||
const mtx::events::collections::TimelineEvent &event);
|
const mtx::events::collections::TimelineEvent &event);
|
||||||
|
@ -216,7 +216,6 @@ public:
|
||||||
std::optional<std::pair<uint64_t, std::string>>
|
std::optional<std::pair<uint64_t, std::string>>
|
||||||
lastVisibleEvent(const std::string &room_id, std::string_view event_id);
|
lastVisibleEvent(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);
|
||||||
|
|
|
@ -547,8 +547,8 @@ EventStore::edits(const std::string &event_id)
|
||||||
edits.end(),
|
edits.end(),
|
||||||
[this, c](const mtx::events::collections::TimelineEvents &a,
|
[this, c](const mtx::events::collections::TimelineEvents &a,
|
||||||
const mtx::events::collections::TimelineEvents &b) {
|
const mtx::events::collections::TimelineEvents &b) {
|
||||||
return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) <
|
return c->getEventIndex(this->room_id_, mtx::accessors::event_id(a)) <
|
||||||
c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b));
|
c->getEventIndex(this->room_id_, mtx::accessors::event_id(b));
|
||||||
});
|
});
|
||||||
|
|
||||||
return edits;
|
return edits;
|
||||||
|
|
|
@ -23,9 +23,11 @@
|
||||||
#include <mtx/responses/media.hpp>
|
#include <mtx/responses/media.hpp>
|
||||||
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
#include "Cache_p.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "CombinedImagePackModel.h"
|
#include "CombinedImagePackModel.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "EventAccessors.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "TimelineModel.h"
|
#include "TimelineModel.h"
|
||||||
|
@ -37,6 +39,28 @@
|
||||||
|
|
||||||
static constexpr size_t INPUT_HISTORY_SIZE = 10;
|
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<uint64_t, std::string_view, std::greater<>> 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
|
QUrl
|
||||||
MediaUpload::thumbnailDataUrl() const
|
MediaUpload::thumbnailDataUrl() const
|
||||||
{
|
{
|
||||||
|
@ -384,6 +408,31 @@ replaceMatrixToMarkdownLink(QString input)
|
||||||
return 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
|
void
|
||||||
InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
|
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";
|
text.format = "org.matrix.custom.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room->edit().isEmpty()) {
|
text.relations = generateRelations();
|
||||||
if (!room->reply().isEmpty()) {
|
if (!room->reply().isEmpty() && room->thread().isEmpty() && room->edit().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()) {
|
|
||||||
auto related = room->relatedInfo(room->reply());
|
auto related = room->relatedInfo(room->reply());
|
||||||
|
|
||||||
// Skip reply fallbacks to users who would cause a room ping with the fallback.
|
// 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 =
|
text.formatted_body =
|
||||||
utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString();
|
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);
|
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
|
||||||
|
@ -471,14 +509,7 @@ InputBar::emote(const QString &msg, bool rainbowify)
|
||||||
emote.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
|
emote.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
emote.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
@ -498,14 +529,7 @@ InputBar::notice(const QString &msg, bool rainbowify)
|
||||||
notice.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
|
notice.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
notice.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(notice, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
@ -548,14 +572,7 @@ InputBar::image(const QString &filename,
|
||||||
image.info.thumbnail_info.mimetype = "image/png";
|
image.info.thumbnail_info.mimetype = "image/png";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
image.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
@ -577,14 +594,7 @@ InputBar::file(const QString &filename,
|
||||||
else
|
else
|
||||||
file.url = url.toStdString();
|
file.url = url.toStdString();
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
file.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
@ -611,14 +621,7 @@ InputBar::audio(const QString &filename,
|
||||||
else
|
else
|
||||||
audio.url = url.toStdString();
|
audio.url = url.toStdString();
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
audio.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
|
||||||
}
|
}
|
||||||
|
@ -667,14 +670,7 @@ InputBar::video(const QString &filename,
|
||||||
video.info.thumbnail_info.mimetype = "image/png";
|
video.info.thumbnail_info.mimetype = "image/png";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
video.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
|
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.h = sticker.info.h;
|
||||||
sticker.info.thumbnail_info.w = sticker.info.w;
|
sticker.info.thumbnail_info.w = sticker.info.w;
|
||||||
|
|
||||||
if (!room->reply().isEmpty()) {
|
sticker.relations = generateRelations();
|
||||||
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()});
|
|
||||||
}
|
|
||||||
|
|
||||||
room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
|
room->sendMessageEvent(sticker, mtx::events::EventType::Sticker);
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,6 +266,8 @@ private:
|
||||||
const QSize &thumbnailDimensions,
|
const QSize &thumbnailDimensions,
|
||||||
const QString &blurhash);
|
const QString &blurhash);
|
||||||
|
|
||||||
|
mtx::common::Relations generateRelations() const;
|
||||||
|
|
||||||
void startUploadFromPath(const QString &path);
|
void startUploadFromPath(const QString &path);
|
||||||
void startUploadFromMimeData(const QMimeData &source, const QString &format);
|
void startUploadFromMimeData(const QMimeData &source, const QString &format);
|
||||||
void startUpload(std::unique_ptr<QIODevice> dev, const QString &orgPath, const QString &format);
|
void startUpload(std::unique_ptr<QIODevice> dev, const QString &orgPath, const QString &format);
|
||||||
|
|
|
@ -508,6 +508,7 @@ TimelineModel::roleNames() const
|
||||||
{Trustlevel, "trustlevel"},
|
{Trustlevel, "trustlevel"},
|
||||||
{EncryptionError, "encryptionError"},
|
{EncryptionError, "encryptionError"},
|
||||||
{ReplyTo, "replyTo"},
|
{ReplyTo, "replyTo"},
|
||||||
|
{ThreadId, "threadId"},
|
||||||
{Reactions, "reactions"},
|
{Reactions, "reactions"},
|
||||||
{RoomId, "roomId"},
|
{RoomId, "roomId"},
|
||||||
{RoomName, "roomName"},
|
{RoomName, "roomName"},
|
||||||
|
@ -725,8 +726,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
|
||||||
case EncryptionError:
|
case EncryptionError:
|
||||||
return events.decryptionError(event_id(event));
|
return events.decryptionError(event_id(event));
|
||||||
|
|
||||||
case ReplyTo:
|
case ReplyTo: {
|
||||||
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
|
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: {
|
case Reactions: {
|
||||||
auto id = relations(event).replaces().value_or(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));
|
||||||
|
@ -1205,12 +1210,6 @@ TimelineModel::openUserProfile(QString userid)
|
||||||
emit manager_->openProfile(userProfile);
|
emit manager_->openProfile(userProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
TimelineModel::replyAction(const QString &id)
|
|
||||||
{
|
|
||||||
setReply(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
TimelineModel::unpin(const QString &id)
|
TimelineModel::unpin(const QString &id)
|
||||||
{
|
{
|
||||||
|
@ -1265,12 +1264,6 @@ TimelineModel::pin(const QString &id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
TimelineModel::editAction(QString id)
|
|
||||||
{
|
|
||||||
setEdit(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
RelatedInfo
|
RelatedInfo
|
||||||
TimelineModel::relatedInfo(const QString &id)
|
TimelineModel::relatedInfo(const QString &id)
|
||||||
{
|
{
|
||||||
|
@ -2672,6 +2665,26 @@ TimelineModel::formatMemberEvent(const QString &id)
|
||||||
return rendered;
|
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
|
void
|
||||||
TimelineModel::setEdit(const QString &newEdit)
|
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()) {
|
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
|
||||||
auto e = *ev;
|
auto e = *ev;
|
||||||
setReply(QString::fromStdString(mtx::accessors::relations(e).reply_to().value_or("")));
|
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);
|
auto msgType = mtx::accessors::msg_type(e);
|
||||||
if (msgType == mtx::events::MessageType::Text ||
|
if (msgType == mtx::events::MessageType::Text ||
|
||||||
|
|
|
@ -180,6 +180,7 @@ class TimelineModel : public QAbstractListModel
|
||||||
Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
|
Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged)
|
||||||
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(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit)
|
||||||
|
Q_PROPERTY(QString thread READ thread WRITE setThread NOTIFY threadChanged RESET resetThread)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
|
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
|
||||||
Q_PROPERTY(QString roomId READ roomId CONSTANT)
|
Q_PROPERTY(QString roomId READ roomId CONSTANT)
|
||||||
|
@ -240,6 +241,7 @@ public:
|
||||||
Trustlevel,
|
Trustlevel,
|
||||||
EncryptionError,
|
EncryptionError,
|
||||||
ReplyTo,
|
ReplyTo,
|
||||||
|
ThreadId,
|
||||||
Reactions,
|
Reactions,
|
||||||
RoomId,
|
RoomId,
|
||||||
RoomName,
|
RoomName,
|
||||||
|
@ -281,8 +283,6 @@ public:
|
||||||
Q_INVOKABLE void forwardMessage(const QString &eventId, QString roomId);
|
Q_INVOKABLE void forwardMessage(const QString &eventId, QString roomId);
|
||||||
Q_INVOKABLE void viewDecryptedRawMessage(const QString &id);
|
Q_INVOKABLE void viewDecryptedRawMessage(const QString &id);
|
||||||
Q_INVOKABLE void openUserProfile(QString userid);
|
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 unpin(const QString &id);
|
||||||
Q_INVOKABLE void pin(const QString &id);
|
Q_INVOKABLE void pin(const QString &id);
|
||||||
Q_INVOKABLE void showReadReceipts(QString id);
|
Q_INVOKABLE void showReadReceipts(QString id);
|
||||||
|
@ -383,6 +383,9 @@ public slots:
|
||||||
QString edit() const { return edit_; }
|
QString edit() const { return edit_; }
|
||||||
void setEdit(const QString &newEdit);
|
void setEdit(const QString &newEdit);
|
||||||
void resetEdit();
|
void resetEdit();
|
||||||
|
QString thread() const { return thread_; }
|
||||||
|
void setThread(const QString &newThread);
|
||||||
|
void resetThread();
|
||||||
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
|
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
|
||||||
void clearTimeline() { events.clearTimeline(); }
|
void clearTimeline() { events.clearTimeline(); }
|
||||||
void resetState();
|
void resetState();
|
||||||
|
@ -420,6 +423,7 @@ signals:
|
||||||
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 editChanged(QString reply);
|
||||||
|
void threadChanged(QString id);
|
||||||
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
|
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
|
||||||
void showRawMessageDialog(QString rawMessage);
|
void showRawMessageDialog(QString rawMessage);
|
||||||
void paginationInProgressChanged(const bool);
|
void paginationInProgressChanged(const bool);
|
||||||
|
@ -466,7 +470,7 @@ private:
|
||||||
mutable EventStore events;
|
mutable EventStore events;
|
||||||
|
|
||||||
QString currentId, currentReadId;
|
QString currentId, currentReadId;
|
||||||
QString reply_, edit_;
|
QString reply_, edit_, thread_;
|
||||||
QString textBeforeEdit, replyBeforeEdit;
|
QString textBeforeEdit, replyBeforeEdit;
|
||||||
std::vector<QString> typingUsers_;
|
std::vector<QString> typingUsers_;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
|
#include <QMovie>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
#include <QSGImageNode>
|
#include <QSGImageNode>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
@ -34,8 +35,12 @@ MxcAnimatedImage::startDownload()
|
||||||
|
|
||||||
QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
|
QByteArray mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)).toUtf8();
|
||||||
|
|
||||||
static const auto formats = QMovie::supportedFormats();
|
static const auto movieFormats = QMovie::supportedFormats();
|
||||||
animatable_ = formats.contains(mimeType.split('/').back());
|
QByteArray imageFormat;
|
||||||
|
const auto imageFormats = QImageReader::imageFormatsForMimeType(mimeType);
|
||||||
|
if (!imageFormats.isEmpty())
|
||||||
|
imageFormat = imageFormats.front();
|
||||||
|
animatable_ = movieFormats.contains(imageFormat);
|
||||||
animatableChanged();
|
animatableChanged();
|
||||||
|
|
||||||
if (!animatable_)
|
if (!animatable_)
|
||||||
|
@ -66,14 +71,14 @@ MxcAnimatedImage::startDownload()
|
||||||
|
|
||||||
QPointer<MxcAnimatedImage> self = this;
|
QPointer<MxcAnimatedImage> self = this;
|
||||||
|
|
||||||
auto processBuffer = [this, mimeType, encryptionInfo, self](QIODevice &device) {
|
auto processBuffer = [this, imageFormat, encryptionInfo, self](QIODevice &device) {
|
||||||
if (!self)
|
if (!self)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (buffer.isOpen()) {
|
if (buffer.isOpen()) {
|
||||||
movie.stop();
|
frameTimer.stop();
|
||||||
movie.setDevice(nullptr);
|
movie->setDevice(nullptr);
|
||||||
buffer.close();
|
buffer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,21 +97,22 @@ MxcAnimatedImage::startDownload()
|
||||||
nhlog::net()->error("Failed to setup animated image buffer: {}", e.what());
|
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(
|
nhlog::ui()->info(
|
||||||
"Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
|
"Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
|
||||||
movie.setFormat(mimeType);
|
movie->setFormat(imageFormat);
|
||||||
movie.setDevice(&buffer);
|
movie->setDevice(&buffer);
|
||||||
|
|
||||||
if (height() != 0 && width() != 0)
|
if (height() != 0 && width() != 0)
|
||||||
movie.setScaledSize(this->size().toSize());
|
movie->setScaledSize(this->size().toSize());
|
||||||
if (buffer.bytesAvailable() <
|
|
||||||
4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM
|
if (movie->supportsAnimation())
|
||||||
movie.setCacheMode(QMovie::CacheAll);
|
frameTimer.setInterval(movie->nextImageDelay());
|
||||||
|
|
||||||
if (play_)
|
if (play_)
|
||||||
movie.start();
|
frameTimer.start();
|
||||||
else
|
else
|
||||||
movie.jumpToFrame(0);
|
movie->jumpToImage(0);
|
||||||
emit loadedChanged();
|
emit loadedChanged();
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
@ -159,9 +165,9 @@ MxcAnimatedImage::geometryChanged(const QRectF &newGeometry, const QRectF &oldGe
|
||||||
|
|
||||||
if (newGeometry.size() != oldGeometry.size()) {
|
if (newGeometry.size() != oldGeometry.size()) {
|
||||||
if (height() != 0 && width() != 0) {
|
if (height() != 0 && width() != 0) {
|
||||||
QSizeF r = movie.scaledSize();
|
QSizeF r = movie->scaledSize();
|
||||||
r.scale(newGeometry.size(), Qt::KeepAspectRatio);
|
r.scale(newGeometry.size(), Qt::KeepAspectRatio);
|
||||||
movie.setScaledSize(r.toSize());
|
movie->setScaledSize(r.toSize());
|
||||||
imageDirty = true;
|
imageDirty = true;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
@ -184,16 +190,15 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD
|
||||||
n->setFlags(QSGNode::OwnedByParent);
|
n->setFlags(QSGNode::OwnedByParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto img = movie.currentImage();
|
n->setSourceRect(currentFrame.rect());
|
||||||
n->setSourceRect(img.rect());
|
if (!currentFrame.isNull())
|
||||||
if (!img.isNull())
|
n->setTexture(window()->createTextureFromImage(currentFrame));
|
||||||
n->setTexture(window()->createTextureFromImage(std::move(img)));
|
|
||||||
else {
|
else {
|
||||||
delete n;
|
delete n;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSizeF r = img.size();
|
QSizeF r = currentFrame.size();
|
||||||
r.scale(size(), Qt::KeepAspectRatio);
|
r.scale(size(), Qt::KeepAspectRatio);
|
||||||
|
|
||||||
n->setRect((width() - r.width()) / 2, (height() - r.height()) / 2, r.width(), r.height());
|
n->setRect((width() - r.width()) / 2, (height() - r.height()) / 2, r.width(), r.height());
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QMovie>
|
#include <QImageReader>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
@ -24,10 +24,11 @@ class MxcAnimatedImage : public QQuickItem
|
||||||
public:
|
public:
|
||||||
MxcAnimatedImage(QQuickItem *parent = nullptr)
|
MxcAnimatedImage(QQuickItem *parent = nullptr)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
|
, movie(new QImageReader())
|
||||||
{
|
{
|
||||||
connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
|
connect(this, &MxcAnimatedImage::eventIdChanged, &MxcAnimatedImage::startDownload);
|
||||||
connect(this, &MxcAnimatedImage::roomChanged, &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);
|
setFlag(QQuickItem::ItemHasContents);
|
||||||
// setAcceptHoverEvents(true);
|
// setAcceptHoverEvents(true);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +56,10 @@ public:
|
||||||
{
|
{
|
||||||
if (play_ != newPlay) {
|
if (play_ != newPlay) {
|
||||||
play_ = newPlay;
|
play_ = newPlay;
|
||||||
movie.setPaused(!play_);
|
if (play_)
|
||||||
|
frameTimer.start();
|
||||||
|
else
|
||||||
|
frameTimer.stop();
|
||||||
emit playChanged();
|
emit playChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,10 +77,16 @@ signals:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void startDownload();
|
void startDownload();
|
||||||
void newFrame(int frame)
|
void newFrame()
|
||||||
{
|
{
|
||||||
currentFrame = frame;
|
if (movie->currentImageNumber() > 0 && !movie->canRead() && movie->imageCount() > 1) {
|
||||||
imageDirty = true;
|
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();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +96,9 @@ private:
|
||||||
QString filename_;
|
QString filename_;
|
||||||
bool animatable_ = false;
|
bool animatable_ = false;
|
||||||
QBuffer buffer;
|
QBuffer buffer;
|
||||||
QMovie movie;
|
std::unique_ptr<QImageReader> movie;
|
||||||
int currentFrame = 0;
|
bool imageDirty = true;
|
||||||
bool imageDirty = true;
|
bool play_ = true;
|
||||||
bool play_ = true;
|
QTimer frameTimer;
|
||||||
|
QImage currentFrame;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue