You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nheko/resources/qml/MessageView.qml

1066 lines
42 KiB

// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
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
1 year ago
property int padding: Nheko.paddingMedium
property string searchString: ""
property Room roommodel: room
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
function onHideMenu() {
1 year ago
messageContextMenu.close();
replyContextMenu.close();
}
1 year ago
target: MainWindow
}
ScrollBar {
id: scrollbar
1 year ago
anchors.bottom: parent.bottom
1 year ago
anchors.right: parent.right
anchors.top: parent.top
parent: chat.parent
}
ListView {
id: chat
1 year ago
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
1 year ago
ScrollBar.vertical: scrollbar
anchors.fill: parent
anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0
// 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
1 year ago
displayMarginBeginning: height / 2
displayMarginEnd: height / 2
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
1 year ago
delegate: TimelineEvent {
1 year ago
id: wrapper
ListView.delayRemove: true
width: chat.delegateMaxWidth
height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10)
anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter
//room: chatRoot.roommodel
required property var day
required property bool isSender
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
required property int userPowerlevel
required property bool isEdited
required property bool isEncrypted
required property var reactions
required property int status
required property int trustlevel
required property int type
required property bool isEditable
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
property alias hovered: messageHover.hovered
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
property string userPowerlevel: wrapper.userPowerlevel
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
z: 4
},
Rectangle {
anchors.fill: gridContainer
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent"
// this looks better without margins
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText)
}
},
RowLayout {
id: gridContainer
width: wrapper.width
y: section.visible && section.active ? section.y + section.height : 0
Item {
Layout.preferredWidth: wrapper.avatarMargin
}
AbstractButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Part of a thread")
ToolTip.visible: hovered
Layout.fillHeight: true
visible: wrapper.threadId
Layout.preferredWidth: 4
onClicked: room.thread = wrapper.threadId
Rectangle {
id: threadLine
anchors.fill: parent
color: TimelineManager.userColor(wrapper.threadId, palette.base)
}
}
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
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 {
spacing: 0
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,
]
}
Item {
// spacer to fill width if needed
Layout.fillWidth: true
}
RowLayout {
id: metadata
property int iconSize: Math.floor(fontMetrics.ascent * scaling)
property double scaling: Settings.bubbles ? 0.75 : 1
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.preferredWidth: implicitWidth
spacing: 2
visible: !isStateEvent
StatusIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
eventId: wrapper.eventId
height: parent.iconSize
status: wrapper.status
width: parent.iconSize
}
Image {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edited")
ToolTip.visible: editHovered.hovered
height: parent.iconSize
source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == room.edit) ? palette.highlight : palette.buttonText)
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
visible: wrapper.isEdited || wrapper.eventId == room.edit
width: parent.iconSize
HoverHandler {
id: editHovered
}
}
ImageButton {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Part of a thread")
ToolTip.visible: hovered
buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base)
height: parent.iconSize
image: ":/icons/icons/ui/thread.svg"
visible: wrapper.threadId
width: parent.iconSize
onClicked: room.thread = threadId
}
EncryptionIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
encrypted: wrapper.isEncrypted
height: parent.iconSize
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
trust: wrapper.trustlevel
visible: room.isEncrypted
width: parent.iconSize
}
Label {
id: ts
Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredWidth: implicitWidth
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate)
ToolTip.visible: ma.hovered
color: palette.inactive.text
font.pointSize: fontMetrics.font.pointSize * parent.scaling
text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
HoverHandler {
id: ma
}
}
}
},
Item {
id: messageActionsAnchor
anchors.fill: gridContainer
property alias hovered: messageHover.hovered
HoverHandler {
id: messageHover
onHoveredChanged: () => {
if (!Settings.mobileMode && hovered) {
if (!messageActions.hovered) {
messageActions.model = wrapper;
messageActions.attached = wrapper;
messageActions.anchors.bottomMargin = -gridContainer.y
}
}
}
}
},
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
}
}
]
1 year ago
}
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
onCountChanged: {
// Mark timeline as read
1 year ago
if (atYEnd && room)
model.currentIndex = 0;
}
1 year ago
TimelineFilter {
id: filteredTimeline
1 year ago
filterByContent: chatRoot.searchString
filterByThread: room ? room.thread : ""
source: room
}
Control {
id: messageActions
property Item attached: null
// use comma to update on scroll
1 year ago
property alias model: row.model
hoverEnabled: true
1 year ago
padding: Nheko.paddingSmall
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
z: 10
parent: chat.contentItem
anchors.bottom: attached?.top
anchors.right: attached?.right
background: Rectangle {
border.color: palette.buttonText
border.width: 1
1 year ago
color: palette.window
radius: padding
}
contentItem: RowLayout {
id: row
property var model
spacing: messageActions.padding
Repeater {
model: Settings.recentReactions
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
delegate: AbstractButton {
id: button
property color buttonTextColor: palette.buttonText
1 year ago
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
1 year ago
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
1 year ago
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
1 year ago
fillMode: Image.PreserveAspectFit
source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
sourceSize.height: button.height
sourceSize.width: button.width
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
}
}
ImageButton {
1 year ago
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
1 year ago
visible: !!row.model && row.model.isEditable
width: 16
onClicked: {
1 year ago
if (row.model.isEditable)
room.edit = row.model.eventId;
}
}
ImageButton {
id: reactButton
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("React")
1 year ago
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/smile-add.svg"
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
width: 16
1 year ago
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();
})
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
1 year ago
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)
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Reply")
1 year ago
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
}
ImageButton {
1 year ago
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"
1 year ago
visible: !!row.model && filteredTimeline.filterByContent
width: 16
onClicked: {
topBar.searchString = "";
room.showEvent(row.model.eventId);
}
}
ImageButton {
id: optionsButton
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Options")
1 year ago
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/options.svg"
width: 16
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)
}
}
}
Shortcut {
sequence: StandardKey.MoveToPreviousPage
1 year ago
onActivated: {
chat.contentY = chat.contentY - chat.height * 0.9;
chat.returnToBounds();
}
}
Shortcut {
sequence: StandardKey.MoveToNextPage
1 year ago
onActivated: {
chat.contentY = chat.contentY + chat.height * 0.9;
chat.returnToBounds();
}
}
Shortcut {
sequence: StandardKey.Cancel
1 year ago
onActivated: {
1 year ago
if (room.input.uploads.length > 0)
room.input.declineUploads();
1 year ago
else if (room.reply)
room.reply = undefined;
else if (room.edit)
room.edit = undefined;
else
1 year ago
room.thread = undefined;
TimelineManager.focusMessageInput();
}
}
// These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
// Better solution welcome.
Shortcut {
sequence: "Alt+Up"
1 year ago
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
1 year ago
onActivated: {
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
room.reply = idx >= 0 ? room.indexToId(idx) : null;
}
}
Shortcut {
sequence: "Alt+F"
1 year ago
onActivated: {
if (room.reply) {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(room.reply);
forwardMess.open();
room.reply = null;
timelineRoot.destroyOnClose(forwardMess);
}
}
}
Shortcut {
sequence: "Ctrl+E"
1 year ago
onActivated: {
room.edit = room.reply;
}
}
Timer {
id: readTimer
1 year ago
interval: 1000
// force current read index to update
onTriggered: {
if (room)
1 year ago
room.setCurrentIndex(room.currentIndex);
}
}
Component {
id: sectionHeader
Column {
1 year ago
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
spacing: 8
1 year ago
topPadding: userName_.visible ? 4 : 0
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
width: parentWidth
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
color: palette.text
height: Math.round(fontMetrics.height * 1.4)
horizontalAlignment: Text.AlignHCenter
1 year ago
text: room ? room.formatDateSeparator(timestamp) : ""
verticalAlignment: Text.AlignVCenter
1 year ago
visible: room && previousMessageDay !== day
width: contentWidth * 1.2
background: Rectangle {
color: palette.window
1 year ago
radius: parent.height / 2
}
}
Row {
id: userInfo
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
1 year ago
height: userName_.height
spacing: 8
visible: !isStateEvent && (!isSender || !Settings.bubbles)
1 year ago
Avatar {
id: messageUserAvatar
1 year ago
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);
}
1 year ago
target: room
}
1 year ago
AbstractButton {
id: userNameButton
PowerlevelIndicator {
id: powerlevelIndicator
anchors.left: parent.left
//anchors.horizontalCenter: parent.horizontalCenter
powerlevel: userPowerlevel
height: fontMetrics.lineSpacing
width: fontMetrics.lineSpacing
sourceSize.width: fontMetrics.lineSpacing
sourceSize.height: fontMetrics.lineSpacing
permissions: room ? room.permissions : null
visible: isAdmin || isModerator
}
1 year ago
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userId
ToolTip.visible: hovered
leftPadding: powerlevelIndicator.visible ? 16 : 0
1 year ago
leftInset: 0
rightInset: 0
rightPadding: 0
contentItem: Label {
1 year ago
id: userName_
1 year ago
color: TimelineManager.userColor(userId, palette.base)
text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText)
1 year ago
textFormat: Text.RichText
}
1 year ago
onClicked: room.openUserProfile(userId)
TextMetrics {
id: userNameTextMetrics
elide: Text.ElideRight
elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3)
text: userName
}
NhekoCursorShape {
1 year ago
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}
Label {
id: statusMsg
1 year ago
property string userStatus: Presence.userStatus(userId)
1 year ago
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)
1 year ago
HoverHandler {
id: statusMsgHoverHandler
}
Connections {
function onPresenceChanged(id) {
if (id == userId)
statusMsg.userStatus = Presence.userStatus(userId);
}
3 years ago
1 year ago
target: Presence
}
}
}
}
}
}
Platform.Menu {
id: messageContextMenu
property string eventId
property int eventType
property bool isEditable
1 year ago
property bool isEncrypted
property bool isSender
1 year ago
property string link
property string text
property string threadId
function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
threadId = threadId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
1 year ago
text = text_;
else
1 year ago
text = "";
if (link_)
1 year ago
link = link_;
else
1 year ago
link = "";
if (showAt_)
1 year ago
open(showAt_);
else
1 year ago
open();
}
Component {
id: removeReason
1 year ago
InputDialog {
id: removeReasonDialog
property string eventId
prompt: qsTr("Enter reason for removal or hit enter for no reason:")
1 year ago
title: qsTr("Reason for removal")
onAccepted: function (text) {
room.redactEvent(eventId, text);
}
}
}
Platform.MenuItem {
1 year ago
enabled: visible
text: qsTr("Go to &message")
visible: filteredTimeline.filterByContent
onTriggered: function () {
topBar.searchString = "";
room.showEvent(messageContextMenu.eventId);
}
1 year ago
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
1 year ago
visible: messageContextMenu.text
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
1 year ago
visible: messageContextMenu.link
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
text: qsTr("Re&act")
1 year ago
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
1 year ago
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")
1 year ago
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.reply = (messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Edit")
1 year ago
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.edit = (messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Thread")
1 year ago
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
1 year ago
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("&Read receipts")
1 year ago
onTriggered: room.showReadReceipts(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("&Forward")
1 year ago
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")
1 year ago
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("View decrypted raw message")
1 year ago
// 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")
1 year ago
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")
1 year ago
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")
1 year ago
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")
1 year ago
visible: messageContextMenu.eventId
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
Platform.Menu {
id: replyContextMenu
property string eventId
1 year ago
property string link
property string text
function show(text_, link_, eventId_) {
text = text_;
link = link_;
eventId = eventId_;
open();
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
1 year ago
visible: replyContextMenu.text
onTriggered: Clipboard.text = replyContextMenu.text
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
1 year ago
visible: replyContextMenu.link
onTriggered: Clipboard.text = replyContextMenu.link
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Go to quoted message")
1 year ago
visible: true
onTriggered: room.showEvent(replyContextMenu.eventId)
}
}
RoundButton {
id: toEndButton
1 year ago
property int fullWidth: 40
1 year ago
flat: true
1 year ago
height: width
hoverEnabled: true
1 year ago
radius: width / 2
width: 0
background: Rectangle {
border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
border.width: 1
1 year ago
color: toEndButton.down ? palette.highlight : palette.button
opacity: enabled ? 1 : 0.3
radius: toEndButton.radius
}
states: [
State {
name: ""
1 year ago
PropertyChanges {
target: toEndButton
width: 0
}
},
State {
name: "shown"
when: !chat.atYEnd
1 year ago
PropertyChanges {
target: toEndButton
width: toEndButton.fullWidth
}
}
]
transitions: Transition {
from: ""
reversible: true
1 year ago
to: "shown"
SequentialAnimation {
1 year ago
PauseAnimation {
duration: 500
}
PropertyAnimation {
duration: 200
1 year ago
easing.type: Easing.InOutQuad
properties: "width"
target: toEndButton
}
}
}
1 year ago
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)
}
}
}