diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less
index 969c873c7..ffe061fa3 100644
--- a/customize.dist/src/less2/include/forms.less
+++ b/customize.dist/src/less2/include/forms.less
@@ -122,7 +122,7 @@
&.small {
line-height: initial;
padding: 5px;
- height: auto;
+ height: auto !important;
}
&:hover, &:not(:disabled):not(.disabled):active, &:focus {
diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less
index ccb4564ab..407ec8004 100644
--- a/www/calendar/app-calendar.less
+++ b/www/calendar/app-calendar.less
@@ -178,6 +178,44 @@
}
}
+ .cp-calendar-add-notif {
+ flex-flow: column;
+ align-items: baseline !important;
+ margin: 10px 0;
+ .cp-notif-label {
+ color: @cp_sidebar-hint;
+ margin-right: 20px;
+ }
+ * {
+ font-size: @colortheme_app-font-size;
+ font-weight: normal;
+ }
+ & > div {
+ display: flex;
+ }
+ .cp-calendar-notif-list {
+ display: flex;
+ flex-flow: column;
+ .cp-notif-entry {
+ span:not(:last-child) {
+ margin-right: 20px;
+ }
+ }
+ }
+ .cp-notif-empty {
+ display: none;
+ }
+ .cp-calendar-notif-list:empty ~ .cp-notif-empty {
+ display: block;
+ }
+ .cp-calendar-notif-form {
+ align-items: center;
+ input {
+ width: 100px;
+ }
+ }
+ }
+
.cp-calendar-close {
height: auto;
line-height: initial;
diff --git a/www/calendar/export.js b/www/calendar/export.js
new file mode 100644
index 000000000..6019290c6
--- /dev/null
+++ b/www/calendar/export.js
@@ -0,0 +1,199 @@
+// This file is used when a user tries to export the entire CryptDrive.
+// Calendars will be exported using this format instead of plain text.
+define([
+ '/customize/pages.js',
+], function (Pages) {
+ var module = {};
+
+ var getICSDate = function (str) {
+ var date = new Date(str);
+
+ var m = date.getUTCMonth() + 1;
+ var d = date.getUTCDate();
+ var h = date.getUTCHours();
+ var min = date.getUTCMinutes();
+
+ var year = date.getUTCFullYear().toString();
+ var month = m < 10 ? "0" + m : m.toString();
+ var day = d < 10 ? "0" + d : d.toString();
+ var hours = h < 10 ? "0" + h : h.toString();
+ var minutes = min < 10 ? "0" + min : min.toString();
+
+ return year + month + day + "T" + hours + minutes + "00Z";
+ }
+
+
+ var getDate = function (str, end) {
+ var date = new Date(str);
+ if (end) {
+ date.setDate(date.getDate() + 1);
+ }
+ var m = date.getUTCMonth() + 1;
+ var d = date.getUTCDate();
+
+ var year = date.getUTCFullYear().toString();
+ var month = m < 10 ? "0" + m : m.toString();
+ var day = d < 10 ? "0" + d : d.toString();
+
+ return year+month+day;
+ };
+
+ var MINUTE = 60;
+ var HOUR = MINUTE * 60;
+ var DAY = HOUR * 24;
+
+
+ module.main = function (userDoc) {
+ var content = userDoc.content;
+ var md = userDoc.metadata;
+
+ var ICS = [
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//CryptPad//CryptPad Calendar '+Pages.versionString+'//EN',
+ 'METHOD:PUBLISH',
+ ];
+
+ Object.keys(content).forEach(function (uid) {
+ var data = content[uid];
+ // DTSTAMP: now...
+ // UID: uid
+ var start, end;
+ if (data.isAllDay && data.startDay && data.endDay) {
+ start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
+ end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
+ } else {
+ start = "DTSTART:"+getICSDate(data.start);
+ end = "DTEND:"+getICSDate(data.end);
+ }
+
+ Array.prototype.push.apply(ICS, [
+ 'BEGIN:VEVENT',
+ 'DTSTAMP:'+getICSDate(+new Date()),
+ 'UID:'+uid,
+ start,
+ end,
+ 'SUMMARY:'+ data.title,
+ 'LOCATION:'+ data.location,
+ ]);
+
+ if (Array.isArray(data.reminders)) {
+ data.reminders.forEach(function (valueMin) {
+ var time = valueMin * 60;
+ var days = Math.floor(time / DAY);
+ time -= days * DAY;
+ var hours = Math.floor(time / HOUR);
+ time -= hours * HOUR;
+ var minutes = Math.floor(time / MINUTE);
+ time -= minutes * MINUTE;
+ var seconds = time;
+
+ var str = "-P" + days + "D";
+ if (hours || minutes || seconds) {
+ str += "T" + hours + "H" + minutes + "M" + seconds + "S";
+ }
+ Array.prototype.push.apply(ICS, [
+ 'BEGIN:VALARM',
+ 'ACTION:DISPLAY',
+ 'DESCRIPTION:This is an event reminder',
+ 'TRIGGER:'+str,
+ 'END:VALARM'
+ ]);
+ });
+ }
+
+ if (Array.isArray(data.cp_hidden)) {
+ Array.prototype.push.apply(ICS, data.cp_hidden);
+ }
+
+ ICS.push('END:VEVENT');
+ });
+
+ ICS.push('END:VCALENDAR');
+
+ 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') {
+ // Email notification: keep it in "hidden" and create a cryptpad notification
+ hidden.push(al.toString());
+ }
+ 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 bb5bed41c..43e5f15df 100644
--- a/www/calendar/inner.js
+++ b/www/calendar/inner.js
@@ -1,5 +1,6 @@
define([
'jquery',
+ 'json.sortify',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js',
'/bower_components/nthen/index.js',
@@ -15,17 +16,20 @@ define([
'/customize/messages.js',
'/customize/application_config.js',
'/lib/calendar/tui-calendar.min.js',
+ '/calendar/export.js',
'/common/inner/share.js',
'/common/inner/access.js',
'/common/inner/properties.js',
'/common/jscolor.js',
+ '/bower_components/file-saver/FileSaver.min.js',
'css!/lib/calendar/tui-calendar.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/calendar/app-calendar.less',
], function (
$,
+ JSONSortify,
Crypto,
Toolbar,
nThen,
@@ -41,9 +45,11 @@ define([
Messages,
AppConfig,
Calendar,
+ Export,
Share, Access, Properties
)
{
+ var SaveAs = window.saveAs;
var APP = window.APP = {
calendars: {}
};
@@ -77,6 +83,14 @@ Messages.calendar_loc = "Location";
Messages.calendar_location = "Location: {0}";
Messages.calendar_allDay = "All day";
+Messages.calendar_minutes = "Minutes";
+Messages.calendar_hours = "Hours";
+Messages.calendar_days = "Days";
+
+Messages.calendar_notifications = "Reminders";
+Messages.calendar_addNotification = "Add reminder";
+Messages.calendar_noNotification = "None";
+
var onCalendarsUpdate = Util.mkEvent();
var newCalendar = function (data, cb) {
@@ -103,6 +117,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); }
@@ -230,11 +250,17 @@ Messages.calendar_allDay = "All day";
})()) { getTime = undefined; }
var templates = {
+ popupSave: function (obj) {
+ APP.editModalData = obj.data && obj.data.root;
+ return Messages.settings_save;
+ },
+ popupUpdate: function(obj) {
+ APP.editModalData = obj.data && obj.data.root;
+ return Messages.calendar_update;
+ },
monthGridHeaderExceed: function(hiddenSchedules) {
return '' + Messages._getKey('calendar_more', [hiddenSchedules]) + '';
},
- popupSave: function () { return Messages.settings_save; },
- popupUpdate: function() { return Messages.calendar_update; },
popupEdit: function() { return Messages.poll_edit; },
popupDelete: function() { return Messages.kanban_delete; },
popupDetailLocation: function(schedule) {
@@ -453,6 +479,79 @@ Messages.calendar_allDay = "All day";
return true;
}
});
+
+ if (!data.readOnly) {
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'fa fa-upload',
+ },
+ content: h('span', Messages.importButton),
+ action: function (e) {
+ 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;
+ }
+ });
+ }
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'fa fa-download',
+ },
+ content: h('span', Messages.exportButton),
+ action: function (e) {
+ e.stopPropagation();
+ var cal = APP.calendars[id];
+ var suggestion = Util.find(cal, ['content', 'metadata', 'title']);
+ var types = [];
+ types.push({
+ tag: 'a',
+ attributes: {
+ 'data-value': '.ics',
+ 'href': '#'
+ },
+ content: '.ics'
+ });
+ var dropdownConfig = {
+ text: '.ics', // Button initial text
+ caretDown: true,
+ options: types, // Entries displayed in the menu
+ isSelect: true,
+ initialValue: '.ics',
+ common: common
+ };
+ var $select = UIElements.createDropdown(dropdownConfig);
+ UI.prompt(Messages.exportPrompt,
+ Util.fixFileName(suggestion), function (filename)
+ {
+ if (!(typeof(filename) === 'string' && filename)) { return; }
+ var ext = $select.getValue();
+ filename = filename + ext;
+ var blob = Export.main(cal.content);
+ SaveAs(blob, filename);
+ }, {
+ typeInput: $select[0]
+ });
+ return true;
+ }
+ });
+
+
+
options.push({
tag: 'a',
attributes: {
@@ -498,7 +597,7 @@ Messages.calendar_allDay = "All day";
if (cal.owned) {
key += Messages.calendar_deleteOwned;
}
- UI.confirm(Messages.calendar_deleteConfirm, function (yes) {
+ UI.confirm(key, function (yes) {
if (!yes) { return; }
deleteCalendar({
id: id,
@@ -681,6 +780,7 @@ Messages.calendar_allDay = "All day";
// ie: recurrenceRule: DAILY|{uid}
// Use template to hide "recurrenceRule" from the detailPopup or at least to use
// a non technical value
+ var reminders = APP.notificationsEntries;
var startDate = event.start._date;
var endDate = event.end._date;
@@ -694,6 +794,7 @@ Messages.calendar_allDay = "All day";
start: +startDate,
isAllDay: event.isAllDay,
end: +endDate,
+ reminders: reminders,
};
newEvent(schedule, function (err) {
@@ -711,6 +812,12 @@ Messages.calendar_allDay = "All day";
if (changes.start) { changes.start = +new Date(changes.start._date); }
var old = event.schedule;
+ var oldReminders = Util.find(APP.calendars, [old.calendarId, 'content', 'content', old.id, 'reminders']);
+ var reminders = APP.notificationsEntries;
+ if (JSONSortify(oldReminders || []) !== JSONSortify(reminders)) {
+ changes.reminders = reminders;
+ }
+
updateEvent({
ev: old,
changes: changes
@@ -811,6 +918,113 @@ Messages.calendar_allDay = "All day";
};
+ var parseNotif = function (minutes) {
+ var res = {
+ unit: 'minutes',
+ value: minutes
+ };
+ var hours = minutes / 60;
+ if (!Number.isInteger(hours)) { return res; }
+ res.unit = 'hours';
+ res.value = hours;
+ var days = hours / 24;
+ if (!Number.isInteger(days)) { return res; }
+ res.unit = 'days';
+ res.value = days;
+ return res;
+ };
+ var getNotificationDropdown = function () {
+ var ev = APP.editModalData;
+ var calId = ev.selectedCal.id;
+ // XXX DEFAULT HERE [10] ==> 10 minutes before the event
+ var oldReminders = Util.find(APP.calendars, [calId, 'content', 'content', ev.id, 'reminders']) || [10];
+ APP.notificationsEntries = [];
+ var number = h('input.tui-full-calendar-content', {
+ type: "number",
+ value: 10,
+ min: 1,
+ max: 60
+ });
+ var $number = $(number);
+ var options = ['minutes', 'hours', 'days'].map(function (k) {
+ return {
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-reminder',
+ 'data-value': k,
+ 'href': '#',
+ },
+ content: Messages['calendar_'+k]
+ // Messages.calendar_minutes
+ // Messages.calendar_hours
+ // Messages.calendar_days
+ };
+ });
+ var dropdownConfig = {
+ text: Messages.calendar_minutes,
+ options: options, // Entries displayed in the menu
+ isSelect: true,
+ common: common,
+ buttonCls: 'btn btn-secondary',
+ caretDown: true,
+ };
+
+ var $block = UIElements.createDropdown(dropdownConfig);
+ $block.setValue('minutes');
+ var $types = $block.find('a');
+ $types.click(function () {
+ var mode = $(this).attr('data-value');
+ var max = mode === "minutes" ? 60 : 24;
+ $number.attr('max', max);
+ if ($number.val() > max) { $number.val(max); }
+ });
+ var addNotif = h('button.btn.btn-primary.fa.fa-plus');
+ var $list = $(h('div.cp-calendar-notif-list'));
+ var listContainer = h('div.cp-calendar-notif-list-container', [
+ h('span.cp-notif-label', Messages.calendar_notifications),
+ $list[0],
+ h('span.cp-notif-empty', Messages.calendar_noNotification)
+ ]);
+ var addNotification = function (unit, value) {
+ var unitValue = (unit === "minutes") ? 1 : (unit === "hours" ? 60 : (60*24));
+ var del = h('button.btn.btn-danger.small.fa.fa-times');
+ var minutes = value * unitValue;
+ if ($list.find('[data-minutes="'+minutes+'"]').length) { return; }
+ var span = h('span.cp-notif-entry', {
+ 'data-minutes': minutes
+ }, [
+ h('span', value),
+ h('span', Messages['calendar_'+unit]),
+ del
+ ]);
+ $(del).click(function () {
+ $(span).remove();
+ var idx = APP.notificationsEntries.indexOf(minutes);
+ APP.notificationsEntries.splice(idx, 1);
+ });
+ $list.append(span);
+ APP.notificationsEntries.push(minutes);
+ };
+ $(addNotif).click(function () {
+ var unit = $block.getValue();
+ var value = $number.val();
+ addNotification(unit, value);
+ });
+ oldReminders.forEach(function (minutes) {
+ var p = parseNotif(minutes);
+ addNotification(p.unit, p.value);
+ });
+ return h('div.tui-full-calendar-popup-section.cp-calendar-add-notif', [
+ listContainer,
+ h('div.cp-calendar-notif-form', [
+ h('span.cp-notif-label', Messages.calendar_addNotification),
+ number,
+ $block[0],
+ addNotif
+ ])
+ ]);
+ };
+
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
@@ -900,6 +1114,10 @@ Messages.calendar_allDay = "All day";
var isUpdate = Boolean($el.find('#tui-full-calendar-schedule-title').val());
if (!isUpdate) { $el.find('.tui-full-calendar-dropdown-menu li').first().click(); }
+ var $button = $el.find('.tui-full-calendar-section-button-save');
+ var div = getNotificationDropdown();
+ $button.before(div);
+
var $cbox = $el.find('#tui-full-calendar-schedule-allday');
var $start = $el.find('.tui-full-calendar-section-start-date');
var $dash = $el.find('.tui-full-calendar-section-date-dash');
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 602c52100..471bd5011 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/cryptpad-common.js b/www/common/cryptpad-common.js
index 04dabb02d..7d6fee8b7 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -2288,7 +2288,8 @@ define([
cache: rdyCfg.cache,
noDrive: rdyCfg.noDrive,
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
- driveEvents: !rdyCfg.noDrive //rdyCfg.driveEvents // Boolean
+ driveEvents: !rdyCfg.noDrive, //rdyCfg.driveEvents // Boolean
+ lastVisit: Number(localStorage.lastVisit) || undefined
};
common.userHash = userHash;
@@ -2560,6 +2561,12 @@ define([
AppConfig.afterLogin(common, waitFor());
}
}).nThen(function () {
+ // Last visit is used to warn you about missed events from your calendars
+ localStorage.lastVisit = +new Date();
+ setInterval(function () {
+ // Bump last visit every minute
+ localStorage.lastVisit = +new Date();
+ }, 60000);
f(void 0, env);
if (typeof(window.onhashchange) === 'function') { window.onhashchange(); }
});
diff --git a/www/common/notifications.js b/www/common/notifications.js
index 34c00b092..7a64815cc 100644
--- a/www/common/notifications.js
+++ b/www/common/notifications.js
@@ -461,6 +461,76 @@ define([
}
};
+ Messages.reminder_missed = "You missed {0} on {1}"; // XXX
+ Messages.reminder_now = "{0} is starting!"; // XXX
+ Messages.reminder_inProgress = "{0} has started on {1}"; // XXX
+ Messages.reminder_inProgressAllDay = "{0} is happening today"; // XXX
+ Messages.reminder_minutes = "{0} will start in {1} minutes!"; // XXX
+ Messages.reminder_time = "{0} will start today at {1}!"; // XXX
+ Messages.reminder_date = "{0} will start on {1}!"; // XXX
+ handlers['REMINDER'] = function (common, data) {
+ var content = data.content;
+ var msg = content.msg.content;
+ var missed = content.msg.missed;
+ var start = msg.start;
+ var title = Util.fixHTML(msg.title);
+ content.getFormatText = function () {
+ var now = +new Date();
+
+ // Events that have already started
+ var wasRefresh = content.autorefresh;
+ content.autorefresh = false;
+
+ var nowDateStr = new Date().toLocaleDateString();
+ var startDate = new Date(start);
+ if (msg.isAllDay && msg.startDay) {
+ startDate = new Date(msg.startDay);
+ }
+
+ // Missed events
+ if (start < now && missed) {
+ return Messages._getKey('reminder_missed', [title, startDate.toLocaleString()]);
+ }
+ // Starting now
+ if (start < now && wasRefresh) {
+ return Messages._getKey('reminder_now', [title]);
+ }
+ // In progress, is all day
+ if (start < now && msg.isAllDay) {
+ return Messages._getKey('reminder_inProgressAllDay', [title]);
+ }
+ // In progress, normal event
+ if (start < now) {
+ return Messages._getKey('reminder_inProgress', [title, startDate.toLocaleString()]);
+ }
+
+ // Not started yet
+
+ // No precise time for allDay events
+ if (msg.isAllDay) {
+ return Messages._getKey('reminder_date', [title, startDate.toLocaleDateString()]);
+ }
+
+ // In less than an hour: show countdown in minutes
+ if ((start - now) < 3600000) {
+ var minutes = Math.round((start - now) / 60000);
+ content.autorefresh = true;
+ return Messages._getKey('reminder_minutes', [title, minutes]);
+ }
+
+ // Not today: show full date
+ if (nowDateStr !== startDate.toLocaleDateString()) {
+ return Messages._getKey('reminder_date', [title, startDate.toLocaleString()]);
+ }
+
+ // Today: show time
+ return Messages._getKey('reminder_time', [title, startDate.toLocaleTimeString()]);
+ };
+ if (!content.archived) {
+ content.dismissHandler = defaultDismiss(common, data);
+ }
+ };
+
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
return {
diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js
index b9f25462c..5f39004c1 100644
--- a/www/common/outer/calendar.js
+++ b/www/common/outer/calendar.js
@@ -12,60 +12,6 @@ define([
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {};
-
-/* TODO
-* Calendar
-{
- href,
- roHref,
- channel, (pinning)
- title, (when created from the UI, own calendar has no title)
- color
-}
-
-
-* Own drive
-{
- calendars: {
- uid: calendar,
- uid: calendar
- }
-}
-
-* Team drive
-{
- calendars: {
- uid: calendar,
- uid: calendar
- }
-}
-
-* Calendars are listmap
-{
- content: {},
- metadata: {
- title: "pewpewpew"
- }
-}
-
-ctx.calendars[channel] = {
- lm: lm,
- proxy: lm.proxy?
- stores: [teamId, teamId, 1]
-}
-
-* calendar app can subscribe to this module
- * when a listmap changes, push an update for this calendar to subscribed tabs
-* Ability to open a calendar not stored in the stores but from its URL directly
-* No "userlist" visible in the UI
-* No framework
-
-
-
-
-
-*/
-
var getStore = function (ctx, id) {
if (!id || id === 1) {
return ctx.store;
@@ -109,6 +55,15 @@ ctx.calendars[channel] = {
}, ctx.clients);
};
+ var clearReminders = function (ctx, id) {
+ var calendar = ctx.calendars[id];
+ if (!calendar || !calendar.reminders) { return; }
+ // Clear existing reminders
+ Object.keys(calendar.reminders).forEach(function (uid) {
+ if (!Array.isArray(calendar.reminders[uid])) { return; }
+ calendar.reminders[uid].forEach(function (to) { clearTimeout(to); });
+ });
+ };
var closeCalendar = function (ctx, id) {
var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; }
@@ -116,6 +71,7 @@ ctx.calendars[channel] = {
// If the calendar doesn't exist in any other team, stop it and delete it from ctx
if (!ctxCal.stores.length) {
ctxCal.lm.stop();
+ clearReminders(ctx, id);
delete ctx.calendars[id];
}
};
@@ -133,6 +89,105 @@ ctx.calendars[channel] = {
if (cal.title !== data.title) { cal.title = data.title; }
});
};
+
+ var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
+ var now = +new Date();
+ var ev = Util.clone(_ev);
+ var uid = ev.id;
+
+ // Clear reminders for this event
+ if (Array.isArray(reminders[uid])) {
+ reminders[uid].forEach(function (to) { clearTimeout(to); });
+ }
+ reminders[uid] = [];
+
+ var last = ctx.store.data.lastVisit;
+
+ if (ev.isAllDay) {
+ if (ev.startDay) { ev.start = +new Date(ev.startDay); }
+ if (ev.endDay) {
+ var endDate = new Date(ev.endDay);
+ endDate.setHours(23);
+ endDate.setMinutes(59);
+ endDate.setSeconds(59);
+ ev.end = +endDate;
+ }
+ }
+
+ // XXX add a limit to make sure we don't go too far in the past?
+ var missed = useLastVisit && ev.start > last && ev.end <= now;
+ if (ev.end <= now && !missed) {
+ // No reminder for past events
+ delete reminders[uid];
+ return;
+ }
+
+ var send = function () {
+ var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
+ if (hide) { return; }
+ var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
+ ctx.store.mailbox.showMessage('reminders', {
+ msg: {
+ ctime: ctime,
+ type: "REMINDER",
+ missed: Boolean(missed),
+ content: ev
+ },
+ hash: 'REMINDER|'+uid
+ }, null, function () {
+ });
+ };
+ var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
+
+ var notifs = ev.reminders || [];
+ notifs.sort(function (a, b) {
+ return a - b;
+ });
+
+ notifs.some(function (delayMinutes) {
+ var delay = delayMinutes * 60000;
+ var time = now + delay;
+
+ // setTimeout only work with 32bit timeout values. If the event is too far away,
+ // ignore this event for now
+ // FIXME: call this function again in xxx days to reload these missing timeout?
+ if (ev.start - time >= 2147483647) { return true; }
+
+ // If we're too late to send a notification, send it instantly and ignore
+ // all notifications that were supposed to be sent even earlier
+ if (ev.start <= time) {
+ sendNotif();
+ return true;
+ }
+
+ // It starts in more than "delay": prepare the notification
+ reminders[uid].push(setTimeout(function () {
+ sendNotif();
+ }, (ev.start - time)));
+ });
+ };
+ var addReminders = function (ctx, id, ev) {
+ var calendar = ctx.calendars[id];
+ if (!calendar || !calendar.reminders) { return; }
+ if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
+
+ updateEventReminders(ctx, calendar.reminders, ev);
+ };
+ var addInitialReminders = function (ctx, id, useLastVisit) {
+ var calendar = ctx.calendars[id];
+ if (!calendar || !calendar.reminders) { return; }
+ if (Object.keys(calendar.reminders).length) { return; } // Already initialized
+
+ // No reminders for calendars not stored
+ if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
+
+ // Re-add all reminders
+ var content = Util.find(calendar, ['proxy', 'content']);
+ if (!content) { return; }
+ Object.keys(content).forEach(function (uid) {
+ updateEventReminders(ctx, calendar.reminders, content[uid], useLastVisit);
+ });
+ };
var openChannel = function (ctx, cfg, _cb) {
var cb = Util.once(Util.mkAsync(_cb || function () {}));
var teamId = cfg.storeId;
@@ -192,6 +247,7 @@ ctx.calendars[channel] = {
readOnly: !data.href,
stores: [teamId],
roStores: data.href ? [] : [teamId],
+ reminders: {},
hashes: {}
};
@@ -231,6 +287,7 @@ ctx.calendars[channel] = {
if (c.lm) { c.lm.stop(); }
c.stores = [];
sendUpdate(ctx, c);
+ clearReminders(ctx, channel);
delete ctx.calendars[channel];
};
@@ -289,6 +346,7 @@ ctx.calendars[channel] = {
c.cacheready = true;
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
+ addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) {
var md = info.metadata;
c.owners = md.owners || [];
@@ -305,9 +363,25 @@ ctx.calendars[channel] = {
}
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
+ addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
+ }).on('change', ['content'], function (o, n, p) {
+ if (p.length === 2 && n && !o) { // New event
+ addReminders(ctx, channel, n);
+ }
+ if (p.length === 2 && !n && o) { // Deleted event
+ addReminders(ctx, channel, {
+ id: p[1],
+ start: 0
+ });
+ }
+ if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
+ setTimeout(function () {
+ addReminders(ctx, channel, proxy.content[p[1]]);
+ });
+ }
}).on('change', ['metadata'], function () {
// if title or color have changed, update our local values
var md = proxy.metadata;
@@ -402,6 +476,7 @@ ctx.calendars[channel] = {
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: storeId,
+ lastVisitNotif: true,
data: cal
});
});
@@ -434,6 +509,23 @@ 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];
+ addReminders(ctx, id, 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);
@@ -638,6 +730,7 @@ ctx.calendars[channel] = {
c.proxy.content[data.id] = data;
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
+ addReminders(ctx, id, data);
sendUpdate(ctx, c);
cb();
});
@@ -686,6 +779,18 @@ ctx.calendars[channel] = {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
}).nThen(function () {
+ if (newC) {
+ // Move reminders to the new calendar
+ addReminders(ctx, id, {
+ id: ev.id,
+ start: 0
+ });
+ addReminders(ctx, ev.calendarId, ev);
+ } else if (changes.start || changes.reminders || changes.isAllDay) {
+ // Update reminders
+ addReminders(ctx, id, ev);
+ }
+
sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); }
cb();
@@ -698,6 +803,10 @@ ctx.calendars[channel] = {
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
+ addReminders(ctx, id, {
+ id: data.id,
+ start: 0
+ });
sendUpdate(ctx, c);
cb();
});
@@ -721,7 +830,7 @@ ctx.calendars[channel] = {
emit: emit,
onReady: Util.mkEvent(true),
calendars: {},
- clients: [],
+ clients: []
};
initializeCalendars(ctx, waitFor(function (err) {
@@ -785,6 +894,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);
diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js
index 7d0653e76..a7269c6e5 100644
--- a/www/common/outer/mailbox.js
+++ b/www/common/outer/mailbox.js
@@ -169,6 +169,18 @@ proxy.mailboxes = {
var dismiss = function (ctx, data, cId, cb) {
var type = data.type;
var hash = data.hash;
+
+ // Reminder messages don't persist
+ if (/^REMINDER\|/.test(hash)) {
+ cb();
+ delete ctx.boxes.reminders.content[hash];
+ hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
+ return clientId !== cId;
+ }));
+ return;
+ }
+
+
var box = ctx.boxes[type];
if (!box) { return void cb({error: 'NOT_LOADED'}); }
var m = box.data || {};
@@ -488,7 +500,13 @@ proxy.mailboxes = {
msg: ctx.boxes[type].content[h],
hash: h
};
- showMessage(ctx, type, message, cId);
+ showMessage(ctx, type, message, cId, function (obj) {
+ if (obj.error) { return; }
+ // Notify only if "requiresNotif" is true
+ if (!message.msg || !message.msg.requiresNotif) { return; }
+ Notify.system(undefined, obj.msg);
+ delete message.msg.requiresNotif;
+ });
});
});
// Subscribe to new notifications
@@ -528,6 +546,10 @@ proxy.mailboxes = {
initializeHistory(ctx);
}
+ ctx.boxes.reminders = {
+ content: {}
+ };
+
Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; }
var m = mailboxes[key];
@@ -568,6 +590,21 @@ proxy.mailboxes = {
});
};
+ mailbox.showMessage = function (type, msg, cId, cb) {
+ if (type === "reminders" && msg) {
+ ctx.boxes.reminders.content[msg.hash] = msg.msg;
+ if (!ctx.clients.length) {
+ ctx.boxes.reminders.content[msg.hash].requiresNotif = true;
+ }
+ // Hide existing messages for this event
+ hideMessage(ctx, type, msg.hash, ctx.clients);
+ }
+ showMessage(ctx, type, msg, cId, function (obj) {
+ Notify.system(undefined, obj.msg);
+ if (cb) { cb(); }
+ });
+ };
+
mailbox.open = function (key, m, cb, team, opts) {
if (TYPES.indexOf(key) === -1 && !team) { return; }
openChannel(ctx, key, m, cb, opts);
diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js
index 861ff8d53..7ea717d35 100644
--- a/www/common/sframe-common-mailbox.js
+++ b/www/common/sframe-common-mailbox.js
@@ -65,6 +65,8 @@ define([
if (/^LOCAL\|/.test(data.content.hash)) {
$(avatar).addClass('preview');
}
+ } else if (data.type === 'reminders') {
+ avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
} else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
@@ -85,6 +87,15 @@ define([
if (typeof(data.content.getFormatText) === "function") {
$(notif).find('.cp-notification-content p').html(data.content.getFormatText());
+ if (data.content.autorefresh) {
+ var it = setInterval(function () {
+ if (!data.content.autorefresh) {
+ clearInterval(it);
+ return;
+ }
+ $(notif).find('.cp-notification-content p').html(data.content.getFormatText());
+ }, 60000);
+ }
}
if (data.content.isClickable) {
@@ -117,7 +128,7 @@ define([
// Call the onMessage handlers
var isNotification = function (type) {
- return type === "notifications" || /^team-/.test(type) || type === "broadcast";
+ return type === "notifications" || /^team-/.test(type) || type === "broadcast" || type === "reminders";
};
var pushMessage = function (data, handler) {
var todo = function (f) {
diff --git a/www/common/toolbar.js b/www/common/toolbar.js
index acdcbd441..119d7fb3d 100644
--- a/www/common/toolbar.js
+++ b/www/common/toolbar.js
@@ -1155,7 +1155,7 @@ MessengerUI, Messages, Pages) {
$button.addClass('fa-bell');
};
- Common.mailbox.subscribe(['notifications', 'team', 'broadcast'], {
+ Common.mailbox.subscribe(['notifications', 'team', 'broadcast', 'reminders'], {
onMessage: function (data, el) {
if (el) {
$(div).prepend(el);
diff --git a/www/lib/ical.min.js b/www/lib/ical.min.js
new file mode 100644
index 000000000..e92401be0
--- /dev/null
+++ b/www/lib/ical.min.js
@@ -0,0 +1,2 @@
+"object"==typeof module?ICAL=module.exports:"object"!=typeof ICAL&&(this.ICAL={}),ICAL.foldLength=75,ICAL.newLineChar="\r\n",ICAL.helpers={updateTimezones:function(t){var e,r,n,i,a,s;if(!t||"vcalendar"!==t.name)return t;for(e=t.getAllSubcomponents(),r=[],n={},a=0;a>18&63,r=a>>12&63,n=a>>6&63,i=63&a,c[u++]=s.charAt(e)+s.charAt(r)+s.charAt(n)+s.charAt(i),o>16&255,r=s>>8&255,n=255&s,c[h++]=64==i?String.fromCharCode(e):64==a?String.fromCharCode(e,r):String.fromCharCode(e,r,n),u=this.changes.length)break}var s=this.changes[n];if(s.utcOffset-s.prevUtcOffset<0&&0=this.changes.length?this.changes.length-1:e},_ensureCoverage:function(t){if(-1==ICAL.Timezone._minimumExpansionYear){var e=ICAL.Time.now();ICAL.Timezone._minimumExpansionYear=e.year}var r=t;if(rICAL.Timezone.MAX_YEAR&&(r=ICAL.Timezone.MAX_YEAR),!this.changes.length||this.expandedUntilYeart)&&l);)n.year=l.year,n.month=l.month,n.day=l.day,n.hour=l.hour,n.minute=l.minute,n.second=l.second,n.isDate=l.isDate,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n)}}else(n=s()).year=i.year,n.month=i.month,n.day=i.day,n.hour=i.hour,n.minute=i.minute,n.second=i.second,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n);return r},toString:function(){return this.tznames?this.tznames:this.tzid}},ICAL.Timezone._compare_change_fn=function(t,e){return t.yeare.year?1:t.monthe.month?1:t.daye.day?1:t.houre.hour?1:t.minutee.minute?1:t.seconde.second?1:0},ICAL.Timezone.convert_time=function(t,e,r){if(t.isDate||e.tzid==r.tzid||e==ICAL.Timezone.localTimezone||r==ICAL.Timezone.localTimezone)return t.zone=r,t;var n=e.utcOffset(t);return t.adjust(0,0,0,-n),n=r.utcOffset(t),t.adjust(0,0,0,n),null},ICAL.Timezone.fromData=function(t){return(new ICAL.Timezone).fromData(t)},ICAL.Timezone.utcTimezone=ICAL.Timezone.fromData({tzid:"UTC"}),ICAL.Timezone.localTimezone=ICAL.Timezone.fromData({tzid:"floating"}),ICAL.Timezone.adjust_change=function(t,e,r,n,i){return ICAL.Time.prototype.adjust.call(t,e,r,n,i,t)},ICAL.Timezone._minimumExpansionYear=-1,ICAL.Timezone.MAX_YEAR=2035,ICAL.Timezone.EXTRA_COVERAGE=5}(),ICAL.TimezoneService=function(){var r,t={get count(){return Object.keys(r).length},reset:function(){r=Object.create(null);var t=ICAL.Timezone.utcTimezone;r.Z=t,r.UTC=t,r.GMT=t},has:function(t){return!!r[t]},get:function(t){return r[t]},register:function(t,e){if(t instanceof ICAL.Component&&"vtimezone"===t.name&&(t=(e=new ICAL.Timezone(t)).tzid),!(e instanceof ICAL.Timezone))throw new TypeError("timezone must be ICAL.Timezone or ICAL.Component");r[t]=e},remove:function(t){return delete r[t]}};return t.reset(),t}(),function(){function t(e){Object.defineProperty(ICAL.Time.prototype,e,{get:function(){return this._pendingNormalization&&(this._normalize(),this._pendingNormalization=!1),this._time[e]},set:function(t){return"isDate"===e&&t&&!this._time.isDate&&this.adjust(0,0,0,0),this._cachedUnixTime=null,this._pendingNormalization=!0,this._time[e]=t}})}ICAL.Time=function(t,e){var r=(this.wrappedJSObject=this)._time=Object.create(null);r.year=0,r.month=1,r.day=1,r.hour=0,r.minute=0,r.second=0,r.isDate=!1,this.fromData(t,e)},ICAL.Time._dowCache={},ICAL.Time._wnCache={},ICAL.Time.prototype={icalclass:"icaltime",_cachedUnixTime:null,get icaltype(){return this.isDate?"date":"date-time"},zone:null,_pendingNormalization:!1,clone:function(){return new ICAL.Time(this._time,this.zone)},reset:function(){this.fromData(ICAL.Time.epochTime),this.zone=ICAL.Timezone.utcTimezone},resetTo:function(t,e,r,n,i,a,s){this.fromData({year:t,month:e,day:r,hour:n,minute:i,second:a,zone:s})},fromJSDate:function(t,e){return t?e?(this.zone=ICAL.Timezone.utcTimezone,this.year=t.getUTCFullYear(),this.month=t.getUTCMonth()+1,this.day=t.getUTCDate(),this.hour=t.getUTCHours(),this.minute=t.getUTCMinutes(),this.second=t.getUTCSeconds()):(this.zone=ICAL.Timezone.localTimezone,this.year=t.getFullYear(),this.month=t.getMonth()+1,this.day=t.getDate(),this.hour=t.getHours(),this.minute=t.getMinutes(),this.second=t.getSeconds()):this.reset(),this._cachedUnixTime=null,this},fromData:function(t,e){if(t)for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r)){if("icaltype"===r)continue;this[r]=t[r]}if(e&&(this.zone=e),!t||"isDate"in t?t&&"isDate"in t&&(this.isDate=t.isDate):this.isDate=!("hour"in t),t&&"timezone"in t){var n=ICAL.TimezoneService.get(t.timezone);this.zone=n||ICAL.Timezone.localTimezone}return t&&"zone"in t&&(this.zone=t.zone),this.zone||(this.zone=ICAL.Timezone.localTimezone),this._cachedUnixTime=null,this},dayOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+e;if(r in ICAL.Time._dowCache)return ICAL.Time._dowCache[r];var n=this.day,i=this.month+(this.month<3?12:0),a=this.year-(this.month<3?1:0),s=n+a+ICAL.helpers.trunc(26*(i+1)/10)+ICAL.helpers.trunc(a/4);return s=((s+=6*ICAL.helpers.trunc(a/100)+ICAL.helpers.trunc(a/400))+7-e)%7+1,ICAL.Time._dowCache[r]=s},dayOfYear:function(){var t=ICAL.Time.isLeapYear(this.year)?1:0;return ICAL.Time.daysInYearPassedMonth[t][this.month-1]+this.day},startOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day-=(this.dayOfWeek()+7-e)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},endOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day+=(7-this.dayOfWeek()+e-ICAL.Time.SUNDAY)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},startOfMonth:function(){var t=this.clone();return t.day=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfMonth:function(){var t=this.clone();return t.day=ICAL.Time.daysInMonth(t.month,t.year),t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startOfYear:function(){var t=this.clone();return t.day=1,t.month=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfYear:function(){var t=this.clone();return t.day=31,t.month=12,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startDoyWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.dayOfWeek()-e;return r<0&&(r+=7),this.dayOfYear()-r},getDominicalLetter:function(){return ICAL.Time.getDominicalLetter(this.year)},nthWeekDay:function(t,e){var r,n=ICAL.Time.daysInMonth(this.month,this.year),i=e,a=0,s=this.clone();if(0<=i){s.day=1,0!=i&&i--,a=s.day;var o=t-s.dayOfWeek();o<0&&(o+=7),a+=o,a-=t,r=t}else{s.day=n,i++,(r=s.dayOfWeek()-t)<0&&(r+=7),r=n-r}return a+(r+=7*i)},isNthWeekDay:function(t,e){var r=this.dayOfWeek();return 0===e&&r===t||this.nthWeekDay(t,e)===this.day},weekNumber:function(t){var e,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+t;if(r in ICAL.Time._wnCache)return ICAL.Time._wnCache[r];var n=this.clone();n.isDate=!0;var i=this.year;12==n.month&&25ICAL.Time.daysInYearPassedMonth[a][12])return a=ICAL.Time.isLeapYear(r)?1:0,n-=ICAL.Time.daysInYearPassedMonth[a][12],r++,ICAL.Time.fromDayOfYear(n,r);i.year=r,i.isDate=!0;for(var s=11;0<=s;s--)if(n>ICAL.Time.daysInYearPassedMonth[a][s]){i.month=s+1,i.day=n-ICAL.Time.daysInYearPassedMonth[a][s];break}return i.auto_normalize=!0,i},ICAL.Time.fromStringv2=function(t){return new ICAL.Time({year:parseInt(t.substr(0,4),10),month:parseInt(t.substr(5,2),10),day:parseInt(t.substr(8,2),10),isDate:!0})},ICAL.Time.fromDateString=function(t){return new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),isDate:!0})},ICAL.Time.fromDateTimeString=function(t,e){if(t.length<19)throw new Error('invalid date-time value: "'+t+'"');var r;return t[19]&&"Z"===t[19]?r="Z":e&&(r=e.getParameter("tzid")),new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),hour:ICAL.helpers.strictParseInt(t.substr(11,2)),minute:ICAL.helpers.strictParseInt(t.substr(14,2)),second:ICAL.helpers.strictParseInt(t.substr(17,2)),timezone:r})},ICAL.Time.fromString=function(t,e){return 10ICAL.Time.THURSDAY&&(r.day+=7),i>ICAL.Time.THURSDAY&&(r.day-=7),r.day-=n-i,r},ICAL.Time.getDominicalLetter=function(t){var e="GFEDCBA",r=(t+(t/4|0)+(t/400|0)-(t/100|0)-1)%7;return ICAL.Time.isLeapYear(t)?e[(6+r)%7]+e[r]:e[r]},ICAL.Time.epochTime=ICAL.Time.fromData({year:1970,month:1,day:1,hour:0,minute:0,second:0,isDate:!1,timezone:"Z"}),ICAL.Time._cmp_attr=function(t,e,r){return t[r]>e[r]?1:t[r] '+e);if(void 0!==r&&rs||0==this.last.day)throw new Error("Malformed values in BYDAY part")}else if(this.has_by_data("BYMONTHDAY")&&this.last.day<0){s=ICAL.Time.daysInMonth(this.last.month,this.last.year);this.last.day=s+this.last.day+1}},next:function(){var t,e=this.last?this.last.clone():null;if(this.rule.count&&this.occurrence_number>=this.rule.count||this.rule.until&&0i)){if(n<0)n=i+(n+1);else if(0===n)continue;-1===a.indexOf(n)&&a.push(n)}return a.sort(function(t,e){return t-e})},_byDayAndMonthDay:function(t){var e,r,n,i,a=this.by_data.BYDAY,s=0,o=a.length,u=0,h=this,c=this.last.day;function l(){for(i=ICAL.Time.daysInMonth(h.last.month,h.last.year),e=h.normalizeByMonthDayRules(h.last.year,h.last.month,h.by_data.BYMONTHDAY),n=e.length;e[s]<=c&&(!t||e[s]!=c)&&s=this.by_data.BYMONTHDAY.length&&(this.by_indices.BYMONTHDAY=0,this.increment_month());e=ICAL.Time.daysInMonth(this.last.month,this.last.year);(a=this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY])<0&&(a=e+a+1),ee?t=0:this.last.day=this.by_data.BYMONTHDAY[0]}return t},next_weekday_by_week:function(){var t=0;if(0==this.next_hour())return t;if(!this.has_by_data("BYDAY"))return 1;for(;;){var e=new ICAL.Time;this.by_indices.BYDAY++,this.by_indices.BYDAY==Object.keys(this.by_data.BYDAY).length&&(this.by_indices.BYDAY=0,t=1);var r=this.by_data.BYDAY[this.by_indices.BYDAY],n=this.ruleDayOfWeek(r)[1];(n-=this.rule.wkst)<0&&(n+=7),e.year=this.last.year,e.month=this.last.month,e.day=this.last.day;var i=e.startDoyWeek(this.rule.wkst);if(!(n+i<1)||t){var a=ICAL.Time.fromDayOfYear(i+n,this.last.year);return this.last.year=a.year,this.last.month=a.month,this.last.day=a.day,t}}},next_year:function(){if(0==this.next_hour())return 0;if(++this.days_index==this.days.length)for(this.days_index=0;this.increment_year(this.rule.interval),this.expand_year_days(this.last.year),0==this.days.length;);return this._nextByYearDay(),1},_nextByYearDay:function(){var t=this.days[this.days_index],e=this.last.year;t<1&&(t+=1,e+=1);var r=ICAL.Time.fromDayOfYear(t,e);this.last.day=r.day,this.last.month=r.month},ruleDayOfWeek:function(t,e){var r=t.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);return r?[parseInt(r[1]||0,10),t=ICAL.Recur.icalDayToNumericDay(r[2],e)]:[0,0]},next_generic:function(t,e,r,n,i){var a=t in this.by_data,s=this.rule.freq==e,o=0;if(i&&0==this[i]())return o;if(a){this.by_indices[t]++;this.by_indices[t];var u=this.by_data[t];this.by_indices[t]==u.length&&(this.by_indices[t]=0,o=1),this.last[r]=u[this.by_indices[t]]}else s&&this["increment_"+r](this.rule.interval);return a&&o&&s&&this["increment_"+n](1),o},increment_monthday:function(t){for(var e=0;er&&(this.last.day-=r,this.increment_month())}},increment_month:function(){if(this.last.day=1,this.has_by_data("BYMONTH"))this.by_indices.BYMONTH++,this.by_indices.BYMONTH==this.by_data.BYMONTH.length&&(this.by_indices.BYMONTH=0,this.increment_year(1)),this.last.month=this.by_data.BYMONTH[this.by_indices.BYMONTH];else{"MONTHLY"==this.rule.freq?this.last.month+=this.rule.interval:this.last.month++,this.last.month--;var t=ICAL.helpers.trunc(this.last.month/12);this.last.month%=12,this.last.month++,0!=t&&this.increment_year(t)}},increment_year:function(t){this.last.year+=t},increment_generic:function(t,e,r,n){this.last[e]+=t;var i=ICAL.helpers.trunc(this.last[e]/r);this.last[e]%=r,0!=i&&this["increment_"+n](i)},has_by_data:function(t){return t in this.rule.parts},expand_year_days:function(t){var e=new ICAL.Time;this.days=[];var r={},n=["BYDAY","BYWEEKNO","BYMONTHDAY","BYMONTH","BYYEARDAY"];for(var i in n)if(n.hasOwnProperty(i)){var a=n[i];a in this.rule.parts&&(r[a]=this.rule.parts[a])}if("BYMONTH"in r&&"BYWEEKNO"in r){var s=1,o={};e.year=t,e.isDate=!0;for(var u=0;ue[0]?1:e[0]>t[0]?-1:0}return t.prototype={THISANDFUTURE:"THISANDFUTURE",exceptions:null,strictExceptions:!1,relateException:function(t){if(this.isRecurrenceException())throw new Error("cannot relate exception to exceptions");if(t instanceof ICAL.Component&&(t=new ICAL.Event(t)),this.strictExceptions&&t.uid!==this.uid)throw new Error("attempted to relate unrelated exception");var e=t.recurrenceId.toString();if((this.exceptions[e]=t).modifiesFuture()){var r=[t.recurrenceId.toUnixTime(),e],n=ICAL.helpers.binsearchInsert(this.rangeExceptions,r,i);this.rangeExceptions.splice(n,0,r)}},modifiesFuture:function(){return!!this.component.hasProperty("recurrence-id")&&this.component.getFirstProperty("recurrence-id").getParameter("range")===this.THISANDFUTURE},findRangeException:function(t){if(!this.rangeExceptions.length)return null;var e=t.toUnixTime(),r=ICAL.helpers.binsearchInsert(this.rangeExceptions,[e],i);if(--r<0)return null;var n=this.rangeExceptions[r];return e', { 'class': 'cp-sidebarlayout-categories' })
.appendTo(APP.$leftside);