← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~mzanetti/reminders-app/conflict-handling into lp:reminders-app

 

Michael Zanetti has proposed merging lp:~mzanetti/reminders-app/conflict-handling into lp:reminders-app.

Commit message:
implement proper conflict handling

Requested reviews:
  Ubuntu Reminders app developers (reminders-app-dev)

For more details, see:
https://code.launchpad.net/~mzanetti/reminders-app/conflict-handling/+merge/253002

For testing, this is how to produce the situation:

* create a note, make sure evernote and the app are synced
* put the app to offline mode (for example using flight mode)
* Do a change on the phone and in the website
* reconnect the app

=> the note should mark the note as conflicting and upon opening it, it should present you with the conflict resolving page.

Also handles the cases if a note has been changed in one place and deleted on another.
-- 
Your team Ubuntu Reminders app developers is requested to review the proposed merge of lp:~mzanetti/reminders-app/conflict-handling into lp:reminders-app.
=== added file 'src/app/qml/components/ConflictDelegate.qml'
--- src/app/qml/components/ConflictDelegate.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/components/ConflictDelegate.qml	2015-03-15 20:04:06 +0000
@@ -0,0 +1,72 @@
+/*
+ * Copyright: 2015 Canonical, Ltd
+ *
+ * This file is part of reminders
+ *
+ * reminders 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; version 3.
+ *
+ * reminders 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 <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.2
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.1
+import Evernote 0.1
+
+ColumnLayout {
+    id: root
+    property var note: null
+
+    signal clicked();
+    signal deleteThis();
+    signal keepThis();
+
+    RowLayout {
+        Label {
+            text: i18n.tr("Notebook:")
+        }
+        Label {
+            text: root.note ? NotesStore.notebook(root.note.notebookGuid).name : ""
+        }
+    }
+
+    NotesDelegate {
+        Layout.fillWidth: true
+
+        title: root.note.title
+        content: root.note.plaintextContent
+
+        resource: root.note.resourceUrls.length > 0 ? root.note.resourceUrls[0] : ""
+        notebookColor: preferences.colorForNotebook(root.note.notebookGuid)
+        reminder: root.note.reminder
+        date: root.note.updated
+        conflicting: true
+
+        onItemClicked: root.clicked();
+        onDeleteNote: root.deleteThis();
+        onKeepThis: root.keepThis();
+        deleted: root.note.deleted
+    }
+    RowLayout {
+        visible: root.note && root.note.reminder
+        Label {
+            text: i18n.tr("Reminder:")
+        }
+        Label {
+            text: root.note ? root.note.reminderTimeString : ""
+            Layout.fillWidth: true
+        }
+        CheckBox {
+            enabled: false
+            checked: root.note ? root.note.reminderDone : false
+        }
+    }
+}

=== modified file 'src/app/qml/components/ListItemWithActions.qml'
--- src/app/qml/components/ListItemWithActions.qml	2015-02-13 01:02:58 +0000
+++ src/app/qml/components/ListItemWithActions.qml	2015-03-15 20:04:06 +0000
@@ -29,6 +29,7 @@
     property bool triggerActionOnMouseRelease: false
     property color color: Theme.palette.normal.background
     property color selectedColor: "#E6E6E6"
+    property color selectedRightActionColor: UbuntuColors.lightAubergine
     property bool selected: false
     property bool selectionMode: false
     property alias internalAnchors: mainContents.anchors
@@ -246,7 +247,7 @@
                        height: units.gu(3)
                        name: modelData.iconName
                        source: modelData.iconSource
-                       color: root.activeAction === modelData ? UbuntuColors.lightAubergine : UbuntuColors.lightGrey
+                       color: root.activeAction === modelData ? root.selectedRightActionColor : UbuntuColors.lightGrey
                    }
               }
            }

=== modified file 'src/app/qml/components/NotebooksDelegate.qml'
--- src/app/qml/components/NotebooksDelegate.qml	2015-03-04 00:23:45 +0000
+++ src/app/qml/components/NotebooksDelegate.qml	2015-03-15 20:04:06 +0000
@@ -39,6 +39,8 @@
         }
     }
 
+    selectedRightActionColor: UbuntuColors.green
+    triggerActionOnMouseRelease: true
     rightSideActions: [
         Action {
             iconName: model.isDefaultNotebook ? "starred" : "non-starred"

=== modified file 'src/app/qml/components/NotesDelegate.qml'
--- src/app/qml/components/NotesDelegate.qml	2015-02-23 18:44:26 +0000
+++ src/app/qml/components/NotesDelegate.qml	2015-03-15 20:04:06 +0000
@@ -37,6 +37,7 @@
     property bool synced
     property bool syncError
     property bool conflicting
+    property bool deleted
     property string notebookColor
 
     signal deleteNote()
@@ -44,6 +45,9 @@
     signal editReminder()
     signal editTags()
 
+    // For conflict handling
+    signal keepThis();
+
     leftSideAction: Action {
         iconName: "delete"
         text: i18n.tr("Delete")
@@ -52,7 +56,18 @@
         }
     }
 
-    rightSideActions: [
+    selectedRightActionColor: UbuntuColors.green
+    triggerActionOnMouseRelease: true
+    rightSideActions: root.conflicting ? conflictActions : enabledRightSideActions
+    property list<Action> conflictActions: [
+        Action {
+            iconName: "tick"
+            onTriggered: {
+                root.keepThis();
+            }
+        }
+    ]
+    property list<Action> enabledRightSideActions: [
         Action {
             iconName: "alarm-clock"
             text: i18n.tr("Reminder")
@@ -114,6 +129,7 @@
                                 font.weight: Font.Light
                                 elide: Text.ElideRight
                                 color: root.notebookColor
+                                font.strikeout: root.deleted
                             }
                             Label {
                                 Layout.fillWidth: true
@@ -126,6 +142,7 @@
                                 maximumLineCount: 2
                                 fontSize: "small"
                                 color: "black"
+                                font.strikeout: root.deleted
                             }
 
                             Label {

=== modified file 'src/app/qml/components/RemindersDelegate.qml'
--- src/app/qml/components/RemindersDelegate.qml	2015-02-13 00:47:37 +0000
+++ src/app/qml/components/RemindersDelegate.qml	2015-03-15 20:04:06 +0000
@@ -41,6 +41,8 @@
         }
     }
 
+    selectedRightActionColor: UbuntuColors.green
+    triggerActionOnMouseRelease: true
     rightSideActions: [
         Action {
             iconSource: root.note.reminderDone ? "image://theme/select" : "../images/unchecked.svg"

=== added file 'src/app/qml/components/ResolveConflictConfirmationDialog.qml'
--- src/app/qml/components/ResolveConflictConfirmationDialog.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/components/ResolveConflictConfirmationDialog.qml	2015-03-15 20:04:06 +0000
@@ -0,0 +1,73 @@
+/*
+ * Copyright: 2015 Canonical, Ltd
+ *
+ * This file is part of reminders
+ *
+ * reminders 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; version 3.
+ *
+ * reminders 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 <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.2
+import Ubuntu.Components 1.1
+import Ubuntu.Components.Popups 1.0
+
+Dialog {
+    id: root
+    title: i18n.tr("Resolve conflict")
+    text: {
+        var fullText;
+        if (keepLocal) {
+            if (remoteDeleted) {
+                fullText = i18n.tr("This will <b>keep the changes made on this device</b> and <b>restore the note on Evernote</b>.");
+            } else if (localDeleted) {
+                fullText = i18n.tr("This will <b>delete the changed note on Evernote</b>.");
+            } else {
+                fullText = i18n.tr("This will <b>keep the changes made on this device</b> and <b>discard any changes made on Evernote</b>.");
+            }
+        } else {
+            if (remoteDeleted) {
+                fullText = i18n.tr("This will <b>delete the changed note on this device</b>.");
+            } else if (localDeleted) {
+                fullText = i18n.tr("This will <b>download the changed note from Evernote</b> and <b>restore it on this device</b>.");
+            } else {
+                fullText = i18n.tr("This will <b>download the changed note from Evernote</b> and <b>discard any changes made on this device</b>.");
+            }
+        }
+        fullText += "<br><br>" + i18n.tr("Are you sure you want to continue?");
+        return fullText;
+    }
+
+    property bool keepLocal: true
+    property bool remoteDeleted: false
+    property bool localDeleted: false
+
+    signal accepted();
+    signal rejected();
+
+    Button {
+        text: i18n.tr("Yes")
+        color: UbuntuColors.green
+        onClicked: {
+            root.accepted();
+            PopupUtils.close(root);
+        }
+    }
+
+    Button {
+        text: i18n.tr("No")
+        color: UbuntuColors.red
+        onClicked: {
+            root.rejected();
+            PopupUtils.close(root)
+        }
+    }
+}

=== modified file 'src/app/qml/components/StatusBar.qml'
--- src/app/qml/components/StatusBar.qml	2015-03-04 00:23:45 +0000
+++ src/app/qml/components/StatusBar.qml	2015-03-15 20:04:06 +0000
@@ -38,6 +38,7 @@
                 id: label
                 width: parent.width - x
                 wrapMode: Text.WordWrap
+                anchors.verticalCenter: parent.verticalCenter
             }
         }
     }

=== modified file 'src/app/qml/components/TagsDelegate.qml'
--- src/app/qml/components/TagsDelegate.qml	2015-03-04 00:23:45 +0000
+++ src/app/qml/components/TagsDelegate.qml	2015-03-15 20:04:06 +0000
@@ -37,6 +37,8 @@
         }
     }
 
+    selectedRightActionColor: UbuntuColors.green
+    triggerActionOnMouseRelease: true
     rightSideActions: [
         Action {
             iconName: "edit"

=== modified file 'src/app/qml/reminders.qml'
--- src/app/qml/reminders.qml	2015-03-08 18:47:51 +0000
+++ src/app/qml/reminders.qml	2015-03-15 20:04:06 +0000
@@ -109,20 +109,41 @@
         pagestack.push(accountPage);
     }
 
-    function displayNote(note) {
+    function displayNote(note, conflictMode) {
         print("displayNote:", note.guid)
         note.load(true);
         if (root.narrowMode) {
             print("creating noteview");
-            var component = Qt.createComponent(Qt.resolvedUrl("ui/NotePage.qml"));
-            var page = component.createObject(root);
+            var page;
+            if (!conflictMode && note.conflicting) {
+                // We want to open the note normally even though it is conflicting! Show the Conflict page instead.
+                var component = Qt.createComponent(Qt.resolvedUrl("ui/NoteConflictPage.qml"));
+                page = component.createObject(root, {note: note});
+                page.displayNote.connect(function(note) { root.displayNote(note, true); } );
+                page.resolveConflict.connect(function(keepLocal) {
+                    var confirmation = PopupUtils.open(Qt.resolvedUrl("components/ResolveConflictConfirmationDialog.qml"), page, {keepLocal: keepLocal, remoteDeleted: note.conflictingNote.deleted, localDeleted: note.deleted});
+                    confirmation.accepted.connect(function() {
+                        print("dialog accepted. keepLocal:", keepLocal)
+                        NotesStore.resolveConflict(note.guid, keepLocal ? NotesStore.KeepLocal : NotesStore.KeepRemote);
+                        pagestack.pop();
+                    });
+                })
+            } else {
+                var component = Qt.createComponent(Qt.resolvedUrl("ui/NotePage.qml"));
+                page = component.createObject(root, {readOnly: conflictMode });
+                page.editNote.connect(function(note) {root.switchToEditMode(note)})
+                page.openTaggedNotes.connect(function(title, tagGuid) {pagestack.pop();root.openTaggedNotes(title, tagGuid, true)})
+            }
             page.note = note;
-            page.editNote.connect(function(note) {root.switchToEditMode(note)})
-            page.openTaggedNotes.connect(function(title, tagGuid) {pagestack.pop();root.openTaggedNotes(title, tagGuid, true)})
             pagestack.push(page)
         } else {
-            var view = sideViewLoader.embed(Qt.resolvedUrl("ui/NoteView.qml"))
-            view.openTaggedNotes.connect(function(title, tagGuid) {root.openTaggedNotes(title, tagGuid, false)})
+            var view;
+            if (note.conflicting) {
+                view = sideViewLoader.embed(Qt.resolvedUrl("ui/NoteConflictView.qml"))
+            } else {
+                view = sideViewLoader.embed(Qt.resolvedUrl("ui/NoteView.qml"))
+                view.openTaggedNotes.connect(function(title, tagGuid) {root.openTaggedNotes(title, tagGuid, false)})
+            }
             view.note = note;
         }
     }

=== added file 'src/app/qml/ui/NoteConflictPage.qml'
--- src/app/qml/ui/NoteConflictPage.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/ui/NoteConflictPage.qml	2015-03-15 20:04:06 +0000
@@ -0,0 +1,40 @@
+/*
+ * Copyright: 2015 Canonical, Ltd
+ *
+ * This file is part of reminders
+ *
+ * reminders 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; version 3.
+ *
+ * reminders 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 <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.1
+import Evernote 0.1
+import "../components"
+
+Page {
+    id: root
+    property alias note: conflictView.note
+    title: i18n.tr("Conflicting changes")
+
+    signal displayNote(var note)
+
+    signal resolveConflict(bool keepLocal);
+
+    NoteConflictView {
+        id: conflictView
+        anchors.fill: parent
+
+        onDisplayNote: root.displayNote(note)
+        onResolveConflict: root.resolveConflict(keepLocal)
+    }
+}

=== added file 'src/app/qml/ui/NoteConflictView.qml'
--- src/app/qml/ui/NoteConflictView.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/ui/NoteConflictView.qml	2015-03-15 20:04:06 +0000
@@ -0,0 +1,104 @@
+/*
+ * Copyright: 2015 Canonical, Ltd
+ *
+ * This file is part of reminders
+ *
+ * reminders 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; version 3.
+ *
+ * reminders 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 <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.1
+import Ubuntu.Components.ListItems 1.0
+import QtQuick.Layouts 1.1
+import Evernote 0.1
+import "../components"
+
+Item {
+    id: root
+
+    property var note: null
+    readonly property var conflictingNote: note.conflictingNote
+
+    signal displayNote(var note);
+    signal resolveConflict(bool keepLocal);
+
+    Flickable {
+        anchors.fill: parent
+        contentHeight: column.height + units.gu(2)
+        clip: true
+
+        ColumnLayout {
+            id: column
+            anchors { left: parent.left; right: parent.right; top: parent.top}
+            anchors.margins: units.gu(1)
+            spacing: units.gu(2)
+
+            RowLayout {
+                spacing: units.gu(2)
+                Icon {
+                    height: units.gu(8)
+                    width: height
+                    name: "weather-severe-alert-symbolic"
+                }
+                Label {
+                    text: i18n.tr("This note has been modified in multiple places. Please choose the version to be opened and delete the other.")
+                    Layout.fillWidth: true
+                    wrapMode: Text.WordWrap
+                }
+            }
+
+            ThinDivider {}
+
+            Label {
+                text: root.note && root.note.deleted ? i18n.tr("Deleted on this device:") : i18n.tr("Modified on this device:")
+                Layout.fillWidth: true
+                font.bold: true
+                wrapMode: Text.WordWrap
+            }
+
+            ConflictDelegate {
+                Layout.fillWidth: true
+                note: root.note
+
+                onClicked: {
+                    if (root.note) {
+                        root.displayNote(note)
+                    }
+                }
+                onDeleteThis: root.resolveConflict(false)
+                onKeepThis: root.resolveConflict(true)
+            }
+
+            ThinDivider {}
+
+            Label {
+                text: root.note && root.conflictingNote && root.note.conflictingNote.deleted ? i18n.tr("Deleted from Evernote:") : i18n.tr("Modified somewhere else:")
+                Layout.fillWidth: true
+                font.bold: true
+                wrapMode: Text.WordWrap
+            }
+
+            ConflictDelegate {
+                Layout.fillWidth: true
+                note: root.note ? root.note.conflictingNote : null
+                onClicked: {
+                    if (root.note && root.note.conflictingNote) {
+                        root.displayNote(root.note.conflictingNote);
+                    }
+                }
+                onDeleteThis: root.resolveConflict(true)
+                onKeepThis: root.resolveConflict(false)
+            }
+        }
+    }
+}

=== modified file 'src/app/qml/ui/NotePage.qml'
--- src/app/qml/ui/NotePage.qml	2015-02-16 22:05:34 +0000
+++ src/app/qml/ui/NotePage.qml	2015-03-15 20:04:06 +0000
@@ -25,31 +25,29 @@
     id: root
     title: noteView.title
     property alias note: noteView.note
+    property bool readOnly: false
 
     signal editNote(var note)
     signal openTaggedNotes(string title, string tagGuid)
 
-    tools: ToolbarItems {
-       ToolbarButton {
-            action: Action {
+    head {
+        actions: !root.readOnly ? normalActions : []
+        property list<Action> normalActions: [
+            Action {
                 text: i18n.tr("Edit")
                 iconName: "edit"
                 onTriggered: {
                     root.editNote(root.note)
                 }
-            }
-        }
-        ToolbarButton {
-            action: Action {
+            },
+            Action {
                 text: note.reminder ? i18n.tr("Edit reminder") : i18n.tr("Set reminder")
                 iconName: note.reminder ? "reminder" : "reminder-new"
                 onTriggered: {
                     pageStack.push(Qt.resolvedUrl("SetReminderPage.qml"), { note: root.note});
                 }
-            }
-        }
-        ToolbarButton {
-            action: Action {
+            },
+            Action {
                 text: i18n.tr("Delete")
                 iconName: "delete"
                 onTriggered: {
@@ -57,8 +55,8 @@
                     pagestack.pop();
                 }
             }
-        }
-   }
+        ]
+    }
 
     NoteView {
         id: noteView

=== modified file 'src/app/qml/ui/NotebooksPage.qml'
--- src/app/qml/ui/NotebooksPage.qml	2015-03-04 00:23:45 +0000
+++ src/app/qml/ui/NotebooksPage.qml	2015-03-15 20:04:06 +0000
@@ -119,7 +119,6 @@
             delegate: NotebooksDelegate {
                 width: parent.width
                 height: units.gu(10)
-                triggerActionOnMouseRelease: true
 
                 onItemClicked: {
                     print("selected notebook:", model.guid)

=== modified file 'src/app/qml/ui/NotesPage.qml'
--- src/app/qml/ui/NotesPage.qml	2015-03-08 18:47:51 +0000
+++ src/app/qml/ui/NotesPage.qml	2015-03-15 20:04:06 +0000
@@ -161,7 +161,6 @@
                       model.updated : model.created
 
             content: model.tagline
-            triggerActionOnMouseRelease: true
             tags: {
                 var tags = new Array();
                 for (var i = 0; i < model.tagGuids.length; i++) {
@@ -176,15 +175,15 @@
             loading: model.loading
             syncError: model.syncError
             conflicting: model.conflicting
+            locked: conflicting
+            deleted: model.deleted
 
             Component.onCompleted: {
                 notes.note(model.guid).load(false);
             }
 
             onItemClicked: {
-                if (!model.conflicting) {
-                    root.selectedNote = NotesStore.note(guid);
-                }
+                root.selectedNote = NotesStore.note(guid);
             }
 
             onDeleteNote: {

=== modified file 'src/app/qml/ui/RemindersPage.qml'
--- src/app/qml/ui/RemindersPage.qml	2015-03-01 22:32:41 +0000
+++ src/app/qml/ui/RemindersPage.qml	2015-03-15 20:04:06 +0000
@@ -59,7 +59,6 @@
             delegate: RemindersDelegate {
                 width: remindersListView.width
                 note: notes.note(guid)
-                triggerActionOnMouseRelease: true
 
                 onItemClicked: {
                     root.selectedNote = NotesStore.note(guid);

=== modified file 'src/app/qml/ui/SearchNotesPage.qml'
--- src/app/qml/ui/SearchNotesPage.qml	2015-03-04 23:24:54 +0000
+++ src/app/qml/ui/SearchNotesPage.qml	2015-03-15 20:04:06 +0000
@@ -90,8 +90,8 @@
                 loading: model.loading
                 syncError: model.syncError
                 conflicting: model.conflicting
+                deleted: model.deleted
 
-                triggerActionOnMouseRelease: true
                 tags: {
                     var tags = new Array();
                     for (var i = 0; i < model.tagGuids.length; i++) {

=== modified file 'src/app/qml/ui/TagsPage.qml'
--- src/app/qml/ui/TagsPage.qml	2015-03-04 19:48:51 +0000
+++ src/app/qml/ui/TagsPage.qml	2015-03-15 20:04:06 +0000
@@ -83,7 +83,6 @@
             delegate: TagsDelegate {
                 width: parent.width
                 height: units.gu(10)
-                triggerActionOnMouseRelease: true
 
                 onItemClicked: {
                     print("selected tag:", model.guid)

=== modified file 'src/libqtevernote/evernoteconnection.cpp'
--- src/libqtevernote/evernoteconnection.cpp	2015-03-09 10:03:03 +0000
+++ src/libqtevernote/evernoteconnection.cpp	2015-03-15 20:04:06 +0000
@@ -38,6 +38,7 @@
 #include <Errors_types.h>
 
 #include <QUrl>
+#include <QTime>
 
 #include <libintl.h>
 
@@ -65,6 +66,9 @@
     m_userStoreHttpClient(0)
 {
     qRegisterMetaType<EvernoteConnection::ErrorCode>("EvernoteConnection::ErrorCode");
+
+    m_reconnectTimer.setSingleShot(true);
+    connect(&m_reconnectTimer, &QTimer::timeout, this, &EvernoteConnection::connectToEvernote);
 }
 
 void EvernoteConnection::setupUserStore()
@@ -148,6 +152,10 @@
 void EvernoteConnection::disconnectFromEvernote()
 {
     qCDebug(dcConnection) << "Disconnecting from Evernote.";
+
+    m_errorMessage.clear();
+    emit errorChanged();
+
     if (!isConnected()) {
         qCWarning(dcConnection()) << "Not connected. Can't disconnect.";
         return;
@@ -314,6 +322,36 @@
             emit errorChanged();
             return false;
         }
+    } catch (const evernote::edam::EDAMUserException &e) {
+        qCWarning(dcConnection) << "EDAMUserException getting note store path:" << e.what() << "EDAM Error Code:" << e.errorCode;
+        switch (e.errorCode) {
+        case evernote::edam::EDAMErrorCode::AUTH_EXPIRED:
+            m_errorMessage = gettext("Authentication for Evernote server expired. Please renew login information in the accounts settings.");
+            break;
+        default:
+            m_errorMessage = QString(gettext("Unknown error connecting to Evernote: %1")).arg(e.errorCode);
+            break;
+        }
+        emit errorChanged();
+        return false;
+    } catch (const evernote::edam::EDAMSystemException &e) {
+        qCWarning(dcConnection) << "EDAMSystemException getting note store path:" << e.what() << e.errorCode;
+        switch (e.errorCode) {
+        case evernote::edam::EDAMErrorCode::RATE_LIMIT_REACHED:
+            m_errorMessage = gettext("Error connecting to Evernote: Rate limit exceeded. Please try again later.");
+            m_reconnectTimer.stop();
+            m_reconnectTimer.start(e.rateLimitDuration * 1000);
+            {
+                QTime time = QTime::fromMSecsSinceStartOfDay(e.rateLimitDuration * 1000);
+                qCDebug(dcConnection) << "Cannot connect. Rate limit exceeded. Reconnecting in" << time.toString("mm:ss");
+            }
+            break;
+        default:
+            m_errorMessage = gettext("Unknown error connecting to Evernote: %1");
+            break;
+        }
+        emit errorChanged();
+        return false;
     } catch (const TTransportException & e) {
         qCWarning(dcConnection) << "Failed to fetch notestore path:" <<  e.what();
         m_errorMessage = QString(gettext("Error connecting to Evernote: Connection failure when downloading server information."));
@@ -321,7 +359,7 @@
         return false;
     } catch (const TException & e) {
         qCWarning(dcConnection) << "Generic Thrift exception when fetching notestore path:" << e.what();
-        m_errorMessage = gettext("Unknown error connecting to Evernote");
+        m_errorMessage = gettext("Unknown error connecting to Evernote.");
         emit errorChanged();
         return false;
     }

=== modified file 'src/libqtevernote/evernoteconnection.h'
--- src/libqtevernote/evernoteconnection.h	2015-03-08 18:47:51 +0000
+++ src/libqtevernote/evernoteconnection.h	2015-03-15 20:04:06 +0000
@@ -28,6 +28,7 @@
 #include <transport/THttpClient.h>
 
 #include <QObject>
+#include <QTimer>
 
 namespace evernote {
 namespace edam {
@@ -141,6 +142,8 @@
 
     evernote::edam::UserStoreClient *m_userstoreClient;
     boost::shared_ptr<THttpClient> m_userStoreHttpClient;
+
+    QTimer m_reconnectTimer;
 };
 
 #endif // EVERNOTECONNECTION_H

=== modified file 'src/libqtevernote/jobs/evernotejob.cpp'
--- src/libqtevernote/jobs/evernotejob.cpp	2015-03-08 18:47:51 +0000
+++ src/libqtevernote/jobs/evernotejob.cpp	2015-03-15 20:04:06 +0000
@@ -100,6 +100,7 @@
             }
         } catch (const evernote::edam::EDAMUserException &e) {
             QString message;
+            EvernoteConnection::ErrorCode errorCode = EvernoteConnection::ErrorCodeUserException;
             switch (e.errorCode) {
             case evernote::edam::EDAMErrorCode::UNKNOWN:
                 message = "Unknown Error: %1";
@@ -118,15 +119,18 @@
                 break;
             case evernote::edam::EDAMErrorCode::LIMIT_REACHED:
                 message = "Limit reached: %1";
+                errorCode = EvernoteConnection::ErrorCodeLimitExceeded;
                 break;
             case evernote::edam::EDAMErrorCode::QUOTA_REACHED:
                 message = "Quota reached: %1";
+                errorCode = EvernoteConnection::ErrorCodeQutaExceeded;
                 break;
             case evernote::edam::EDAMErrorCode::INVALID_AUTH:
                 message = "Invalid auth: %1";
                 break;
             case evernote::edam::EDAMErrorCode::AUTH_EXPIRED:
                 message = "Auth expired: %1";
+                errorCode = EvernoteConnection::ErrorCodeAuthExpired;
                 break;
             case evernote::edam::EDAMErrorCode::DATA_CONFLICT:
                 message = "Data conflict: %1";
@@ -161,26 +165,26 @@
             }
             message = message.arg(QString::fromStdString(e.parameter));
             qCWarning(dcJobQueue) << metaObject()->className() << "EDAMUserException:" << message;
-            emitJobDone(EvernoteConnection::ErrorCodeUserException, message);
+            emitJobDone(errorCode, message);
         } catch (const evernote::edam::EDAMSystemException &e) {
             qCWarning(dcJobQueue) << "EDAMSystemException in" << metaObject()->className() << e.what() << e.errorCode << QString::fromStdString(e.message);
             QString message;
             EvernoteConnection::ErrorCode errorCode;
             switch (e.errorCode) {
             case evernote::edam::EDAMErrorCode::AUTH_EXPIRED:
-                message = gettext("Authentication expired.");
+                message = "Authentication expired.";
                 errorCode = EvernoteConnection::ErrorCodeAuthExpired;
                 break;
             case evernote::edam::EDAMErrorCode::LIMIT_REACHED:
-                message = gettext("Limit exceeded.");
+                message = "Limit exceeded.";
                 errorCode = EvernoteConnection::ErrorCodeLimitExceeded;
                 break;
             case evernote::edam::EDAMErrorCode::RATE_LIMIT_REACHED:
-                message = gettext("Rate limit exceeded.");
+                message = "Rate limit exceeded.";
                 errorCode = EvernoteConnection::ErrorCodeRateLimitExceeded;
                 break;
             case evernote::edam::EDAMErrorCode::QUOTA_REACHED:
-                message = gettext("Quota exceeded.");
+                message = "Quota exceeded.";
                 errorCode = EvernoteConnection::ErrorCodeQutaExceeded;
                 break;
             default:

=== modified file 'src/libqtevernote/jobs/fetchnotejob.cpp'
--- src/libqtevernote/jobs/fetchnotejob.cpp	2015-02-26 22:47:10 +0000
+++ src/libqtevernote/jobs/fetchnotejob.cpp	2015-03-15 20:04:06 +0000
@@ -20,12 +20,13 @@
 
 #include "fetchnotejob.h"
 
-FetchNoteJob::FetchNoteJob(const QString &guid, LoadWhat what, QObject *parent) :
+FetchNoteJob::FetchNoteJob(const QString &guid, LoadWhatFlags what, QObject *parent) :
     NotesStoreJob(parent),
     m_guid(guid),
     m_what(what)
 {
     qRegisterMetaType<LoadWhat>("LoadWhat");
+    qRegisterMetaType<LoadWhatFlags>("LoadWhatFlags");
 }
 
 bool FetchNoteJob::operator==(const EvernoteJob *other) const
@@ -53,7 +54,9 @@
 
 void FetchNoteJob::startJob()
 {
-    client()->getNote(m_result, token().toStdString(), m_guid.toStdString(), m_what == LoadContent, m_what == LoadResources, false, false);
+    // Just in case we error out, make sure the reply can be idenfied by note guid
+    m_result.guid = m_guid.toStdString();
+    client()->getNote(m_result, token().toStdString(), m_guid.toStdString(), m_what.testFlag(LoadContent), m_what.testFlag(LoadResources), false, false);
 }
 
 void FetchNoteJob::emitJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage)

=== modified file 'src/libqtevernote/jobs/fetchnotejob.h'
--- src/libqtevernote/jobs/fetchnotejob.h	2015-02-26 22:47:10 +0000
+++ src/libqtevernote/jobs/fetchnotejob.h	2015-03-15 20:04:06 +0000
@@ -28,18 +28,19 @@
     Q_OBJECT
 public:
     enum LoadWhat {
-        LoadContent,
-        LoadResources
+        LoadContent = 0x01,
+        LoadResources = 0x02
     };
+    Q_DECLARE_FLAGS(LoadWhatFlags, LoadWhat)
 
-    explicit FetchNoteJob(const QString &guid, LoadWhat what, QObject *parent = 0);
+    explicit FetchNoteJob(const QString &guid, LoadWhatFlags what, QObject *parent = 0);
 
     virtual bool operator==(const EvernoteJob *other) const override;
     virtual void attachToDuplicate(const EvernoteJob *other) override;
     virtual QString toString() const override;
 
 signals:
-    void resultReady(EvernoteConnection::ErrorCode error, const QString &errorMessage, const evernote::edam::Note &note, LoadWhat what);
+    void resultReady(EvernoteConnection::ErrorCode error, const QString &errorMessage, const evernote::edam::Note &note, LoadWhatFlags what);
 
 protected:
     void startJob();
@@ -49,7 +50,7 @@
     evernote::edam::NoteStoreClient *m_client;
     QString m_token;
     QString m_guid;
-    LoadWhat m_what;
+    LoadWhatFlags m_what;
 
     evernote::edam::Note m_result;
 

=== modified file 'src/libqtevernote/jobs/savenotejob.cpp'
--- src/libqtevernote/jobs/savenotejob.cpp	2015-03-06 00:47:45 +0000
+++ src/libqtevernote/jobs/savenotejob.cpp	2015-03-15 20:04:06 +0000
@@ -76,6 +76,9 @@
         note.tagGuids = tags;
         note.__isset.tagGuids = true;
 
+        note.active = true;
+        note.__isset.active = true;
+
         note.__isset.attributes = true;
         note.attributes.reminderOrder = m_note->reminderOrder();
         note.attributes.__isset.reminderOrder = true;

=== modified file 'src/libqtevernote/note.cpp'
--- src/libqtevernote/note.cpp	2015-03-09 09:57:36 +0000
+++ src/libqtevernote/note.cpp	2015-03-15 20:04:06 +0000
@@ -41,7 +41,8 @@
     m_loaded(false),
     m_needsContentSync(false),
     m_syncError(false),
-    m_conflicting(false)
+    m_conflicting(false),
+    m_conflictingNote(nullptr)
 {
     setGuid(guid);
     m_cacheFile.setFileName(NotesStore::instance()->storageLocation() + "note-" + guid + ".enml");
@@ -471,6 +472,11 @@
     return m_resources.values();
 }
 
+Note *Note::conflictingNote() const
+{
+    return m_conflictingNote;
+}
+
 QStringList Note::resourceUrls() const
 {
     QList<QString> ret;
@@ -531,7 +537,7 @@
         return;
     }
 
-    Resource *resource = new Resource(fileName.path());
+    Resource *resource = new Resource(fileName.path(), this);
     m_resources.insert(resource->hash(), resource);
     m_content.attachFile(position, resource->hash(), resource->type());
     emit resourcesChanged();
@@ -733,3 +739,15 @@
         emit conflictingChanged();
     }
 }
+
+void Note::setConflictingNote(Note *note)
+{
+    if (m_conflictingNote) {
+        m_conflictingNote->deleteLater();
+    }
+    m_conflictingNote = note;
+    if (m_conflictingNote) {
+        m_conflictingNote->setParent(this);
+    }
+    emit conflictingNoteChanged();
+}

=== modified file 'src/libqtevernote/note.h'
--- src/libqtevernote/note.h	2015-03-09 09:57:36 +0000
+++ src/libqtevernote/note.h	2015-03-15 20:04:06 +0000
@@ -67,6 +67,7 @@
     Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
     Q_PROPERTY(bool synced READ synced NOTIFY syncedChanged)
     Q_PROPERTY(bool syncError READ syncError NOTIFY syncErrorChanged)
+    Q_PROPERTY(Note* conflictingNote READ conflictingNote NOTIFY conflictingNoteChanged)
 
     // When asking the note's richTextContent, usually the embedded images will have their original size.
     // For rendering that content in a WebView or TextEdit, that might not be appropriate as images might
@@ -158,6 +159,7 @@
     Q_INVOKABLE Resource* resource(const QString &hash);
     QList<Resource*> resources() const;
 
+    Note *conflictingNote() const;
 
     Q_INVOKABLE void markTodo(const QString &todoId, bool checked);
     Q_INVOKABLE void attachFile(int position, const QUrl &fileName);
@@ -194,6 +196,7 @@
     void syncedChanged();
     void syncErrorChanged();
     void conflictingChanged();
+    void conflictingNoteChanged();
 
     void renderWidthChanged();
 
@@ -212,6 +215,7 @@
     void setUpdateSequenceNumber(qint32 updateSequenceNumber);
     void setLastSyncedSequenceNumber(qint32 lastSyncedSequenceNumber);
     void setConflicting(bool conflicting);
+    void setConflictingNote(Note *serverNote);
     Resource *addResource(const QString &hash, const QString &fileName, const QString &type, const QByteArray &data = QByteArray());
     void addMissingResource();
     void setMissingResources(int missingResources);
@@ -245,6 +249,8 @@
     bool m_syncError;
     bool m_conflicting;
 
+    Note *m_conflictingNote;
+
     // Needed to be able to call private setLoading (we don't want to have that set by anyone except the NotesStore)
     friend class NotesStore;
 };

=== modified file 'src/libqtevernote/notes.cpp'
--- src/libqtevernote/notes.cpp	2015-03-08 18:51:50 +0000
+++ src/libqtevernote/notes.cpp	2015-03-15 20:04:06 +0000
@@ -25,7 +25,7 @@
     QSortFilterProxyModel(parent),
     m_onlyReminders(false),
     m_onlySearchResults(false),
-    m_showDeleted(false),
+    m_showDeleted(true),
     m_sortOrder(SortOrderDateUpdatedNewest)
 {
     connect(NotesStore::instance(), &NotesStore::loadingChanged, this, &Notes::loadingChanged);

=== modified file 'src/libqtevernote/notesstore.cpp'
--- src/libqtevernote/notesstore.cpp	2015-03-09 09:57:36 +0000
+++ src/libqtevernote/notesstore.cpp	2015-03-15 20:04:06 +0000
@@ -307,9 +307,11 @@
 
     notebook->setLoading(false);
 
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Error creating notebook:" << errorMessage;
         notebook->setSyncError(true);
+        emit notebookChanged(notebook->guid());
         return;
     }
     QString guid = QString::fromStdString(result.guid);
@@ -503,6 +505,8 @@
     }
 
     tag->setLoading(false);
+
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Error creating tag on server:" << errorMessage;
         tag->setSyncError(true);
@@ -540,6 +544,8 @@
         return;
     }
     tag->setLoading(false);
+
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Error updating tag on server" << errorMessage;
         tag->setSyncError(true);
@@ -625,26 +631,8 @@
 
 void NotesStore::fetchNotesJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::NotesMetadataList &results, const QString &filterNotebookGuid)
 {
-    switch (errorCode) {
-    case EvernoteConnection::ErrorCodeNoError:
-        // All is well...
-        break;
-    case EvernoteConnection::ErrorCodeUserException:
-        qCWarning(dcSync) << "FetchNotesJobDone: EDAMUserException:" << errorMessage;
-        m_loading = false;
-        emit loadingChanged();
-        return; // silently discarding...
-    case EvernoteConnection::ErrorCodeConnectionLost:
-        qCWarning(dcSync) << "FetchNotesJobDone: Connection with evernote lost:" << errorMessage;
-        m_loading = false;
-        emit loadingChanged();
-        return; // silently discarding...
-    case EvernoteConnection::ErrorCodeNotFoundExcpetion:
-        qCWarning(dcSync) << "FetchNotesJobDone: Item not found on server:" << errorMessage;
-        m_loading = false;
-        emit loadingChanged();
-        return; // silently discarding...
-    default:
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "FetchNotesJobDone: Failed to fetch notes list:" << errorMessage << errorCode;
         m_loading = false;
         emit loadingChanged();
@@ -702,6 +690,15 @@
                 qCWarning(dcSync) << "remote sequence:" << result.updateSequenceNum;
                 note->setConflicting(true);
                 changedRoles << RoleConflicting;
+
+                // Not setting parent as we don't want to squash the reply.
+                FetchNoteJob::LoadWhatFlags flags = 0x0;
+                flags |= FetchNoteJob::LoadContent;
+                flags |= FetchNoteJob::LoadResources;
+                FetchNoteJob *fetchNoteJob = new FetchNoteJob(note->guid(), flags);
+                fetchNoteJob->setJobPriority(EvernoteJob::JobPriorityMedium);
+                connect(fetchNoteJob, &FetchNoteJob::resultReady, this, &NotesStore::fetchConflictingNoteJobDone);
+                EvernoteConnection::instance()->enqueue(fetchNoteJob);
             }
         }
 
@@ -721,7 +718,7 @@
         qCDebug(dcSync) << "Not all notes fetched yet. Fetching next batch.";
         refreshNotes(filterNotebookGuid, results.startIndex + results.notes.size());
     } else {
-        qCDebug(dcSync) << "Fetched all notes from Evernote. Starting merge of local changes...";
+        qCDebug(dcSync) << "Fetched all notes from Evernote. Starting sync of local-only notes.";
         m_organizerAdapter->startSync();
         m_loading = false;
         emit loadingChanged();
@@ -767,9 +764,14 @@
                 connect(job, &CreateNoteJob::jobDone, this, &NotesStore::createNoteJobDone);
                 EvernoteConnection::instance()->enqueue(job);
             } else {
-                // This note has been deleted from the server... drop it
                 int idx = m_notes.indexOf(note);
-                if (idx > -1) {
+                if (idx == -1) {
+                    qCWarning(dcSync) << "Should sync unhandled note but it is gone by now...";
+                    continue;
+                }
+
+                if (note->synced()) {
+                    qCDebug(dcSync) << "Note has been deleted from the server and not changed locally. Deleting local note:" << note->guid();
                     beginRemoveRows(QModelIndex(), idx, idx);
                     m_notes.removeAt(idx);
                     m_notesHash.remove(note->guid());
@@ -783,10 +785,21 @@
                     settings.endGroup();
 
                     note->deleteLater();
+                } else {
+                    qCDebug(dcSync) << "CONFLICT: Note has been deleted from the server but we have unsynced local changes for note:" << note->guid();
+                    FetchNoteJob::LoadWhatFlags flags = 0x0;
+                    flags |= FetchNoteJob::LoadContent;
+                    flags |= FetchNoteJob::LoadResources;
+                    FetchNoteJob *job = new FetchNoteJob(note->guid(), flags);
+                    connect(job, &FetchNoteJob::resultReady, this, &NotesStore::fetchConflictingNoteJobDone);
+                    EvernoteConnection::instance()->enqueue(job);
+
+                    note->setConflicting(true);
+                    emit dataChanged(index(idx), index(idx), QVector<int>() << RoleConflicting);
                 }
             }
         }
-        qCDebug(dcSync) << "Local changes merged.";
+        qCDebug(dcSync) << "Local-only notes synced.";
     }
 }
 
@@ -812,7 +825,7 @@
     }
 }
 
-void NotesStore::fetchNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhat what)
+void NotesStore::fetchNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhatFlags what)
 {
     FetchNoteJob *job = static_cast<FetchNoteJob*>(sender());
     Note *note = m_notesHash.value(QString::fromStdString(result.guid));
@@ -824,24 +837,10 @@
     QModelIndex noteIndex = index(m_notes.indexOf(note));
     QVector<int> roles;
 
-    switch (errorCode) {
-    case EvernoteConnection::ErrorCodeNoError:
-        // All is well
-        break;
-    case EvernoteConnection::ErrorCodeUserException:
-        qCWarning(dcSync) << "FetchNoteJobDone: EDAMUserException:" << errorMessage;
-        emit dataChanged(noteIndex, noteIndex, roles);
-        return; // silently discarding...
-    case EvernoteConnection::ErrorCodeConnectionLost:
-        qCWarning(dcSync) << "FetchNoteJobDone: Connection with evernote lost:" << errorMessage;
-        emit dataChanged(noteIndex, noteIndex, roles);
-        return; // silently discarding...
-    case EvernoteConnection::ErrorCodeNotFoundExcpetion:
-        qCWarning(dcSync) << "FetchNoteJobDone: Item not found on server:" << errorMessage;
-        emit dataChanged(noteIndex, noteIndex, roles);
-        return; // silently discarding...
-    default:
-        qCWarning(dcSync) << "FetchNoteJobDone: Failed to fetch note content:" << errorMessage << errorCode;
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
+        note->setLoading(false);
+        roles << RoleLoading;
         note->setSyncError(true);
         roles << RoleSyncError;
         emit dataChanged(noteIndex, noteIndex, roles);
@@ -932,6 +931,52 @@
     note->syncToCacheFile(); // Syncs note's content into notes cache
 }
 
+void NotesStore::fetchConflictingNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhatFlags what)
+{
+    Note *note = m_notesHash.value(QString::fromStdString(result.guid));
+    if (!note) {
+        qCWarning(dcSync) << "Fetched conflicting note from server but local note can't be found any more:" << QString::fromStdString(result.guid);
+        return;
+    }
+
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
+        qCWarning(dcSync) << "Failed to fetch conflicting note for" << note->guid();
+        return;
+    }
+
+    qCDebug(dcSync) << "Fetched conflicting note:" << note->guid();
+
+    // Make sure local note is loaded
+    note->loadFromCacheFile();
+
+    Note *serverNote = new Note("conflict-" + note->guid(), result.updateSequenceNum, note);
+    serverNote->setUpdateSequenceNumber(result.updateSequenceNum);
+    serverNote->setLastSyncedSequenceNumber(result.updateSequenceNum);
+    serverNote->setTitle(QString::fromStdString(result.title));
+    serverNote->setNotebookGuid(QString::fromStdString(result.notebookGuid));
+    serverNote->setUpdated(QDateTime::fromMSecsSinceEpoch(result.updated));
+    serverNote->setDeleted(result.deleted > 0);
+    QStringList tagGuids;
+    foreach (const std::string &guid, result.tagGuids) {
+        tagGuids << QString::fromStdString(guid);
+    }
+    serverNote->setTagGuids(tagGuids);
+    serverNote->setReminderOrder(result.attributes.reminderOrder);
+    serverNote->setReminderTime(QDateTime::fromMSecsSinceEpoch(result.attributes.reminderTime));
+    serverNote->setReminderDoneTime(QDateTime::fromMSecsSinceEpoch(result.attributes.reminderDoneTime));
+
+    serverNote->setEnmlContent(QString::fromStdString(result.content));
+
+    foreach (const evernote::edam::Resource &resource, result.resources) {
+        serverNote->addResource(QString::fromStdString(resource.data.bodyHash), QString::fromStdString(resource.attributes.fileName), QString::fromStdString(resource.mime));
+    }
+
+    note->setConflictingNote(serverNote);
+    note->setLoading(false);
+
+}
+
 void NotesStore::refreshNotebooks()
 {
     if (!EvernoteConnection::instance()->isConnected()) {
@@ -951,20 +996,10 @@
     m_notebooksLoading = false;
     emit notebooksLoadingChanged();
 
-    switch (errorCode) {
-    case EvernoteConnection::ErrorCodeNoError:
-        // All is well...
-        break;
-    case EvernoteConnection::ErrorCodeUserException:
-        qCWarning(dcSync) << "FetchNotebooksJobDone: EDAMUserException:" << errorMessage;
-        // silently discarding...
-        return;
-    case EvernoteConnection::ErrorCodeConnectionLost:
-        qCWarning(dcSync) << "FetchNotebooksJobDone: Connection lost:" << errorMessage;
-        return; // silently discarding
-    default:
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "FetchNotebooksJobDone: Failed to fetch notes list:" << errorMessage << errorCode;
-        return; // silently discarding
+        return;
     }
 
     QList<Notebook*> unhandledNotebooks = m_notebooks;
@@ -1063,20 +1098,10 @@
     m_tagsLoading = false;
     emit tagsLoadingChanged();
 
-    switch (errorCode) {
-    case EvernoteConnection::ErrorCodeNoError:
-        // All is well...
-        break;
-    case EvernoteConnection::ErrorCodeUserException:
-        qCWarning(dcSync) << "FetchTagsJobDone: EDAMUserException:" << errorMessage;
-        // silently discarding...
-        return;
-    case EvernoteConnection::ErrorCodeConnectionLost:
-        qCWarning(dcSync) << "FetchTagsJobDone: Connection lost:" << errorMessage;
-        return; // silently discarding
-    default:
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "FetchTagsJobDone: Failed to fetch notes list:" << errorMessage << errorCode;
-        return; // silently discarding
+        return;
     }
 
     QHash<QString, Tag*> unhandledTags = m_tagsHash;
@@ -1203,6 +1228,7 @@
     note->setLoading(false);
     roles << RoleLoading;
 
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Error creating note on server:" << tmpGuid << errorMessage;
         note->setSyncError(true);
@@ -1266,6 +1292,7 @@
         qCWarning(dcNotesStore) << "Can't save note. Guid not found:" << guid;
         return;
     }
+    qCDebug(dcNotesStore) << "Saving note. Setting updateSequenceNumber to:" << note->updateSequenceNumber()+1;
     note->setUpdateSequenceNumber(note->updateSequenceNumber()+1);
     note->setUpdated(QDateTime::currentDateTime());
     syncToCacheFile(note);
@@ -1304,13 +1331,13 @@
     int idx = m_notes.indexOf(note);
     note->setLoading(false);
 
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
-        qCWarning(dcSync) << "Error saving note:" << errorMessage;
+        qCWarning(dcSync) << "Unhandled error saving note:" << errorCode << "Message:" << errorMessage;
         note->setSyncError(true);
         emit dataChanged(index(idx), index(idx), QVector<int>() << RoleLoading << RoleSyncError);
         return;
     }
-    note->setSyncError(false);
 
     note->setUpdateSequenceNumber(result.updateSequenceNum);
     note->setLastSyncedSequenceNumber(result.updateSequenceNum);
@@ -1327,19 +1354,25 @@
 
 void NotesStore::saveNotebookJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Notebook &result)
 {
-    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
-        qCWarning(dcSync) << "Error saving notebook to server" << errorMessage;
-        return;
-    }
-
     Notebook *notebook = m_notebooksHash.value(QString::fromStdString(result.guid));
     if (!notebook) {
         qCWarning(dcSync) << "Save notebook job done but notebook can't be found any more!";
         return;
     }
+
+    handleUserError(errorCode);
+    if (errorCode != EvernoteConnection::ErrorCodeNoError) {
+        qCWarning(dcSync) << "Error saving notebook to server" << errorCode << errorMessage;
+        notebook->setSyncError(true);
+        emit notebookChanged(notebook->guid());
+        return;
+    }
+
+    notebook->setLoading(false);
+    notebook->setSyncError(false);
+
     qCDebug(dcSync) << "Notebooks saved to server:" << notebook->guid();
     updateFromEDAM(result, notebook);
-    notebook->setLoading(false);
     emit notebookChanged(notebook->guid());
     syncToCacheFile(notebook);
 }
@@ -1410,6 +1443,7 @@
 
 void NotesStore::deleteNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const QString &guid)
 {
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Cannot delete note from server:" << errorMessage;
         return;
@@ -1430,6 +1464,7 @@
 
 void NotesStore::expungeNotebookJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const QString &guid)
 {
+    handleUserError(errorCode);
     if (errorCode != EvernoteConnection::ErrorCodeNoError) {
         qCWarning(dcSync) << "Error expunging notebook:" << errorMessage;
         return;
@@ -1643,6 +1678,25 @@
     notebook->setLastSyncedSequenceNumber(evNotebook.updateSequenceNum);
 }
 
+bool NotesStore::handleUserError(EvernoteConnection::ErrorCode errorCode)
+{
+    switch (errorCode) {
+    case EvernoteConnection::ErrorCodeAuthExpired:
+        m_errorQueue.append(gettext("Authentication for Evernote server expired. Please renew login information in the accounts settings."));
+        break;
+    case EvernoteConnection::ErrorCodeLimitExceeded:
+        m_errorQueue.append(gettext("Rate limit for Evernote server exceeded. Please try again later."));
+        break;
+    case EvernoteConnection::ErrorCodeQutaExceeded:
+        m_errorQueue.append(gettext("Upload quota for Evernote server exceed. Please try again later."));
+        break;
+    default:
+        return false;
+    }
+    emit errorChanged();
+    return true;
+}
+
 
 void NotesStore::expungeTag(const QString &guid)
 {
@@ -1682,3 +1736,35 @@
     tag->deleteInfoFile();
     tag->deleteLater();
 }
+
+void NotesStore::resolveConflict(const QString &noteGuid, NotesStore::ConflictResolveMode mode)
+{
+    Note *note = m_notesHash.value(noteGuid);
+    if (!note) {
+        qCWarning(dcNotesStore) << "Should resolve a conflict but can't find note for guid:" << noteGuid;
+        return;
+    }
+    if (!note->conflictingNote()) {
+        qCWarning(dcNotesStore) << "Should resolve a conflict but note doesn't have a conflicting note:" << noteGuid;
+        return;
+    }
+    if (mode == KeepLocal) {
+        qCDebug(dcNotesStore) << "Resolving conflict using local note for note guid:" << noteGuid;
+        note->setUpdateSequenceNumber(note->conflictingNote()->updateSequenceNumber() + 1);
+        note->setConflicting(false);
+        saveNote(note->guid());
+    } else {
+        qCDebug(dcNotesStore) << "Resolving conflict using remote note for note guid:" << noteGuid;
+        Note *newNote = note->conflictingNote();
+        newNote->setParent(this);
+        // Conflicting notes have their guid prefixed, lets correct that
+        newNote->setGuid(note->guid());
+        newNote->setConflicting(false);
+        int idx = m_notes.indexOf(note);
+        m_notesHash[note->guid()] = newNote;
+        m_notes.replace(idx, newNote);
+        emit noteChanged(newNote->guid(), newNote->notebookGuid());
+        emit dataChanged(index(idx), index(idx));
+        saveNote(note->guid());
+    }
+}

=== modified file 'src/libqtevernote/notesstore.h'
--- src/libqtevernote/notesstore.h	2015-03-04 00:23:45 +0000
+++ src/libqtevernote/notesstore.h	2015-03-15 20:04:06 +0000
@@ -51,6 +51,7 @@
 class NotesStore : public QAbstractListModel
 {
     Q_OBJECT
+    Q_ENUMS(ConflictResolveMode)
     // Setting the username will cause notes to be loaded from cache
     // If you want to load the local cache only (not associated with an Evernote account), make sure to set this to ""
     // Note that if you use EvernoteConnection to log in (by setting a token obtained from OA) this username
@@ -91,6 +92,11 @@
         RoleConflicting
     };
 
+    enum ConflictResolveMode {
+        KeepLocal,
+        KeepRemote
+    };
+
     ~NotesStore();
     static NotesStore *instance();
 
@@ -137,6 +143,8 @@
     Q_INVOKABLE void untagNote(const QString &noteGuid, const QString &tagGuid);
     Q_INVOKABLE void expungeTag(const QString &guid);
 
+    Q_INVOKABLE void resolveConflict(const QString &noteGuid, ConflictResolveMode mode);
+
 public slots:
     void refreshNotes(const QString &filterNotebookGuid = QString(), int startIndex = 0);
 
@@ -178,7 +186,8 @@
 private slots:
     void fetchNotesJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::NotesMetadataList &results, const QString &filterNotebookGuid);
     void fetchNotebooksJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const std::vector<evernote::edam::Notebook> &results);
-    void fetchNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhat what);
+    void fetchNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhatFlags what);
+    void fetchConflictingNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result, FetchNoteJob::LoadWhatFlags what);
     void createNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const QString &tmpGuid, const evernote::edam::Note &result);
     void saveNoteJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Note &result);
     void saveNotebookJobDone(EvernoteConnection::ErrorCode errorCode, const QString &errorMessage, const evernote::edam::Notebook &result);
@@ -203,6 +212,8 @@
     QVector<int>    updateFromEDAM(const evernote::edam::NoteMetadata &evNote, Note *note);
     void updateFromEDAM(const evernote::edam::Notebook &evNotebook, Notebook *notebook);
 
+    bool handleUserError(EvernoteConnection::ErrorCode errorCode);
+
 private:
     explicit NotesStore(QObject *parent = 0);
     static NotesStore *s_instance;


Follow ups