More qml files to go in timeline module

This commit is contained in:
James Graham
2025-04-16 18:13:32 +01:00
parent 2d5d2c6c06
commit 195e175186
6 changed files with 4 additions and 4 deletions

View File

@@ -6,6 +6,7 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.timeline
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/timeline
QML_FILES
TimelineView.qml
EventDelegate.qml
HiddenDelegate.qml
MessageDelegate.qml
@@ -54,6 +55,9 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE
TextComponent.qml
ThreadBodyComponent.qml
VideoComponent.qml
DelegateContextMenu.qml
FileDelegateContextMenu.qml
MessageDelegateContextMenu.qml
SOURCES
contentprovider.cpp
locationhelper.cpp

View File

@@ -0,0 +1,372 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
/**
* @brief The base menu for most message types.
*
* This menu supports showing a list of actions to be shown for a particular event
* delegate in a message timeline. The menu supports both desktop and mobile menus
* with different visuals appropriate to the platform.
*
* The menu supports both a list of main actions and the ability to define sub menus
* using the nested action parameter.
*
* For event types that need alternate actions this class can be used as a base and
* the actions and nested actions can be overwritten to show the alternate items.
*/
KirigamiComponents.ConvergentContextMenu {
id: root
/**
* @brief The current connection for the account accessing the event.
*/
required property NeoChatConnection connection
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var author
/**
* @brief The display text of the message as plain text.
*/
required property string plainText
/**
* @brief The text the user currently has selected.
*/
property string selectedText: ""
/**
* @brief The link the user has currently hovered.
*/
property string hoveredLink: ""
/**
* Some common actions shared between menus
*/
component ViewSourceAction: Kirigami.Action {
visible: NeoChatConfig.developerTools
text: i18n("View Source")
icon.name: "code-context"
onTriggered: RoomManager.viewEventSource(root.eventId)
}
component RemoveMessageAction: Kirigami.Action {
visible: author.isLocalMember || currentRoom.canSendState("redact")
text: i18nc("@action:button", "Remove…")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Message"),
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title:dialog", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.redactEvent(root.eventId, reason);
});
}
}
component ReplyMessageAction: QQC2.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
currentRoom.mainCache.replyId = eventId;
currentRoom.editCache.editId = "";
RoomManager.requestFullScreenClose();
}
}
component ReportMessageAction: Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
icon.name: "dialog-warning-symbolic"
visible: !author.isLocalMember
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report Message"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this message"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report")
}, {
title: i18nc("@title", "Report Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.reportEvent(root.eventId, reason);
});
}
}
component ShowUserAction: Kirigami.Action {
text: i18nc("@action:inmenu", "Show User")
icon.name: "username-copy"
onTriggered: {
RoomManager.resolveResource(author.id)
}
}
component PinMessageAction: Kirigami.Action {
readonly property bool pinned: currentRoom.isEventPinned(root.eventId)
visible: currentRoom.canSendState("m.room.pinned_events")
text: pinned ? i18nc("@action:button 'Unpin' as in 'Unpin this message'", "Unpin") : i18nc("@action:button 'Pin' as in 'Pin the message in the room'", "Pin")
icon.name: pinned ? "window-unpin-symbolic" : "pin-symbolic"
onTriggered: pinned ? currentRoom.unpinEvent(root.eventId) : currentRoom.pinEvent(root.eventId)
}
headerContentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
source: root.author.avatarUrl
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Kirigami.Heading {
level: 4
text: root.author.htmlSafeDisplayName
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
QQC2.Label {
text: root.plainText
textFormat: Text.PlainText
elide: Text.ElideRight
onLinkActivated: RoomManager.resolveResource(link, "join")
Layout.fillWidth: true
}
}
}
Kirigami.Action {
visible: Kirigami.Settings.isMobile
displayComponent: RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: Delegates.RoundedItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
root.item.close();
}
}
}
}
}
/*
}
}
Component {
id: mobileMenu
Kirigami.OverlayDrawer {
id: drawer
height: stackView.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
parent: applicationWindow().overlay
QQC2.StackView {
id: stackView
width: parent.width
implicitHeight: currentItem.implicitHeight
Component {
id: nestedActionsComponent
ColumnLayout {
id: actionLayout
property string title: ""
property list<Kirigami.Action> actions
width: parent.width
spacing: 0
RowLayout {
QQC2.ToolButton {
icon.name: 'draw-arrow-back'
onClicked: stackView.pop()
}
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: actionLayout.title
wrapMode: Text.WordWrap
}
}
Repeater {
id: listViewAction
model: actionLayout.actions
FormCard.FormButtonDelegate {
icon.name: modelData.icon.name
icon.color: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered();
root.item.close();
}
}
}
}
}
initialItem: ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
id: avatar
source: author.avatarUrl
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: author.htmlSafeDisplayName
wrapMode: Text.WordWrap
}
QQC2.Label {
text: plainText
Layout.fillWidth: true
wrapMode: Text.WordWrap
onLinkActivated: RoomManager.resolveResource(link, "join")
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: root.actions
DelegateChooser {
role: "separator"
DelegateChoice {
roleValue: true
FormCard.FormDelegateSeparator {
visible: modelData.visible
}
}
DelegateChoice {
roleValue: false
FormCard.FormButtonDelegate {
icon.name: modelData.icon.name
icon.color: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered();
root.item.close();
}
}
}
}
}
Repeater {
model: root.nestedActions
FormCard.FormButtonDelegate {
action: modelData
visible: modelData.visible
onClicked: {
stackView.push(nestedActionsComponent, {
title: modelData.text,
actions: modelData.children
});
}
}
}
}
}
}
}
asynchronous: true
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
function open() {
active = true;
}
onStatusChanged: if (status == Loader.Ready) {
if (Kirigami.Settings.isMobile) {
item.open();
} else {
item.popup();
}
}*/
}

