moment/src/gui/Pages/Chat/Timeline/EventContent.qml
miruka c59d54877a Add Keys.Messages.seen_tooltips keybind
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.
2021-03-03 17:14:55 -04:00

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}">&nbsp;` : // U+29D7
model.read_by_count ?
`color="${theme.chat.message.readCounter}">&nbsp;⦿&nbsp;` +
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 {}
}