Merge branch 'reminders' into staging
This commit is contained in:
commit
ae34456b55
13 changed files with 801 additions and 64 deletions
|
@ -122,7 +122,7 @@
|
|||
&.small {
|
||||
line-height: initial;
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
&:hover, &:not(:disabled):not(.disabled):active, &:focus {
|
||||
|
|
|
@ -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;
|
||||
|
|
199
www/calendar/export.js
Normal file
199
www/calendar/export.js
Normal file
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
@ -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 '<span class="tui-full-calendar-weekday-grid-more-schedules">' + Messages._getKey('calendar_more', [hiddenSchedules]) + '</span>';
|
||||
},
|
||||
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');
|
||||
|
|
|
@ -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 = $('<input>', {type:"file"});
|
||||
if (cfg && cfg.accept) {
|
||||
|
|
|
@ -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(); }
|
||||
});
|
||||
|
|
|
@ -461,6 +461,76 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
Messages.reminder_missed = "You missed <b>{0}</b> on {1}"; // XXX
|
||||
Messages.reminder_now = "<b>{0}</b> is starting!"; // XXX
|
||||
Messages.reminder_inProgress = "<b>{0}</b> has started on {1}"; // XXX
|
||||
Messages.reminder_inProgressAllDay = "<b>{0}</b> is happening today"; // XXX
|
||||
Messages.reminder_minutes = "<b>{0}</b> will start in {1} minutes!"; // XXX
|
||||
Messages.reminder_time = "<b>{0}</b> will start today at {1}!"; // XXX
|
||||
Messages.reminder_date = "<b>{0}</b> 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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
2
www/lib/ical.min.js
vendored
Normal file
2
www/lib/ical.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -95,6 +95,9 @@ define([
|
|||
'kanban': [ // Msg.settings_cat_kanban
|
||||
'cp-settings-kanban-tags',
|
||||
],
|
||||
'notifications': [
|
||||
'cp-settings-notif-calendar'
|
||||
],
|
||||
'subscription': {
|
||||
onClick: function() {
|
||||
var urls = common.getMetadataMgr().getPrivateData().accounts;
|
||||
|
@ -1562,6 +1565,43 @@ define([
|
|||
cb($d);
|
||||
}, true);
|
||||
|
||||
Messages.settings_notifCalendarTitle = "Calendar notifications"; // XXX
|
||||
Messages.settings_notifCalendarHint = "You can disable completely calendar notifications for incoming events.";
|
||||
Messages.settings_notifCalendarCheckbox = "Enable calendar notifications";
|
||||
|
||||
makeBlock('notif-calendar', function(cb) { // Msg.settings_notifCalendarHint, .settings_notifCalendarTitle
|
||||
|
||||
var $cbox = $(UI.createCheckbox('cp-settings-cache',
|
||||
Messages.settings_notifCalendarCheckbox,
|
||||
false, { label: { class: 'noTitle' } }));
|
||||
var spinner = UI.makeSpinner($cbox);
|
||||
|
||||
var $checkbox = $cbox.find('input').on('change', function() {
|
||||
spinner.spin();
|
||||
var val = !$checkbox.is(':checked');
|
||||
common.setAttribute(['general', 'calendar', 'hideNotif'], val, function(e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
// error: restore previous value
|
||||
if (val) { $checkbox.attr('checked', ''); }
|
||||
else { $checkbox.attr('checked', 'checked'); }
|
||||
spinner.hide();
|
||||
return void console.error(e);
|
||||
}
|
||||
spinner.done();
|
||||
});
|
||||
});
|
||||
|
||||
common.getAttribute(['general', 'calendar', 'hideNotif'], function(e, val) {
|
||||
if (e) { return void console.error(e); }
|
||||
if (!val) {
|
||||
$checkbox.attr('checked', 'checked');
|
||||
}
|
||||
});
|
||||
|
||||
cb($cbox[0]);
|
||||
}, true);
|
||||
|
||||
// Settings app
|
||||
|
||||
var createUsageButton = function() {
|
||||
|
@ -1591,8 +1631,10 @@ define([
|
|||
subscription: 'fa fa-star-o',
|
||||
kanban: 'cptools cptools-kanban',
|
||||
style: 'cptools cptools-palette',
|
||||
notifications: 'fa fa-bell'
|
||||
};
|
||||
|
||||
Messages.settings_cat_notifications = Messages.notificationsPage;
|
||||
var createLeftside = function() {
|
||||
var $categories = $('<div>', { 'class': 'cp-sidebarlayout-categories' })
|
||||
.appendTo(APP.$leftside);
|
||||
|
|
Loading…
Reference in a new issue