View File

@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtCore as Core
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Dialogs as Dialogs
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief The menu for media messages.
*
* This component just overloads the actions and nested actions of the base menu
* to what is required for a media item.
*
* @sa DelegateContextMenu
*/
DelegateContextMenu {
id: root
/**
* @brief The MIME type of the media.
*/
property string mimeType
/**
* @brief Progress info when downloading files.
*
* @sa Quotient::FileTransferInfo
*/
required property var progressInfo
DelegateContextMenu.ReplyMessageAction {}
Kirigami.Action {
separator: true
}
QQC2.Action {
text: i18nc("@action:inmenu", "Open Image")
icon.name: "document-open"
onTriggered: {
currentRoom.openEventMediaExternally(root.eventId);
}
}
QQC2.Action {
text: i18nc("@action:inmenu", "Save Image…")
icon.name: "document-save"
onTriggered: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
dialog.selectedFile = currentRoom.fileNameToDownload(eventId);
dialog.open();
}
}
QQC2.Action {
text: i18nc("@action:inmenu", "Copy Image")
icon.name: "edit-copy"
onTriggered: {
currentRoom.copyEventMedia(root.eventId);
}
}
Kirigami.Action {
separator: true
}
Kirigami.Action {
visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
text: i18n("Remove…")
icon.name: "edit-delete-remove"
icon.color: "red"
onTriggered: {
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Message"),
placeholder: i18nc("@info:placeholder", "Reason for removing this message"),
actionText: i18nc("@action:button 'Remove' as in 'Remove this message'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title:dialog", "Remove Message"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
currentRoom.redactEvent(root.eventId, reason);
});
}
}
DelegateContextMenu.PinMessageAction {}
DelegateContextMenu.ReportMessageAction {}
DelegateContextMenu.ShowUserAction {}
Kirigami.Action {
separator: true
visible: viewSourceAction.visible
}
DelegateContextMenu.ViewSourceAction {
id: viewSourceAction
}
ShareAction {
id: shareAction
inputData: {
"urls": [filename],
"mimeType": [root.mimeType]
}
room: currentRoom
eventId: root.eventId
property string filename: Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)
}
readonly property Component saveAsDialog: Dialogs.FileDialog {
fileMode: Dialogs.FileDialog.SaveFile
currentFolder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : Core.StandardPaths.writableLocation(Core.StandardPaths.DownloadLocation)
onAccepted: {
if (!selectedFile) {
return;
}
NeoChatConfig.lastSaveDirectory = currentFolder;
NeoChatConfig.save();
currentRoom.downloadFile(eventId, selectedFile);
}
}
}

View File

