diff --git a/framework/src/domain/maillistmodel.cpp b/framework/src/domain/maillistmodel.cpp index e2f52d13..143c8a60 100644 --- a/framework/src/domain/maillistmodel.cpp +++ b/framework/src/domain/maillistmodel.cpp @@ -1,402 +1,435 @@ /* Copyright (c) 2016 Michael Bohlender 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 "maillistmodel.h" #include MailListModel::MailListModel(QObject *parent) : QSortFilterProxyModel(parent) { setDynamicSortFilter(true); sort(0, Qt::DescendingOrder); setFilterCaseSensitivity(Qt::CaseInsensitive); } MailListModel::~MailListModel() { } void MailListModel::setFilter(const QString &filter) { if (filter.length() < 3 && !filter.isEmpty()) { return; } auto oldQuery = mQuery; auto query = mQuery; if (!filter.isEmpty()) { auto f = filter; if (mCurrentQueryItem.isEmpty()) { using namespace Sink::ApplicationDomain; query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); } query.filter({}, Sink::QueryBase::Comparator(f, Sink::QueryBase::Comparator::Fulltext)); } runQuery(query); mQuery = oldQuery; } QString MailListModel::filter() const { return {}; } QHash< int, QByteArray > MailListModel::roleNames() const { QHash roles; roles[Subject] = "subject"; roles[Sender] = "sender"; roles[SenderName] = "senderName"; roles[To] = "to"; roles[Cc] = "cc"; roles[Bcc] = "bcc"; roles[Date] = "date"; roles[Unread] = "unread"; roles[Important] = "important"; roles[Draft] = "draft"; roles[Sent] = "sent"; roles[Trash] = "trash"; roles[Id] = "id"; roles[MimeMessage] = "mimeMessage"; roles[DomainObject] = "domainObject"; roles[ThreadSize] = "threadSize"; roles[Mail] = "mail"; roles[Incomplete] = "incomplete"; roles[Status] = "status"; return roles; } static QString join(const QList &contacts) { QStringList list; for (const auto &contact : contacts) { if (!contact.name.isEmpty()) { list << QString("%1 <%2>").arg(contact.name).arg(contact.emailAddress); } else { list << contact.emailAddress; } } return list.join(", "); } void MailListModel::fetchMail(Sink::ApplicationDomain::Mail::Ptr mail) { if (mail && !mail->getFullPayloadAvailable() && !mFetchedMails.contains(mail->identifier())) { qDebug() << "Fetching mail: " << mail->identifier() << mail->getSubject(); mFetchedMails.insert(mail->identifier()); Sink::Store::synchronize(Sink::SyncScope{*mail}).exec(); } } QVariant MailListModel::data(const QModelIndex &idx, int role) const { auto srcIdx = mapToSource(idx); auto mail = srcIdx.data(Sink::Store::DomainObjectRole).value(); switch (role) { case Subject: return mail->getSubject(); case Sender: return mail->getSender().emailAddress; case SenderName: return mail->getSender().name; case To: return join(mail->getTo()); case Cc: return join(mail->getCc()); case Bcc: return join(mail->getBcc()); case Date: return mail->getDate(); case Unread: if (mail->isAggregate()) { return mail->getCollectedProperty().contains(true); } else { return mail->getUnread(); } case Important: if (mail->isAggregate()) { return mail->getCollectedProperty().contains(true); } else { return mail->getImportant(); } case Draft: return mail->getDraft(); case Sent: return mail->getSent(); case Trash: return mail->getTrash(); case Id: return mail->identifier(); case DomainObject: return QVariant::fromValue(mail); case MimeMessage: if (mFetchMails) { const_cast(this)->fetchMail(mail); } return mail->getMimeMessage(); case ThreadSize: return mail->count(); case Mail: return QVariant::fromValue(mail); case Incomplete: return !mail->getFullPayloadAvailable(); case Status: const auto status = srcIdx.data(Sink::Store::StatusRole).toInt(); if (status == Sink::ApplicationDomain::SyncStatus::SyncInProgress) { return InProgressStatus; } if (status == Sink::ApplicationDomain::SyncStatus::SyncError) { return ErrorStatus; } return NoStatus; } return QSortFilterProxyModel::data(idx, role); } bool MailListModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto leftDate = left.data(Sink::Store::DomainObjectRole).value()->getDate(); const auto rightDate = right.data(Sink::Store::DomainObjectRole).value()->getDate(); if (leftDate == rightDate) { return left.data(Sink::Store::DomainObjectRole).value()->identifier() < right.data(Sink::Store::DomainObjectRole).value()->identifier(); } return leftDate < rightDate; } bool MailListModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { auto idx = sourceModel()->index(sourceRow, 0, sourceParent); auto regExp = filterRegExp(); if (regExp.isEmpty()) { return true; } auto mail = idx.data(Sink::Store::DomainObjectRole).value(); return mail->getSubject().contains(regExp) || mail->getSender().name.contains(regExp); } void MailListModel::runQuery(const Sink::Query &query) { if (query.getBaseFilters().isEmpty() && query.ids().isEmpty()) { mQuery = {}; m_model.clear(); setSourceModel(nullptr); } else { mQuery = query; m_model = Sink::Store::loadModel(query); setSourceModel(m_model.data()); } } void MailListModel::setParentFolder(const QVariant &parentFolder) { using namespace Sink::ApplicationDomain; auto folder = parentFolder.value(); if (!folder) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == folder->identifier()) { return; } mCurrentQueryItem = folder->identifier(); bool isThreaded = true; if (folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::drafts) || folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::sent)) { isThreaded = false; } Sink::Query query = [&] { if (isThreaded) { return Sink::StandardQueries::threadLeaders(*folder); } else { Sink::Query query; query.setId("threadleaders-unthreaded"); if (!folder->resourceInstanceIdentifier().isEmpty()) { query.resourceFilter(folder->resourceInstanceIdentifier()); } query.filter(*folder); query.sort(); return query; } }(); if (!folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash)) { //Filter trash if this is not a trash folder query.filter(false); } query.setFlags(Sink::Query::LiveQuery); query.limit(100); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); mFetchMails = false; qDebug() << "Running folder query: " << folder->resourceInstanceIdentifier() << folder->identifier(); //Latest mail on top sort(0, Qt::DescendingOrder); runQuery(query); } QVariant MailListModel::parentFolder() const { return QVariant(); } void MailListModel::setMail(const QVariant &variant) { using namespace Sink::ApplicationDomain; auto mail = variant.value(); if (!mail) { mCurrentQueryItem.clear(); setSourceModel(nullptr); return; } if (mCurrentQueryItem == mail->identifier()) { return; } mCurrentQueryItem = mail->identifier(); Sink::Query query = Sink::StandardQueries::completeThread(*mail); query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query: " << mail->resourceInstanceIdentifier() << mail->identifier(); //Latest mail at the bottom sort(0, Qt::AscendingOrder); runQuery(query); } QVariant MailListModel::mail() const { return QVariant(); } void MailListModel::setShowDrafts(bool) { using namespace Sink::ApplicationDomain; Sink::Query query; query.setFlags(Sink::Query::LiveQuery); query.filter(true); query.filter(false); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query for drafts: "; //Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } bool MailListModel::showDrafts() const { return false; } void MailListModel::setShowInbox(bool) { using namespace Sink::ApplicationDomain; Sink::Query folderQuery{}; folderQuery.containsFilter(Sink::ApplicationDomain::SpecialPurpose::Mail::inbox); folderQuery.request(); folderQuery.request(); Sink::Query query; query.setFlags(Sink::Query::LiveQuery); query.filter(folderQuery); query.sort(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); query.request(); mFetchMails = true; mFetchedMails.clear(); qDebug() << "Running mail query for drafts: "; //Latest mail at the top sort(0, Qt::DescendingOrder); runQuery(query); } bool MailListModel::showInbox() const { return false; } + +void MailListModel::setEntityId(const QString &id) +{ + qDebug() << "Running mail query for mail with ID:" << id; + using namespace Sink::ApplicationDomain; + Sink::Query query; + query.setFlags(Sink::Query::LiveQuery); + query.filter(id.toUtf8()); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + query.request(); + mFetchMails = true; + mFetchedMails.clear(); + // Latest mail at the top + sort(0, Qt::DescendingOrder); + runQuery(query); +} + +QString MailListModel::entityId() const +{ + return {}; +} diff --git a/framework/src/domain/maillistmodel.h b/framework/src/domain/maillistmodel.h index f83656b9..a6965915 100644 --- a/framework/src/domain/maillistmodel.h +++ b/framework/src/domain/maillistmodel.h @@ -1,104 +1,108 @@ /* Copyright (c) 2016 Michael Bohlender 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. */ #pragma once #include #include #include #include class MailListModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY (QVariant parentFolder READ parentFolder WRITE setParentFolder) Q_PROPERTY (QVariant mail READ mail WRITE setMail) Q_PROPERTY (bool showDrafts READ showDrafts WRITE setShowDrafts) Q_PROPERTY (bool showInbox READ showInbox WRITE setShowInbox) + Q_PROPERTY (QString entityId READ entityId WRITE setEntityId) Q_PROPERTY (QString filter READ filter WRITE setFilter) public: enum Status { NoStatus, InProgressStatus, ErrorStatus }; Q_ENUMS(Status) MailListModel(QObject *parent = Q_NULLPTR); ~MailListModel(); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE; enum Roles { Subject = Qt::UserRole + 1, Sender, SenderName, To, Cc, Bcc, Date, Unread, Important, Draft, Sent, Trash, Id, MimeMessage, DomainObject, ThreadSize, Mail, Incomplete, Status }; QHash roleNames() const Q_DECL_OVERRIDE; void runQuery(const Sink::Query &query); void setParentFolder(const QVariant &parentFolder); QVariant parentFolder() const; void setMail(const QVariant &mail); QVariant mail() const; void setFilter(const QString &mail); QString filter() const; void setShowDrafts(bool); bool showDrafts() const; void setShowInbox(bool); bool showInbox() const; + void setEntityId(const QString &id); + QString entityId() const; + private: void fetchMail(Sink::ApplicationDomain::Mail::Ptr mail); QSharedPointer m_model; bool mFetchMails = false; QSet mFetchedMails; QByteArray mCurrentQueryItem; Sink::Query mQuery; }; diff --git a/framework/src/sinkfabric.cpp b/framework/src/sinkfabric.cpp index 7d780ce3..8492f272 100644 --- a/framework/src/sinkfabric.cpp +++ b/framework/src/sinkfabric.cpp @@ -1,244 +1,251 @@ /* 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 { auto accountId = message["accountId"].value(); 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()); } if (type == "contacts") { scope.setType(); } else if (type == "mail") { scope.setType(); } else if (type == "folder") { scope.setType(); } else { //Only synchronize folders by default for now scope.setType(); } SinkLog() << "Synchronizing... AccountId: " << accountId << " Type: " << scope.type(); Store::synchronize(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 == "toggleImportant"/*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 == "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: message["message"] = QObject::tr("Connection lost."); break; 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."); } 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/tests/teststore.cpp b/tests/teststore.cpp index e5a3ea88..9d56dd33 100644 --- a/tests/teststore.cpp +++ b/tests/teststore.cpp @@ -1,214 +1,229 @@ /* Copyright (c) 2018 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 "teststore.h" #include #include #include #include #include #include #include "framework/src/domain/mime/mailtemplates.h" using namespace Kube; static void iterateOverObjects(const QVariantList &list, std::function callback) { for (const auto &entry : list) { auto object = entry.toMap(); callback(object); } } static QStringList toStringList(const QVariantList &list) { QStringList s; for (const auto &e : list) { s << e.toString(); } return s; } static QByteArrayList toByteArrayList(const QVariantList &list) { QByteArrayList s; for (const auto &e : list) { s << e.toByteArray(); } return s; } static void createMail(const QVariantMap &object, const QByteArray &folder = {}) { using namespace Sink::ApplicationDomain; auto toAddresses = toStringList(object["to"].toList()); auto ccAddresses = toStringList(object["cc"].toList()); auto bccAddresses = toStringList(object["bcc"].toList()); KMime::Types::Mailbox mb; mb.fromUnicodeString("identity@example.org"); auto msg = MailTemplates::createMessage({}, toAddresses, ccAddresses, bccAddresses, mb, object["subject"].toString(), object["body"].toString(), object["bodyIsHtml"].toBool(), {}, {}, {}); if (object.contains("messageId")) { msg->messageID(true)->from7BitString(object["messageId"].toByteArray()); } if (object.contains("inReplyTo")) { msg->inReplyTo(true)->from7BitString(object["inReplyTo"].toByteArray()); } if (object.contains("date")) { msg->date(true)->setDateTime(QDateTime::fromString(object["date"].toString(), Qt::ISODate)); } msg->assemble(); auto mail = ApplicationDomainType::createEntity(object["resource"].toByteArray()); mail.setMimeMessage(msg->encodedContent(true)); mail.setUnread(object["unread"].toBool()); if (!folder.isEmpty()) { mail.setFolder(folder); } Sink::Store::create(mail).exec().waitForFinished(); } static void createFolder(const QVariantMap &object) { using namespace Sink::ApplicationDomain; auto folder = ApplicationDomainType::createEntity(object["resource"].toByteArray()); folder.setName(object["name"].toString()); folder.setSpecialPurpose(toByteArrayList(object["specialpurpose"].toList())); Sink::Store::create(folder).exec().waitForFinished(); iterateOverObjects(object.value("mails").toList(), [=](const QVariantMap &object) { createMail(object, folder.identifier()); }); } void TestStore::setup(const QVariantMap &map) { using namespace Sink::ApplicationDomain; iterateOverObjects(map.value("accounts").toList(), [&] (const QVariantMap &object) { auto account = ApplicationDomainType::createEntity("", object["id"].toByteArray()); account.setName(object["name"].toString()); Sink::Store::create(account).exec().waitForFinished(); }); QByteArrayList resources; iterateOverObjects(map.value("resources").toList(), [&] (const QVariantMap &object) { resources << object["id"].toByteArray(); auto resource = [&] { using namespace Sink::ApplicationDomain; auto resource = ApplicationDomainType::createEntity("", object["id"].toByteArray()); if (object["type"] == "dummy") { resource.setResourceType("sink.dummy"); } else if (object["type"] == "mailtransport") { resource.setResourceType("sink.mailtransport"); } else { Q_ASSERT(false); } return resource; }(); resource.setAccount(object["account"].toByteArray()); Sink::Store::create(resource).exec().waitForFinished(); Sink::SecretStore::instance().insert(resource.identifier(), "secret"); }); iterateOverObjects(map.value("identities").toList(), [] (const QVariantMap &object) { auto identity = Sink::ApplicationDomain::Identity{}; identity.setAccount(object["account"].toByteArray()); identity.setAddress(object["address"].toString()); identity.setName(object["name"].toString()); Sink::Store::create(identity).exec().waitForFinished(); }); iterateOverObjects(map.value("folders").toList(), createFolder); iterateOverObjects(map.value("mails").toList(), [] (const QVariantMap &map) { createMail(map); }); Sink::ResourceControl::flushMessageQueue(resources).exec().waitForFinished(); } QVariant TestStore::load(const QByteArray &type, const QVariantMap &filter) { using namespace Sink::ApplicationDomain; const auto list = loadList(type, filter); if (!list.isEmpty()) { + if (list.size() > 1) { + qWarning() << "While loading" << type << "with filter" << filter + << "; got multiple elements, but returning the first one."; + } return list.first(); } return {}; } - template QVariantList toVariantList(const QList &list) { QVariantList result; std::transform(list.constBegin(), list.constEnd(), std::back_inserter(result), [] (const T &m) { return QVariant::fromValue(T::Ptr::create(m)); }); Q_ASSERT(list.size() == result.size()); return result; } QVariantList TestStore::loadList(const QByteArray &type, const QVariantMap &filter) { using namespace Sink::ApplicationDomain; Sink::Query query; if (filter.contains("resource")) { query.resourceFilter(filter.value("resource").toByteArray()); } + + for (QVariantMap::const_iterator it = filter.begin(); it != filter.end(); ++it) { + if (it.key() == "messageId") { + query.filter(it.value()); + } else if (it.key() == "draft") { + query.filter(it.value()); + } else if (it.key() == "subject") { + query.filter(it.value()); + } + } + if (type == "mail") { return toVariantList(Sink::Store::read(query)); } if (type == "folder") { return toVariantList(Sink::Store::read(query)); } if (type == "resource") { return toVariantList(Sink::Store::read(query)); } if (type == "account") { return toVariantList(Sink::Store::read(query)); } Q_ASSERT(false); return {}; } QVariantMap TestStore::read(const QVariant &object) { using namespace Sink::ApplicationDomain; QVariantMap map; if (auto mail = object.value()) { + map.insert("uid", mail->identifier()); map.insert("subject", mail->getSubject()); map.insert("draft", mail->getDraft()); return map; } Q_ASSERT(false); return {}; } diff --git a/views/log/main.qml b/views/log/main.qml new file mode 100644 index 00000000..f4f20582 --- /dev/null +++ b/views/log/main.qml @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2018 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.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.0 + +import org.kube.framework 1.0 as Kube +import org.kube.test 1.0 +import "qml" + +ApplicationWindow { + id: app + height: Screen.desktopAvailableHeight * 0.8 + width: Screen.desktopAvailableWidth * 0.8 + + Component.onCompleted: { + var initialState = { + accounts: [{ + id: "account1", + name: "Test Account" + }], + identities: [{ + account: "account1", + name: "Test Identity", + address: "identity@example.org" + }], + resources: [{ + id: "resource1", + account: "account1", + type: "dummy" + }, + { + id: "resource2", + account: "account1", + type: "mailtransport" + }], + folders: [{ + id: "folder1", + resource: "resource1", + name: "Folder 1", + specialpurpose: ["drafts"], + mails: [{ + resource: "resource1", + messageId: "", + date: "2017-07-24T15:46:29", + subject: "subject1", + body: "body", + to: ["to@example.org"], + cc: ["cc@example.org"], + bcc: ["bcc@example.org"], + draft: true + }, + { + resource: "resource1", + messageId: "", + date: "2017-07-23T15:46:29", + subject: "LooooooooooooooooooooooooooooooooooooooooooooooooooooooooonggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggEnd", + body: "LooooooooooooooooooooooooooooooooooooooooooooooooooooooooonggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggEnd\nbody\nbody\n\n\nbody\n\n\nbody\n\n\nbody\n\n\nbody\nbody\n\n\nbody\n\n\nbody\n\n\nbody\n\n\nbody\n\n\n\n\n\n\n\n\nbody\nbody\n\n\nbody\n\n\nbody\n\n\nbody\n\n\nbody\n\n\nbody", + to: ["toLoooooooooooooooooooooooooooooooooooooooooooooooooong@example.org"], + cc: ["ccLoooooooooooooooooooooooooooooooooooooooooooooooooong@example.org"], + bcc: ["bccLoooooooooooooooooooooooooooooooooooooooooooooooooong@example.org"], + draft: true + } + ] + }], + } + TestStore.setup(initialState) + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": Kube.Notifications.loginError, + message: "merge1", + resource: "resource1", + entities: [] + } + ); + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": Kube.Notifications.hostNotFoundError, + message: "merge1", + resource: "resource1", + entities: [] + } + ) + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": Kube.Notifications.connectionError, + message: "merge1", + resource: "resource1", + entities: [] + } + ) + var mail = TestStore.load("mail", {messageId: "msg1@test.com"}) + var mail_uid = TestStore.read(mail).uid + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": Kube.Notifications.transmissionError, + message: "merge1", + resource: "resource1", + entities: [mail_uid] + } + ) + var mail2 = TestStore.load("mail", {messageId: "msg2@test.com"}) + var mail2_uid = TestStore.read(mail2).uid + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": Kube.Notifications.transmissionError, + message: "merge1", + resource: "resource1", + entities: [mail2_uid] + } + ) + Kube.Fabric.postMessage( + Kube.Messages.notification, + { + "type": Kube.Notifications.error, + "subtype": "customSubType", + message: "merge1", + resource: "resource1", + entities: [] + } + ) + } + + View { + anchors.fill: parent + } +} diff --git a/views/log/qml/View.qml b/views/log/qml/View.qml index 14e2d543..7e95e20f 100644 --- a/views/log/qml/View.qml +++ b/views/log/qml/View.qml @@ -1,366 +1,411 @@ /* * 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 Controls import QtQuick.Controls 2.0 as Controls2 import org.kube.framework 1.0 as Kube Controls.SplitView { id: root property bool pendingError: false; Controls2.StackView.onActivated: { root.pendingError = 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: { if (message.type == Kube.Notifications.error) { root.pendingError = true } - var error = {timestamp: new Date(), message: message.message, details: message.details, resource: message.resource} + + var error = { + timestamp: new Date(), + message: message.message, + details: message.details, + resource: message.resource, + // TODO: if we passed entities as a list, it would get + // converted to a ListModel, in all likelihood because of + // ListDelegate, which we should rewrite in C++ + entities: {elements: message.entities} + } + if (logModel.count > 0) { var lastEntry = logModel.get(0) //Merge if we get an entry of the same subtype if (lastEntry.subtype && lastEntry.subtype == message.subtype) { logModel.set(0, {type: message.type, subtype: message.subtype, errors: [error].concat(lastEntry.errors)}) return } } logModel.insert(0, {type: message.type, subtype: message.subtype, errors: [error]}) } } Kube.Label { anchors.centerIn: parent visible: listView.count == 0 text: qsTr("Nothing here...") } Kube.ListView { id: listView anchors { fill: parent } clip: true model: ListModel { id: logModel objectName: "logModel" } onCurrentItemChanged: { var error = currentItem.currentData.errors.get(0) if (!!error.resource) { details.resourceId = error.resource } details.message = error.message + "\n" + error.details details.timestamp = error.timestamp if (!!currentItem.currentData.subtype) { details.subtype = currentItem.currentData.subtype } else { details.subtype = "" } + + details.entities = error.entities } delegate: Kube.ListDelegate { 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: Kube.Colors.disabledTextColor text: model.errors.get(0).message } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom rightMargin: Kube.Units.smallSpacing } text: Qt.formatDateTime(model.errors.get(0).timestamp, " hh:mm:ss dd MMM yyyy") font.italic: true color: Kube.Colors.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 var entities: details.entities 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 } 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: accountName + ": " + qsTr("Please check your credentials.") 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: accountName + ": " + qsTr("Please check your network connection and settings.") 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 { 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 { - Kube.Heading { - id: heading - text: qsTr("Failed to send the message.") - color: Kube.Colors.warningColor - } - Kube.Label { - id: subHeadline - text: accountName - color: Kube.Colors.disabledTextColor - wrapMode: Text.Wrap + spacing: Kube.Units.largeSpacing + + Repeater { + model: Kube.MailListModel { + entityId: entities.elements[0] + } + delegate: Column { + id: subHeadline + + Kube.Label { + text: qsTr("Account") + ": " + accountName + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + text: qsTr("Subject") + ": " + model.subject + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + text: qsTr("To") + ": " + model.to + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + Kube.Label { + visible: !!model.cc + text: qsTr("Cc") + ": " + model.cc; + color: Kube.Colors.disabledTextColor + wrapMode: Text.Wrap + } + + } } } + Kube.Button { text: qsTr("Try again") onClicked: { Kube.Fabric.postMessage(Kube.Messages.sendOutbox, {}) } } } } } }