Allow "replying" to an event with a file
Send a pseudo-reply consisting of two messages: a `m.text` which is just a reply with an empty body, then the file event itself. This is a workaround to the restriction imposed by the Matrix API, which prevents us from simply attaching a reply to a media event: https://matrix.org/docs/spec/client_server/latest#rich-replies
This commit is contained in:
parent
6d9a013d5d
commit
0d2be820fe
|
@ -617,7 +617,12 @@ class MatrixClient(nio.AsyncClient):
|
||||||
self.upload_tasks[uuid].cancel()
|
self.upload_tasks[uuid].cancel()
|
||||||
|
|
||||||
|
|
||||||
async def send_clipboard_image(self, room_id: str, image: bytes) -> None:
|
async def send_clipboard_image(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
image: bytes,
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
"""Send a clipboard image passed from QML as a `m.image` message."""
|
"""Send a clipboard image passed from QML as a `m.image` message."""
|
||||||
|
|
||||||
prefix = datetime.now().strftime("%Y%m%d-%H%M%S.")
|
prefix = datetime.now().strftime("%Y%m%d-%H%M%S.")
|
||||||
|
@ -633,16 +638,28 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
return Path(temp.name)
|
return Path(temp.name)
|
||||||
|
|
||||||
await self.send_file(room_id, get_path)
|
await self.send_file(room_id, get_path, reply_to_event_id)
|
||||||
|
|
||||||
|
|
||||||
async def send_file(self, room_id: str, path: PathCallable) -> None:
|
async def send_file(
|
||||||
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message."""
|
self,
|
||||||
|
room_id: str,
|
||||||
|
path: PathCallable,
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message.
|
||||||
|
|
||||||
|
The Matrix client-server API states that media messages can't have a
|
||||||
|
reply attached.
|
||||||
|
Thus, if a `reply_to_event_id` is passed, we send a pseudo-reply as two
|
||||||
|
events: a `m.text` one with the reply but an empty body, then the
|
||||||
|
actual media.
|
||||||
|
"""
|
||||||
|
|
||||||
item_uuid = uuid4()
|
item_uuid = uuid4()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._send_file(item_uuid, room_id, path)
|
await self._send_file(item_uuid, room_id, path, reply_to_event_id)
|
||||||
except (nio.TransferCancelledError, asyncio.CancelledError):
|
except (nio.TransferCancelledError, asyncio.CancelledError):
|
||||||
self.upload_monitors.pop(item_uuid, None)
|
self.upload_monitors.pop(item_uuid, None)
|
||||||
self.upload_tasks.pop(item_uuid, None)
|
self.upload_tasks.pop(item_uuid, None)
|
||||||
|
@ -650,9 +667,13 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
|
|
||||||
async def _send_file(
|
async def _send_file(
|
||||||
self, item_uuid: UUID, room_id: str, path: PathCallable,
|
self,
|
||||||
|
item_uuid: UUID,
|
||||||
|
room_id: str,
|
||||||
|
path: PathCallable,
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Upload and monitor a file + thumbnail and send the built event."""
|
"""Upload and monitor a file + thumbnail and send the built event(s)"""
|
||||||
|
|
||||||
# TODO: this function is way too complex, and most of it should be
|
# TODO: this function is way too complex, and most of it should be
|
||||||
# refactored into nio.
|
# refactored into nio.
|
||||||
|
@ -860,6 +881,11 @@ class MatrixClient(nio.AsyncClient):
|
||||||
del self.upload_tasks[item_uuid]
|
del self.upload_tasks[item_uuid]
|
||||||
del self.models[room_id, "uploads"][str(upload_item.id)]
|
del self.models[room_id, "uploads"][str(upload_item.id)]
|
||||||
|
|
||||||
|
if reply_to_event_id:
|
||||||
|
await self.send_text(
|
||||||
|
room_id=room_id, text="", reply_to_event_id=reply_to_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
await self._local_echo(
|
await self._local_echo(
|
||||||
room_id,
|
room_id,
|
||||||
transaction_id,
|
transaction_id,
|
||||||
|
|
|
@ -6,8 +6,11 @@ import Qt.labs.platform 1.1
|
||||||
HFileDialogOpener {
|
HFileDialogOpener {
|
||||||
property string userId
|
property string userId
|
||||||
property string roomId
|
property string roomId
|
||||||
|
property string replyToEventId: ""
|
||||||
property bool destroyWhenDone: false
|
property bool destroyWhenDone: false
|
||||||
|
|
||||||
|
signal replied()
|
||||||
|
|
||||||
|
|
||||||
fill: false
|
fill: false
|
||||||
dialog.title: qsTr("Select a file to send")
|
dialog.title: qsTr("Select a file to send")
|
||||||
|
@ -16,14 +19,17 @@ HFileDialogOpener {
|
||||||
onFilesPicked: {
|
onFilesPicked: {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const path = Qt.resolvedUrl(file).replace(/^file:/, "")
|
const path = Qt.resolvedUrl(file).replace(/^file:/, "")
|
||||||
|
const args = [roomId, path, replyToEventId || undefined]
|
||||||
|
|
||||||
py.callClientCoro(userId, "send_file", [roomId, path], () => {
|
py.callClientCoro(userId, "send_file", args, () => {
|
||||||
if (destroyWhenDone) destroy()
|
if (destroyWhenDone) destroy()
|
||||||
|
|
||||||
}, (type, args, error, traceback) => {
|
}, (type, args, error, traceback) => {
|
||||||
console.error(`python:\n${traceback}`)
|
console.error(`python:\n${traceback}`)
|
||||||
if (destroyWhenDone) destroy()
|
if (destroyWhenDone) destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (replyToUserId) replied()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,14 @@ Item {
|
||||||
readonly property bool composerHasFocus:
|
readonly property bool composerHasFocus:
|
||||||
Boolean(loader.item && loader.item.composer.hasFocus)
|
Boolean(loader.item && loader.item.composer.hasFocus)
|
||||||
|
|
||||||
|
function clearReplyTo() {
|
||||||
|
if (! replyToEventId) return
|
||||||
|
|
||||||
|
replyToEventId = ""
|
||||||
|
replyToUserId = ""
|
||||||
|
replyToDisplayName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onFocusChanged: if (focus && loader.item) loader.item.composer.takeFocus()
|
onFocusChanged: if (focus && loader.item) loader.item.composer.takeFocus()
|
||||||
onReadyChanged: longLoading = false
|
onReadyChanged: longLoading = false
|
||||||
|
|
|
@ -98,14 +98,6 @@ HTextArea {
|
||||||
py.callClientCoro(userId, "room_typing", [chat.roomId, typing])
|
py.callClientCoro(userId, "room_typing", [chat.roomId, typing])
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearReplyTo() {
|
|
||||||
if (! chat.replyToEventId) return
|
|
||||||
|
|
||||||
chat.replyToEventId = ""
|
|
||||||
chat.replyToUserId = ""
|
|
||||||
chat.replyToDisplayName = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewLine() {
|
function addNewLine() {
|
||||||
let indents = 0
|
let indents = 0
|
||||||
const parts = lineText.split(indent)
|
const parts = lineText.split(indent)
|
||||||
|
@ -171,7 +163,9 @@ HTextArea {
|
||||||
userId: chat.userId,
|
userId: chat.userId,
|
||||||
roomId: chat.roomId,
|
roomId: chat.roomId,
|
||||||
roomName: chat.roomInfo.display_name,
|
roomName: chat.roomInfo.display_name,
|
||||||
|
replyToEventId: chat.replyToEventId,
|
||||||
},
|
},
|
||||||
|
popup => popup.replied.connect(chat.clearReplyTo),
|
||||||
)
|
)
|
||||||
|
|
||||||
Keys.onEscapePressed:
|
Keys.onEscapePressed:
|
||||||
|
|
|
@ -28,7 +28,9 @@ HButton {
|
||||||
roomId: chat.roomId,
|
roomId: chat.roomId,
|
||||||
roomName: chat.roomInfo.display_name,
|
roomName: chat.roomInfo.display_name,
|
||||||
filePath: Clipboard.text.trim(),
|
filePath: Clipboard.text.trim(),
|
||||||
|
replyToEventId: chat.replyToEventId,
|
||||||
},
|
},
|
||||||
|
popup => popup.replied.connect(chat.clearReplyTo),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +38,8 @@ HButton {
|
||||||
id: sendFilePicker
|
id: sendFilePicker
|
||||||
userId: chat.userId
|
userId: chat.userId
|
||||||
roomId: chat.roomId
|
roomId: chat.roomId
|
||||||
|
replyToEventId: chat.replyToEventId
|
||||||
|
onReplied: chat.clearReplyTo()
|
||||||
|
|
||||||
HShortcut {
|
HShortcut {
|
||||||
sequences: window.settings.keys.sendFile
|
sequences: window.settings.keys.sendFile
|
||||||
|
|
|
@ -16,6 +16,9 @@ HColumnPopup {
|
||||||
property string userId
|
property string userId
|
||||||
property string roomId
|
property string roomId
|
||||||
property string roomName
|
property string roomName
|
||||||
|
property string replyToEventId: ""
|
||||||
|
|
||||||
|
signal replied()
|
||||||
|
|
||||||
|
|
||||||
contentWidthLimit: theme.controls.popup.defaultWidth * 1.25
|
contentWidthLimit: theme.controls.popup.defaultWidth * 1.25
|
||||||
|
@ -26,11 +29,14 @@ HColumnPopup {
|
||||||
text: qsTr("Send")
|
text: qsTr("Send")
|
||||||
icon.name: "confirm-uploading-file"
|
icon.name: "confirm-uploading-file"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
py.callClientCoro(
|
const args = [
|
||||||
popup.userId,
|
popup.roomId,
|
||||||
"send_clipboard_image",
|
Clipboard.image,
|
||||||
[popup.roomId, Clipboard.image],
|
popup.replyToEventId || undefined,
|
||||||
)
|
]
|
||||||
|
|
||||||
|
py.callClientCoro(popup.userId, "send_clipboard_image", args)
|
||||||
|
if (popup.replyToEventId) popup.replied()
|
||||||
popup.close()
|
popup.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,12 @@ HColumnPopup {
|
||||||
property string roomId
|
property string roomId
|
||||||
property string roomName
|
property string roomName
|
||||||
property string filePath
|
property string filePath
|
||||||
|
property string replyToEventId: ""
|
||||||
|
|
||||||
readonly property string fileName: filePath.split("/").slice(-1)[0]
|
readonly property string fileName: filePath.split("/").slice(-1)[0]
|
||||||
|
|
||||||
|
signal replied()
|
||||||
|
|
||||||
|
|
||||||
contentWidthLimit: theme.controls.popup.defaultWidth * 1.25
|
contentWidthLimit: theme.controls.popup.defaultWidth * 1.25
|
||||||
|
|
||||||
|
@ -25,9 +28,14 @@ HColumnPopup {
|
||||||
text: qsTr("Send")
|
text: qsTr("Send")
|
||||||
icon.name: "confirm-uploading-file"
|
icon.name: "confirm-uploading-file"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
py.callClientCoro(
|
const args = [
|
||||||
popup.userId, "send_file", [popup.roomId, filePath],
|
popup.roomId,
|
||||||
)
|
popup.filePath,
|
||||||
|
popup.replyToEventId || undefined,
|
||||||
|
]
|
||||||
|
|
||||||
|
py.callClientCoro(popup.userId, "send_file", args)
|
||||||
|
if (popup.replyToEventId) popup.replied()
|
||||||
popup.close()
|
popup.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user