@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtTextToSpeech
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
/**
* @brief The menu for normal messages.
*
* This component just overloads the actions and nested actions of the base menu
* to what is required for a message item.
*
* @sa DelegateContextMenu
*/
DelegateContextMenu {
id: root
/**
* @brief The delegate type of the message.
*/
required property int messageComponentType
/**
* @brief The display text of the message as rich text.
*/
required property string htmlText
Kirigami.Action {
text: i18n("Edit")
icon.name: "document-edit"
onTriggered: {
currentRoom.editCache.editId = eventId;
currentRoom.mainCache.replyId = "";
currentRoom.mainCache.threadId = "";
}
visible: root.author.isLocalMember && root.messageComponentType === MessageComponentType.Text
}
DelegateContextMenu.ReplyMessageAction {}
QQC2.Action {
text: i18nc("@action:inmenu As in 'Forward this message'", "Forward…")
icon.name: "mail-forward-symbolic"
onTriggered: {
let page = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Forward Message"),
width: Kirigami.Units.gridUnit * 25
});
page.chosen.connect(function (targetRoomId) {
root.connection.room(targetRoomId).postHtmlMessage(root.plainText, root.htmlText.length > 0 ? root.htmlText : root.plainText);
page.closeDialog();
});
}
}
Kirigami.Action {
separator: true
}
DelegateContextMenu.RemoveMessageAction {}
Kirigami.Action {
text: i18nc("@action:inmenu", "Copy Link Address")
icon.name: "edit-copy"
visible: root.hoveredLink.length > 0
onTriggered: Clipboard.saveText(root.hoveredLink)
}
QQC2.Action {
text: i18nc("@action:inmenu", "Copy Text")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(root.selectedText.length > 0 ? root.selectedText : root.plainText)
}
QQC2.Action {
text: i18nc("@action:inmenu", "Copy Message Link")
icon.name: "link-symbolic"
onTriggered: {
Clipboard.saveText("https://matrix.to/#/" + currentRoom.id + "/" + root.eventId);
}
}
QQC2.Action {
text: i18nc("@action:inmenu", "Read Text Aloud")
icon.name: "audio-speakers-symbolic"
onTriggered: {
TextToSpeechWrapper.say(i18nc("@info text-to-speech %1 is author %2 is message text", "%1 said %2", root.author.displayName, root.plainText))
}
}
Kirigami.Action {
separator: true
}
DelegateContextMenu.PinMessageAction {}
DelegateContextMenu.ReportMessageAction {}
DelegateContextMenu.ShowUserAction {}
Kirigami.Action {
separator: true
visible: viewSourceAction.visible
}
DelegateContextMenu.ViewSourceAction {
id: viewSourceAction
}
Kirigami.Action {
separator: true
visible: webShortcutModel.enabled
}
Kirigami.Action {
id: webShortcutModelAction
text: i18n("Search for '%1'", webshortcutModel.trunkatedSearchText)
icon.name: "search-symbolic"
visible: webshortcutModel.enabled
readonly property Instantiator instantiator: Instantiator {
model: WebShortcutModel {
id: webshortcutModel
selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText
onOpenUrl: url => RoomManager.resolveResource(url.toString())
}
delegate: QQC2.Action {
text: model.display
icon.name: model.decoration
onTriggered: webshortcutModel.trigger(model.edit)
}
onObjectAdded: (index, object) => webShortcutModelAction.children.push(object)
}
}
Kirigami.Action {
text: i18n("Configure Web Shortcuts…")
icon.name: "configure"
visible: !Controller.isFlatpak && webshortcutModel.enabled
onTriggered: webshortcutModel.configureWebShortcuts()
}
}

View File

