diff --git a/www/calendar/export.js b/www/calendar/export.js index 0d5921e73..e470c5d33 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -99,13 +99,12 @@ define([ 'TRIGGER:'+str, 'END:VALARM' ]); - // XXX ACTION:EMAIL - // XXX ATTENDEE:mailto:xxx@xxx.xxx - // XXX SUMMARY:Alarm notification }); } - // XXX add hidden data (from imports) + if (Array.isArray(data.cp_hidden)) { + Array.prototype.push.apply(ICS, data.cp_hidden); + } ICS.push('END:VEVENT'); }); @@ -115,6 +114,86 @@ define([ return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' }); }; + module.import = function (content, id, cb) { + require(['/lib/ical.min.js'], function () { + var ICAL = window.ICAL; + var res = {}; + + try { + var jcalData = ICAL.parse(content); + var vcalendar = new ICAL.Component(jcalData); + } catch (e) { + return void cb(e); + } + + var method = vcalendar.getFirstPropertyValue('method'); + if (method !== "PUBLISH") { return void cb('NOT_SUPPORTED'); } + + var events = vcalendar.getAllSubcomponents('vevent'); + events.forEach(function (ev) { + var uid = ev.getFirstPropertyValue('uid'); + if (!uid) { return; } + + // Get start and end time + var isAllDay = false; + var start = ev.getFirstPropertyValue('dtstart'); + var end = ev.getFirstPropertyValue('dtend'); + if (start.isDate && end.isDate) { + isAllDay = true; + start = String(start); + end.adjust(-1); // Substract one day + end = String(end); + } else { + start = +start.toJSDate(); + end = +end.toJSDate(); + } + + // Store other properties + var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp']; + var hidden = []; + ev.getAllProperties().forEach(function (p) { + if (used.indexOf(p.name) !== -1) { return; } + // This is an unused property + hidden.push(p.toICALString()); + }); + + // Get reminders + var reminders = []; + ev.getAllSubcomponents('valarm').forEach(function (al) { + var action = al.getFirstPropertyValue('action'); + if (action !== 'DISPLAY') { + // XXX email: maybe keep a notification in CryptPad? + hidden.push(al.toString()); + return; + } + var trigger = al.getFirstPropertyValue('trigger'); + var minutes = -trigger.toSeconds() / 60; + if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); } + }); + + // Create event + res[uid] = { + calendarId: id, + id: uid, + category: 'time', + title: ev.getFirstPropertyValue('summary'), + location: ev.getFirstPropertyValue('location'), + isAllDay: isAllDay, + start: start, + end: end, + reminders: reminders, + cp_hidden: hidden + }; + + if (!hidden.length) { delete res[uid].cp_hidden; } + if (!reminders.length) { delete res[uid].reminders; } + + }); + + cb(null, res); + }); + }; + return module; }); diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 9986206f0..a33bff39c 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -107,6 +107,12 @@ Messages.calendar_allDay = "All day"; cb(null, obj); }); }; + var importICSCalendar = function (data, cb) { + APP.module.execCommand('IMPORT_ICS', data, function (obj) { + if (obj && obj.error) { return void cb(obj.error); } + cb(null, obj); + }); + }; var newEvent = function (data, cb) { APP.module.execCommand('CREATE_EVENT', data, function (obj) { if (obj && obj.error) { return void cb(obj.error); } @@ -466,7 +472,21 @@ Messages.calendar_allDay = "All day"; }, content: h('span', Messages.importButton), action: function (e) { - e.stopPropagation(); + UIElements.importContent('text/calendar', function (res) { + Export.import(res, id, function (err, json) { + if (err) { return void UI.warn(Messages.importError); } + importICSCalendar({ + id: id, + json: json + }, function (err) { + if (err) { return void UI.warn(Messages.error); } + UI.log(Messages.saved); + }); + + }); + }, { + accept: ['.ics'] + })(); return true; } }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6ec1cf420..578e716e3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -127,7 +127,7 @@ define([ dcAlert = undefined; }; - var importContent = function (type, f, cfg) { + var importContent = UIElements.importContent = function (type, f, cfg) { return function () { var $files = $('', {type:"file"}); if (cfg && cfg.accept) { diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index b9f25462c..bd17b37f8 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -434,6 +434,22 @@ ctx.calendars[channel] = { }); }; + var importICSCalendar = function (ctx, data, cId, cb) { + var id = data.id; + var c = ctx.calendars[id]; + if (!c || !c.proxy) { return void cb({error: "ENOENT"}); } + var json = data.json; + c.proxy.content = c.proxy.content || {}; + Object.keys(json).forEach(function (uid) { + c.proxy.content[uid] = json[uid]; + }); + + Realtime.whenRealtimeSyncs(c.lm.realtime, function () { + sendUpdate(ctx, c); + cb(); + }); + }; + var openCalendar = function (ctx, data, cId, cb) { var secret = Hash.getSecrets('calendar', data.hash, data.password); var hash = Hash.getEditHashFromKeys(secret); @@ -785,6 +801,10 @@ ctx.calendars[channel] = { if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } return void importCalendar(ctx, data, clientId, cb); } + if (cmd === 'IMPORT_ICS') { + if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } + return void importICSCalendar(ctx, data, clientId, cb); + } if (cmd === 'ADD') { if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } return void addCalendar(ctx, data, clientId, cb);