matrixion/resources/qml/MessageView.qml

1244 lines
46 KiB
QML
Raw Normal View History

// SPDX-FileCopyrightText: Nheko Contributors
//
2021-03-05 02:35:15 +03:00
// SPDX-License-Identifier: GPL-3.0-or-later
2021-12-13 02:43:05 +03:00
import "./components"
import "./delegates"
import "./emoji"
import "./ui"
import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
Item {
id: chatRoot
property int availableWidth: width
2023-06-02 02:45:24 +03:00
property int padding: Nheko.paddingMedium
2022-10-06 22:59:59 +03:00
property string searchString: ""
2023-06-22 20:54:17 +03:00
property Room roommodel: room
2022-10-06 22:59:59 +03:00
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
function onHideMenu() {
2023-06-02 02:45:24 +03:00
messageContextMenu.close();
replyContextMenu.close();
}
2023-06-02 02:45:24 +03:00
target: MainWindow
}
ScrollBar {
id: scrollbar
2023-06-02 02:45:24 +03:00
anchors.bottom: parent.bottom
2023-06-02 02:45:24 +03:00
anchors.right: parent.right
anchors.top: parent.top
parent: chat.parent
}
2022-02-20 12:09:22 +03:00
ListView {
id: chat
2023-06-02 02:45:24 +03:00
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0)
readonly property alias filteringInProgress: filteredTimeline.filteringInProgress
2023-06-02 02:45:24 +03:00
ScrollBar.vertical: scrollbar
anchors.fill: parent
anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0
2022-02-20 12:09:22 +03:00
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
//onModelChanged: if (room) room.sendReset()
//reuseItems: true
boundsBehavior: Flickable.StopAtBounds
2023-06-02 02:45:24 +03:00
displayMarginBeginning: height / 2
displayMarginEnd: height / 2
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
2022-02-20 12:09:22 +03:00
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
2023-06-02 02:45:24 +03:00
2023-06-22 20:54:17 +03:00
delegate: EventDelegateChooser {
2023-06-02 02:45:24 +03:00
id: wrapper
ListView.delayRemove: true
width: chat.delegateMaxWidth
2023-08-14 02:11:27 +03:00
height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10)
anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter
//room: chatRoot.roommodel
2023-06-22 20:54:17 +03:00
required property var day
required property bool isSender
required property bool isStateEvent
//required property var previousMessageDay
//required property bool previousMessageIsStateEvent
//required property string previousMessageUserId
required property int index
property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day)
property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent)
property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId)
required property date timestamp
required property string userId
required property string userName
required property string threadId
2023-08-14 02:11:27 +03:00
required property int userPowerlevel
required property var reactions
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (wrapper.threadId ? 6 : 0) // align bubble with section header
data: [
Loader {
id: section
property var day: wrapper.day
property bool isSender: wrapper.isSender
property bool isStateEvent: wrapper.isStateEvent
property int parentWidth: wrapper.width
property var previousMessageDay: wrapper.previousMessageDay
property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
property string previousMessageUserId: wrapper.previousMessageUserId
property date timestamp: wrapper.timestamp
property string userId: wrapper.userId
property string userName: wrapper.userName
2023-08-14 02:11:27 +03:00
property string userPowerlevel: wrapper.userPowerlevel
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
z: 4
},
GridLayout {
id: gridContainer
width: wrapper.width
y: section.visible && section.active ? section.y + section.height : 0
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
2023-08-14 02:11:27 +03:00
Layout.leftMargin: wrapper.avatarMargin // align bubble with section header
AbstractButton {
id: replyRow
visible: wrapper.reply
Layout.fillWidth: true
Layout.maximumHeight: timelineView.height / 8
Layout.preferredWidth: replyRowLay.implicitWidth
Layout.preferredHeight: replyRowLay.implicitHeight
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
clip: true
contentItem: RowLayout {
id: replyRowLay
anchors.fill: parent
Rectangle {
id: replyLine
Layout.fillHeight: true
color: replyRow.userColor
Layout.preferredWidth: 4
}
ColumnLayout {
AbstractButton {
id: replyUserButton
Layout.fillWidth: true
contentItem: ElidedLabel {
id: userName_
fullText: wrapper.reply?.userName ?? ''
color: replyRow.userColor
textFormat: Text.RichText
width: parent.width
elideWidth: width
}
onClicked: room.openUserProfile(wrapper.reply?.userId)
}
data: [
replyUserButton,
wrapper.reply,
]
}
}
background: Rectangle {
width: replyRow.implicitContentWidth
color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1))
}
}
data: [
replyRow, wrapper.main,
]
}
Rectangle {
color: 'yellow'
Layout.preferredWidth: 100
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignRight | Qt.AlignTop
}
},
2023-08-14 02:11:27 +03:00
Reactions {
id: reactionRow
eventId: wrapper.eventId
layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight
reactions: wrapper.reactions
width: wrapper.width - wrapper.avatarMargin
x: wrapper.avatarMargin
anchors {
//left: row.bubbleOnRight ? undefined : row.left
//right: row.bubbleOnRight ? row.right : undefined
top: gridContainer.bottom
topMargin: -4
}
},
Rectangle {
id: unreadRow
color: palette.highlight
height: visible ? 3 : 0
visible: (wrapper.index > 0 && (room.fullyReadEventId == wrapper.eventId))
anchors {
left: parent.left
right: parent.right
top: reactionRow.bottom
topMargin: 5
}
},
Rectangle {
width: Math.min(contentColumn.implicitWidth, contentColumn.width)
height: contentColumn.implicitHeight
color: "blue"
opacity: 0.2
}
]
2023-06-22 20:54:17 +03:00
EventDelegateChoice {
roleValues: [
MtxEvent.TextMessage,
MtxEvent.NoticeMessage,
2023-07-08 20:22:50 +03:00
MtxEvent.ElementEffectMessage,
MtxEvent.UnknownMessage,
2023-06-22 20:54:17 +03:00
]
TextMessage {
2023-07-08 20:22:50 +03:00
keepFullText: true
required property string userId
required property string userName
required property string formattedBody
required property int type
2023-07-08 20:22:50 +03:00
color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text
font.italic: type == MtxEvent.NoticeMessage
formatted: formattedBody
2023-07-08 20:22:50 +03:00
Layout.fillWidth: true
//Layout.maximumWidth: implicitWidth
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.EmoteMessage,
]
TextMessage {
keepFullText: true
required property string userId
required property string userName
2023-07-08 20:22:50 +03:00
required property string formattedBody
formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody
color: TimelineManager.userColor(userId, palette.base)
font.italic: true
Layout.fillWidth: true
//Layout.maximumWidth: implicitWidth
2023-06-22 20:54:17 +03:00
2023-06-02 02:45:24 +03:00
}
}
2023-07-08 20:22:50 +03:00
EventDelegateChoice {
roleValues: [
MtxEvent.CanonicalAlias,
MtxEvent.ServerAcl,
MtxEvent.Name,
MtxEvent.Topic,
MtxEvent.Avatar,
MtxEvent.PinnedEvents,
MtxEvent.ImagePackInRoom,
MtxEvent.SpaceParent,
MtxEvent.RoomCreate,
MtxEvent.PowerLevels,
MtxEvent.PolicyRuleUser,
MtxEvent.PolicyRuleRoom,
MtxEvent.PolicyRuleServer,
MtxEvent.RoomJoinRules,
MtxEvent.RoomHistoryVisibility,
MtxEvent.RoomGuestAccess,
]
TextMessage {
keepFullText: true
required property string userId
required property string userName
required property string formattedStateEvent
isOnlyEmoji: false
text: formattedStateEvent
formatted: ''
body: ''
horizontalAlignment: Text.AlignHCenter
2023-07-08 20:22:50 +03:00
color: palette.buttonText
font.italic: true
Layout.fillWidth: true
//Layout.maximumWidth: implicitWidth
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.CallInvite,
]
TextMessage {
keepFullText: true
required property string userId
required property string userName
required property string callType
isOnlyEmoji: false
body: formatted
formatted: {
switch (callType) {
case "voice":
return qsTr("%1 placed a voice call.").arg(TimelineManager.escapeEmoji(userName));
case "video":
return qsTr("%1 placed a video call.").arg(TimelineManager.escapeEmoji(userName));
default:
return qsTr("%1 placed a call.").arg(TimelineManager.escapeEmoji(userName));
}
}
color: palette.buttonText
font.italic: true
Layout.fillWidth: true
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.CallAnswer,
MtxEvent.CallReject,
MtxEvent.CallSelectAnswer,
MtxEvent.CallHangUp,
MtxEvent.CallCandidates,
MtxEvent.CallNegotiate,
]
TextMessage {
keepFullText: true
required property string userId
required property string userName
required property int type
isOnlyEmoji: false
body: formatted
formatted: {
switch (type) {
case MtxEvent.CallAnswer:
return qsTr("%1 answered the call.").arg(TimelineManager.escapeEmoji(userName));
case MtxEvent.CallReject:
return qsTr("%1 rejected the call.").arg(TimelineManager.escapeEmoji(userName));
case MtxEvent.CallSelectAnswer:
return qsTr("%1 selected answer.").arg(TimelineManager.escapeEmoji(userName));
case MtxEvent.CallHangUp:
return qsTr("%1 ended the call.").arg(TimelineManager.escapeEmoji(userName));
case MtxEvent.CallCandidates:
return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName));
case MtxEvent.CallNegotiate:
return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName));
}
}
color: palette.buttonText
font.italic: true
Layout.fillWidth: true
}
}
2023-07-08 20:22:50 +03:00
EventDelegateChoice {
roleValues: [
MtxEvent.ImageMessage,
MtxEvent.Sticker,
]
ImageMessage {
Layout.fillWidth: true
containerHeight: timelineView.height
Layout.maximumWidth: tempWidth
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.FileMessage,
]
FileMessage {
Layout.fillWidth: true
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.VideoMessage,
MtxEvent.AudioMessage,
]
PlayableMediaMessage {
Layout.fillWidth: true
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.Encrypted,
]
Encrypted {
Layout.fillWidth: true
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.Encryption,
]
EncryptionEnabled {
Layout.fillWidth: true
}
}
2023-07-28 20:30:13 +03:00
EventDelegateChoice {
roleValues: [
MtxEvent.Redacted
]
Redacted {
Layout.fillWidth: true
required property string userId
required property string userName
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.Member
]
ColumnLayout {
id: member
required property string userId
required property string userName
required property bool isReply
required property Room room
required property string formattedStateEvent
NoticeMessage {
body: formatted
isOnlyEmoji: false
isReply: tombstone.isReply
keepFullText: true
isStateEvent: true
Layout.fillWidth: true
formatted: member.formattedStateEvent
}
Button {
visible: room.showAcceptKnockButton(eventId)
Layout.alignment: Qt.AlignHCenter
text: qsTr("Allow them in")
onClicked: room.acceptKnock(member.eventId)
}
}
}
EventDelegateChoice {
roleValues: [
MtxEvent.Tombstone
]
ColumnLayout {
id: tombstone
required property string userId
required property string userName
required property string body
required property bool isReply
required property Room room
required property string eventId
NoticeMessage {
body: formatted
isOnlyEmoji: false
isReply: tombstone.isReply
keepFullText: true
isStateEvent: true
Layout.fillWidth: true
formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body)
}
Button {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Go to replacement room")
onClicked: tombstone.room.joinReplacementRoom(tombstone.eventId)
}
}
}
2023-06-22 20:54:17 +03:00
EventDelegateChoice {
roleValues: [
]
MatrixText {
Layout.fillWidth: true
required property string typeString
text: "Unsupported: " + typeString
required property string userId
required property string userName
2023-06-22 20:54:17 +03:00
}
2023-06-02 02:45:24 +03:00
}
}
footer: Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Nheko.paddingLarge
// hacky, but works
height: loadingSpinner.height + 2 * Nheko.paddingLarge
visible: (room && room.paginationInProgress) || chat.filteringInProgress
Spinner {
id: loadingSpinner
anchors.centerIn: parent
anchors.margins: Nheko.paddingLarge
foreground: palette.mid
running: (room && room.paginationInProgress) || chat.filteringInProgress
z: 3
}
}
Window.onActiveChanged: readTimer.running = Window.active
2022-02-20 12:09:22 +03:00
onCountChanged: {
// Mark timeline as read
2023-06-02 02:45:24 +03:00
if (atYEnd && room)
model.currentIndex = 0;
2022-02-20 12:09:22 +03:00
}
2023-06-02 02:45:24 +03:00
TimelineFilter {
id: filteredTimeline
2021-12-13 02:43:05 +03:00
2023-06-02 02:45:24 +03:00
filterByContent: chatRoot.searchString
filterByThread: room ? room.thread : ""
source: room
}
2022-03-08 01:16:18 +03:00
Control {
2022-02-20 12:09:22 +03:00
id: messageActions
2021-12-13 02:43:05 +03:00
2022-02-20 12:09:22 +03:00
property Item attached: null
// use comma to update on scroll
2022-03-08 01:16:18 +03:00
property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
2023-06-02 02:45:24 +03:00
property alias model: row.model
2021-12-13 02:43:05 +03:00
hoverEnabled: true
2023-06-02 02:45:24 +03:00
padding: Nheko.paddingSmall
2022-03-08 01:16:18 +03:00
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
2022-02-20 12:09:22 +03:00
x: attached ? attachedPos.x : 0
y: attached ? attachedPos.y + Nheko.paddingSmall : 0
2022-02-20 12:09:22 +03:00
z: 10
2022-03-08 01:16:18 +03:00
background: Rectangle {
border.color: palette.buttonText
2022-03-08 01:19:56 +03:00
border.width: 1
2023-06-02 02:45:24 +03:00
color: palette.window
2022-03-08 01:19:56 +03:00
radius: padding
2022-02-20 12:09:22 +03:00
}
contentItem: RowLayout {
2022-02-20 12:09:22 +03:00
id: row
2022-02-20 12:09:22 +03:00
property var model
2022-02-20 12:09:22 +03:00
spacing: messageActions.padding
2022-02-20 12:09:22 +03:00
Repeater {
model: Settings.recentReactions
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
delegate: AbstractButton {
id: button
property color buttonTextColor: palette.buttonText
2023-06-02 02:45:24 +03:00
property color highlightColor: palette.highlight
required property string modelData
property bool showImage: modelData.startsWith("mxc://")
//Layout.preferredHeight: fontMetrics.height
Layout.alignment: Qt.AlignBottom
focusPolicy: Qt.NoFocus
height: showImage ? 16 : buttonText.implicitHeight
implicitHeight: showImage ? 16 : buttonText.implicitHeight
2023-06-02 02:45:24 +03:00
implicitWidth: showImage ? 16 : buttonText.implicitWidth
width: showImage ? 16 : buttonText.implicitWidth
onClicked: {
room.input.reaction(row.model.eventId, modelData);
TimelineManager.focusMessageInput();
}
Label {
id: buttonText
anchors.centerIn: parent
color: button.hovered ? button.highlightColor : button.buttonTextColor
font.family: Settings.emojiFont
horizontalAlignment: Text.AlignHCenter
2023-06-02 02:45:24 +03:00
padding: 0
text: button.modelData
verticalAlignment: Text.AlignVCenter
visible: !button.showImage
}
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
2023-06-02 02:45:24 +03:00
fillMode: Image.PreserveAspectFit
source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
sourceSize.height: button.height
sourceSize.width: button.width
}
2023-06-19 02:38:40 +03:00
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
}
2022-02-20 12:09:22 +03:00
}
ImageButton {
2023-06-02 02:45:24 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
2022-02-20 12:09:22 +03:00
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
2023-06-02 02:45:24 +03:00
visible: !!row.model && row.model.isEditable
width: 16
2022-02-20 12:09:22 +03:00
onClicked: {
2023-06-02 02:45:24 +03:00
if (row.model.isEditable)
room.edit = row.model.eventId;
}
2022-02-20 12:09:22 +03:00
}
ImageButton {
id: reactButton
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("React")
2023-06-02 02:45:24 +03:00
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/smile-add.svg"
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
width: 16
2023-06-02 02:45:24 +03:00
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) {
var event_id = row.model ? row.model.eventId : "";
room.input.reaction(event_id, plaintext);
TimelineManager.focusMessageInput();
})
}
2022-09-30 04:27:05 +03:00
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
2023-06-02 02:45:24 +03:00
ToolTip.visible: hovered
hoverEnabled: true
image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
onClicked: room.thread = (row.model.threadId || row.model.eventId)
2022-09-30 04:27:05 +03:00
}
2022-02-20 12:09:22 +03:00
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Reply")
2023-06-02 02:45:24 +03:00
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/reply.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
onClicked: room.reply = row.model.eventId
2022-02-20 12:09:22 +03:00
}
ImageButton {
2023-06-02 02:45:24 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Go to message")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/go-to.svg"
2023-06-02 02:45:24 +03:00
visible: !!row.model && filteredTimeline.filterByContent
width: 16
onClicked: {
topBar.searchString = "";
room.showEvent(row.model.eventId);
}
}
2022-02-20 12:09:22 +03:00
ImageButton {
id: optionsButton
2022-02-20 12:09:22 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Options")
2023-06-02 02:45:24 +03:00
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/options.svg"
width: 16
2022-09-30 04:27:05 +03:00
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)
}
}
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
2023-03-02 21:35:14 +03:00
chat.contentY = chat.contentY - chat.height * 0.9;
2022-02-20 12:09:22 +03:00
chat.returnToBounds();
}
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: StandardKey.MoveToNextPage
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
2023-03-02 21:35:14 +03:00
chat.contentY = chat.contentY + chat.height * 0.9;
2022-02-20 12:09:22 +03:00
chat.returnToBounds();
}
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: StandardKey.Cancel
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
2023-06-02 02:45:24 +03:00
if (room.input.uploads.length > 0)
room.input.declineUploads();
2023-06-02 02:45:24 +03:00
else if (room.reply)
room.reply = undefined;
else if (room.edit)
room.edit = undefined;
2022-09-30 04:27:05 +03:00
else
2023-06-02 02:45:24 +03:00
room.thread = undefined;
TimelineManager.focusMessageInput();
}
2022-02-20 12:09:22 +03:00
}
// These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
// Better solution welcome.
2022-02-20 12:09:22 +03:00
Shortcut {
sequence: "Alt+Up"
2023-06-02 02:45:24 +03:00
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: "Alt+Down"
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
room.reply = idx >= 0 ? room.indexToId(idx) : null;
}
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: "Alt+F"
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
if (room.reply) {
2022-02-20 12:09:22 +03:00
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(room.reply);
2022-02-20 12:09:22 +03:00
forwardMess.open();
room.reply = null;
timelineRoot.destroyOnClose(forwardMess);
}
}
2022-02-20 12:09:22 +03:00
}
Shortcut {
sequence: "Ctrl+E"
2023-06-02 02:45:24 +03:00
2022-02-20 12:09:22 +03:00
onActivated: {
room.edit = room.reply;
}
2022-02-20 12:09:22 +03:00
}
Timer {
id: readTimer
2023-06-02 02:45:24 +03:00
interval: 1000
2022-02-20 12:09:22 +03:00
// force current read index to update
onTriggered: {
if (room)
2023-06-02 02:45:24 +03:00
room.setCurrentIndex(room.currentIndex);
}
2022-02-20 12:09:22 +03:00
}
Component {
id: sectionHeader
Column {
2023-06-02 02:45:24 +03:00
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
2022-02-20 12:09:22 +03:00
spacing: 8
2023-06-02 02:45:24 +03:00
topPadding: userName_.visible ? 4 : 0
2022-02-20 12:09:22 +03:00
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
width: parentWidth
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
color: palette.text
2022-02-20 12:09:22 +03:00
height: Math.round(fontMetrics.height * 1.4)
horizontalAlignment: Text.AlignHCenter
2023-06-02 02:45:24 +03:00
text: room ? room.formatDateSeparator(timestamp) : ""
2022-02-20 12:09:22 +03:00
verticalAlignment: Text.AlignVCenter
2023-06-02 02:45:24 +03:00
visible: room && previousMessageDay !== day
width: contentWidth * 1.2
2022-02-20 12:09:22 +03:00
background: Rectangle {
color: palette.window
2023-06-02 02:45:24 +03:00
radius: parent.height / 2
2022-02-20 12:09:22 +03:00
}
}
Row {
id: userInfo
2022-02-20 12:09:22 +03:00
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
2022-03-24 03:35:42 +03:00
2023-06-02 02:45:24 +03:00
height: userName_.height
spacing: 8
visible: !isStateEvent && (!isSender || !Settings.bubbles)
2022-03-24 03:35:42 +03:00
2023-06-02 02:45:24 +03:00
Avatar {
id: messageUserAvatar
2022-03-24 03:35:42 +03:00
2023-06-02 02:45:24 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userid
ToolTip.visible: messageUserAvatar.hovered
displayName: userName
height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
userid: userId
width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
onClicked: room.openUserProfile(userId)
}
Connections {
function onRoomAvatarUrlChanged() {
messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
}
function onScrollToIndex(index) {
chat.positionViewAtIndex(index, ListView.Center);
2022-03-24 03:35:42 +03:00
}
2023-06-02 02:45:24 +03:00
target: room
2022-03-24 03:35:42 +03:00
}
2023-08-10 11:15:12 +03:00
2023-06-02 02:45:24 +03:00
AbstractButton {
id: userNameButton
2022-03-24 03:35:42 +03:00
2023-08-10 11:15:12 +03:00
PowerlevelIndicator {
id: powerlevelIndicator
anchors.left: parent.left
//anchors.horizontalCenter: parent.horizontalCenter
powerlevel: userPowerlevel
permissions: room ? room.permissions : null
visible: isAdmin || isModerator
}
2023-06-02 02:45:24 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userId
ToolTip.visible: hovered
2023-08-10 11:15:12 +03:00
leftPadding: powerlevelIndicator.visible ? 16 : 0
2023-06-02 02:45:24 +03:00
leftInset: 0
rightInset: 0
rightPadding: 0
2022-03-08 01:16:18 +03:00
2023-06-03 01:30:44 +03:00
contentItem: Label {
2023-06-02 02:45:24 +03:00
id: userName_
2023-06-02 02:45:24 +03:00
color: TimelineManager.userColor(userId, palette.base)
2023-06-03 01:30:44 +03:00
text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText)
2023-06-02 02:45:24 +03:00
textFormat: Text.RichText
}
2023-06-02 02:45:24 +03:00
onClicked: room.openUserProfile(userId)
2023-06-03 01:30:44 +03:00
TextMetrics {
id: userNameTextMetrics
elide: Text.ElideRight
elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3)
text: userName
}
2023-06-19 02:38:40 +03:00
NhekoCursorShape {
2023-06-02 02:45:24 +03:00
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}
Label {
id: statusMsg
2023-06-02 02:45:24 +03:00
property string userStatus: Presence.userStatus(userId)
2022-02-20 12:09:22 +03:00
2023-06-02 02:45:24 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("%1's status message").arg(userName)
ToolTip.visible: statusMsgHoverHandler.hovered
anchors.baseline: userNameButton.baseline
color: palette.buttonText
elide: Text.ElideRight
font.italic: true
font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8)
text: userStatus.replace(/\n/g, " ")
textFormat: Text.PlainText
width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing)
2022-02-20 12:09:22 +03:00
2023-06-02 02:45:24 +03:00
HoverHandler {
id: statusMsgHoverHandler
}
Connections {
function onPresenceChanged(id) {
if (id == userId)
statusMsg.userStatus = Presence.userStatus(userId);
}
2021-07-22 14:55:12 +03:00
2023-06-02 02:45:24 +03:00
target: Presence
}
}
}
}
}
}
Platform.Menu {
id: messageContextMenu
property string eventId
property int eventType
property bool isEditable
2023-06-02 02:45:24 +03:00
property bool isEncrypted
property bool isSender
2023-06-02 02:45:24 +03:00
property string link
property string text
property string threadId
2022-09-30 04:27:05 +03:00
function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
2022-09-30 04:27:05 +03:00
threadId = threadId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
2023-06-02 02:45:24 +03:00
text = text_;
else
2023-06-02 02:45:24 +03:00
text = "";
if (link_)
2023-06-02 02:45:24 +03:00
link = link_;
else
2023-06-02 02:45:24 +03:00
link = "";
if (showAt_)
2023-06-02 02:45:24 +03:00
open(showAt_);
else
2023-06-02 02:45:24 +03:00
open();
}
Component {
id: removeReason
2023-06-02 02:45:24 +03:00
InputDialog {
id: removeReasonDialog
property string eventId
prompt: qsTr("Enter reason for removal or hit enter for no reason:")
2023-06-02 02:45:24 +03:00
title: qsTr("Reason for removal")
onAccepted: function (text) {
room.redactEvent(eventId, text);
}
}
}
Platform.MenuItem {
2023-06-02 02:45:24 +03:00
enabled: visible
text: qsTr("Go to &message")
visible: filteredTimeline.filterByContent
onTriggered: function () {
topBar.searchString = "";
room.showEvent(messageContextMenu.eventId);
}
2023-06-02 02:45:24 +03:00
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.text
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.link
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
text: qsTr("Re&act")
2023-06-02 02:45:24 +03:00
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
2023-06-02 02:45:24 +03:00
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
room.input.reaction(messageContextMenu.eventId, plaintext);
TimelineManager.focusMessageInput();
})
}
Platform.MenuItem {
text: qsTr("Repl&y")
2023-06-02 02:45:24 +03:00
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
2022-09-30 04:27:05 +03:00
onTriggered: room.reply = (messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Edit")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
2022-09-30 04:27:05 +03:00
onTriggered: room.edit = (messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Thread")
2023-06-02 02:45:24 +03:00
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
2022-09-30 04:27:05 +03:00
onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId)
}
2021-12-11 08:10:41 +03:00
Platform.MenuItem {
enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
2023-06-02 02:45:24 +03:00
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
2021-12-11 08:10:41 +03:00
onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId)
}
Platform.MenuItem {
2022-09-30 04:27:05 +03:00
text: qsTr("&Read receipts")
2023-06-02 02:45:24 +03:00
2021-07-24 21:42:40 +03:00
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("&Forward")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
timelineRoot.destroyOnClose(forwardMess);
}
}
Platform.MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
2023-06-02 02:45:24 +03:00
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("View decrypted raw message")
2023-06-02 02:45:24 +03:00
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Remo&ve message")
2023-06-02 02:45:24 +03:00
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
onTriggered: function () {
var dialog = removeReason.createObject(timelineRoot);
dialog.eventId = messageContextMenu.eventId;
dialog.show();
dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog);
}
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Save as")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
onTriggered: room.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Open in external program")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
onTriggered: room.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy link to eve&nt")
2023-06-02 02:45:24 +03:00
visible: messageContextMenu.eventId
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
2021-08-25 17:10:55 +03:00
Platform.Menu {
id: replyContextMenu
2022-03-24 03:35:42 +03:00
property string eventId
2023-06-02 02:45:24 +03:00
property string link
property string text
2021-08-25 17:10:55 +03:00
2022-03-24 03:35:42 +03:00
function show(text_, link_, eventId_) {
2021-08-25 17:10:55 +03:00
text = text_;
link = link_;
2022-03-24 03:35:42 +03:00
eventId = eventId_;
2021-08-25 17:10:55 +03:00
open();
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
2023-06-02 02:45:24 +03:00
visible: replyContextMenu.text
2021-08-25 17:10:55 +03:00
onTriggered: Clipboard.text = replyContextMenu.text
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
2023-06-02 02:45:24 +03:00
visible: replyContextMenu.link
2021-08-25 17:10:55 +03:00
onTriggered: Clipboard.text = replyContextMenu.link
}
Platform.MenuItem {
enabled: visible
2021-09-14 00:17:03 +03:00
text: qsTr("&Go to quoted message")
2023-06-02 02:45:24 +03:00
visible: true
onTriggered: room.showEvent(replyContextMenu.eventId)
2021-08-25 17:10:55 +03:00
}
}
2022-03-30 00:11:25 +03:00
RoundButton {
id: toEndButton
2023-06-02 02:45:24 +03:00
2022-03-30 00:11:25 +03:00
property int fullWidth: 40
2023-06-02 02:45:24 +03:00
2022-03-30 00:11:25 +03:00
flat: true
2023-06-02 02:45:24 +03:00
height: width
hoverEnabled: true
2023-06-02 02:45:24 +03:00
radius: width / 2
width: 0
background: Rectangle {
border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
border.width: 1
2023-06-02 02:45:24 +03:00
color: toEndButton.down ? palette.highlight : palette.button
opacity: enabled ? 1 : 0.3
radius: toEndButton.radius
}
states: [
State {
name: ""
2023-06-02 02:45:24 +03:00
PropertyChanges {
target: toEndButton
width: 0
}
},
State {
name: "shown"
when: !chat.atYEnd
2023-06-02 02:45:24 +03:00
PropertyChanges {
target: toEndButton
width: toEndButton.fullWidth
}
}
]
transitions: Transition {
from: ""
reversible: true
2023-06-02 02:45:24 +03:00
to: "shown"
2022-03-30 00:11:25 +03:00
SequentialAnimation {
2023-06-02 02:45:24 +03:00
PauseAnimation {
duration: 500
}
2022-03-30 00:11:25 +03:00
PropertyAnimation {
duration: 200
2023-06-02 02:45:24 +03:00
easing.type: Easing.InOutQuad
properties: "width"
target: toEndButton
2022-03-30 00:11:25 +03:00
}
}
}
2023-06-02 02:45:24 +03:00
onClicked: function () {
chat.positionViewAtBeginning();
TimelineManager.focusMessageInput();
}
anchors {
bottom: parent.bottom
bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
right: scrollbar.left
rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
}
Image {
id: buttonImg
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
fillMode: Image.PreserveAspectFit
source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
}
2022-03-30 00:11:25 +03:00
}
}