diff --git a/framework/qml/Notifications.qml b/framework/qml/Notifications.qml index 1593f416..9e975a86 100644 --- a/framework/qml/Notifications.qml +++ b/framework/qml/Notifications.qml @@ -1,33 +1,34 @@ /* Copyright (C) 2017 Michael Bohlender, 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) any later version. 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ pragma Singleton import QtQuick 2.7 Item { property string error: "error" property string info: "info" property string progress: "progress" property string loginError: "loginError" property string hostNotFoundError: "hostNotFoundError" property string connectionError: "connectionError" property string transmissionError: "transmissionError" + property string messageSent: "messageSent" } diff --git a/framework/src/sinkfabric.cpp b/framework/src/sinkfabric.cpp index ea14b01c..5110c2af 100644 --- a/framework/src/sinkfabric.cpp +++ b/framework/src/sinkfabric.cpp @@ -1,272 +1,281 @@ /* Copyright (c) 2016 Christian Mollekopf 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) 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. */ #include "sinkfabric.h" #include #include #include #include #include #include #include "fabric.h" #include "keyring.h" using namespace Kube; using namespace Sink; using namespace Sink::ApplicationDomain; class SinkListener : public Kube::Fabric::Listener { public: SinkListener() = default; void notify(const QString &id, const QVariantMap &message) { SinkLog() << "Received message: " << id << message; if (id == "synchronize"/*Kube::Messages::synchronize*/) { if (auto folder = message["folder"].value()) { SinkLog() << "Synchronizing folder " << folder->resourceInstanceIdentifier() << folder->identifier(); auto scope = SyncScope().resourceFilter(folder->resourceInstanceIdentifier()).filter(QVariant::fromValue(folder->identifier())); scope.setType(); Store::synchronize(scope).exec(); } else if (message.contains("specialPurpose")) { auto specialPurpose = message["specialPurpose"].value(); //Synchronize all drafts folders if (specialPurpose == "drafts") { //TODO or rather just synchronize draft mails and have resource figure out what that means? Sink::Query folderQuery{}; folderQuery.containsFilter(Sink::ApplicationDomain::SpecialPurpose::Mail::drafts); folderQuery.request(); folderQuery.request(); Store::fetch(folderQuery) .then([] (const QList &folders) { for (const auto f : folders) { auto scope = SyncScope().resourceFilter(f->resourceInstanceIdentifier()).filter(QVariant::fromValue(f->identifier())); scope.setType(); Store::synchronize(scope).exec(); } }).exec(); } } else { const auto accountId = message["accountId"].value(); const auto type = message["type"].value(); SyncScope scope; if (!accountId.isEmpty()) { //FIXME this should work with either string or bytearray, but is apparently very picky scope.resourceFilter(accountId.toLatin1()); } scope.setType(type.toUtf8()); SinkLog() << "Synchronizing... AccountId: " << accountId << " Type: " << scope.type(); Store::synchronize(scope).exec(); } } if (id == "abortSynchronization"/*Kube::Messages::abortSynchronization*/) { const auto accountId = message["accountId"].value(); SyncScope scope; if (!accountId.isEmpty()) { //FIXME this should work with either string or bytearray, but is apparently very picky scope.resourceFilter(accountId.toLatin1()); } Store::abortSynchronization(scope).exec(); } if (id == "sendOutbox"/*Kube::Messages::synchronize*/) { Query query; query.containsFilter(ResourceCapabilities::Mail::transport); auto job = Store::fetchAll(query) .each([=](const SinkResource::Ptr &resource) -> KAsync::Job { return Store::synchronize(SyncScope{}.resourceFilter(resource->identifier())); }); job.exec(); } if (id == "markAsRead"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { mail->setUnread(false); Store::modify(*mail).exec(); } } if (id == "markAsUnread"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { mail->setUnread(true); Store::modify(*mail).exec(); } } if (id == "setImportant"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { mail->setImportant(message["important"].toBool()); Store::modify(*mail).exec(); } } if (id == "moveToTrash"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { mail->setTrash(true); Store::modify(*mail).exec(); } } if (id == "restoreFromTrash") { if (auto mail = message["mail"].value()) { mail->setTrash(false); Store::modify(*mail).exec(); } } if (id == "moveToDrafts"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { mail->setDraft(true); Store::modify(*mail).exec(); } } if (id == "moveToFolder"/*Kube::Messages::synchronize*/) { if (auto mail = message["mail"].value()) { auto folder = message["folder"].value(); mail->setFolder(*folder); Store::modify(*mail).exec(); } } if (id == "unlockKeyring") { auto accountId = message["accountId"].value(); Kube::AccountKeyring{accountId}.unlock(); } } }; class SinkNotifier { public: SinkNotifier() : mNotifier{Sink::Query{Sink::Query::LiveQuery}} { mNotifier.registerHandler([] (const Sink::Notification ¬ification) { Notification n; SinkLog() << "Received notification: " << notification; QVariantMap message; if (notification.type == Sink::Notification::Warning) { message["type"] = "warning"; QVariantList entities; for(const auto &entity : notification.entities) { entities << entity; } message["entities"] = entities; message["resource"] = QString{notification.resource}; if (notification.code == Sink::ApplicationDomain::TransmissionError) { message["message"] = QObject::tr("Failed to send message."); message["subtype"] = "transmissionError"; } else { return; } } else if (notification.type == Sink::Notification::Status) { return; } else if (notification.type == Sink::Notification::Error) { message["type"] = "error"; message["resource"] = QString{notification.resource}; message["details"] = notification.message; switch(notification.code) { case Sink::ApplicationDomain::ConnectionError: message["message"] = QObject::tr("Failed to connect to server."); message["subtype"] = "connectionError"; break; case Sink::ApplicationDomain::NoServerError: message["message"] = QObject::tr("Host not found."); message["subtype"] = "hostNotFoundError"; break; case Sink::ApplicationDomain::LoginError: message["message"] = QObject::tr("Failed to login."); message["subtype"] = "loginError"; break; case Sink::ApplicationDomain::ConfigurationError: message["message"] = QObject::tr("Configuration error."); break; case Sink::ApplicationDomain::ConnectionLostError: //Ignore connection lost errors. We don't need them in the log view. return; case Sink::ApplicationDomain::MissingCredentialsError: message["message"] = QObject::tr("No credentials available."); break; default: //Ignore unknown errors, they are not going to help. return; } Fabric::Fabric{}.postMessage("errorNotification", message); } else if (notification.type == Sink::Notification::Info) { if (notification.code == Sink::ApplicationDomain::TransmissionSuccess) { message["type"] = "info"; message["message"] = QObject::tr("A message has been sent."); + message["subtype"] = "messageSent"; + + QVariantList entities; + for(const auto &entity : notification.entities) { + entities << entity; + } + message["entities"] = entities; + + message["resource"] = QString{notification.resource}; } else if (notification.code == Sink::ApplicationDomain::NewContentAvailable) { message["type"] = "info"; if (!notification.entities.isEmpty()) { message["folderId"] = notification.entities.first(); } } else if (notification.code == Sink::ApplicationDomain::SyncInProgress) { message["type"] = "progress"; message["progress"] = 0; message["total"] = 1; if (!notification.entities.isEmpty()) { message["folderId"] = notification.entities.first(); } message["resourceId"] = notification.resource; Fabric::Fabric{}.postMessage("progressNotification", message); return; } else { return; } } else if (notification.type == Sink::Notification::Progress) { message["type"] = "progress"; message["progress"] = notification.progress; message["total"] = notification.total; if (!notification.entities.isEmpty()) { message["folderId"] = notification.entities.first(); } message["resourceId"] = notification.resource; Fabric::Fabric{}.postMessage("progressNotification", message); return; } else { return; } Fabric::Fabric{}.postMessage("notification", message); }); } Sink::Notifier mNotifier; }; class SinkFabric::Private { SinkNotifier notifier; SinkListener listener; }; SinkFabric::SinkFabric() : QObject(), d(new SinkFabric::Private) { } SinkFabric::~SinkFabric() { delete d; } SinkFabric &SinkFabric::instance() { static SinkFabric instance; return instance; } diff --git a/views/log/qml/View.qml b/views/log/qml/View.qml index bd5a3162..4cdd2a23 100644 --- a/views/log/qml/View.qml +++ b/views/log/qml/View.qml @@ -1,410 +1,468 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * 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) any later version. * * 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.3 as Controls1 import QtQuick.Controls 2 import org.kube.framework 1.0 as Kube Controls1.SplitView { id: root property bool pendingError: false; property bool pendingNotification: false; onPendingErrorChanged: { Kube.Fabric.postMessage(Kube.Messages.errorPending, {errorPending: pendingError}) } onPendingNotificationChanged: { Kube.Fabric.postMessage(Kube.Messages.notificationPending, {notificationPending: pendingNotification}) } StackView.onActivated: { root.pendingError = false; root.pendingNotification = false; //Always select the latest notification listView.currentIndex = 0 } Item { id: accountList width: parent.width/3 Layout.fillHeight: true Kube.Listener { filter: Kube.Messages.notification onMessageReceived: { //Ignore noise that we can't usefully render anyways if (!message.message) { return } //Avoid highlighting the iconbutton again if we're already looking at this view. if (root.StackView.status != StackView.Active) { if (message.type == Kube.Notifications.error) { root.pendingError = true } root.pendingNotification = true } logModel.insert(message) } } Kube.Label { anchors.centerIn: parent visible: listView.count == 0 text: qsTr("Nothing here...") } Kube.ListView { id: listView anchors { fill: parent } clip: true model: Kube.LogModel { id: logModel objectName: "logModel" onEntryAdded: { Kube.Fabric.postMessage(Kube.Messages.displayNotification, message) } } onCurrentItemChanged: { if (!!currentItem.currentData.resource) { details.resourceId = currentItem.currentData.resource } details.message = currentItem.currentData.message + "\n" + currentItem.currentData.details details.timestamp = currentItem.currentData.timestamp details.entities = currentItem.currentData.entities if (!!currentItem.currentData.subtype) { details.subtype = currentItem.currentData.subtype } else { details.subtype = "" } } delegate: Kube.ListDelegate { id: delegateRoot border.color: Kube.Colors.buttonColor border.width: 1 Kube.Label { id: description anchors { top: parent.top topMargin: Kube.Units.smallSpacing left: parent.left leftMargin: Kube.Units.largeSpacing } height: Kube.Units.gridUnit width: parent.width - Kube.Units.largeSpacing * 2 text: model.type == Kube.Notifications.error ? qsTr("Error") : qsTr("Info") } Kube.Label { id: message anchors { topMargin: Kube.Units.smallSpacing top: description.bottom left: parent.left leftMargin: Kube.Units.largeSpacing } height: Kube.Units.gridUnit width: parent.width - Kube.Units.largeSpacing * 2 maximumLineCount: 1 elide: Text.ElideRight color: delegateRoot.disabledTextColor text: model.message } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom rightMargin: Kube.Units.smallSpacing } text: Qt.formatDateTime(model.timestamp, " hh:mm:ss dd MMM yyyy") font.italic: true color: delegateRoot.disabledTextColor font.pointSize: Kube.Units.smallFontSize } } } } Item { id: details property string subtype: "" property date timestamp property string message: "" property string resourceId: "" property var entities: [] Kube.ModelIndexRetriever { id: retriever model: Kube.AccountsModel { resourceId: details.resourceId } } Loader { id: detailsLoader visible: message != "" clip: true anchors { fill: parent margins: Kube.Units.largeSpacing } property date timestamp: details.timestamp property string message: details.message property string resourceId: details.resourceId property string accountId: retriever.currentData ? retriever.currentData.accountId : "" property string accountName: retriever.currentData ? retriever.currentData.name : "" property string entityId: (details.entities && details.entities.length != 0) ? details.entities[0] : "" function getComponent(subtype) { if (subtype == Kube.Notifications.loginError) { return loginErrorComponent } if (subtype == Kube.Notifications.hostNotFoundError) { return hostNotFoundErrorComponent } if (subtype == Kube.Notifications.connectionError) { return hostNotFoundErrorComponent } if (subtype == Kube.Notifications.transmissionError) { return transmissionErrorComponent } + if (subtype == Kube.Notifications.messageSent) { + return transmissionSuccessComponent + } return detailsComponent } sourceComponent: getComponent(details.subtype) } } Component { id: detailsComponent Rectangle { color: Kube.Colors.viewBackgroundColor GridLayout { id: gridLayout Layout.minimumWidth: 0 anchors { top: parent.top left: parent.left right: parent.right } columns: 2 Kube.Label { text: qsTr("Account:") visible: accountName } Kube.Label { Layout.fillWidth: true text: accountName visible: accountName elide: Text.ElideRight } Kube.Label { text: qsTr("Account Id:") visible: accountId } Kube.Label { text: accountId visible: accountId Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Resource Id:") visible: resourceId } Kube.Label { text: resourceId visible: resourceId Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Timestamp:") } Kube.Label { text: Qt.formatDateTime(timestamp, " hh:mm:ss dd MMM yyyy") Layout.fillWidth: true elide: Text.ElideRight } Kube.Label { text: qsTr("Message:") Layout.alignment: Qt.AlignTop } Kube.Label { text: message Layout.fillWidth: true wrapMode: Text.Wrap } Item { Layout.columnSpan: 2 Layout.fillHeight: true Layout.fillWidth: true } } Kube.SelectableItem { layout: gridLayout } } } Component { id: loginErrorComponent Item { Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Column { Kube.Heading { id: heading text: qsTr("Failed to login") color: Kube.Colors.warningColor } Kube.Label { id: subHeadline text: qsTr("%1: please check your credentials.").arg(accountName) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } Kube.Button { text: qsTr("Change Password") onClicked: { Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) Kube.Fabric.postMessage(Kube.Messages.requestLogin, {accountId: accountId}) } } } } } Component { id: hostNotFoundErrorComponent Item { Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Column { Kube.Heading { id: heading text: qsTr("Host not found") color: Kube.Colors.warningColor } Kube.Label { id: subHeadline text: qsTr("%1: please check your network connection and settings.").arg(accountName) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } Kube.Button { text: qsTr("Account Settings") onClicked: { Kube.Fabric.postMessage(Kube.Messages.componentDone, {}) Kube.Fabric.postMessage(Kube.Messages.requestAccountsConfiguration, {}) } } } } } Component { id: transmissionErrorComponent Item { id: componentRoot Column { anchors { top: parent.top left: parent.left right: parent.right } spacing: Kube.Units.largeSpacing Kube.Heading { id: heading text: qsTr("Failed to send the message.") color: Kube.Colors.warningColor } Column { spacing: Kube.Units.largeSpacing Repeater { model: Kube.MailListModel { entityId: componentRoot.parent ? componentRoot.parent.entityId : "" } delegate: Column { id: subHeadline Kube.Label { text: qsTr("Account: %1").arg(accountName) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { text: qsTr("Subject: %1").arg(model.subject) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { text: qsTr("To: %1").arg(model.to) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } Kube.Label { visible: !!model.cc text: qsTr("Cc: %1").arg(model.cc) color: Kube.Colors.disabledTextColor wrapMode: Text.Wrap } } } } Kube.Button { text: qsTr("Try Again") onClicked: { Kube.Fabric.postMessage(Kube.Messages.sendOutbox, {}) } } } } } + Component { + id: transmissionSuccessComponent + Item { + id: componentRoot + Column { + anchors { + top: parent.top + left: parent.left + right: parent.right + } + spacing: Kube.Units.largeSpacing + + Kube.Heading { + id: heading + text: qsTr("Succeeded to send the message.") + } + + Column { + spacing: Kube.Units.largeSpacing + + Repeater { + model: Kube.MailListModel { + entityId: componentRoot.parent ? componentRoot.parent.entityId : "" + } + delegate: Column { + id: subHeadline + + Kube.Label { + text: qsTr("Account: %1").arg(accountName) + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + text: qsTr("Subject: %1").arg(model.subject) + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + text: qsTr("To: %1").arg(model.to) + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + visible: !!model.cc + text: qsTr("Cc: %1").arg(model.cc) + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + } + } + } + } + } + } + }