When there's a one line (emote or non-message) event with its avatar shown and combinable events follow it, that first event delegate's height is supposed to be one line with the avatar overflowing into the following event's blank space. This behavior hasn't been working for a long time. This commit restores it, along with improving the related code's quality.
288 lines
8.5 KiB
QML
288 lines
8.5 KiB
QML
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
import QtQuick 2.12
|
|
import QtQuick.Layouts 1.12
|
|
import Clipboard 0.1
|
|
import "../../.."
|
|
import "../../../Base"
|
|
import "../../../PythonBridge"
|
|
|
|
HColumnLayout {
|
|
id: eventDelegate
|
|
|
|
property var hoveredMediaTypeUrl: [] // [] or [mediaType, url, title]
|
|
|
|
property var fetchProfilesFuture: null
|
|
|
|
// Remember timeline goes from newest message at index 0 to oldest
|
|
readonly property var previousModel: eventList.model.get(model.index + 1)
|
|
readonly property var nextModel: eventList.model.get(model.index - 1)
|
|
readonly property QtObject currentModel: model
|
|
|
|
readonly property bool compact: window.settings.compactMode
|
|
readonly property bool checked: model.id in eventList.checked
|
|
readonly property bool isOwn: chat.userId === model.sender_id
|
|
readonly property bool isRedacted: model.event_type === "RedactedEvent"
|
|
readonly property bool onRight: ! eventList.ownEventsOnLeft && isOwn
|
|
readonly property bool combine: eventList.canCombine(previousModel, model)
|
|
readonly property bool asOneLine: eventList.renderEventAsOneLine(model)
|
|
readonly property bool talkBreak:
|
|
eventList.canTalkBreak(previousModel, model)
|
|
readonly property bool dayBreak:
|
|
eventList.canDayBreak(previousModel, model)
|
|
|
|
readonly property int cursorShape:
|
|
eventContent.hoveredLink || hoveredMediaTypeUrl.length === 3 ?
|
|
Qt.PointingHandCursor :
|
|
|
|
eventContent.hoveredSelectable ? Qt.IBeamCursor :
|
|
|
|
Qt.ArrowCursor
|
|
|
|
readonly property int separationSpacing:
|
|
dayBreak ? theme.spacing * 4 :
|
|
talkBreak ? theme.spacing * 6 :
|
|
combine ? theme.spacing / (compact ? 4 : 2) :
|
|
theme.spacing * (compact ? 1 : 2)
|
|
|
|
readonly property alias eventContent: eventContent
|
|
|
|
function json() {
|
|
let event = ModelStore.get(chat.userId, chat.roomId, "events")
|
|
.get(model.index)
|
|
event = JSON.parse(JSON.stringify(event))
|
|
event.source = JSON.parse(event.source)
|
|
return JSON.stringify(event, null, 4)
|
|
}
|
|
|
|
function openContextMenu() {
|
|
contextMenu.media = eventDelegate.hoveredMediaTypeUrl
|
|
contextMenu.link = eventContent.hoveredLink
|
|
contextMenu.popup()
|
|
}
|
|
|
|
function toggleChecked() {
|
|
eventList.toggleCheck(model.index)
|
|
}
|
|
|
|
|
|
width: eventList.width - eventList.leftMargin - eventList.rightMargin
|
|
|
|
// Needed because of eventList's MouseArea which steals the
|
|
// HSelectableLabel's MouseArea hover events
|
|
onCursorShapeChanged: eventList.cursorShape = cursorShape
|
|
|
|
Component.onCompleted: if (model.fetch_profile)
|
|
fetchProfilesFuture = py.callClientCoro(
|
|
chat.userId,
|
|
"get_event_profiles",
|
|
[chat.roomId, model.id],
|
|
() => { fetchProfilesFuture = null }
|
|
)
|
|
|
|
Component.onDestruction:
|
|
if (fetchProfilesFuture) fetchProfilesFuture.cancel()
|
|
|
|
ListView.onRemove: eventList.uncheck(model.id)
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
visible: model.event_type !== "RoomCreateEvent"
|
|
Layout.preferredHeight: separationSpacing
|
|
}
|
|
|
|
Daybreak {
|
|
visible: dayBreak
|
|
|
|
Layout.fillWidth: true
|
|
Layout.minimumWidth: parent.width
|
|
Layout.bottomMargin: separationSpacing
|
|
}
|
|
|
|
EventContent {
|
|
id: eventContent
|
|
|
|
Layout.fillWidth: true
|
|
|
|
Behavior on x { HNumberAnimation {} }
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton
|
|
acceptedModifiers: Qt.NoModifier
|
|
onTapped: toggleChecked()
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton
|
|
acceptedModifiers: Qt.ShiftModifier
|
|
onTapped: eventList.checkFromLastToHere(model.index)
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedButtons: Qt.RightButton
|
|
acceptedPointerTypes: PointerDevice.GenericPointer | PointerDevice.Pen
|
|
onTapped: openContextMenu()
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
|
|
onLongPressed: openContextMenu()
|
|
}
|
|
|
|
HMenu {
|
|
id: contextMenu
|
|
|
|
property var media: []
|
|
property string link: ""
|
|
|
|
readonly property bool isEncryptedMedia:
|
|
Object.keys(JSON.parse(model.media_crypt_dict)).length > 0
|
|
|
|
onClosed: {
|
|
media = []
|
|
link = ""
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "toggle-select-message"
|
|
text: eventDelegate.checked ? qsTr("Deselect") : qsTr("Select")
|
|
onTriggered: eventDelegate.toggleChecked()
|
|
}
|
|
|
|
HMenuItem {
|
|
visible: eventList.selectedCount >= 2
|
|
icon.name: "deselect-all-messages"
|
|
text: qsTr("Deselect all")
|
|
onTriggered: eventList.checked = {}
|
|
}
|
|
|
|
HMenuItem {
|
|
visible: model.index !== 0
|
|
icon.name: "select-until-here"
|
|
text: qsTr("Select until here")
|
|
onTriggered: eventList.checkFromLastToHere(model.index)
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "open-externally"
|
|
text: qsTr("Open externally")
|
|
visible: Boolean(model.media_url)
|
|
onTriggered: eventList.openMediaExternally(model)
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "copy-local-path"
|
|
text: qsTr("Copy local path")
|
|
visible: Boolean(model.media_local_path)
|
|
onTriggered:
|
|
Clipboard.text =
|
|
model.media_local_path.replace(/^file:\/\//, "")
|
|
}
|
|
|
|
HMenuItem {
|
|
id: copyMedia
|
|
icon.name: "copy-link"
|
|
text:
|
|
contextMenu.media.length === 0 ||
|
|
contextMenu.isEncryptedMedia ?
|
|
"" :
|
|
|
|
contextMenu.media[0] === Utils.Media.File ?
|
|
qsTr("Copy file address") :
|
|
|
|
contextMenu.media[0] === Utils.Media.Image ?
|
|
qsTr("Copy image address") :
|
|
|
|
contextMenu.media[0] === Utils.Media.Video ?
|
|
qsTr("Copy video address") :
|
|
|
|
qsTr("Copy audio address")
|
|
|
|
visible: Boolean(text)
|
|
onTriggered: Clipboard.text = model.media_http_url
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "copy-link"
|
|
text: qsTr("Copy link address")
|
|
visible: Boolean(contextMenu.link)
|
|
onTriggered: Clipboard.text = contextMenu.link
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "copy-text"
|
|
text:
|
|
eventList.selectedCount ? qsTr("Copy selection") :
|
|
contextMenu.media.length > 0 ? qsTr("Copy filename") :
|
|
qsTr("Copy text")
|
|
|
|
onTriggered: {
|
|
if (! eventList.selectedCount){
|
|
Clipboard.text = JSON.parse(model.source).body
|
|
return
|
|
}
|
|
|
|
eventList.copySelectedDelegates()
|
|
}
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "reply-to"
|
|
text: qsTr("Reply")
|
|
|
|
onTriggered: {
|
|
chat.replyToEventId = model.id
|
|
chat.replyToUserId = model.sender_id
|
|
chat.replyToDisplayName = model.sender_name
|
|
}
|
|
}
|
|
|
|
HMenuItemPopupSpawner {
|
|
readonly property var events: {
|
|
eventList.selectedCount ?
|
|
eventList.redactableCheckedEvents :
|
|
|
|
eventList.canRedact(currentModel) ?
|
|
[model] :
|
|
|
|
[]
|
|
}
|
|
|
|
icon.name: "remove-message"
|
|
text: qsTr("Remove")
|
|
enabled: properties.eventSenderAndIds.length
|
|
|
|
popup: "Popups/RedactPopup.qml"
|
|
properties: ({
|
|
preferUserId: chat.userId,
|
|
roomId: chat.roomId,
|
|
eventSenderAndIds: events.map(ev => [ev.sender_id, ev.id]),
|
|
|
|
onlyOwnMessageWarning:
|
|
! chat.roomInfo.can_redact_all &&
|
|
events.length < eventList.selectedCount
|
|
})
|
|
}
|
|
|
|
HMenuItem {
|
|
icon.name: "debug"
|
|
text: qsTr("Debug this event")
|
|
onTriggered:
|
|
mainUI.debugConsole.toggle(eventContent, "t.parent.json()")
|
|
}
|
|
|
|
HMenuItemPopupSpawner {
|
|
icon.name: "clear-messages"
|
|
text: qsTr("Clear messages")
|
|
|
|
popup: "Popups/ClearMessagesPopup.qml"
|
|
properties: ({
|
|
userId: chat.userId,
|
|
roomId: chat.roomId,
|
|
preClearCallback: eventList.uncheckAll,
|
|
})
|
|
}
|
|
}
|
|
}
|