mirror of https://github.com/Nheko-Reborn/nheko
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.
722 lines
26 KiB
722 lines
26 KiB
// SPDX-FileCopyrightText: Nheko Contributors
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import "./ui"
|
|
import "./dialogs"
|
|
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
|
|
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() {
|
|
messageContextMenuC.close();
|
|
replyContextMenuC.close();
|
|
}
|
|
|
|
target: MainWindow
|
|
}
|
|
|
|
Connections {
|
|
function onScrollToIndex(index) {
|
|
chat.positionViewAtIndex(index, ListView.Center);
|
|
chat.updateLastScroll();
|
|
}
|
|
|
|
target: room
|
|
}
|
|
|
|
ScrollBar {
|
|
id: scrollbar
|
|
|
|
anchors.bottom: parent.bottom
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
parent: chat.parent
|
|
}
|
|
ListView {
|
|
id: chat
|
|
|
|
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
|
|
|
|
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
|
|
displayMarginBeginning: height / 4
|
|
displayMarginEnd: height / 4
|
|
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
|
|
//pixelAligned: true
|
|
spacing: 2
|
|
verticalLayoutDirection: ListView.BottomToTop
|
|
|
|
property int lastScrollPos: 0
|
|
|
|
// Fixup the scroll position when the height changes. Without this, the view is kept around the center of the currently visible content, while we usually want to stick to the bottom.
|
|
function updateLastScroll() {
|
|
lastScrollPos = (contentY+height);
|
|
}
|
|
onMovementEnded: updateLastScroll()
|
|
onModelChanged: updateLastScroll()
|
|
onHeightChanged: contentY = (lastScrollPos-height)
|
|
|
|
Component {
|
|
id: defaultMessageStyle
|
|
|
|
TimelineDefaultMessageStyle {
|
|
messageActions: messageActionsC
|
|
messageContextMenu: messageContextMenuC
|
|
replyContextMenu: replyContextMenuC
|
|
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
|
|
data: [
|
|
Connections {
|
|
function onMovementEnded() {
|
|
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) {
|
|
room.currentIndex = index;
|
|
}
|
|
}
|
|
target: chat
|
|
}
|
|
]
|
|
}
|
|
}
|
|
Component {
|
|
id: bubbleMessageStyle
|
|
|
|
TimelineBubbleMessageStyle {
|
|
messageActions: messageActionsC
|
|
messageContextMenu: messageContextMenuC
|
|
replyContextMenu: replyContextMenuC
|
|
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
|
|
data: [
|
|
Connections {
|
|
function onMovementEnded() {
|
|
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) {
|
|
room.currentIndex = index;
|
|
}
|
|
}
|
|
target: chat
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
|
|
footer: Item {
|
|
width: chat.delegateMaxWidth
|
|
// 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
|
|
if (atYEnd && room)
|
|
model.currentIndex = 0;
|
|
}
|
|
|
|
TimelineFilter {
|
|
id: filteredTimeline
|
|
|
|
filterByContent: chatRoot.searchString
|
|
filterByThread: room ? room.thread : ""
|
|
source: room
|
|
}
|
|
Control {
|
|
id: messageActionsC
|
|
|
|
property Item attached: null
|
|
// use comma to update on scroll
|
|
property alias model: row.model
|
|
|
|
hoverEnabled: true
|
|
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
|
|
color: palette.window
|
|
radius: padding
|
|
}
|
|
contentItem: RowLayout {
|
|
id: row
|
|
|
|
property var model
|
|
|
|
spacing: messageActionsC.padding
|
|
|
|
Repeater {
|
|
model: Settings.recentReactions
|
|
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
|
|
|
|
delegate: AbstractButton {
|
|
id: button
|
|
|
|
property color buttonTextColor: palette.buttonText
|
|
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
|
|
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
|
|
padding: 0
|
|
text: button.modelData
|
|
verticalAlignment: Text.AlignVCenter
|
|
visible: !button.showImage
|
|
}
|
|
Image {
|
|
// Workaround, can't get icon.source working for now...
|
|
anchors.fill: parent
|
|
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 {
|
|
ToolTip.delay: Nheko.tooltipDelay
|
|
ToolTip.text: qsTr("Edit")
|
|
ToolTip.visible: hovered
|
|
buttonTextColor: palette.buttonText
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/edit.svg"
|
|
visible: !!row.model && row.model.isEditable
|
|
Layout.preferredWidth: 16
|
|
|
|
onClicked: {
|
|
if (row.model.isEditable)
|
|
room.edit = row.model.eventId;
|
|
}
|
|
}
|
|
ImageButton {
|
|
id: reactButton
|
|
|
|
ToolTip.delay: Nheko.tooltipDelay
|
|
ToolTip.text: qsTr("React")
|
|
ToolTip.visible: hovered
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/smile-add.svg"
|
|
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
|
|
Layout.preferredWidth: 16
|
|
|
|
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")
|
|
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
|
|
Layout.preferredWidth: 16
|
|
|
|
onClicked: room.thread = (row.model.threadId || row.model.eventId)
|
|
}
|
|
ImageButton {
|
|
ToolTip.delay: Nheko.tooltipDelay
|
|
ToolTip.text: qsTr("Reply")
|
|
ToolTip.visible: hovered
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/reply.svg"
|
|
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
|
Layout.preferredWidth: 16
|
|
|
|
onClicked: room.reply = row.model.eventId
|
|
}
|
|
ImageButton {
|
|
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"
|
|
visible: !!row.model && filteredTimeline.filterByContent
|
|
Layout.preferredWidth: 16
|
|
|
|
onClicked: {
|
|
topBar.searchString = "";
|
|
room.showEvent(row.model.eventId);
|
|
}
|
|
}
|
|
ImageButton {
|
|
id: optionsButton
|
|
|
|
ToolTip.delay: Nheko.tooltipDelay
|
|
ToolTip.text: qsTr("Options")
|
|
ToolTip.visible: hovered
|
|
hoverEnabled: true
|
|
image: ":/icons/icons/ui/options.svg"
|
|
Layout.preferredWidth: 16
|
|
|
|
onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
|
|
}
|
|
}
|
|
}
|
|
Shortcut {
|
|
sequences: [StandardKey.MoveToPreviousPage]
|
|
|
|
onActivated: {
|
|
chat.contentY = chat.contentY - chat.height * 0.9;
|
|
chat.returnToBounds();
|
|
}
|
|
}
|
|
Shortcut {
|
|
sequences: [StandardKey.MoveToNextPage]
|
|
|
|
onActivated: {
|
|
chat.contentY = chat.contentY + chat.height * 0.9;
|
|
chat.returnToBounds();
|
|
}
|
|
}
|
|
Shortcut {
|
|
sequences: [StandardKey.Cancel]
|
|
|
|
onActivated: {
|
|
if (room.input.uploads.length > 0)
|
|
room.input.declineUploads();
|
|
else if (room.reply)
|
|
room.reply = undefined;
|
|
else if (room.edit)
|
|
room.edit = undefined;
|
|
else
|
|
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"
|
|
|
|
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
|
|
}
|
|
Shortcut {
|
|
sequence: "Alt+Down"
|
|
|
|
onActivated: {
|
|
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
|
|
room.reply = idx >= 0 ? room.indexToId(idx) : null;
|
|
}
|
|
}
|
|
Shortcut {
|
|
sequence: "Alt+F"
|
|
|
|
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"
|
|
|
|
onActivated: {
|
|
room.edit = room.reply;
|
|
}
|
|
}
|
|
Timer {
|
|
id: readTimer
|
|
|
|
interval: 1000
|
|
|
|
// force current read index to update
|
|
onTriggered: {
|
|
if (room)
|
|
room.setCurrentIndex(room.currentIndex);
|
|
}
|
|
}
|
|
}
|
|
Menu {
|
|
id: messageContextMenuC
|
|
|
|
property string eventId
|
|
property int eventType
|
|
property bool isEditable
|
|
property bool isEncrypted
|
|
property bool isSender
|
|
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_)
|
|
text = text_;
|
|
else
|
|
text = "";
|
|
if (link_)
|
|
link = link_;
|
|
else
|
|
link = "";
|
|
if (showAt_)
|
|
popup(showAt_);
|
|
else
|
|
popup();
|
|
}
|
|
|
|
Component {
|
|
id: removeReason
|
|
|
|
InputDialog {
|
|
id: removeReasonDialog
|
|
|
|
property string eventId
|
|
|
|
prompt: qsTr("Enter reason for removal or hit enter for no reason:")
|
|
title: qsTr("Reason for removal")
|
|
|
|
onAccepted: function (text) {
|
|
room.redactEvent(eventId, text);
|
|
}
|
|
}
|
|
}
|
|
Component {
|
|
id: reportDialog
|
|
|
|
ReportMessage {}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (messageContextMenuC.popupType != undefined) {
|
|
messageContextMenuC.popupType = 2; // Popup.Native with fallback on older Qt (<6.8.0)
|
|
}
|
|
}
|
|
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Go to &message")
|
|
visible: filteredTimeline.filterByContent
|
|
|
|
onTriggered: function () {
|
|
topBar.searchString = "";
|
|
room.showEvent(messageContextMenuC.eventId);
|
|
}
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Copy")
|
|
visible: messageContextMenuC.text
|
|
|
|
onTriggered: Clipboard.text = messageContextMenuC.text
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Copy &link location")
|
|
visible: messageContextMenuC.link
|
|
|
|
onTriggered: Clipboard.text = messageContextMenuC.link
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
id: reactionOption
|
|
|
|
text: qsTr("Re&act")
|
|
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
|
|
|
|
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
|
|
room.input.reaction(messageContextMenuC.eventId, plaintext);
|
|
TimelineManager.focusMessageInput();
|
|
})
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Repl&y")
|
|
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
|
|
|
|
onTriggered: room.reply = (messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Edit")
|
|
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
|
|
|
onTriggered: room.edit = (messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Thread")
|
|
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
|
|
|
|
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
|
|
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
|
|
|
|
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Read receipts")
|
|
|
|
onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Forward")
|
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
|
|
|
|
onTriggered: {
|
|
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
|
|
forwardMess.setMessageEventId(messageContextMenuC.eventId);
|
|
forwardMess.open();
|
|
timelineRoot.destroyOnClose(forwardMess);
|
|
}
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Mark as read")
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("View raw message")
|
|
|
|
onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("View decrypted raw message")
|
|
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
|
|
visible: messageContextMenuC.isEncrypted
|
|
|
|
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Remo&ve message")
|
|
visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
|
|
|
|
onTriggered: function () {
|
|
var dialog = removeReason.createObject(timelineRoot);
|
|
dialog.eventId = messageContextMenuC.eventId;
|
|
dialog.show();
|
|
dialog.forceActiveFocus();
|
|
timelineRoot.destroyOnClose(dialog);
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: qsTr("Report message")
|
|
enabled: visible
|
|
onTriggered: function () {
|
|
var dialog = reportDialog.createObject(timelineRoot, {"eventId": messageContextMenuC.eventId});
|
|
dialog.show();
|
|
dialog.forceActiveFocus();
|
|
timelineRoot.destroyOnClose(dialog);
|
|
}
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Save as")
|
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
|
|
|
|
onTriggered: room.saveMedia(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Open in external program")
|
|
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
|
|
|
|
onTriggered: room.openMedia(messageContextMenuC.eventId)
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Copy link to eve&nt")
|
|
visible: messageContextMenuC.eventId
|
|
|
|
onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
|
|
}
|
|
}
|
|
Component {
|
|
id: forwardCompleterComponent
|
|
|
|
ForwardCompleter {
|
|
}
|
|
}
|
|
Menu {
|
|
id: replyContextMenuC
|
|
|
|
property string eventId
|
|
property string link
|
|
property string text
|
|
|
|
function show(text_, link_, eventId_) {
|
|
text = text_;
|
|
link = link_;
|
|
eventId = eventId_;
|
|
open();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (replyContextMenuC.popupType != undefined) {
|
|
replyContextMenuC.popupType = 2; // Popup.Native with fallback on older Qt (<6.8.0)
|
|
}
|
|
}
|
|
|
|
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Copy")
|
|
visible: replyContextMenuC.text
|
|
|
|
onTriggered: Clipboard.text = replyContextMenuC.text
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("Copy &link location")
|
|
visible: replyContextMenuC.link
|
|
|
|
onTriggered: Clipboard.text = replyContextMenuC.link
|
|
}
|
|
MenuItem {
|
|
enabled: visible
|
|
text: qsTr("&Go to quoted message")
|
|
visible: true
|
|
|
|
onTriggered: room.showEvent(replyContextMenuC.eventId)
|
|
}
|
|
}
|
|
RoundButton {
|
|
id: toEndButton
|
|
|
|
property int fullWidth: 40
|
|
|
|
flat: true
|
|
height: width
|
|
hoverEnabled: true
|
|
radius: width / 2
|
|
width: 0
|
|
|
|
background: Rectangle {
|
|
border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
|
|
border.width: 1
|
|
color: toEndButton.down ? palette.highlight : palette.button
|
|
opacity: enabled ? 1 : 0.3
|
|
radius: toEndButton.radius
|
|
}
|
|
states: [
|
|
State {
|
|
name: ""
|
|
|
|
PropertyChanges {
|
|
toEndButton.width: 0
|
|
}
|
|
},
|
|
State {
|
|
name: "shown"
|
|
when: !chat.atYEnd
|
|
|
|
PropertyChanges {
|
|
toEndButton.width: toEndButton.fullWidth
|
|
}
|
|
}
|
|
]
|
|
transitions: Transition {
|
|
from: ""
|
|
reversible: true
|
|
to: "shown"
|
|
|
|
SequentialAnimation {
|
|
PauseAnimation {
|
|
duration: 500
|
|
}
|
|
PropertyAnimation {
|
|
duration: 200
|
|
easing.type: Easing.InOutQuad
|
|
properties: "width"
|
|
target: toEndButton
|
|
}
|
|
}
|
|
}
|
|
|
|
onClicked: function () {
|
|
chat.positionViewAtBeginning();
|
|
TimelineManager.focusMessageInput();
|
|
chat.updateLastScroll();
|
|
}
|
|
|
|
anchors {
|
|
bottom: parent.bottom
|
|
bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
|
|
right: scrollbar.left
|
|
rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
|
|
}
|
|
Image {
|
|
anchors.fill: parent
|
|
anchors.margins: Nheko.paddingMedium
|
|
fillMode: Image.PreserveAspectFit
|
|
source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
|
|
}
|
|
}
|
|
}
|
|
|