More qml files to go in timeline module
This commit is contained in:
@@ -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
|
||||
|
||||
372
src/timeline/DelegateContextMenu.qml
Normal file
372
src/timeline/DelegateContextMenu.qml
Normal 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();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
131
src/timeline/FileDelegateContextMenu.qml
Normal file
131
src/timeline/FileDelegateContextMenu.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/timeline/MessageDelegateContextMenu.qml
Normal file
145
src/timeline/MessageDelegateContextMenu.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
394
src/timeline/TimelineView.qml
Normal file
394
src/timeline/TimelineView.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user