Toggle display of the focused message's seen counter tooltip, which shows which user have this message as their last seen and when did they send that information. When this mode is active, you can move the focus to other messages and the tooltip will update itself. If a message doesn't have a counter, it won't have a tooltip.
380 lines
13 KiB
QML
380 lines
13 KiB
QML
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
import QtQuick 2.12
|
|
import QtQuick.Layouts 1.12
|
|
import "../../../Base"
|
|
import "../../.."
|
|
|
|
HRowLayout {
|
|
id: eventContent
|
|
|
|
readonly property var mentions: JSON.parse(model.mentions)
|
|
|
|
readonly property string mentionsCSS: {
|
|
const lines = []
|
|
|
|
for (const [name, link] of mentions) {
|
|
if (! link.match(/^https?:\/\/matrix.to\/#\/@.+/)) continue
|
|
|
|
lines.push(
|
|
`.mention[data-mention='${utils.escapeHtml(name)}'] ` +
|
|
`{ color: ${utils.nameColor(name)} }`
|
|
)
|
|
}
|
|
|
|
return "<style type='text/css'>" + lines.join("\n") + "</style>"
|
|
}
|
|
|
|
readonly property string senderText:
|
|
asOneLine || onRight || combine ? "" : (
|
|
`<${compact ? "span" : "div"} class='sender'>` +
|
|
utils.coloredNameHtml(model.sender_name, model.sender_id) +
|
|
(compact ? ": " : "") +
|
|
(compact ? "</span>" : "</div>")
|
|
)
|
|
property string contentText: utils.processedEventText(model)
|
|
readonly property string timeText: utils.formatTime(model.date, false)
|
|
|
|
readonly property string stateText:
|
|
`<a href="#state-text" style="text-decoration: none">` +
|
|
`<font size=${theme.fontSize.small}px><font ` + (
|
|
model.is_local_echo ?
|
|
`color="${theme.chat.message.localEcho}"> ⧗` : // U+29D7
|
|
|
|
model.read_by_count ?
|
|
`color="${theme.chat.message.readCounter}"> ⦿ ` +
|
|
model.read_by_count : // U+29BF
|
|
|
|
">"
|
|
) + "</font></font></a>"
|
|
|
|
readonly property bool pureMedia: ! contentText && linksRepeater.count
|
|
|
|
readonly property bool hoveredSelectable: contentHover.hovered
|
|
readonly property string hoveredLink:
|
|
linksRepeater.lastHovered && linksRepeater.lastHovered.hovered ?
|
|
linksRepeater.lastHovered.mediaUrl :
|
|
contentLabel.hoveredLink
|
|
|
|
readonly property alias contentLabel: contentLabel
|
|
|
|
readonly property int xOffset:
|
|
onRight ?
|
|
Math.min(
|
|
contentColumn.width - contentLabel.paintedWidth -
|
|
contentLabel.leftPadding - contentLabel.rightPadding,
|
|
|
|
contentColumn.width - linksRepeater.widestChild -
|
|
(
|
|
pureMedia ?
|
|
0 : contentLabel.leftPadding + contentLabel.rightPadding
|
|
),
|
|
) :
|
|
0
|
|
|
|
readonly property int maxMessageWidth:
|
|
contentText.includes("<pre>") || contentText.includes("<table>") ?
|
|
-1 :
|
|
window.settings.Chat.max_messages_line_length < 0 ?
|
|
-1 :
|
|
Math.ceil(
|
|
mainUI.fontMetrics.averageCharacterWidth *
|
|
window.settings.Chat.max_messages_line_length
|
|
)
|
|
|
|
readonly property alias selectedText: contentLabel.selectedPlainText
|
|
|
|
spacing: theme.chat.message.horizontalSpacing
|
|
layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight
|
|
|
|
Item {
|
|
id: avatarWrapper
|
|
visible: ! onRight
|
|
opacity: combine ? 0 : 1
|
|
|
|
Layout.alignment: Qt.AlignTop
|
|
|
|
Layout.preferredWidth:
|
|
compact ?
|
|
theme.chat.message.collapsedAvatarSize :
|
|
theme.chat.message.avatarSize
|
|
|
|
Layout.preferredHeight:
|
|
combine ?
|
|
1 :
|
|
|
|
compact || (
|
|
asOneLine &&
|
|
nextModel &&
|
|
eventList.canCombine(model, nextModel)
|
|
) ?
|
|
theme.chat.message.collapsedAvatarSize :
|
|
|
|
theme.chat.message.avatarSize
|
|
|
|
HUserAvatar {
|
|
id: avatar
|
|
clientUserId: chat.userId
|
|
userId: model.sender_id
|
|
displayName: model.sender_name
|
|
mxc: model.sender_avatar
|
|
width: parent.width
|
|
height: combine ? 1 : parent.Layout.preferredWidth
|
|
radius: theme.chat.message.avatarRadius
|
|
}
|
|
}
|
|
|
|
HColumnLayout {
|
|
id: contentColumn
|
|
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
HSelectableLabel {
|
|
id: contentLabel
|
|
visible: ! pureMedia
|
|
enableLinkActivation: ! eventList.selectedCount
|
|
|
|
selectByMouse:
|
|
eventList.selectedCount <= 1 &&
|
|
eventDelegate.checked &&
|
|
textSelectionBlocker.point.scenePosition === Qt.point(0, 0)
|
|
|
|
topPadding: theme.chat.message.verticalSpacing
|
|
bottomPadding: topPadding
|
|
leftPadding: eventContent.spacing
|
|
rightPadding: leftPadding
|
|
|
|
color: model.event_type === "RoomMessageNotice" ?
|
|
theme.chat.message.noticeBody :
|
|
theme.chat.message.body
|
|
|
|
font.italic: model.event_type === "RoomMessageEmote"
|
|
wrapMode: TextEdit.Wrap
|
|
textFormat: Text.RichText
|
|
text:
|
|
// CSS
|
|
theme.chat.message.styleInclude + mentionsCSS +
|
|
|
|
// Sender name & message body
|
|
(
|
|
compact && contentText.match(/^\s*<(p|h[1-6])>/) ?
|
|
contentText.replace(
|
|
/(^\s*<(p|h[1-6])>)/, "$1" + senderText,
|
|
) :
|
|
senderText + contentText
|
|
) +
|
|
|
|
// Time
|
|
// For some reason, if there's only one space,
|
|
// times will be on their own lines most of the time.
|
|
" " +
|
|
`<font size=${theme.fontSize.small}px ` +
|
|
`color=${theme.chat.message.date}>` +
|
|
timeText +
|
|
"</font>" +
|
|
|
|
stateText
|
|
|
|
transform: Translate { x: xOffset }
|
|
|
|
Layout.maximumWidth: eventContent.maxMessageWidth
|
|
Layout.fillWidth: true
|
|
|
|
onSelectedTextChanged: if (selectedPlainText) {
|
|
eventList.delegateWithSelectedText = model.id
|
|
eventList.selectedText = selectedPlainText
|
|
} else if (eventList.delegateWithSelectedText === model.id) {
|
|
eventList.delegateWithSelectedText = ""
|
|
eventList.selectedText = ""
|
|
}
|
|
|
|
Connections {
|
|
target: eventList
|
|
onCheckedChanged: contentLabel.deselect()
|
|
onDelegateWithSelectedTextChanged: {
|
|
if (eventList.delegateWithSelectedText !== model.id)
|
|
contentLabel.deselect()
|
|
}
|
|
}
|
|
|
|
HoverHandler { id: contentHover }
|
|
|
|
PointHandler {
|
|
id: mousePointHandler
|
|
|
|
property bool checkedNow: false
|
|
|
|
acceptedButtons: Qt.LeftButton
|
|
acceptedModifiers: Qt.NoModifier
|
|
acceptedPointerTypes:
|
|
PointerDevice.GenericPointer | PointerDevice.Eraser
|
|
|
|
onActiveChanged: {
|
|
if (active &&
|
|
! eventDelegate.checked &&
|
|
(! parent.hoveredLink ||
|
|
! parent.enableLinkActivation)) {
|
|
|
|
eventList.check(model.index)
|
|
checkedNow = true
|
|
}
|
|
|
|
if (! active && eventDelegate.checked) {
|
|
checkedNow ?
|
|
checkedNow = false :
|
|
eventList.uncheck(model.index)
|
|
}
|
|
}
|
|
}
|
|
|
|
PointHandler {
|
|
id: mouseShiftPointHandler
|
|
acceptedButtons: Qt.LeftButton
|
|
acceptedModifiers: Qt.ShiftModifier
|
|
acceptedPointerTypes:
|
|
PointerDevice.GenericPointer | PointerDevice.Eraser
|
|
|
|
onActiveChanged: {
|
|
if (active &&
|
|
! eventDelegate.checked &&
|
|
(! parent.hoveredLink ||
|
|
! parent.enableLinkActivation)) {
|
|
|
|
eventList.checkFromLastToHere(model.index)
|
|
}
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
id: touchTapHandler
|
|
acceptedButtons: Qt.LeftButton
|
|
acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
|
|
onTapped:
|
|
if (! parent.hoveredLink || ! parent.enableLinkActivation)
|
|
eventDelegate.toggleChecked()
|
|
}
|
|
|
|
TapHandler {
|
|
id: textSelectionBlocker
|
|
acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
|
|
}
|
|
|
|
HToolTip {
|
|
readonly property bool keyboardShow:
|
|
eventList.showFocusedSeenTooltips &&
|
|
eventList.currentIndex === model.index &&
|
|
model.read_by_count > 0
|
|
|
|
instant: keyboardShow
|
|
visible:
|
|
eventContent.hoveredLink === "#state-text" || keyboardShow
|
|
|
|
label.textFormat: HLabel.StyledText
|
|
text: {
|
|
if (! visible) return ""
|
|
|
|
const members =
|
|
ModelStore.get(chat.userId, chat.roomId, "members")
|
|
|
|
const readBy = Object.entries(
|
|
JSON.parse(model.last_read_by)
|
|
).sort((a, b) => a[1] - b[1]) // sort by values (dates)
|
|
|
|
const lines = []
|
|
|
|
for (const [userId, epoch] of readBy) {
|
|
const member = members.find(userId)
|
|
|
|
const by = utils.coloredNameHtml(
|
|
member ? member.display_name: userId, userId,
|
|
)
|
|
const at = utils.formatRelativeTime(
|
|
new Date(epoch) - model.date,
|
|
)
|
|
lines.push(qsTr("Seen by %1 %2 after").arg(by).arg(at))
|
|
}
|
|
|
|
return lines.join("<br>")
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: contentBackground
|
|
width: Math.max(
|
|
parent.paintedWidth +
|
|
parent.leftPadding + parent.rightPadding,
|
|
|
|
linksRepeater.summedWidth +
|
|
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
|
|
)
|
|
height: contentColumn.height
|
|
radius: theme.chat.message.radius
|
|
z: -100
|
|
color: eventDelegate.checked &&
|
|
! contentLabel.selectedPlainText &&
|
|
! mousePointHandler.active &&
|
|
! mouseShiftPointHandler.active ?
|
|
theme.chat.message.checkedBackground :
|
|
|
|
isOwn?
|
|
theme.chat.message.ownBackground :
|
|
|
|
theme.chat.message.background
|
|
|
|
Behavior on color { HColorAnimation {} }
|
|
|
|
Rectangle {
|
|
visible: model.event_type === "RoomMessageNotice"
|
|
// y: parent.height / 2 - height / 2
|
|
width: theme.chat.message.noticeLineWidth
|
|
height: parent.height
|
|
radius: parent.radius
|
|
color: utils.nameColor(
|
|
model.sender_name || model.sender_id.substring(1),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
HRepeater {
|
|
id: linksRepeater
|
|
|
|
property EventMediaLoader lastHovered: null
|
|
|
|
model: {
|
|
const links = JSON.parse(eventDelegate.currentModel.links)
|
|
|
|
if (eventDelegate.currentModel.media_url)
|
|
links.push(eventDelegate.currentModel.media_url)
|
|
|
|
return links
|
|
}
|
|
|
|
EventMediaLoader {
|
|
singleMediaInfo: eventDelegate.currentModel
|
|
mediaUrl: modelData
|
|
showSender: pureMedia ? senderText : ""
|
|
showDate: pureMedia ? timeText : ""
|
|
showLocalEcho: pureMedia && (
|
|
singleMediaInfo.is_local_echo ||
|
|
singleMediaInfo.read_by_count
|
|
) ? stateText : ""
|
|
|
|
transform: Translate { x: xOffset }
|
|
|
|
onHoveredChanged: if (hovered) linksRepeater.lastHovered = this
|
|
|
|
Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding
|
|
Layout.leftMargin: pureMedia ? 0 : eventContent.spacing
|
|
Layout.rightMargin: pureMedia ? 0 : eventContent.spacing
|
|
Layout.preferredWidth: item ? item.width : -1
|
|
Layout.preferredHeight: item ? item.height : -1
|
|
}
|
|
}
|
|
}
|
|
|
|
HSpacer {}
|
|
}
|