diff --git a/src/apps/qml/qml/MainComponent.qml b/src/apps/qml/qml/MainComponent.qml index b68749af..0bf362e8 100644 --- a/src/apps/qml/qml/MainComponent.qml +++ b/src/apps/qml/qml/MainComponent.qml @@ -1,913 +1,909 @@ /* * Copyright 2016 Riccardo Iaconelli * Copyright (C) 2017-2020 Laurent Montel * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ import QtQuick 2.9 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.5 as QQC2 import org.kde.kirigami 2.7 as Kirigami import Ruqola 1.0 + +import "common" + Component { id: mainComponent Kirigami.Page { id: mainWidget title: appid.selectedRoom ? appid.selectedRoom.displayRoomName : "" leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 actions { left: Kirigami.Action { icon.name: "preferences-desktop-notification" icon.color: "transparent" tooltip: i18n("Configure Notification") visible: appid.selectedRoom onTriggered: { notificationsDialogLoader.active = true; } } main: Kirigami.Action { id: showUsersAction icon.name: "system-users" tooltip: i18n("List of Users") visible: appid.selectedRoom checkable: true } right: Kirigami.Action { icon.name: "edit-find" tooltip: i18n("Search Messages") visible: appid.selectedRoom onTriggered: { searchMessageDialogLoader.active = true; } } contextualActions: [ Kirigami.Action { visible: appid.selectedRoom text: i18n("Channel Info") tooltip: i18n("Channel Info") onTriggered: { var channelType = appid.selectedRoom.channelType; if (channelType === "c" || channelType === "p") { //Only for debug // if (channelType === "c") { // appid.rocketChatAccount.channelInfo(appid.selectedRoom.rid); // } else { // appid.rocketChatAccount.groupInfo(appid.selectedRoom.rid); // } //For testing channelInfoDialogLoader.active = true } else if (channelType === "d") { privateChannelInfoDialogLoader.active = true } else { console.log(RuqolaDebugCategorySingleton.category,"channel type " + appid.selectedRoom.channelType) } } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.autoTranslateEnabled text: i18n("Auto-Translate") onTriggered: { autoTranslateConfigDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom text: i18n("Mentions") onTriggered: { appid.rocketChatAccount.getListMessages(appid.selectedRoomID, ListMessagesModel.MentionsMessages); showListMessageDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.hasPinnedMessagesSupport text: i18n("Pinned Messages") onTriggered: { appid.rocketChatAccount.getListMessages(appid.selectedRoomID, ListMessagesModel.PinnedMessages); showListMessageDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.hasStarredMessagesSupport text: i18n("Starred Messages") onTriggered: { appid.rocketChatAccount.getListMessages(appid.selectedRoomID, ListMessagesModel.StarredMessages); showListMessageDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.hasSnippetedMessagesSupport text: i18n("Snippeted Messages") onTriggered: { appid.rocketChatAccount.getListMessages(appid.selectedRoomID, ListMessagesModel.SnipperedMessages); showListMessageDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.discussionEnabled text: i18n("Discussions") onTriggered: { appid.rocketChatAccount.discussionsInRoom(appid.selectedRoomID); showDiscussionsInRoomDialogLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom && appid.rocketChatAccount.threadsEnabled text: i18n("Threads") onTriggered: { appid.rocketChatAccount.threadsInRoom(appid.selectedRoomID); showThreadsInRoomDialogLoader.active = true } }, Kirigami.Action { id: menuVideoChatAction property bool shouldBeVisible: false visible: shouldBeVisible && appid.selectedRoom text: i18n("Video Chat") onTriggered: { appid.rocketChatAccount.createJitsiConfCall(appid.selectedRoomID); } }, Kirigami.Action { text: i18n("Add User In Room") visible: appid.selectedRoom ? appid.selectedRoom.canBeModify : false onTriggered: { var channelType = appid.selectedRoom.channelType; if (channelType === "c" || channelType === "p") { addUserDialogLoader.active = true } } }, Kirigami.Action { visible: appid.selectedRoom text: i18n("Take a Video Message") onTriggered: { takeVideoMessageLoader.active = true; } }, Kirigami.Action { visible: appid.selectedRoom text: i18n("Load Recent History") onTriggered: { appid.rocketChatAccount.loadHistory(appid.selectedRoomID); } }, Kirigami.Action { visible: appid.selectedRoom text: i18n("Show Files Attachment In Room") onTriggered: { showFilesInRoomDialogLoader.active = true } } ] } onContextualActionsAboutToShow: { menuVideoChatAction.shouldBeVisible = appid.rocketChatAccount.jitsiEnabled } globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar titleDelegate: RowLayout { QQC2.ToolButton { icon.name: "favorite" checkable: true visible: appid.selectedRoom && !appid.selectedRoom.isDiscussionRoom Accessible.onPressAction: onClicked Binding on checked { value: appid.selectedRoom && appid.selectedRoom.favorite } onToggled: { appid.rocketChatAccount.changeFavorite(appid.selectedRoomID, checked) } } QQC2.ToolButton { icon.name: "draw-arrow-back" Accessible.onPressAction: onClicked visible: appid.selectedRoom && appid.selectedRoom.isDiscussionRoom onClicked: { appid.switchToRoom(appid.selectedRoom.parentRid) } } Kirigami.Icon { source: "encrypted" //FIXME height: Kirigami.Units.iconSizes.medium width: height visible: appid.selectedRoom && appid.selectedRoom.encrypted } Kirigami.Icon { source: "preferences-desktop-locale" height: Kirigami.Units.iconSizes.medium width: height visible: appid.selectedRoom && appid.selectedRoom.autoTranslate MouseArea { hoverEnabled: true anchors.fill: parent QQC2.ToolTip { text: i18n("Auto-Translate Activated") } } } Kirigami.Heading { text: appid.selectedRoom ? appid.selectedRoom.displayRoomName : "" level: 3 font.bold: true } Item { Layout.fillWidth: true } } header: Column { spacing: Kirigami.Units.smallSpacing QQC2.Label { visible: appid.selectedRoom && (appid.selectedRoom.topic !== "") text: appid.selectedRoom ? appid.selectedRoom.topic : "" font.italic: true anchors.right: parent.right anchors.left: parent.left anchors.margins: 2*Kirigami.Units.smallSpacing wrapMode: QQC2.Label.Wrap textFormat: Qt.RichText onLinkActivated: { RuqolaUtils.openUrl(link); } } QQC2.Label { visible: appid.selectedRoom && (appid.selectedRoom.announcement !== "") text: appid.selectedRoom ? appid.selectedRoom.announcement : "" anchors.right: parent.right anchors.left: parent.left anchors.margins: 2*Kirigami.Units.smallSpacing wrapMode: QQC2.Label.Wrap onLinkActivated: { RuqolaUtils.openUrl(link); } textFormat: Qt.RichText } QQC2.Label { visible: appid.selectedRoom && (appid.selectedRoom.description !== "") text: appid.selectedRoom ? appid.selectedRoom.description : "" font.italic: true anchors.right: parent.right anchors.left: parent.left anchors.margins: 2*Kirigami.Units.smallSpacing wrapMode: QQC2.Label.Wrap textFormat: Qt.RichText onLinkActivated: { RuqolaUtils.openUrl(link); } } Kirigami.Separator { anchors.right: parent.right anchors.left: parent.left visible: appid.selectedRoom } Flow { id: topBarUserList readonly property bool isActive: showUsersAction.checked anchors { left: parent.left right: parent.right margins: Kirigami.Units.smallSpacing } opacity: topBarUserList.isActive ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 650; easing.type: Easing.InOutQuad } } Repeater { id: repeaterUser model: parent.opacity > 0.5 ? appid.userModel : 0 RowLayout { Kirigami.Icon { source: model.iconstatus //FIXME height: Kirigami.Units.iconSizes.small width: height } Loader { id: userMenuLoader active: false property var posX property var posY sourceComponent: UserMenu { id: userMenu x: userMenuLoader.posX y: userMenuLoader.posY userId: model.userid can_manage_users: appid.selectedRoom.canChangeRoles ownUserId: appid.rocketChatAccount.userID isAdirectChannel: appid.selectedRoom.channelType === "d" onKickUser: { appid.rocketChatAccount.kickUser(appid.selectedRoomID, userId, appid.selectedRoom.channelType) } onChangeRole: { appid.rocketChatAccount.changeRoles(appid.selectedRoomID, userId, appid.selectedRoom.channelType, type) } onIgnoreUser: { if (userId !== appid.rocketChatAccount.userID) { appid.rocketChatAccount.ignoreUser(appid.selectedRoomID, userId, ignore) } } onOpenConversation: { if (userId !== appid.rocketChatAccount.userID) { //console.log("userId " + userId) openDirectChannelDialog.username = model.username; openDirectChannelDialog.open() } } onAboutToShow: { hasLeaderRole = appid.selectedRoom.userHasLeaderRole(model.userid) hasModeratorRole = appid.selectedRoom.userHasModeratorRole(model.userid) hasOwnerRole = appid.selectedRoom.userHasOwnerRole(model.userid) } Component.onCompleted: { open() } onAboutToHide: { userMenuLoader.active = false } } } - QQC2.Label { + ClickableLabel { text: model.displayname onLinkActivated: { if (model.userid !== appid.rocketChatAccount.userID) { openDirectChannelDialog.username = link; openDirectChannelDialog.open() } } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - - onClicked: { - if (mouse.button === Qt.RightButton) { - userMenuLoader.posX = mouse.x - userMenuLoader.posY = mouse.y - if (userMenuLoader.active) - userMenuLoader.active = false - else - userMenuLoader.active = true - } - } + onContextMenuRequested: { + userMenuLoader.posX = mouse.x + userMenuLoader.posY = mouse.y + if (userMenuLoader.active) + userMenuLoader.active = false + else + userMenuLoader.active = true } } } } SearchLabel { width: parent.width hasFullList: appid.userModel ? appid.userModel.hasFullList : false numberOfElements: repeaterUser.count onLoadMoreElements: { appid.rocketChatAccount.loadMoreUsersInRoom(appid.selectedRoomID, appid.selectedRoom.channelType) } } Item { width: parent.width height: topBarUserList.isActive ? 1 : 0 Kirigami.Separator { height: parent.height width: height > 0 ? parent.width : 0 anchors.centerIn: parent Behavior on width { NumberAnimation { duration: 650; easing.type: Easing.InOutQuad } } } } } } Clipboard { id: clipboard } ActiveChat { id: activeChat model: appid.messageModel rcAccount: appid.rocketChatAccount roomId: appid.selectedRoomID anchors.fill: parent anchors.bottomMargin: Kirigami.Units.largeSpacing clip: true useMenuMessage: true onMoreHistoryRequested: { if (roomId !== "") { console.log("Fetching more history for room: " + roomId); rcAccount.loadHistory(roomId) } } onPinMessage: { appid.rocketChatAccount.pinMessage(messageId, pinned) } onEditMessage: { userInputMessage.messageId = messageId; userInputMessage.setOriginalMessage(messageStr) //console.log(RuqolaDebugCategorySingleton.category, "edit! messageId : " + messageId + " messageStr " + messageStr) } onCopyMessage: { clipboard.text = messageStr //console.log(RuqolaDebugCategorySingleton.category, "copy! messageId : " + messageId + " messageStr " + messageStr) } onReplyMessage: { console.log(RuqolaDebugCategorySingleton.category, "Not implemented reply message : " + messageId) userInputMessage.messageId = messageId; } onSetFavoriteMessage: { appid.rocketChatAccount.starMessage(messageId, starred) } onIgnoreUser: { appid.rocketChatAccount.ignoreUser(roomId, userId, ignored) } onCreateDiscussion: { createDiscussionDialogLoader.roomId = roomId createDiscussionDialogLoader.messageId = messageId createDiscussionDialogLoader.originalMessage = originalMessage createDiscussionDialogLoader.active = true //TODO add message text too } onOpenChannel: { openChannelDialogLoader.channelName = channel openChannelDialogLoader.active = true; } onOpenDirectChannel: { openDirectChannelDialog.username = userName; openDirectChannelDialog.open() } onJitsiCallConfActivated: { appid.rocketChatAccount.joinJitsiConfCall(roomId) } onReportMessage: { reportMessageDialogLoader.messageId = messageId reportMessageDialogLoader.active = true } onDeleteMessage: { deleteMessageDialogLoader.messageId = messageId deleteMessageDialogLoader.active = true } onDownloadAttachment: { downloadFileDialog.fileToSaveUrl = url downloadFileDialog.open() } onDisplayImage: { displayImageDialog.iUrl = imageUrl displayImageDialog.title = title displayImageDialog.isAnimatedImage = isAnimatedImage displayImageDialog.clearScaleAndOpen(); } onDeleteReaction: { appid.rocketChatAccount.reactOnMessage(messageId, emoji, false) } onAddReaction: { appid.rocketChatAccount.reactOnMessage(messageId, emoji, true) } onOpenThread: { appid.rocketChatAccount.getThreadMessages(threadMessageId) showThreadMessageDialogLoader.threadMessageId = threadMessageId; showThreadMessageDialogLoader.threadPreviewText = threadPreviewText showThreadMessageDialogLoader.active = true } onOpenDiscussion: { //console.log(RuqolaDebugCategorySingleton.category, "Open discussion " + discussionRoomId) appid.switchToRoom(discussionRoomId) } onReplyInThread: { //console.log(RuqolaDebugCategorySingleton.category,"reply in thread " + messageId) userInputMessage.threadmessageId = messageId; } onShowUserInfo: { showUserInfoDialogLoader.userId = userId showUserInfoDialogLoader.active = true; console.log("Open user info " + userId) } onShowOriginalOrTranslatedMessage: { console.log("Translate or not " + messageId + " translate " + showOriginal) //TODO } onShowDisplayAttachment: { console.log("Change display Attachment " + messageId + " show? " + displayAttachment) appid.rocketChatAccount.changeDisplayAttachment(appid.selectedRoomID, messageId, displayAttachment) } Loader { id: openChannelDialogLoader active: false property string channelName sourceComponent: OpenChannelDialog { id: openChannelDialog parent: appid.pageStack onOpenChannel: { appid.rocketChatAccount.openChannel(channelName); } onRejected: { reportMessageDialogLoader.active = false } onAccepted: { reportMessageDialogLoader.active = false } Component.onCompleted: { channelName = openChannelDialogLoader.channelName open() } } } Loader { id: showUserInfoDialogLoader active: false property string userId sourceComponent: UserInfoDialog { id: userInfoDialog parent: appid.pageStack onRejected: { showUserInfoDialogLoader.active = false } onAccepted: { showUserInfoDialogLoader.active = false } Component.onCompleted: { userInfo = appid.rocketChatAccount.userWrapper(showUserInfoDialogLoader.userId) //open() //REACTIVATE IT } } } OpenDirectChannelDialog { //TODO Port to loader id: openDirectChannelDialog parent: appid.pageStack onOpenDirectChannel: { if (appid.rocketChatAccount.userName !== userName) { appid.rocketChatAccount.openDirectChannel(userName); } } } Loader { id: createDiscussionDialogLoader active: false property string roomId property string messageId property string originalMessage sourceComponent: CreateDiscussionDialog { id: createDiscussionDialog parent: appid.pageStack onRejected: { createDiscussionDialogLoader.active = false } onAccepted: { createDiscussionDialogLoader.active = false } Component.onCompleted: { roomName = appid.selectedRoom.displayRoomName roomId = createDiscussionDialogLoader.roomId messageId = createDiscussionDialogLoader.messageId originalMessage = createDiscussionDialogLoader.originalMessage clearAndOpen() } onCreateNewDiscussion: { appid.rocketChatAccount.createDiscussion(parentRoomName, discussionTitle, replyMessage, msgId); } } } Loader { id: reportMessageDialogLoader active: false property string messageId sourceComponent: ReportMessageDialog { parent: appid.pageStack id: reportMessageDialog onReportMessage: { appid.rocketChatAccount.deleteMessage(messageId, message) } onRejected: { reportMessageDialogLoader.active = false } onAccepted: { reportMessageDialogLoader.active = false } Component.onCompleted: { msgId = reportMessageDialogLoader.messageId initializeAndOpen() } } } Loader { id: deleteMessageDialogLoader active: false property string messageId sourceComponent: DeleteMessageDialog { id: deleteMessageDialog parent: appid.pageStack onDeleteMessage: { appid.rocketChatAccount.deleteMessage(messageId, appid.selectedRoomID) } Component.onCompleted: { msgId = deleteMessageDialogLoader.messageId open() } onRejected: { deleteMessageDialogLoader.active = false } onAccepted: { deleteMessageDialogLoader.active = false } } } DownloadFileDialog { id: downloadFileDialog onAccepted: { if (fileUrl != "") { console.log(RuqolaDebugCategorySingleton.category, "You chose: " + fileUrl) appid.rocketChatAccount.downloadFile(fileToSaveUrl, fileUrl) } else { console.log(RuqolaDebugCategorySingleton.category, "No file selected"); } } } DisplayImageDialog { id: displayImageDialog parent: appid.pageStack } Loader { id: uploadFileDialogLoader active: false sourceComponent: UploadFileDialog { id: uploadFileDialog parent: appid.pageStack onUploadFile: { appid.rocketChatAccount.uploadFile(appid.selectedRoomID, description, messageText, filename) } Component.onCompleted: { initializeAndOpen() } onRejected: { uploadFileDialogLoader.active = false } onAccepted: { uploadFileDialogLoader.active = false } } } Loader { id: autoTranslateConfigDialogLoader active: false sourceComponent: AutoTranslateConfigDialog { id: autoTranslateConfigDialog parent: appid.pageStack roomInfo: appid.selectedRoom onRejected: { autoTranslateConfigDialogLoader.active = false } onAccepted: { autoTranslateConfigDialogLoader.active = false } Component.onCompleted: { open() } onChangeAutoTranslateSettings: { appid.rocketChatAccount.autoTranslateSaveAutoTranslateSettings(roomId, value) } onChangeAutoTranslateLanguageSettings: { appid.rocketChatAccount.autoTranslateSaveLanguageSettings(roomId, lang) } } } Loader { id: showDiscussionsInRoomDialogLoader active: false sourceComponent: ShowDiscussionsInRoomDialog { id: showDiscussionsInRoomDialog parent: appid.pageStack discussionsModel: appid.discussionsModel roomId: appid.selectedRoomID Component.onCompleted: { initializeAndOpen() } onOpenDiscussion: { appid.switchToRoom(discussionId) showDiscussionsInRoomDialogLoader.active = false } onRejected: { showDiscussionsInRoomDialogLoader.active = false } onAccepted: { showDiscussionsInRoomDialogLoader.active = false } } } Loader { id: showThreadsInRoomDialogLoader active: false sourceComponent: ShowThreadsInRoomDialog { id: showThreadsInRoomDialog parent: appid.pageStack threadsModel: appid.threadsModel roomId: appid.selectedRoomID onOpenThread: { appid.rocketChatAccount.getThreadMessages(threadMessageId) showThreadMessageDialogLoader.threadMessageId = threadMessageId showThreadMessageDialogLoader.threadPreviewText = threadPreviewText showThreadsInRoomDialogLoader.active = false showThreadMessageDialogLoader.active = true } onRejected: { showThreadsInRoomDialogLoader.active = false } onAccepted: { showThreadsInRoomDialogLoader.active = false } Component.onCompleted: { initializeAndOpen() } } } Loader { id: showFilesInRoomDialogLoader active: false sourceComponent: ShowFilesInRoomDialog { id: showFilesInRoomDialog parent: appid.pageStack filesModel: appid.filesModel roomId: appid.selectedRoomID channelType: appid.selectedRoom.channelType onDownloadFile: { downloadFileDialog.fileToSaveUrl = file downloadFileDialog.open() } Component.onCompleted: { appid.rocketChatAccount.roomFiles(appid.selectedRoomID, appid.selectedRoom.channelType); initializeAndOpen() } onDeleteFile: { appid.rocketChatAccount.deleteFileMessage(appid.selectedRoomID, fileid, appid.selectedRoom.channelType) } onRejected: { showFilesInRoomDialogLoader.active = false } onAccepted: { showFilesInRoomDialogLoader.active = false } } } Loader { id: showListMessageDialogLoader active: false sourceComponent: ListMessagesDialogBase { id: showListMessageDialog parent: appid.pageStack listMessagesModel: appid.listMessagesModel onAccepted: { showListMessageDialogLoader.active = false } onRejected: { showListMessageDialogLoader.active = false } Component.onCompleted: { open() } } } Loader { id: showThreadMessageDialogLoader active: false property string threadMessageId property string threadPreviewText sourceComponent: ShowThreadMessagesDialog { id: showThreadMessageDialog parent: appid.pageStack threadMessagesModel: appid.threadMessagesModel onAccepted: { showThreadMessageDialogLoader.active = false } onRejected: { showThreadMessageDialogLoader.active = false } Component.onCompleted: { showThreadMessageDialog.threadMessageId = showThreadMessageDialogLoader.threadMessageId; showThreadMessageDialog.threadPreviewText = showThreadMessageDialogLoader.threadPreviewText showThreadMessageDialog.open() } } } } Keys.onEscapePressed: { appid.rocketChatAccount.clearUnreadMessages(appid.selectedRoomID); } Keys.forwardTo: [activeChat] footer: QQC2.ToolBar { position: QQC2.ToolBar.Footer visible: appid.selectedRoom ColumnLayout { anchors.fill: parent QQC2.Label { id: channelInfo font.bold: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: appid.selectedRoom && appid.selectedRoom.roomMessageInfo.length > 0 text: appid.selectedRoom ? appid.selectedRoom.roomMessageInfo : "" } QQC2.Label { id: typingInfo Layout.leftMargin: Kirigami.Units.smallSpacing visible: text.length > 0 } UserInput { id: userInputMessage rcAccount: appid.rocketChatAccount inputCompleterModel: rcAccount.inputCompleterModel visible: appid.selectedRoom && (appid.selectedRoom.roomMessageInfo.length === 0) messageLineText: rcAccount.getUserCurrentMessage(appid.selectedRoomID) selectedRoomId: appid.selectedRoomID onTextEditing: { rcAccount.textEditing(appid.selectedRoomID, str.length == 0) appid.userInputMessageText = str; } onClearUnreadMessages: { rcAccount.clearUnreadMessages(appid.selectedRoomID) } onUploadFile: { uploadFileDialogLoader.active = true } } Connections { target: appid.rocketChatAccount.receiveTypingNotificationManager onNotificationChanged: { //console.log(RuqolaDebugCategorySingleton.category, "Typing in roomId: " + roomId + " str " + notificationStr); if (appid.selectedRoomID === roomId) { var wasAtEnd = activeChat.atYEnd; typingInfo.text = notificationStr; if (wasAtEnd) { activeChat.positionViewAtEnd(); } } } onClearNotification: { typingInfo.text = ""; } } } } }// mainWidget Item } diff --git a/src/apps/qml/qml/common/ClickableLabel.qml b/src/apps/qml/qml/common/ClickableLabel.qml new file mode 100644 index 00000000..1f4694b1 --- /dev/null +++ b/src/apps/qml/qml/common/ClickableLabel.qml @@ -0,0 +1,54 @@ +/* + Copyright (c) 2020 Kevin Funk + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2 of the License or + ( at your option ) version 3 or, at the discretion of KDE e.V. + ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.5 as QQC2 + +/** + * A rich text label that is suitable for use on Desktop + * + * Features: + * - Modifies the cursor shape when a link in the label is hovered + * - Handles right mouse button pressed and emits a contextMenuRequested signal + */ +QQC2.Label { + id: root + + property alias mouseArea: mouseArea + + signal contextMenuRequested(var mouse) + + textFormat: Text.RichText + + MouseArea { + id: mouseArea + + anchors.fill: parent + + acceptedButtons: Qt.RightButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: { + if (mouse.button === Qt.RightButton) { + root.contextMenuRequested(mouse) + } + } + } +} diff --git a/src/apps/qml/qml/messages/AttachmentMessageImage.qml b/src/apps/qml/qml/messages/AttachmentMessageImage.qml index 6fc54b41..b48821f4 100644 --- a/src/apps/qml/qml/messages/AttachmentMessageImage.qml +++ b/src/apps/qml/qml/messages/AttachmentMessageImage.qml @@ -1,176 +1,168 @@ /* Copyright (c) 2017-2020 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.9 import QtQuick.Controls 2.5 as QQC2 import org.kde.kirigami 2.7 as Kirigami import Ruqola 1.0 import QtQuick.Layouts 1.12 import "../common" UserMessage { id: root attachments: Repeater { id: repearterAttachments model: i_attachments Row { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing Column { - QQC2.Label { + ClickableLabel { id: imageTitle text: model.modelData.title === "" ? "" : model.modelData.imageTitle visible: model.modelData.title !== "" wrapMode: QQC2.Label.NoWrap anchors.leftMargin: Kirigami.Units.smallSpacing anchors.rightMargin: Kirigami.Units.smallSpacing - textFormat: Text.RichText onLinkActivated: { messageMain.displayImage(imageUrl.source, model.modelData.title, model.modelData.isAnimatedImage) } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - - onClicked: { - if (mouse.button === Qt.RightButton) { - messageMenuLoader.posX = mouse.x - messageMenuLoader.posY = mouse.y - if (messageMenuLoader.active) - messageMenuLoader.active = false - else - messageMenuLoader.active = true - } - } + onContextMenuRequested: { + messageMenuLoader.posX = mouse.x + messageMenuLoader.posY = mouse.y + if (messageMenuLoader.active) + messageMenuLoader.active = false + else + messageMenuLoader.active = true } } Image { id: imageUrl visible: model.modelData.isAnimatedImage readonly property int imageHeight: model.modelData.imageHeight === -1 ? 200 : Math.min(200, model.modelData.imageHeight) source: rcAccount.attachmentUrl(model.modelData.link) asynchronous: true fillMode: Image.PreserveAspectFit //Don't use really imageWidth otherwise it will be too big //width: 200 //model.modelData.imageWidth === -1 ? 200 : model.modelData.imageWidth height: 0 sourceSize.width: 200 sourceSize.height: 200 onStatusChanged: { if(status == Image.Error){ console.log(RuqolaDebugCategorySingleton.category, "Image load error! Trying to reload. " + source) } } MouseArea { anchors.fill: parent onClicked: { if(status === Image.Error) { console.log(RuqolaDebugCategorySingleton.category, "Image not loaded."); } else { messageMain.displayImage(imageUrl.source, imageTitle.text, model.modelData.isAnimatedImage) } } } } AnimatedImage { id: imageAnimatedUrl visible: model.modelData.isAnimatedImage readonly property int imageHeight: model.modelData.imageHeight === -1 ? 200 : Math.min(200, model.modelData.imageHeight) source: rcAccount.attachmentUrl(model.modelData.link) asynchronous: true fillMode: Image.PreserveAspectFit //Don't use really imageWidth otherwise it will be too big //width: 200 //model.modelData.imageWidth === -1 ? 200 : model.modelData.imageWidth height: 0 onStatusChanged: { if(status == Image.Error){ console.log(RuqolaDebugCategorySingleton.category, "Image load error! Trying to reload. " + source) } } onHeightChanged: { playing = height > 0; } MouseArea { anchors.fill: parent onClicked: { if(status === Image.Error) { console.log(RuqolaDebugCategorySingleton.category, "Image not loaded."); } else { messageMain.displayImage(imageAnimatedUrl.source, imageTitle.text, model.modelData.isAnimatedImage) } } } } QQC2.Label { text: model.modelData.description wrapMode: QQC2.Label.Wrap anchors.leftMargin: Kirigami.Units.smallSpacing anchors.rightMargin: Kirigami.Units.smallSpacing visible: model.modelData.description !== "" MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { if (mouse.button === Qt.RightButton) { menu.x = mouse.x menu.y = mouse.y menu.open(); } } } } } ShowHideButton { targetAnimation: model.modelData.isAnimatedImage ? imageAnimatedUrl : imageUrl defaultHeight: model.modelData.isAnimatedImage ? imageAnimatedUrl.imageHeight : Url.imageHeight onHiddenChanged: { messageMain.showDisplayAttachment(i_messageID, state) } } DownloadButton { id: download onDownloadButtonClicked: { messageMain.downloadAttachment(model.modelData.link) } } Connections { target: rcAccount onFileDownloaded: { //console.log(RuqolaDebugCategorySingleton.category, " IMAGE SUPPORT: " + filePath + " cacheImageUrl :" + cacheImageUrl + " model.modelData.link: " + model.modelData.link) if (filePath === model.modelData.link) { console.log(RuqolaDebugCategorySingleton.category, "Image updated: " + cacheImageUrl) imageUrl.source = cacheImageUrl; imageAnimatedUrl.source = cacheImageUrl; } } } } } } diff --git a/src/apps/qml/qml/messages/UserMessage.qml b/src/apps/qml/qml/messages/UserMessage.qml index 3b101b79..2e172858 100644 --- a/src/apps/qml/qml/messages/UserMessage.qml +++ b/src/apps/qml/qml/messages/UserMessage.qml @@ -1,307 +1,298 @@ /* * Copyright 2016 Riccardo Iaconelli * Copyright (c) 2017-2020 Laurent Montel * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ import QtQuick 2.9 import org.kde.kirigami 2.7 as Kirigami import QtQuick.Controls 2.5 as QQC2 import QtQuick.Layouts 1.12 import Ruqola 1.0 import "../common" MessageBase { id: root property alias attachments: attachmentsLayout.children implicitHeight: mainLayout.height Loader { id: messageMenuLoader active: false property var posX property var posY sourceComponent: MessageMenu { id: menu x: messageMenuLoader.posX y: messageMenuLoader.posY can_edit_message: i_can_edit_message user_ignored : i_user_ignored starred: i_starred pinned_message: i_pinned showTranslatedMessage: i_showTranslatedMessage Component.onCompleted: { open() } onAboutToHide: { messageMenuLoader.active = false; } } } ColumnLayout { id: mainLayout width: parent.width spacing: 0 Item { id: topSpacer width: parent.width height: Kirigami.Units.smallSpacing } RowLayout { AvatarImage { id: avatarRect Layout.alignment: Qt.AlignTop avatarurl: i_avatar aliasname: i_aliasname username: i_username onShowUserInfo: { messageMain.showUserInfo(i_own_username) } visible: !i_groupable } ColumnLayout { spacing: Kirigami.Units.smallSpacing / 2 // reduce spacing a little Layout.alignment: Qt.AlignTop GridLayout { rowSpacing: 0 columnSpacing: Kirigami.Units.smallSpacing columns: compactViewMode ? -1 : 1 // user name label + roles info in one row RowLayout { Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.rightMargin: Kirigami.Units.smallSpacing QQC2.Label { id: usernameLabel font.bold: true text: i_aliasname !== "" ? i_aliasname + ' @' + i_username : '@' + i_username MouseArea { anchors.fill: parent enabled: i_username !== appid.rocketChatAccount.userName hoverEnabled: true cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.RightButton) { if (i_useMenuMessage) { messageMenuLoader.posX = mouse.x messageMenuLoader.posY = mouse.y if (messageMenuLoader.active) messageMenuLoader.active = false else messageMenuLoader.active = true } } else { messageMain.linkActivated("ruqola:/user/" + i_username) } } } visible: !i_groupable } Kirigami.Icon { id: rolesInfo source: "documentinfo" width: height height: 18 visible: i_roles.length > 0 opacity: rolesInfoMA.containsMouse ? 1.0 : 0.6 MouseArea { id: rolesInfoMA hoverEnabled: true anchors.fill: parent } QQC2.ToolTip.visible: rolesInfoMA.containsMouse QQC2.ToolTip.text: i_roles } Kirigami.Icon { id: editedInfo source: "document-edit" width: height height: 18 visible: i_editedByUserName !== "" opacity: editedInfoMA.containsMouse ? 1.0 : 0.6 MouseArea { id: editedInfoMA hoverEnabled: true anchors.fill: parent } QQC2.ToolTip.visible: editedInfoMA.containsMouse QQC2.ToolTip.text: visible ? i18n("Edited by %1", i_editedByUserName) : "" } } QQC2.Label { id: threadPreview // TODO: I think the whole thread preview item needs to be visually redesigned... /// no eliding possible with rich text, cf. QTBUG-16567, fake it /// not ideal, see: https://stackoverflow.com/a/29923358 function elidedText(s, length) { var elidedText = s.substring(0, length) if (s.length > length) elidedText += "..." return elidedText } Layout.fillWidth: !compactViewMode Layout.alignment: Qt.AlignLeft | Qt.AlignTop visible: i_threadPreview.length > 0 textFormat: Text.RichText color: "red" //Convert to kirigami color font.pointSize: textLabel.font.pointSize - 1 text: compactViewMode ? elidedText(i_threadPreview, 30) : i_threadPreview wrapMode: compactViewMode ? Text.NoWrap : Text.Wrap MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton | Qt.LeftButton onClicked: { //console.log("open thread " + i_tmid) messageMain.openThread(i_tmid, i_threadPreview) } } } - QQC2.Label { + + ClickableLabel { id: textLabel Layout.fillWidth: true - textFormat: Text.RichText - - text: i_messageText wrapMode: QQC2.Label.Wrap onLinkActivated: messageMain.linkActivated(link) - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - - onClicked: { - if (i_useMenuMessage) { - if (mouse.button === Qt.RightButton) { - messageMenuLoader.posX = mouse.x - messageMenuLoader.posY = mouse.y - if (messageMenuLoader.active) - messageMenuLoader.active = false - else - messageMenuLoader.active = true - } - } + onContextMenuRequested: { + if (i_useMenuMessage) { + messageMenuLoader.posX = mouse.x + messageMenuLoader.posY = mouse.y + if (messageMenuLoader.active) + messageMenuLoader.active = false + else + messageMenuLoader.active = true } } } ColumnLayout { id: urlColumn Layout.fillWidth: true //TODO //Reactivate when we have a parsed url ! //see info about bugs // Repeater { // id: repeaterUrl // model: i_urls // Text { // //Display it only if url != text otherwise it's not necessary // visible: model.modelData.url !== i_originalMessage // width: urlColumn.width // text: model.modelData.description === "" ? // RuqolaUtils.markdownToRichText(model.modelData.url) : // RuqolaUtils.markdownToRichText(model.modelData.description) // wrapMode: QQC2.Label.Wrap // textFormat: Text.RichText // onLinkActivated: messageMain.linkActivated(link) // } // } RowLayout { Layout.fillWidth: true RepeaterReactions { id: repearterReactions model: i_reactions onAddReaction: { messageMain.addReaction(i_messageID, emoji) } onDeleteReaction: { messageMain.deleteReaction(i_messageID, emoji) } } } } } ColumnLayout { id: attachmentsLayout Layout.fillWidth: true } ThreadLabel { Layout.fillWidth: true onOpenThread: { console.log(RuqolaDebugCategorySingleton.category, " OPen thread " + i_messageID) messageMain.openThread(i_messageID, i_messageText) } } } ReactionsPopup { Layout.alignment: Qt.AlignTop visible: i_useMenuMessage showIcon: root.hovered onInsertReaction: { messageMain.addReaction(i_messageID, emoji) } } TimestampText { id: timestampText Layout.alignment: Qt.AlignTop timestamp: i_timestamp visible: !i_groupable } } Item { id: bottomSpacer width: parent.width height: Kirigami.Units.smallSpacing } } } diff --git a/src/apps/qml/qml/qml.qrc b/src/apps/qml/qml/qml.qrc index 09cad5d2..c2dc4c47 100644 --- a/src/apps/qml/qml/qml.qrc +++ b/src/apps/qml/qml/qml.qrc @@ -1,95 +1,96 @@ Desktop.qml FancyMessageDelegate.qml RoomsView.qml RoomDelegate.qml Login.qml UserInput.qml LoginPage.qml ExtraColors.qml ActiveChat.qml PersonsListView.qml CreateNewChannelDialog.qml OpenDirectChannelDialog.qml ChannelInfoDialog.qml DeleteMessageDialog.qml AddUserDialog.qml DeleteRoomDialog.qml TakeVideoMessageDialog.qml DebugCategory.qml ConfigureServerList.qml MainComponent.qml RoomsComponent.qml ArchiveRoomDialog.qml DownloadFileDialog.qml UploadFileDialog.qml DisplayImageDialog.qml EncryptedConversationDialog.qml SearchChannelDialog.qml ShowFilesInRoomDialog.qml MessageLine.qml ShowSearchMessageDialog.qml DeleteAccountDialog.qml OpenChannelDialog.qml PrivateChannelInfoDialog.qml PasswordLineEdit.qml NotificationOptionsDialog.qml LeaveChannelDialog.qml DeleteFileAttachmentDialog.qml UserInfoDialog.qml CreateNewAccountDialog.qml ChannelPasswordDialog.qml ServerInfoDialog.qml NotificationAlertCombobox.qml UserMenu.qml JobErrorMessageDialog.qml ReportMessageDialog.qml CreateDiscussionDialog.qml ShowDiscussionsInRoomDialog.qml ShowThreadsInRoomDialog.qml ShowThreadMessagesDialog.qml DiscussionMessageDelegate.qml DiscussionMessage.qml FilesInRoomDelegate.qml FileInRoom.qml SearchLabel.qml AutoTranslateLanguageCombobox.qml AutoTranslateConfigDialog.qml CustomUserStatusDialog.qml ListMessagesDialogBase.qml messages/SystemMessage.qml messages/UserMessage.qml messages/AttachmentMessageAudio.qml messages/AttachmentMessageFile.qml messages/AttachmentMessageImage.qml messages/AttachmentMessageVideo.qml messages/JitsiVideoMessage.qml messages/NewDateLabel.qml messages/MessageBase.qml messages/MessageMenu.qml messages/TimestampText.qml messages/ShowHideButton.qml messages/RepeaterReactions.qml messages/DiscussionLabel.qml messages/ThreadLabel.qml messages/ReactionsPopup.qml common/DownloadButton.qml common/AvatarImage.qml common/DeleteButton.qml common/EmoticonMenu.qml common/StatusCombobox.qml + common/ClickableLabel.qml icons/systray.png icons/attach-button.jpg js/message.js js/convert.js