mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-07 08:27:03 +03:00
Added Nextcloud save to Calendar #569
This commit is contained in:
parent
e183764e51
commit
797c95877e
5 changed files with 154 additions and 19 deletions
|
|
@ -30,6 +30,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$this->addJs('js/messagelist.js');
|
$this->addJs('js/messagelist.js');
|
||||||
|
|
||||||
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
||||||
|
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
if ('PopupsCompose' === e.detail.viewModelTemplateID) {
|
if ('PopupsCompose' === e.detail.viewModelTemplateID) {
|
||||||
let view = e.detail;
|
let view = e.detail;
|
||||||
view.nextcloudAttach = () => {
|
view.nextcloudAttach = () => {
|
||||||
rl.ncFiles.selectFiles().then(files => {
|
rl.nextcloud.selectFiles().then(files => {
|
||||||
files && files.forEach(file => {
|
files && files.forEach(file => {
|
||||||
let attachment = view.addAttachmentHelper(
|
let attachment = view.addAttachmentHelper(
|
||||||
Jua?.randomId(),
|
Jua?.randomId(),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
.filter(v => v);
|
.filter(v => v);
|
||||||
if (hashes.length) {
|
if (hashes.length) {
|
||||||
view.saveNextcloudLoading(true);
|
view.saveNextcloudLoading(true);
|
||||||
rl.ncFiles.selectFolder().then(folder => {
|
rl.nextcloud.selectFolder().then(folder => {
|
||||||
if (folder) {
|
if (folder) {
|
||||||
rl.fetchJSON('./?/Json/&q[]=/0/', {}, {
|
rl.fetchJSON('./?/Json/&q[]=/0/', {}, {
|
||||||
Action: 'AttachmentsActions',
|
Action: 'AttachmentsActions',
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
view.nextcloudSaveMsg = () => {
|
view.nextcloudSaveMsg = () => {
|
||||||
rl.ncFiles.selectFolder().then(folder => {
|
rl.nextcloud.selectFolder().then(folder => {
|
||||||
let msg = view.message();
|
let msg = view.message();
|
||||||
folder && rl.pluginRemoteRequest(
|
folder && rl.pluginRemoteRequest(
|
||||||
(iError, data) => {
|
(iError, data) => {
|
||||||
|
|
@ -59,6 +59,28 @@
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
view.nextcloudICS = ko.computed(() => {
|
||||||
|
let msg = view.message();
|
||||||
|
return msg
|
||||||
|
? msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType)
|
||||||
|
: null;
|
||||||
|
}, {'pure':true});
|
||||||
|
|
||||||
|
view.nextcloudSaveICS = () => {
|
||||||
|
let attachment = view.nextcloudICS();
|
||||||
|
attachment && rl.nextcloud.selectCalendar().then(href => {
|
||||||
|
console.dir({href: href});
|
||||||
|
fetch(attachment.linkDownload(), {
|
||||||
|
mode: 'same-origin',
|
||||||
|
cache: 'no-cache',
|
||||||
|
redirect: 'error',
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
|
.then(text => rl.nextcloud.calendarPut(href, text));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -71,6 +93,11 @@
|
||||||
+ '<i class="fontastic" data-bind="visible: !saveNextcloudError(), css: {\'icon-spinner\': saveNextcloudLoading()}">💾</i>'
|
+ '<i class="fontastic" data-bind="visible: !saveNextcloudError(), css: {\'icon-spinner\': saveNextcloudLoading()}">💾</i>'
|
||||||
+ '<span class="g-ui-link" data-bind="click: saveNextcloud" data-i18n="NEXTCLOUD/SAVE_ATTACHMENTS"></span>'
|
+ '<span class="g-ui-link" data-bind="click: saveNextcloud" data-i18n="NEXTCLOUD/SAVE_ATTACHMENTS"></span>'
|
||||||
+ '</span>'));
|
+ '</span>'));
|
||||||
|
|
||||||
|
// https://github.com/nextcloud/calendar/issues/4684
|
||||||
|
// attachmentsControls.append(Element.fromHTML('<span data-bind="visible: nextcloudICS" data-icon="📅">'
|
||||||
|
// + '<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></span>'
|
||||||
|
// + '</span>'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgMenu = template.content.querySelector('#more-view-dropdown-id + menu');
|
const msgMenu = template.content.querySelector('#more-view-dropdown-id + menu');
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,34 @@
|
||||||
|
|
||||||
const
|
const
|
||||||
namespace = 'DAV:',
|
namespace = 'DAV:',
|
||||||
|
nsCalDAV = 'urn:ietf:params:xml:ns:caldav',
|
||||||
|
|
||||||
propfindBody = `<?xml version="1.0"?>
|
propfindFiles = `<?xml version="1.0"?>
|
||||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||||
<d:prop>
|
<d:prop>
|
||||||
<d:resourcetype />
|
<d:resourcetype/>
|
||||||
<oc:size />
|
<oc:size/>
|
||||||
<d:getcontentlength />
|
<d:getcontentlength/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`,
|
||||||
|
|
||||||
|
propfindCal = `<?xml version="1.0"?>
|
||||||
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:current-user-privilege-set/>
|
||||||
|
<d:displayname/>
|
||||||
</d:prop>
|
</d:prop>
|
||||||
</d:propfind>`,
|
</d:propfind>`,
|
||||||
|
|
||||||
xmlParser = new DOMParser(),
|
xmlParser = new DOMParser(),
|
||||||
pathRegex = /.*\/remote.php\/dav\/files\/[^/]+/g,
|
pathRegex = /.*\/remote.php\/dav\/[^/]+\/[^/]+/g,
|
||||||
|
|
||||||
getDavElementsByTagName = (parent, localName) => parent.getElementsByTagNameNS(namespace, localName),
|
getDavElementsByTagName = (parent, localName) => parent.getElementsByTagNameNS(namespace, localName),
|
||||||
getDavElementByTagName = (parent, localName) => getDavElementsByTagName(parent, localName)?.item(0),
|
getDavElementByTagName = (parent, localName) => getDavElementsByTagName(parent, localName)?.item(0),
|
||||||
getElementByTagName = (parent, localName) => +parent.getElementsByTagName(localName)?.item(0),
|
getElementByTagName = (parent, localName) => +parent.getElementsByTagName(localName)?.item(0),
|
||||||
|
|
||||||
davFetch = (path, options) => {
|
davFetch = (mode, path, options) => {
|
||||||
if (!parent.OC.requestToken) {
|
if (!parent.OC.requestToken) {
|
||||||
return Promise.reject(new Error('OC.requestToken missing'));
|
return Promise.reject(new Error('OC.requestToken missing'));
|
||||||
}
|
}
|
||||||
|
|
@ -32,21 +42,23 @@ const
|
||||||
headers: {}
|
headers: {}
|
||||||
}, options);
|
}, options);
|
||||||
options.headers.requesttoken = parent.OC.requestToken;
|
options.headers.requesttoken = parent.OC.requestToken;
|
||||||
return fetch(cfg.WebDAV + '/files/' + cfg.UID + path, options);
|
return fetch(cfg.WebDAV + '/' + mode + '/' + cfg.UID + path, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
createDirectory = path => davFetch(path, { method: 'MKCOL' }),
|
davFetchFiles = (path, options) => davFetch('files', path, options),
|
||||||
|
|
||||||
|
createDirectory = path => davFetchFiles(path, { method: 'MKCOL' }),
|
||||||
|
|
||||||
fetchFiles = path => {
|
fetchFiles = path => {
|
||||||
if (!parent.OC.requestToken) {
|
if (!parent.OC.requestToken) {
|
||||||
return Promise.reject(new Error('OC.requestToken missing'));
|
return Promise.reject(new Error('OC.requestToken missing'));
|
||||||
}
|
}
|
||||||
return davFetch(path, {
|
return davFetchFiles(path, {
|
||||||
method: 'PROPFIND',
|
method: 'PROPFIND',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/xml; charset=utf-8'
|
'Content-Type': 'application/xml; charset=utf-8'
|
||||||
},
|
},
|
||||||
body: propfindBody
|
body: propfindFiles
|
||||||
})
|
})
|
||||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
.then(text => {
|
.then(text => {
|
||||||
|
|
@ -61,10 +73,10 @@ const
|
||||||
const
|
const
|
||||||
e = responseList.item(i),
|
e = responseList.item(i),
|
||||||
elem = {
|
elem = {
|
||||||
name: decodeURIComponent(getDavElementByTagName(e, 'href').innerHTML)
|
name: getDavElementByTagName(e, 'href').textContent
|
||||||
.replace(pathRegex, '').replace(/\/$/, ''),
|
.replace(pathRegex, '').replace(/\/$/, ''),
|
||||||
isFile: false
|
isFile: false
|
||||||
}
|
};
|
||||||
if (getDavElementsByTagName(getDavElementByTagName(e, 'resourcetype'), 'collection').length) {
|
if (getDavElementsByTagName(getDavElementByTagName(e, 'resourcetype'), 'collection').length) {
|
||||||
// skip current directory
|
// skip current directory
|
||||||
if (elem.name === path) {
|
if (elem.name === path) {
|
||||||
|
|
@ -72,8 +84,8 @@ const
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elem.isFile = true;
|
elem.isFile = true;
|
||||||
elem.size = getDavElementByTagName(e, 'getcontentlength')?.innerHTML
|
elem.size = getDavElementByTagName(e, 'getcontentlength')?.textContent
|
||||||
|| getElementByTagName(e, 'oc:size')?.innerHTML;
|
|| getElementByTagName(e, 'oc:size')?.textContent;
|
||||||
}
|
}
|
||||||
elemList.push(elem);
|
elemList.push(elem);
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +214,100 @@ close() {}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.ncFiles = {
|
class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
||||||
|
constructor() {
|
||||||
|
super('NextcloudCalendars');
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuild(dom) {
|
||||||
|
this.tree = dom.querySelector('#sm-nc-calendars');
|
||||||
|
this.tree.addEventListener('click', event => {
|
||||||
|
let el = event.target;
|
||||||
|
if (el.matches('button')) {
|
||||||
|
this.select = el.href;
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Happens after showModal()
|
||||||
|
beforeShow(fResolve) {
|
||||||
|
this.select = '';
|
||||||
|
this.fResolve = fResolve;
|
||||||
|
this.tree.innerHTML = '';
|
||||||
|
davFetch('calendars', '/', {
|
||||||
|
method: 'PROPFIND',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: propfindCal
|
||||||
|
})
|
||||||
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
|
.then(text => {
|
||||||
|
const
|
||||||
|
responseList = getDavElementsByTagName(
|
||||||
|
xmlParser.parseFromString(text, 'application/xml').documentElement,
|
||||||
|
'response'
|
||||||
|
);
|
||||||
|
for (let i = 0; i < responseList.length; ++i) {
|
||||||
|
const e = responseList.item(i);
|
||||||
|
if (getDavElementByTagName(e, 'resourcetype').getElementsByTagNameNS(nsCalDAV, 'calendar').length) {
|
||||||
|
// && getDavElementsByTagName(getDavElementByTagName(e, 'current-user-privilege-set'), 'write').length) {
|
||||||
|
const li = document.createElement('li'),
|
||||||
|
btn = document.createElement('button');
|
||||||
|
li.textContent = getDavElementByTagName(e, 'displayname').textContent;
|
||||||
|
btn.href = getDavElementByTagName(e, 'href').textContent
|
||||||
|
.replace(pathRegex, '').replace(/\/$/, '');
|
||||||
|
btn.textContent = 'select';
|
||||||
|
btn.className = 'button-vue';
|
||||||
|
btn.style.marginLeft = '1em';
|
||||||
|
li.append(btn);
|
||||||
|
this.tree.append(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
this.fResolve(this.select);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
beforeShow() {} // Happens before showModal()
|
||||||
|
onShow() {} // Happens after showModal()
|
||||||
|
afterShow() {} // Happens after showModal() animation transitionend
|
||||||
|
onHide() {} // Happens before animation transitionend
|
||||||
|
afterHide() {} // Happens after animation transitionend
|
||||||
|
close() {}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.nextcloud = {
|
||||||
|
selectCalendar: () =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
NextcloudCalendarsPopupView.showModal([
|
||||||
|
href => resolve(href),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
|
||||||
|
calendarPut: (path, event) => {
|
||||||
|
// Validation error in iCalendar: A calendar object on a CalDAV server MUST NOT have a METHOD property.
|
||||||
|
event = event.replace(/METHOD:.+\r?\n/i, '');
|
||||||
|
|
||||||
|
let m = event.match(/UID:(.+)/);
|
||||||
|
davFetch('calendars', path + '/' + m[1] + '.ics', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/calendar'
|
||||||
|
},
|
||||||
|
body: event
|
||||||
|
})
|
||||||
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
|
.then(text => {
|
||||||
|
console.dir({event_response:text});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
selectFolder: () =>
|
selectFolder: () =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
NextcloudFilesPopupView.showModal([
|
NextcloudFilesPopupView.showModal([
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
[NEXTCLOUD]
|
[NEXTCLOUD]
|
||||||
SAVE_ATTACHMENTS = "Save in Nextcloud"
|
SAVE_ATTACHMENTS = "Save in Nextcloud"
|
||||||
SAVE_EML = "Save as .eml in Nextcloud"
|
SAVE_EML = "Save as .eml in Nextcloud"
|
||||||
|
SAVE_ICS = "Put in Calendar"
|
||||||
SELECT_FOLDER = "Select folder"
|
SELECT_FOLDER = "Select folder"
|
||||||
SELECT_FILES = "Select file(s)"
|
SELECT_FILES = "Select file(s)"
|
||||||
ATTACH_FILES = "Attach Nextcloud files"
|
ATTACH_FILES = "Attach Nextcloud files"
|
||||||
|
SELECT_CALENDAR = "Select calendar"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue