From 9e34640aed1189c5994736179d361c6701fdabc8 Mon Sep 17 00:00:00 2001 From: Cyrille Bollu Date: Mon, 13 May 2019 17:04:05 +0200 Subject: [PATCH 1/6] Implements Drag'n'Drop support for classifying messages in folders. Signed-off-by: Cyrille Bollu --- package.json | 1 + src/main.js | 2 + src/service/MessageService.js | 10 + src/store/actions.js | 28 ++ src/store/actions.js.orig | 548 ---------------------------------- src/views/Home.vue | 35 ++- 6 files changed, 75 insertions(+), 549 deletions(-) delete mode 100644 src/store/actions.js.orig diff --git a/package.json b/package.json index fd4d9a551e..af52e7cc8d 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "vue-slide-up-down": "^2.0.1", "vue-tabs-component": "^1.5.0", "vuex": "^3.1.3", + "vue-draggable": "^2.0.2", "vuex-router-sync": "^5.0.0" }, "browserslist": [ diff --git a/src/main.js b/src/main.js index 1562f693a4..f9c9ef0ef2 100644 --- a/src/main.js +++ b/src/main.js @@ -21,6 +21,7 @@ */ import Vue from 'vue' +import VueDraggable from 'vue-draggable' import {getRequestToken} from '@nextcloud/auth' import {sync} from 'vuex-router-sync' import {generateFilePath} from '@nextcloud/router' @@ -42,6 +43,7 @@ Vue.mixin(Nextcloud) Vue.use(VueShortKey, {prevent: ['input', 'div']}) Vue.use(VTooltip) +Vue.use(VueDraggable) const getPreferenceFromPage = (key) => { const elem = document.getElementById(key) diff --git a/src/service/MessageService.js b/src/service/MessageService.js index 0be155b339..4ca99e13c7 100644 --- a/src/service/MessageService.js +++ b/src/service/MessageService.js @@ -146,3 +146,13 @@ export function deleteMessage(accountId, folderId, id) { return axios.delete(url).then((resp) => resp.data) } + +export function moveMessage(accountId, startFolderId, targetFolderId, id) { + const url = generateUrl('/apps/mail/api/accounts/{accountId}/folders/{startFolderId}/messages/{id}/move', { + accountId, + startFolderId, + id, + }) + + return HttpClient.post(url, {destAccountId: accountId, destFolderId: targetFolderId}).then(resp => resp.data) +} diff --git a/src/store/actions.js b/src/store/actions.js index ba7bf1d5d0..2a5c6393eb 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -56,6 +56,7 @@ import { fetchEnvelope, fetchEnvelopes, fetchMessage, + moveMessage, setEnvelopeFlag, syncEnvelopes, } from '../service/MessageService' @@ -534,6 +535,33 @@ export default { newUid: uid, }) }, + moveMessage({getters, commit}, data) { + const folder = getters.getFolder(data.accountId, data.startFolderId) + commit('removeEnvelope', { + accountId: data.accountId, + folder, + id: data.msgId, + }) + return moveMessage(data.accountId, data.startFolderId, data.targetFolderId, data.msgId) + .then(() => { + commit('removeMessage', { + accountId: data.accountId, + folder, + id: data.msgId, + }) + console.log('message moved') + }) + .catch(err => { + console.error('could not move message', err) + const env = getters.getEnvelope(data.accountId, data.startFolderId, data.msgId) + commit('addEnvelope', { + accountId: data.accountId, + folder, + env, + }) + throw err + }) + }, deleteMessage({getters, commit}, {accountId, folderId, id}) { commit('removeEnvelope', {accountId, folderId, id}) diff --git a/src/store/actions.js.orig b/src/store/actions.js.orig deleted file mode 100644 index ad1aea2872..0000000000 --- a/src/store/actions.js.orig +++ /dev/null @@ -1,548 +0,0 @@ -/** - * @copyright 2019 Christoph Wurst - * - * @author 2019 Christoph Wurst - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import flatMapDeep from 'lodash/fp/flatMapDeep' -import orderBy from 'lodash/fp/orderBy' -import { - andThen, - complement, - curry, - identity, - filter, - flatten, - gt, - head, - last, - map, - pipe, - prop, - propEq, - slice, - tap, - where, -} from 'ramda' - -import {savePreference} from '../service/PreferenceService' -import { - create as createAccount, - update as updateAccount, - patch as patchAccount, - updateSignature, - deleteAccount, - fetch as fetchAccount, - fetchAll as fetchAllAccounts, -} from '../service/AccountService' -import {fetchAll as fetchAllFolders, create as createFolder, markFolderRead} from '../service/FolderService' -import { - deleteMessage, - fetchEnvelope, - fetchEnvelopes, - fetchMessage, - setEnvelopeFlag, - syncEnvelopes, -} from '../service/MessageService' -import logger from '../logger' -import {normalizedEnvelopeListId} from './normalization' -import {showNewMessagesNotification} from '../service/NotificationService' -import {parseUid} from '../util/EnvelopeUidParser' -import {matchError} from '../errors/match' -import SyncIncompleteError from '../errors/SyncIncompleteError' - -const PAGE_SIZE = 20 - -const sliceToPage = slice(0, PAGE_SIZE) - -const findIndividualFolders = curry((getFolders, specialRole) => - pipe( - filter(complement(prop('isUnified'))), - map(prop('accountId')), - map(getFolders), - flatten, - filter(propEq('specialRole', specialRole)) - ) -) - -const combineEnvelopeLists = pipe(flatten, orderBy(prop('dateInt'), 'desc')) - -export default { - savePreference({commit, getters}, {key, value}) { - return savePreference(key, value).then(({value}) => { - commit('savePreference', { - key, - value, - }) - }) - }, - fetchAccounts({commit, getters}) { - return fetchAllAccounts().then(accounts => { - accounts.forEach(account => commit('addAccount', account)) - return getters.accounts - }) - }, - fetchAccount({commit}, id) { - return fetchAccount(id).then(account => { - commit('addAccount', account) - return account - }) - }, - createAccount({commit}, config) { - return createAccount(config).then(account => { - logger.debug(`account ${account.id} created, fetching folders …`, account) - return fetchAllFolders(account.id) - .then(folders => { - account.folders = folders - commit('addAccount', account) - }) - .then(() => console.info("new account's folders fetched")) - .then(() => account) - }) - }, - updateAccount({commit}, config) { - return updateAccount(config).then(account => { - console.debug('account updated', account) - commit('editAccount', account) - return account - }) - }, - patchAccount({commit}, {account, data}) { - return patchAccount(account, data).then(account => { - console.debug('account patched', account, data) - commit('editAccount', data) - return account - }) - }, - updateAccountSignature({commit}, {account, signature}) { - return updateSignature(account, signature).then(() => { - console.debug('account signature updated') - const updated = Object.assign({}, account, {signature}) - commit('editAccount', updated) - return account - }) - }, - deleteAccount({commit}, account) { - return deleteAccount(account.id).catch(err => { - console.error('could not delete account', err) - throw err - }) - }, - createFolder({commit}, {account, name}) { - return createFolder(account.id, name).then(folder => { - console.debug(`folder ${name} created for account ${account.id}`, {folder}) - commit('addFolder', {account, folder}) - }) - }, - moveAccount({commit, getters}, {account, up}) { - const accounts = getters.accounts - const index = accounts.indexOf(account) - if (up) { - const previous = accounts[index - 1] - accounts[index - 1] = account - accounts[index] = previous - } else { - const next = accounts[index + 1] - accounts[index + 1] = account - accounts[index] = next - } - return Promise.all( - accounts.map((account, idx) => { - if (account.id === 0) { - return - } - commit('saveAccountsOrder', {account, order: idx}) - return patchAccount(account, {order: idx}) - }) - ) - }, - markFolderRead({dispatch}, {account, folderId}) { - return markFolderRead(account.id, folderId).then( - dispatch('syncEnvelopes', { - accountId: account.id, - folderId: folderId, - }) - ) - }, - fetchEnvelope({commit, getters}, uid) { - const {accountId, folderId, id} = parseUid(uid) - - const cached = getters.getEnvelope(accountId, folderId, id) - if (cached) { - return cached - } - - return fetchEnvelope(accountId, folderId, id).then(envelope => { - // Only commit if not undefined (not found) - if (envelope) { - commit('addEnvelope', { - accountId, - folderId, - envelope, - }) - } - - // Always use the object from the store - return getters.getEnvelope(accountId, folderId, id) - }) - }, - fetchEnvelopes({state, commit, getters, dispatch}, {accountId, folderId, query}) { - const folder = getters.getFolder(accountId, folderId) - - if (folder.isUnified) { - const fetchIndividualLists = pipe( - map(f => - dispatch('fetchEnvelopes', { - accountId: f.accountId, - folderId: f.id, - query, - }) - ), - Promise.all.bind(Promise), - andThen(map(sliceToPage)) - ) - const fetchUnifiedEnvelopes = pipe( - findIndividualFolders(getters.getFolders, folder.specialRole), - fetchIndividualLists, - andThen(combineEnvelopeLists), - andThen(sliceToPage), - andThen( - tap( - map(envelope => - commit('addEnvelope', { - accountId, - folderId, - envelope, - query, - }) - ) - ) - ) - ) - - return fetchUnifiedEnvelopes(getters.accounts) - } - - return pipe( - fetchEnvelopes, - andThen( - tap( - map(envelope => - commit('addEnvelope', { - accountId, - folderId, - query, - envelope, - }) - ) - ) - ) - )(accountId, folderId, query) - }, - fetchNextEnvelopePage({commit, getters, dispatch}, {accountId, folderId, query}) { - const folder = getters.getFolder(accountId, folderId) - - if (folder.isUnified) { - const getIndivisualLists = curry((query, f) => getters.getEnvelopes(f.accountId, f.id, query)) - const individualCursor = curry((query, f) => - prop('dateInt', last(getters.getEnvelopes(f.accountId, f.id, query))) - ) - const cursor = individualCursor(query, folder) - - if (cursor === undefined) { - throw new Error('Unified list has no tail') - } - const nextLocalUnifiedEnvelopePage = pipe( - findIndividualFolders(getters.getFolders, folder.specialRole), - map(getIndivisualLists(query)), - combineEnvelopeLists, - filter( - where({ - dateInt: gt(cursor), - }) - ), - sliceToPage - ) - // We know the next page based on local data - // We have to fetch individual pages only if the page ends in the known - // next page. If it ended before, there is no data to fetch anyway. If - // it ends after, we have all the relevant data already - const needsFetch = curry((query, nextPage, f) => { - const c = individualCursor(query, f) - return nextPage.length < PAGE_SIZE || (c <= head(nextPage).dateInt && c >= last(nextPage).dateInt) - }) - - const foldersToFetch = accounts => - pipe( - findIndividualFolders(getters.getFolders, folder.specialRole), - filter(needsFetch(query, nextLocalUnifiedEnvelopePage(accounts))) - )(accounts) - - const fs = foldersToFetch(getters.accounts) - - if (fs.length) { - return pipe( - map(f => - dispatch('fetchNextEnvelopePage', { - accountId: f.accountId, - folderId: f.id, - query, - }) - ), - Promise.all.bind(Promise), - andThen(() => - dispatch('fetchNextEnvelopePage', { - accountId, - folderId, - query, - }) - ) - )(fs) - } - - const page = nextLocalUnifiedEnvelopePage(getters.accounts) - page.map(envelope => - commit('addEnvelope', { - accountId, - folderId, - query, - envelope, - }) - ) - return page - } - - const list = folder.envelopeLists[normalizedEnvelopeListId(query)] - const lastEnvelopeId = last(list) - if (typeof lastEnvelopeId === 'undefined') { - console.error('folder is empty', list) - return Promise.reject(new Error('Local folder has no envelopes, cannot determine cursor')) - } - const lastEnvelope = getters.getEnvelopeById(lastEnvelopeId) - if (typeof lastEnvelope === 'undefined') { - return Promise.reject(new Error('Cannot find last envelope. Required for the folder cursor')) - } - - return fetchEnvelopes(accountId, folderId, query, lastEnvelope.dateInt).then(envelopes => { - envelopes.forEach(envelope => - commit('addEnvelope', { - accountId, - folderId, - query, - envelope, - }) - ) - return envelopes - }) - }, - syncEnvelopes({commit, getters, dispatch}, {accountId, folderId, query, init = false}) { - const folder = getters.getFolder(accountId, folderId) - - if (folder.isUnified) { - return Promise.all( - getters.accounts - .filter(account => !account.isUnified) - .map(account => - Promise.all( - getters - .getFolders(account.id) - .filter(f => f.specialRole === folder.specialRole) - .map(folder => - dispatch('syncEnvelopes', { - accountId: account.id, - folderId: folder.id, - query, - init, - }) - ) - ) - ) - ) - } - - const uids = getters.getEnvelopes(accountId, folderId, query).map(env => env.id) - -<<<<<<< HEAD - return syncEnvelopes(accountId, folderId, uids, init) - .then(syncData => { - const unifiedFolder = getters.getUnifiedFolder(folder.specialRole) -======= - return syncEnvelopes(accountId, folderId, uids, query, init).then(syncData => { - const unifiedFolder = getters.getUnifiedFolder(folder.specialRole) ->>>>>>> Simplify the virtual favorite inbox - - syncData.newMessages.forEach(envelope => { - commit('addEnvelope', { - accountId, - folderId, - envelope, - query, - }) - if (unifiedFolder) { - commit('addEnvelope', { - accountId: unifiedFolder.accountId, - folderId: unifiedFolder.id, - envelope, - query, - }) - } - }) - syncData.changedMessages.forEach(envelope => { - commit('addEnvelope', { - accountId, - folderId, - envelope, - query, - }) - }) - syncData.vanishedMessages.forEach(id => { - commit('removeEnvelope', { - accountId, - folderId, - id, - query, - }) - // Already removed from unified inbox - }) - - return syncData.newMessages - }) - .catch(error => { - return matchError(error, { - [SyncIncompleteError.getName()]() { - console.warn('(initial) sync is incomplete, retriggering') - return dispatch('syncEnvelopes', {accountId, folderId, query, init}) - }, - default(error) { - console.error('Could not sync envelopes: ' + error.message, error) - }, - }) - }) - }, - syncInboxes({getters, dispatch}) { - return Promise.all( - getters.accounts - .filter(a => !a.isUnified) - .map(account => { - return Promise.all( - getters.getFolders(account.id).map(folder => { - if (folder.specialRole !== 'inbox') { - return - } - - return dispatch('syncEnvelopes', { - accountId: account.id, - folderId: folder.id, - }) - }) - ) - }) - ).then(results => { - const newMessages = flatMapDeep(identity)(results).filter(m => m !== undefined) - if (newMessages.length > 0) { - showNewMessagesNotification(newMessages) - } - }) - }, - toggleEnvelopeFlagged({commit, getters}, envelope) { - // Change immediately and switch back on error - const oldState = envelope.flags.flagged - commit('flagEnvelope', { - envelope, - flag: 'flagged', - value: !oldState, - }) - - setEnvelopeFlag(envelope.accountId, envelope.folderId, envelope.id, 'flagged', !oldState).catch(e => { - console.error('could not toggle message flagged state', e) - - // Revert change - commit('flagEnvelope', { - envelope, - flag: 'flagged', - value: oldState, - }) - }) - }, - toggleEnvelopeSeen({commit, getters}, envelope) { - // Change immediately and switch back on error - const oldState = envelope.flags.unseen - commit('flagEnvelope', { - envelope, - flag: 'unseen', - value: !oldState, - }) - - setEnvelopeFlag(envelope.accountId, envelope.folderId, envelope.id, 'unseen', !oldState).catch(e => { - console.error('could not toggle message unseen state', e) - - // Revert change - commit('flagEnvelope', { - envelope, - flag: 'unseen', - value: oldState, - }) - }) - }, - fetchMessage({commit}, uid) { - const {accountId, folderId, id} = parseUid(uid) - return fetchMessage(accountId, folderId, id).then(message => { - // Only commit if not undefined (not found) - if (message) { - commit('addMessage', { - accountId, - folderId, - message, - }) - } - - return message - }) - }, - replaceDraft({getters, commit}, {draft, uid, data}) { - commit('updateDraft', { - draft, - data, - newUid: uid, - }) - }, - deleteMessage({getters, commit}, {accountId, folderId, id}) { - commit('removeEnvelope', {accountId, folderId, id}) - - return deleteMessage(accountId, folderId, id) - .then(() => { - const folder = getters.getFolder(accountId, folderId) - if (!folder) { - logger.error('could not find folder', {accountId, folderId}) - return - } - commit('removeMessage', {accountId, folder, id}) - console.log('message removed') - }) - .catch(err => { - console.error('could not delete message', err) - const envelope = getters.getEnvelope(accountId, folderId, id) - if (envelope) { - commit('addEnvelope', {accountId, folderId, envelope}) - } else { - logger.error('could not find envelope', {accountId, folderId, id}) - } - throw err - }) - }, -} diff --git a/src/views/Home.vue b/src/views/Home.vue index e47e8caab8..97f7ad804b 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,5 +1,16 @@