diff --git a/framework/qml/MailViewer.qml b/framework/qml/MailViewer.qml index d28767b8..91661908 100644 --- a/framework/qml/MailViewer.qml +++ b/framework/qml/MailViewer.qml @@ -1,630 +1,662 @@ /* * Copyright (C) 2016 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.7 import QtQuick.Controls 1.4 as Controls1 import QtQuick.Controls 2 import QtQuick.Layouts 1.1 import org.kube.components.mailviewer 1.0 as MV import org.kube.framework 1.0 as Kube Rectangle { id: root property variant message; property variant subject; property variant sender; property variant senderName; property variant to; property variant cc; property variant bcc; property variant date; property variant trash; property variant draft; property variant sent; property bool incomplete: false; property bool current: false; property bool unread; property alias searchString: mailViewer.searchString property alias autoLoadImages: mailViewer.autoLoadImages property bool collapsed: draft || sent implicitHeight: mainLayout.height + 2 * Kube.Units.largeSpacing Shortcut { sequence: "V" onActivated: debugPopupComponent.createObject(root).open() enabled: root.current } //highlight active mails border.width: current ? 1 : 0 border.color: Kube.Colors.highlightColor color: Kube.Colors.viewBackgroundColor Kube.MessageParser { id: messageParser message: root.message } property var partModel: messageParser.parts property var attachmentModel: messageParser.attachments states: [ State { name: "full" }, State { name: "incomplete"; when: root.incomplete PropertyChanges { target: attachments; visible: false} PropertyChanges { target: body; visible: false} PropertyChanges { target: footer; visible: false} PropertyChanges { target: incompleteBody; visible: true} }, State { name: "collapsed"; when: root.collapsed PropertyChanges { target: attachments; visible: false} PropertyChanges { target: body; visible: false} PropertyChanges { target: footer; visible: false} PropertyChanges { target: collapsedBody; visible: true} } ] state: "full" Column { id: mainLayout anchors { top: parent.top left: parent.left right: parent.right margins: Kube.Units.largeSpacing } height: childrenRect.height spacing: Kube.Units.smallSpacing //BEGIN header Item { id: header Layout.fillWidth: true anchors { left: parent.left right: parent.right } height: headerContent.height + Kube.Units.smallSpacing states: [ State { name: "small" PropertyChanges { target: subject; wrapMode: Text.NoWrap} PropertyChanges { target: recipients; visible: true} PropertyChanges { target: to; visible: false} PropertyChanges { target: cc; visible: false} PropertyChanges { target: bcc; visible: false} }, State { name: "details" PropertyChanges { target: subject; wrapMode: Text.WrapAnywhere} PropertyChanges { target: recipients; visible: false} PropertyChanges { target: to; visible: true} PropertyChanges { target: cc; visible: root.cc} PropertyChanges { target: bcc; visible: root.bcc} } ] state: "small" Kube.Label { id: date_label anchors { right: seperator.right top: parent.top } text: Qt.formatDateTime(root.date, "dd MMM yyyy hh:mm") font.pointSize: Kube.Units.tinyFontSize opacity: 0.75 } Column { id: headerContent anchors { //left: to_l.right horizontalCenter: parent.horizontalCenter } //spacing: Kube.Units.smallSpacing width: parent.width Row{ id: from width: parent.width spacing: Kube.Units.smallSpacing clip: true Kube.SelectableLabel { id: senderName text: root.senderName font.weight: Font.DemiBold opacity: 0.75 states: [ State { name: "sent"; when: root.sent PropertyChanges { target: senderName; text: qsTr("Sent from") } }, State { name: "draft"; when: root.draft PropertyChanges { target: senderName; text: qsTr("Draft from") } } ] } Kube.SelectableLabel { width: parent.width - senderName.width - date_label.width - Kube.Units.largeSpacing text: root.sender elide: Text.ElideRight opacity: 0.75 clip: true Kube.TextButton { text: qsTr("Send mail to") onClicked: Kube.Fabric.postMessage(Kube.Messages.compose, {"recipients": [root.sender]}) } + + Kube.TextButton { + text: qsTr("Add to addressbook") + onClicked: contactPopupComponent.createObject(root, {emailAddress: root.sender}).open() + } + } + } + + Component { + id: contactPopupComponent + + Kube.Popup { + id: popup + + property var emailAddress: null + + parent: ApplicationWindow.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + width: personView.width + height: personView.height + padding: 0 + PersonView { + id: personView + controller: Kube.ContactController { + id: contactController + contact: root.contact + // emailAddress: popup.emailAddress + } + } + //TODO this doesn't work + Component.onCompleted: contactController.mails.set([root.sender]) } } Kube.SelectableLabel { id: subject width: to.width text: root.subject elide: Text.ElideRight opacity: 0.75 font.italic: true states: [ State { name: "trash"; when: root.trash PropertyChanges { target: subject; text: qsTr("Trash: %1").arg(root.subject) } } ] } Kube.SelectableLabel { id: recipients width: parent.width - goDown.width - Kube.Units.smallSpacing text:"to: "+ root.to + " " + root.cc + " " + root.bcc elide: Text.ElideRight opacity: 0.75 } Kube.SelectableLabel { id: to width: parent.width - goDown.width - Kube.Units.smallSpacing text:"to: " + root.to wrapMode: Text.WordWrap opacity: 0.75 } Kube.SelectableLabel { id: cc width: parent.width - goDown.width - Kube.Units.smallSpacing text:"cc: " + root.cc wrapMode: Text.WordWrap opacity: 0.75 } Kube.SelectableLabel { id: bcc width: parent.width - goDown.width - Kube.Units.smallSpacing text:"bcc: " + root.bcc wrapMode: Text.WordWrap opacity: 0.75 } } Rectangle { id: goDown anchors { bottom: seperator.top right: seperator.right } //Only show the expand button if there is something to expand visible: recipients.truncated || root.cc || root.bcc height: Kube.Units.gridUnit width: height color: Kube.Colors.backgroundColor Kube.IconButton { anchors.fill: parent activeFocusOnTab: false iconName: header.state === "details" ? Kube.Icons.goUp : Kube.Icons.goDown onClicked: { header.state === "details" ? header.state = "small" : header.state = "details" } } } Rectangle { id: seperator anchors { left: parent.left right: parent.right bottom: parent.bottom } height: 1 color: Kube.Colors.textColor opacity: 0.5 } } //END header Item { anchors { left: parent.left right: parent.right } visible: attachments.visible || htmlButton.visible height: Math.max(attachments.height, htmlButton.height) Kube.TextButton { id: htmlButton anchors { left: parent.left top: parent.top } opacity: 0.5 visible: root.partModel ? root.partModel.containsHtml : false text: root.partModel ? (root.partModel.showHtml ? "Plain" : "Html") : "" onClicked: { root.partModel.showHtml = !root.partModel.showHtml } } Flow { id: attachments anchors { left: parent.left right: parent.right } visible: !root.incomplete && !root.collapsed width: header.width - Kube.Units.largeSpacing height: visible ? implicitHeight : 0 layoutDirection: Qt.RightToLeft spacing: Kube.Units.smallSpacing clip: true Repeater { model: root.attachmentModel delegate: AttachmentDelegate { name: model.name type: model.type icon: model.iconName clip: true actionIcon: Kube.Icons.save_inverted actionTooltip: qsTr("Save attachment") onExecute: root.attachmentModel.saveAttachmentToDisk(root.attachmentModel.index(index, 0)) onClicked: root.attachmentModel.openAttachment(root.attachmentModel.index(index, 0)) onPublicKeyImport: root.attachmentModel.importPublicKey(root.attachmentModel.index(index, 0)) } } } } Item { id: body visible: true anchors { left: parent.left right: parent.right } height: visible ? mailViewer.height + 20 : 0 MV.MailViewer { id: mailViewer anchors.top: body.top anchors.left: body.left anchors.right: body.right model: root.partModel } } Kube.Label { id: incompleteBody anchors { left: parent.left right: parent.right } visible: root.incomplete height: visible ? implicitHeight : 0 text: qsTr("Incomplete body...") color: Kube.Colors.textColor enabled: false states: [ State { name: "inprogress"; when: model.status == Kube.MailListModel.InProgressStatus PropertyChanges { target: incompleteBody; text: qsTr("Downloading message...") } }, State { name: "error"; when: model.status == Kube.MailListModel.ErrorStatus PropertyChanges { target: incompleteBody; text: qsTr("Failed to download message...") } } ] } Kube.IconButton { id: collapsedBody anchors { left: parent.left right: parent.right } visible: false enabled: false iconName: Kube.Icons.goDown } Item { id: footer property var mail: model.mail property string subject: model.subject anchors { left: parent.left right: parent.right } visible: true height: visible ? Kube.Units.gridUnit : 0 width: parent.width Kube.TextButton { anchors{ verticalCenter: parent.verticalCenter left: parent.left } activeFocusOnTab: false text: model.trash ? qsTr("Delete Mail") : model.draft ? qsTr("Discard") : qsTr("Move to trash") opacity: 0.5 onClicked: { if (model.trash) { Kube.Fabric.postMessage(Kube.Messages.remove, {"mail": model.mail}) } else { Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": model.mail}) } } } Row { anchors { verticalCenter: parent.verticalCenter right: parent.right } spacing: Kube.Units.smallSpacing Kube.Button { visible: !model.trash && !model.draft activeFocusOnTab: false text: qsTr("Share") onClicked: { Kube.Fabric.postMessage(Kube.Messages.forward, {"mail": model.mail}) } } Kube.Button { visible: !model.trash activeFocusOnTab: false text: model.draft ? qsTr("Edit") : qsTr("Reply") onClicked: { if (model.draft) { Kube.Fabric.postMessage(Kube.Messages.edit, {"mail": model.mail}) } else { Kube.Fabric.postMessage(Kube.Messages.reply, {"mail": model.mail}) } } } Row { Kube.ExtensionPoint { extensionPoint: "extensions/mailview" context: {"mail": footer.mail, "subject": footer.subject, "accountId": Kube.Context.currentAccountId} } } } } } //ColumnLayout //Dimm unread messages (but never treat messages as unread that we have sent ourselves) Rectangle { anchors.fill: parent color: Kube.Colors.buttonColor opacity: 0.4 visible: root.unread && !root.sent } MouseArea { enabled: root.collapsed hoverEnabled: root.collapsed anchors.fill: parent onClicked: { root.collapsed = !root.collapsed } Rectangle { anchors.fill: parent color: Kube.Colors.highlightColor opacity: 0.4 visible: root.collapsed && parent.containsMouse } } Component { id: debugPopupComponent Kube.Popup { id: debugPopup modal: true parent: ApplicationWindow.overlay closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent x: (parent.width - width)/2 y: Kube.Units.largeSpacing width: parent.width / 2 height: parent.height - Kube.Units.largeSpacing * 2 clip: true Flickable { id: flickable anchors.fill: parent ScrollBar.vertical: Kube.ScrollBar {} contentHeight: content.height contentWidth: parent.width Column { id: content width: flickable.width height: childrenRect.height TextEdit { id: structure width: parent.width readOnly: true selectByMouse: true textFormat: TextEdit.PlainText wrapMode: TextEdit.Wrap height: implicitHeight text: messageParser.structureAsString } TextEdit { id: rawContent width: parent.width readOnly: true selectByMouse: true textFormat: TextEdit.PlainText wrapMode: TextEdit.Wrap height: implicitHeight text: messageParser.rawContent.substring(0, 100000) //The TextEdit deals poorly with messages that are too large. } Rectangle { color: "black" height: 2 } Controls1.TreeView { id: mailStructure width: parent.width height: implicitHeight Controls1.TableViewColumn { role: "type" title: "Type" } Controls1.TableViewColumn { role: "embedded" title: "Embedded" } Controls1.TableViewColumn { role: "securityLevel" title: "SecurityLevel" } Controls1.TableViewColumn { role: "content" title: "Content" } model: messageParser.parts itemDelegate: Item { property variant currentData: styleData.value Text { anchors.fill: parent color: styleData.textColor elide: Text.ElideRight text: styleData.value ? styleData.value : "" textFormat: Text.PlainText } MouseArea { anchors.fill: parent onClicked: { textEdit.text = styleData.value } } } } Controls1.TreeView { id: attachmentsTree width: parent.width height: implicitHeight Controls1.TableViewColumn { role: "type" title: "Type" } Controls1.TableViewColumn { role: "name" title: "Name" } Controls1.TableViewColumn { role: "size" title: "Size" } model: messageParser.attachments } TextEdit { id: textEdit width: parent.width readOnly: true selectByMouse: true textFormat: TextEdit.PlainText wrapMode: TextEdit.Wrap height: implicitHeight } } } } } } diff --git a/framework/qml/PersonView.qml b/framework/qml/PersonView.qml new file mode 100644 index 00000000..37cfc4d7 --- /dev/null +++ b/framework/qml/PersonView.qml @@ -0,0 +1,82 @@ + /* + 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.7 +import QtQuick.Controls 2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.0 as Dialogs + + +import org.kube.framework 1.0 as Kube + + +Item { + id: root + + property var controller: null + + ColumnLayout { + RowLayout { + spacing: Kube.Units.smallSpacing + + Kube.TextField { + width: Kube.Units.gridUnit * 15 + placeholderText: qsTr("First name") + backgroundColor: "white" + text: controller.firstName + onTextChanged: controller.firstName = text + } + + Kube.TextField { + width: Kube.Units.gridUnit * 15 + placeholderText: qsTr("Last name") + backgroundColor: "white" + text: controller.lastName + onTextChanged: controller.lastName = text + } + } + + RowLayout { + spacing: Kube.Units.largeSpacing + Kube.ComboBox { + width: parent.width + + model: Kube.EntityModel { + id: addressbookModel + type: "addressbook" + roles: ["name"] + } + textRole: "name" + onCurrentIndexChanged: { + if (currentIndex >= 0) { + controller.addressbook = addressbookModel.data(currentIndex).object + } + } + } + Kube.PositiveButton { + id: createButton + text: qsTr("Create Contact") + onClicked: { + controller.saveAction.execute() + root.done() + } + } + } + } +} diff --git a/framework/src/domain/contactcontroller.cpp b/framework/src/domain/contactcontroller.cpp index 682d3d71..c52498ea 100644 --- a/framework/src/domain/contactcontroller.cpp +++ b/framework/src/domain/contactcontroller.cpp @@ -1,190 +1,221 @@ /* * 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. */ #include "contactcontroller.h" #include #include #include #include using namespace Sink::ApplicationDomain; class MailsController : public Kube::ListPropertyController { public: MailsController() : Kube::ListPropertyController{{"email", "isMain"}} { } void set(const QStringList &list) { for (const auto &email: list) { add({{"email", email}, {"isMain", false}}); } } QList get() { return getList("email"); } }; class PhonesController : public Kube::ListPropertyController { public: PhonesController() : Kube::ListPropertyController{{"number"}} { } void set(const QStringList &list) { for (const auto &number: list) { add({{"number", number}}); } } QList get() { return getList("number"); } }; ContactController::ContactController() : Kube::Controller(), controller_mails{new MailsController}, controller_phones(new PhonesController), action_save{new Kube::ControllerAction{this, &ContactController::save}} { updateSaveAction(); } void ContactController::save() { using namespace Sink; using namespace Sink::ApplicationDomain; const auto addressbook = getAddressbook(); if (!addressbook) { qWarning() << "No addressbook selected"; return; } auto populateAddressee = [this] (KContacts::Addressee &addressee) { addressee.setGivenName(getFirstName()); addressee.setFamilyName(getLastName()); addressee.setFormattedName(getFirstName() + " " + getLastName()); addressee.setEmails(static_cast(mailsController())->get()); //TODO phone numbers, addresses, ... }; if (auto c = mContact.value()) { Contact contact = *c; //Apply the changed properties on top of what's existing KContacts::Addressee addressee = KContacts::VCardConverter{}.parseVCard(contact.getVcard()); populateAddressee(addressee); contact.setVcard(KContacts::VCardConverter{}.createVCard(addressee, KContacts::VCardConverter::v3_0)); contact.setAddressbook(*addressbook); auto job = Store::modify(contact) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the contact: " << error; } emit done(); }); run(job); } else { Contact contact(addressbook->resourceInstanceIdentifier()); KContacts::Addressee addressee; populateAddressee(addressee); contact.setVcard(KContacts::VCardConverter{}.createVCard(addressee, KContacts::VCardConverter::v3_0)); contact.setAddressbook(*addressbook); auto job = Store::create(contact) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the contact: " << error; } emit done(); }); run(job); } } void ContactController::updateSaveAction() { saveAction()->setEnabled(!getFirstName().isEmpty()); } void ContactController::loadContact(const QVariant &variant) { using namespace Sink; mContact = variant; if (auto c = variant.value()) { setAddressbook(ApplicationDomainType::Ptr::create(ApplicationDomainType::createEntity(c->resourceInstanceIdentifier(), c->getAddressbook()))); const auto addressee = KContacts::VCardConverter{}.parseVCard(c->getVcard()); setName(c->getFn()); setFirstName(addressee.givenName()); setLastName(addressee.familyName()); static_cast(mailsController())->set(addressee.emails()); QStringList numbers; for (const auto &n : addressee.phoneNumbers()) { numbers << n.number(); } static_cast(phonesController())->set(numbers); for(const auto &a :addressee.addresses()) { setStreet(a.street()); setCity(a.locality()); setCountry(a.country()); break; } setCompany(addressee.organization()); setJobTitle(addressee.role()); setImageData(addressee.photo().rawData()); } } +void ContactController::loadByEmail(const QString &email) +{ + using namespace Sink; + using namespace Sink::ApplicationDomain; + + static_cast(mailsController())->set({email}); + + //TODO query for contact by email address + //We probably can't atm., perhaps just load all and filter in memory for the time being? + // Query query; + // query.request(); + // query.request(); + // query.request(); + // query.filter(icalEvent->uid().toUtf8()); + // Store::fetchAll(query).then([this](const QList &contacts) { + // if (events.isEmpty()) { + // setState(InvitationState::Unknown); + // populateFromEvent(*icalEvent); + // setStart(icalEvent->dtStart()); + // setEnd(icalEvent->dtEnd()); + // setUid(icalEvent->uid().toUtf8()); + // return KAsync::null(); + // } + +} + void ContactController::remove() { if (auto c = mContact.value()) { run(Sink::Store::remove(*c)); } } QVariant ContactController::contact() const { return mContact; } + +QString ContactController::emailAddress() const +{ + return {}; +} diff --git a/framework/src/domain/contactcontroller.h b/framework/src/domain/contactcontroller.h index ea853048..0ed8e6fc 100644 --- a/framework/src/domain/contactcontroller.h +++ b/framework/src/domain/contactcontroller.h @@ -1,68 +1,71 @@ /* * 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 once #include "kube_export.h" #include #include #include #include #include #include "controller.h" class KUBE_EXPORT ContactController : public Kube::Controller { Q_OBJECT // Input properties Q_PROPERTY(QVariant contact READ contact WRITE loadContact) + Q_PROPERTY(QString emailAddress READ emailAddress WRITE loadByEmail) //Interface properties KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId) KUBE_CONTROLLER_PROPERTY(QString, FirstName, firstName) KUBE_CONTROLLER_PROPERTY(QString, LastName, lastName) KUBE_CONTROLLER_PROPERTY(QString, Name, name) KUBE_CONTROLLER_PROPERTY(QString, Street, street) KUBE_CONTROLLER_PROPERTY(QString, City, city) KUBE_CONTROLLER_PROPERTY(QString, Country, country) KUBE_CONTROLLER_PROPERTY(QString, Company, company) KUBE_CONTROLLER_PROPERTY(QString, JobTitle, jobTitle) KUBE_CONTROLLER_PROPERTY(QByteArray, ImageData, imageData) KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::ApplicationDomainType::Ptr, Addressbook, addressbook) KUBE_CONTROLLER_LISTCONTROLLER(mails) KUBE_CONTROLLER_LISTCONTROLLER(phones) KUBE_CONTROLLER_ACTION(save) public: explicit ContactController(); Q_INVOKABLE void loadContact(const QVariant &contact); + Q_INVOKABLE void loadByEmail(const QString &email); Q_INVOKABLE void remove(); QVariant contact() const; + QString emailAddress() const; private slots: void updateSaveAction(); private: QVariant mContact; };