@@ -0,0 +1,394 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import QtQuick.Window
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigami as Kirigami
import org.kde.kitemmodels
import org.kde.neochat
import org.kde.neochat.timeline
QQC2.ScrollView {
id: root
required property NeoChatRoom currentRoom
onCurrentRoomChanged: {
roomChanging = true;
roomChangingTimer.restart();
applicationWindow().hoverLinkIndicator.text = "";
messageListView.positionViewAtBeginning();
hasScrolledUpBefore = false;
}
property bool roomChanging: false
required property Item page
/**
* @brief The TimelineModel to use.
*
* Required so that new events can be requested when the end of the current
* local timeline is reached.
*/
required property TimelineModel timelineModel
/**
* @brief The MessageFilterModel to use.
*
* This model has the filtered list of events that should be shown in the timeline.
*/
required property MessageFilterModel messageFilterModel
readonly property bool atYEnd: messageListView.atYEnd
property alias interactive: messageListView.interactive
/// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false
signal focusChatBar
QQC2.ScrollBar.vertical.interactive: false
ListView {
id: messageListView
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined
// Spacing needs to be zero or the top sectionLabel overlay will be disrupted.
// This is because itemAt returns null in the spaces.
// All spacing should be handled by the delegates themselves
spacing: 0
verticalLayoutDirection: ListView.BottomToTop
clip: true
interactive: Kirigami.Settings.isMobile
bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2)
model: root.messageFilterModel
onCountChanged: if (root.roomChanging) {
root.positionViewAtBeginning();
}
Timer {
interval: 1000
running: messageListView.atYBeginning
triggeredOnStart: true
onTriggered: {
if (messageListView.atYBeginning && root.timelineModel.timelineMessageModel.canFetchMore(root.timelineModel.index(0, 0))) {
root.timelineModel.timelineMessageModel.fetchMore(root.timelineModel.index(0, 0));
}
}
repeat: true
}
// HACK: The view should do this automatically but doesn't.
onAtYBeginningChanged: if (atYBeginning && root.timelineModel.timelineMessageModel.canFetchMore(root.timelineModel.index(0, 0))) {
root.timelineModel.timelineMessageModel.fetchMore(root.timelineModel.index(0, 0));
}
Timer {
id: roomChangingTimer
interval: 1000
onTriggered: {
root.roomChanging = false;
markReadIfVisibleTimer.reset();
RoomManager.activateUserModel();
}
}
onAtYEndChanged: if (!root.roomChanging) {
if (atYEnd && root.hasScrolledUpBefore) {
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
root.currentRoom.markAllMessagesAsRead();
}
root.hasScrolledUpBefore = false;
} else if (!atYEnd) {
root.hasScrolledUpBefore = true;
}
}
// Not rendered because the sections are part of the MessageDelegate.qml, this is only so that items have the section property available for use by sectionBanner.
// This is due to the fact that the ListView verticalLayout is BottomToTop.
// This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective).
// There is currently no option to put section headings at the bottom in qml.
section.property: "section"
function sectionBannerIndex() {
let center = messageListView.x + messageListView.width / 2;
let yStart = messageListView.y + messageListView.contentY;
let index = -1;
let i = 0;
while (index === -1 && i < 100) {
index = messageListView.indexAt(center, yStart + i);
i++;
}
return index;
}
footer: Item {
z: 3
width: root.width
visible: !NeoChatConfig.blur
SectionDelegate {
id: sectionDelegate
anchors.leftMargin: state === "alignLeft" ? Kirigami.Units.largeSpacing : 0
state: NeoChatConfig.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: sectionDelegate
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: sectionDelegate
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
width: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.width : 0
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
}
}
footerPositioning: ListView.OverlayHeader
delegate: EventDelegate {
room: root.currentRoom
}
KirigamiComponents.FloatingButton {
id: goReadMarkerFab
anchors {
right: parent.right
top: parent.top
topMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
}
implicitWidth: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2
padding: Kirigami.Units.largeSpacing
z: 2
visible: (!root.currentRoom?.partiallyReadStats.empty())
text: root.currentRoom.readMarkerLoaded ? i18n("Jump to first unread message") : i18n("Jump to oldest loaded message")
action: Kirigami.Action {
onTriggered: {
if (!Kirigami.Settings.isMobile) {
root.focusChatBar();
}
goReadMarkerFab.textChanged()
messageListView.goToEvent(root.currentRoom.lastFullyReadEventId);
}
icon.name: "go-up"
shortcut: "Shift+PgUp"
}
QQC2.ToolTip {
id: goReadMarkerFabTooltip
text: goReadMarkerFab.text
delay: Kirigami.Units.toolTipDelay
visible: goReadMarkerFab.hovered
}
}
KirigamiComponents.FloatingButton {
id: goMarkAsReadFab
anchors {
right: parent.right
bottom: parent.bottom
bottomMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
}
implicitWidth: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Settings.hasTransientTouchInput ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 2
padding: Kirigami.Units.largeSpacing
z: 2
visible: !messageListView.atYEnd
action: Kirigami.Action {
onTriggered: {
messageListView.goToLastMessage();
root.currentRoom.markAllMessagesAsRead();
}
icon.name: "go-down"
}
QQC2.ToolTip {
text: i18n("Jump to latest message")
}
}
Component.onCompleted: {
positionViewAtBeginning();
}
DropArea {
id: dropAreaFile
anchors.fill: parent
onDropped: root.currentRoom.mainCache.attachmentPath = drop.urls[0]
enabled: !Controller.isFlatpak
}
QQC2.Pane {
visible: dropAreaFile.containsDrag
anchors {
fill: parent
margins: Kirigami.Units.gridUnit
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
text: i18n("Drag items here to share them")
}
}
TypingPane {
id: typingPane
visible: root.currentRoom && root.currentRoom.otherMembersTyping.length > 0
labelText: visible ? i18ncp("Message displayed when some users are typing", "%2 is typing", "%2 are typing", root.currentRoom.otherMembersTyping.length, root.currentRoom.otherMembersTyping.map(member => member.displayName).join(", ")) : ""
anchors.left: parent.left
anchors.bottom: parent.bottom
height: visible ? implicitHeight : 0
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
z: 2
}
function goToEvent(eventID) {
const index = eventToIndex(eventID);
if (index == -1) {
messageListView.positionViewAtEnd();
return;
}
messageListView.positionViewAtIndex(index, ListView.Center);
itemAtIndex(index).isTemporaryHighlighted = true;
}
HoverActions {
id: hoverActions
currentRoom: root.currentRoom
onFocusChatBar: root.focusChatBar()
}
onContentYChanged: hoverActions.updatePosition()
Connections {
target: root.timelineModel
function onRowsInserted() {
markReadIfVisibleTimer.reset();
}
}
Timer {
id: markReadIfVisibleTimer
running: messageListView.allUnreadVisible() && applicationWindow().active && (root.currentRoom.timelineSize > 0 || root.currentRoom.allHistoryLoaded) && applicationWindow().pageStack.visibleItems.includes(root.page)
interval: 10000
onTriggered: root.currentRoom.markAllMessagesAsRead()
function reset() {
restart();
running = Qt.binding(function () {
return messageListView.allUnreadVisible() && applicationWindow().active && (root.currentRoom.timelineSize > 0 || root.currentRoom.allHistoryLoaded) && applicationWindow().pageStack.visibleItems.includes(root.page);
});
}
}
function goToLastMessage() {
root.currentRoom.markAllMessagesAsRead();
// scroll to the very end, i.e to messageListView.YEnd
messageListView.positionViewAtIndex(0, ListView.End);
}
function eventToIndex(eventID) {
const index = root.timelineModel.timelineMessageModel.eventIdToRow(eventID);
if (index === -1)
return -1;
return root.messageFilterModel.mapFromSource(root.timelineModel.index(index, 0)).row;
}
function firstVisibleIndex() {
let center = messageListView.x + messageListView.width / 2;
let index = -1;
let i = 0;
while (index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i);
i++;
}
return index;
}
function lastVisibleIndex() {
let center = messageListView.x + messageListView.width / 2;
let index = -1;
let i = 0;
while (index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i);
i++;
}
return index;
}
function allUnreadVisible() {
let readMarkerRow = eventToIndex(root.currentRoom.lastFullyReadEventId);
if (readMarkerRow >= 0 && readMarkerRow < firstVisibleIndex() && messageListView.atYEnd) {
return true;
}
return false;
}
function setHoverActionsToDelegate(delegate) {
hoverActions.delegate = delegate;
}
}
function goToLastMessage() {
messageListView.goToLastMessage();
}
function pageUp() {
const newContentY = messageListView.contentY - messageListView.height / 2;
const minContentY = messageListView.originY + messageListView.topMargin;
messageListView.contentY = Math.max(newContentY, minContentY);
messageListView.returnToBounds();
}
function pageDown() {
const newContentY = messageListView.contentY + messageListView.height / 2;
const maxContentY = messageListView.originY + messageListView.bottomMargin + messageListView.contentHeight - messageListView.height;
messageListView.contentY = Math.min(newContentY, maxContentY);
messageListView.returnToBounds();
}
function positionViewAtBeginning() {
messageListView.positionViewAtBeginning();
}
function goToEvent(eventId) {
messageListView.goToEvent(eventId);
}
}