Merge branch 'calendar' into staging

pull/1/head
ansuz 4 years ago
commit 93ea996f86

@ -58,6 +58,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 100000; // alertify container z-index: 100000; // alertify container
outline: none;
font: @colortheme_app-font; font: @colortheme_app-font;
.cp-checkmark { .cp-checkmark {

@ -419,3 +419,7 @@
@cp_flatpickr-bg: @cryptpad_color_grey_800; @cp_flatpickr-bg: @cryptpad_color_grey_800;
@cp_flatpickr-highlight: @cryptpad_color_brand_300; @cp_flatpickr-highlight: @cryptpad_color_brand_300;
@cp_flatpickr-highlight-text: @cryptpad_color_grey_800; @cp_flatpickr-highlight-text: @cryptpad_color_grey_800;
// Calendar
@cp_calendar-border: @cryptpad_color_grey_600;

@ -419,3 +419,7 @@
@cp_flatpickr-bg: @cryptpad_color_grey_50; @cp_flatpickr-bg: @cryptpad_color_grey_50;
@cp_flatpickr-highlight: @cryptpad_color_brand_fadest; @cp_flatpickr-highlight: @cryptpad_color_brand_fadest;
@cp_flatpickr-highlight-text: @cryptpad_text_col; @cp_flatpickr-highlight-text: @cryptpad_text_col;
// Calendar
@cp_calendar-border: @cryptpad_color_grey_300;

@ -1,5 +1,6 @@
@import (reference) "./colortheme-all.less"; @import (reference) "./colortheme-all.less";
@import (reference) "./variables.less"; @import (reference) "./variables.less";
@import (reference) "./tools.less";
.forms_main() { .forms_main() {
--LessLoader_require: LessLoader_currentFile(); --LessLoader_require: LessLoader_currentFile();
@ -13,14 +14,20 @@
color: @cp_forms-fg; color: @cp_forms-fg;
background-color: @cp_forms-bg; background-color: @cp_forms-bg;
border: 1px solid @cp_forms-border; border: 1px solid @cp_forms-border;
width: 100%;
font-size: 100%; font-size: 100%;
padding: @alertify_padding-base; padding: @alertify_padding-base;
&:not(.tui-full-calendar-content) {
width: 100%;
}
&.tui-full-calendar-content {
font-size: @colortheme_app-font-size;
}
&[readonly] { &[readonly] {
background-color: @cp_forms-readonly; background-color: @cp_forms-readonly;
border-color: @cp_forms-readonly-border; border-color: @cp_forms-readonly-border;
color: @cp_forms-fg; color: @cp_forms-fg;
} }
.tools_placeholder-color();
} }
input:not(.form-control) { input:not(.form-control) {
@ -112,6 +119,11 @@
&.no-margin { &.no-margin {
margin: 0; margin: 0;
} }
&.small {
line-height: initial;
padding: 5px;
height: auto;
}
&:hover, &:not(:disabled):not(.disabled):active, &:focus { &:hover, &:not(:disabled):not(.disabled):active, &:focus {
color: @cp_buttons-fg; color: @cp_buttons-fg;

@ -996,6 +996,9 @@
display: flex; display: flex;
#cp-toolbar-chat-drawer-open { order: 0; } #cp-toolbar-chat-drawer-open { order: 0; }
#cp-toolbar-userlist-drawer-open { order: 1; } #cp-toolbar-userlist-drawer-open { order: 1; }
& > .cp-dropdown-container {
height: @toolbar_line-height;
}
} }
.cp-toolbar-bottom-right { .cp-toolbar-bottom-right {

@ -4,16 +4,20 @@
@color: @cp_forms-placeholder; @color: @cp_forms-placeholder;
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */ &::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: @color; color: @color;
font-weight: normal;
} }
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ &::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @color; color: @color;
opacity: 1; /* Firefox */ opacity: 1; /* Firefox */
font-weight: normal;
} }
&:-ms-input-placeholder { /* Internet Explorer 10-11 */ &:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @color; color: @color;
font-weight: normal;
} }
&::-ms-input-placeholder { /* Microsoft Edge */ &::-ms-input-placeholder { /* Microsoft Edge */
color: @color; color: @color;
font-weight: normal;
} }
} }

@ -1295,12 +1295,19 @@ define([
var end = h('input'); var end = h('input');
var $start = $(start); var $start = $(start);
var $end = $(end); var $end = $(end);
var is24h = false;
try {
is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/);
} catch (e) {}
var endPickr = Flatpickr(end, { var endPickr = Flatpickr(end, {
enableTime: true, enableTime: true,
time_24hr: is24h,
minDate: new Date() minDate: new Date()
}); });
Flatpickr(start, { Flatpickr(start, {
enableTime: true, enableTime: true,
time_24hr: is24h,
minDate: new Date(), minDate: new Date(),
onChange: function () { onChange: function () {
endPickr.set('minDate', new Date($start.val())); endPickr.set('minDate', new Date($start.val()));

@ -0,0 +1,266 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/tools.less';
@import (reference) '../../customize/src/less2/include/avatar.less';
&.cp-app-calendar {
.framework_min_main();
.sidebar-layout_main();
display: flex;
flex-flow: column;
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
padding: 0;
& > div {
margin: 0;
}
.cp-forcehide {
display: none !important;
}
.tui-full-calendar-layout {
background-color: @cp_sidebar-right-bg !important;
color: @cryptpad_text_col;
.tui-full-calendar-month {
.tui-full-calendar-weekday-schedule-time .tui-full-calendar-weekday-schedule-title {
color: @cryptpad_text_col !important; // XXX
}
.tui-full-calendar-extra-date {
.tui-full-calendar-weekday-grid-date {
color: @cp_sidebar-hint !important; // XXX
opacity: 0.5;
}
}
.tui-full-calendar-weekday-grid-date {
color: @cryptpad_text_col !important; // XXX
}
.tui-full-calendar-month-dayname-item span {
color: @cryptpad_text_col !important; // XXX
}
}
.tui-full-calendar-dayname * {
color: @cryptpad_text_col !important; // XXX
}
.tui-full-calendar-month-more {
background-color: @cp_sidebar-right-bg !important;
color: @cryptpad_text_col;
span {
color: @cryptpad_text_col !important;
}
}
}
.tui-full-calendar-timegrid-timezone {
background-color: @cp_sidebar-right-bg !important;
.tui-full-calendar-timegrid-hour {
color: @cryptpad_text_col !important;
}
color: @cryptpad_text_col;
}
.tui-full-calendar-timegrid-gridline, .tui-full-calendar-time-date {
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-splitter, .tui-full-calendar-left, .tui-full-calendar-dayname-container, .tui-full-calendar-weekday-grid-line {
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-popup-container {
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
display: none;
}
}
li.tui-full-calendar-popup-section-item {
padding: 0 6px;
height: 32px;
}
.tui-full-calendar-popup-section-item {
height: auto;
margin: 0;
&:not(li):not(button) {
padding: 0;
margin-top: 5px;
}
#tui-full-calendar-schedule-calendar {
width: 179px;
top: 0;
}
&:not(button) {
border: none;
display: inline-flex;
align-items: center;
&:hover {
background-color: @cp_dropdown-bg-hover;
}
.tui-full-calendar-content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font: @colortheme_app-font;
}
input { flex: 1; }
}
}
.tui-full-calendar-section-date-dash {
height: auto;
}
.tui-full-calendar-section-title, .tui-full-calendar-section-location {
width: 100%;
}
.tui-full-calendar-dropdown-menu {
top: 38px;
width: 221px; // same as button
background-color: @cp_dropdown-bg;
color: @cp_dropdown-fg;
}
.tui-full-calendar-section-state, #tui-full-calendar-schedule-private {
display: none !important;
}
.tui-full-calendar-popup:not(.tui-full-calendar-popup-detail) {
.tui-full-calendar-section-calendar {
width: 221px; // 50%
}
.tui-full-calendar-popup-section {
display: flex;
align-items: center;
flex-wrap: wrap;
.tui-full-calendar-section-start-date, .tui-full-calendar-section-end-date {
flex: 1;
}
.tui-full-calendar-section-allday {
width: 100%;
height: 32px;
}
}
}
.tui-full-calendar-popup-detail {
font: @colortheme_app-font;
color: @cryptpad_text_col;
.tui-full-calendar-popup-container {
padding-bottom: 17px;
}
.tui-full-calendar-popup-detail-date {
font-size: 14px;
}
.tui-full-calendar-section-button {
border: 0;
display: flex;
align-items: center;
button {
flex: 1;
margin: 0;
}
}
.tui-full-calendar-popup-vertical-line {
visibility: hidden;
width: 10px;
}
}
.cp-calendar-close {
height: auto;
line-height: initial;
border: 1px solid;
&:not(:hover) {
background: transparent;
}
}
}
#cp-toolbar .cp-calendar-browse {
display: flex;
align-items: center;
}
#cp-sidebarlayout-leftside {
& > div {
padding: 10px
}
.cp-calendar-new {
display: flex;
align-items: center;
justify-content: space-between;
}
.cp-calendar-list {
.cp-calendar-team {
height: 30px;
.avatar_main(30px);
.cp-avatar {
margin-right: 10px;
}
.cp-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
display: flex;
align-items: center;
justify-content: center;
margin: 5px 0;
}
.cp-calendar-entry {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px;
cursor: pointer;
&.cp-ghost {
padding: 0;
button {
.tools_unselectable();
cursor: pointer;
width: 100%;
display: flex;
justify-content: space-between;
background: transparent;
border: 1px solid @cryptpad_text_col;
height: 36px;
font: @colortheme_app-font;
align-items: center;
color: @cryptpad_text_col;
&:hover {
background: @cp_sidebar-left-active;
}
}
}
&:not(:last-child) {
margin-bottom: 10px;
}
&:hover {
background: fade(@cryptpad_text_col, 10%);
}
&.cp-restricted {
color: @cp_drive-header-fg;
}
&.cp-active {
background: @cp_sidebar-left-active;
}
.tools_unselectable();
.cp-calendar-color {
display: inline-block;
border-radius: 50%;
width: 15px;
height: 15px;
flex-shrink: 0;
}
.cp-calendar-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 5px;
}
}
}
}
.cp-calendar-colorpicker {
width: 100px;
height: 25px;
cursor: pointer;
border: 1px solid @cp_forms-border;
}
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
</head>
<body>
<iframe-placeholder>

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/calendar/inner.js" data-main="/common/sframe-boot.js?ver=1.7" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="cp-app-calendar">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>

@ -0,0 +1,947 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-realtime.js',
'/common/clipboard.js',
'/common/inner/common-mediatag.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/customize/application_config.js',
'/lib/calendar/tui-calendar.min.js',
'/common/inner/share.js',
'/common/inner/access.js',
'/common/inner/properties.js',
'/common/jscolor.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 (
$,
Crypto,
Toolbar,
nThen,
SFCommon,
Util,
Hash,
UI,
UIElements,
Realtime,
Clipboard,
MT,
h,
Messages,
AppConfig,
Calendar,
Share, Access, Properties
)
{
var APP = window.APP = {
calendars: {}
};
var common;
var metadataMgr;
var sframeChan;
Messages.calendar = "Calendar"; // XXX
Messages.calendar_default = "My calendar"; // XXX
Messages.calendar_new = "New calendar"; // XXX
Messages.calendar_day = "Day";
Messages.calendar_week = "Week";
Messages.calendar_month = "Month";
Messages.calendar_today = "Today";
Messages.calendar_deleteConfirm = "Are you sure you want to delete this calendar from your account?";
Messages.calendar_deleteTeamConfirm = "Are you sure you want to delete this calendar from this team?";
Messages.calendar_deleteOwned = " It will still be visible for the users it has been shared with.";
Messages.calendar_errorNoCalendar = "No editable calendar selected!";
Messages.calendar_myCalendars = "My calendars";
Messages.calendar_tempCalendar = "Temp calendar";
Messages.calendar_import = "Import to my calendars";
Messages.calendar_newEvent = "New event";
Messages.calendar_new = "New calendar";
Messages.calendar_dateRange = "{0} - {1}";
Messages.calendar_dateTimeRange = "{0} {1} - {2}";
var onCalendarsUpdate = Util.mkEvent();
var newCalendar = function (data, cb) {
APP.module.execCommand('CREATE', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var updateCalendar = function (data, cb) {
APP.module.execCommand('UPDATE', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var deleteCalendar = function (data, cb) {
APP.module.execCommand('DELETE', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var importCalendar = function (data, cb) {
APP.module.execCommand('IMPORT', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var newEvent = function (data, cb) {
var start = data.start;
var end = data.end;
data.start = +new Date(start._date);
data.end = +new Date(end._date);
APP.module.execCommand('CREATE_EVENT', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var updateEvent = function (data, cb) {
APP.module.execCommand('UPDATE_EVENT', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var deleteEvent = function (data, cb) {
APP.module.execCommand('DELETE_EVENT', data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
};
var getContrast = function (color) {
var rgb = Util.hexToRGB(color);
// http://www.w3.org/TR/AERT#color-contrast
var brightness = Math.round(((parseInt(rgb[0]) * 299) +
(parseInt(rgb[1]) * 587) +
(parseInt(rgb[2]) * 114)) / 1000);
return (brightness > 125) ? 'black' : 'white';
};
var getWeekDays = function () {
var baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday
var weekDays = [];
for(var i = 0; i < 7; i++) {
weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' }));
baseDate.setDate(baseDate.getDate() + 1);
}
return weekDays.map(function (day) { return day.replace(/^./, function (str) { return str.toUpperCase(); }); });
};
var getCalendars = function () {
return Object.keys(APP.calendars).map(function (id) {
var c = APP.calendars[id];
if (c.hidden || c.restricted || c.loading) { return; }
var md = Util.find(c, ['content', 'metadata']);
if (!md) { return void console.error('Ignore calendar without metadata'); }
return {
id: id,
name: Util.fixHTML(md.title),
color: getContrast(md.color),
bgColor: md.color,
dragBgColor: md.color,
borderColor: md.color,
};
}).filter(Boolean);
};
var getSchedules = function () {
var s = [];
Object.keys(APP.calendars).forEach(function (id) {
var c = APP.calendars[id];
if (c.hidden || c.restricted || c.loading) { return; }
var data = c.content || {};
Object.keys(data.content || {}).forEach(function (uid) {
var obj = data.content[uid];
obj.title = Util.fixHTML(obj.title || "");
obj.location = Util.fixHTML(obj.location || "");
if (c.readOnly) {
obj.isReadOnly = true;
}
s.push(data.content[uid]);
});
});
return s;
};
var renderCalendar = function () {
var cal = APP.calendar;
if (!cal) { return; }
cal.clear();
cal.setCalendars(getCalendars());
cal.createSchedules(getSchedules(), true);
cal.render();
};
var onCalendarUpdate = function (data) {
var cal = APP.calendar;
if (data.deleted) {
// Remove this calendar
delete APP.calendars[data.id];
} else {
// Update local data
APP.calendars[data.id] = data;
}
// If calendar if initialized, update it
if (!cal) { return; }
onCalendarsUpdate.fire();
renderCalendar();
};
var getTime = function (time) {
var d = new Date();
d.setHours(time.hour);
d.setMinutes(time.minutes);
return d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
};
// If this browser doesn't support options to toLocaleTimeString, use default layout
if (!(function () {
// Modern browser will return a RangeError if the "locale" argument is invalid.
// Note: the "locale" argument has the same browser compatibility table as the "options"
try {
new Date().toLocaleTimeString('i');
} catch (e) {
return e.name === 'RangeError';
}
})()) { getTime = undefined; }
var templates = {
popupSave: function () {
return Messages.settings_save;
},
timegridDisplayTime: getTime,
timegridDisplayPrimaryTime: getTime,
popupDetailDate: function(isAllDay, start, end) {
var startDate = start._date.toLocaleDateString();
var endDate = end._date.toLocaleDateString();
if (isAllDay) {
if (startDate === endDate) { return startDate; }
return Messages._getKey('calendar_dateRange', [startDate, endDate]);
}
var startTime = getTime({
hour: start._date.getHours(),
minutes: start._date.getMinutes(),
});
var endTime = getTime({
hour: end._date.getHours(),
minutes: end._date.getMinutes(),
});
if (startDate === endDate && startTime === endTime) {
return start._date.toLocaleString();
}
if (startDate === endDate) {
return Messages._getKey('calendar_dateTimeRange', [startDate, startTime, endTime]);
}
return Messages._getKey('calendar_dateRange', [start._date.toLocaleString(), end._date.toLocaleString()]);
}
};
// XXX Note: always create calendars in your own proxy. If you want a team calendar, you can share it with the team later.
var editCalendar = function (id) {
var isNew = !id;
var data = APP.calendars[id];
if (id && !data) { return; }
var md = {};
if (!isNew) { md = Util.find(data, ['content', 'metadata']); }
if (!md) { return; }
// Create form data
var labelTitle = h('label', Messages.kanban_title);
var title = h('input');
var $title = $(title);
$title.val(md.title || Messages.calendar_new);
var labelColor = h('label', Messages.kanban_color);
var $colorPicker = $(h('div.cp-calendar-colorpicker'));
var jscolorL = new window.jscolor($colorPicker[0], { showOnClick: false, valueElement: undefined, zIndex: 100000 });
$colorPicker.click(function() {
jscolorL.show();
});
if (md.color) { jscolorL.fromString(md.color); }
else { jscolorL.fromString(Util.getRandomColor()); }
var form = h('div', [
labelTitle,
title,
labelColor,
$colorPicker[0]
]);
var send = function (obj) {
if (isNew) {
return void newCalendar(obj, function (err) {
if (err) { console.error(err); return void UI.warn(Messages.error); }
UI.log(Messages.saved);
});
}
obj.id = id;
updateCalendar(obj, function (err) {
if (err) { console.error(err); return void UI.warn(Messages.error); }
UI.log(Messages.saved);
});
};
var m = UI.dialog.customModal(form, {
buttons: [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
}, {
className: 'primary',
name: Messages.settings_save,
onClick: function () {
var color = jscolorL.toHEXString();
var title = $title.val();
var obj = {
color: color,
title: title
};
if (!title || !title.trim() ||!/^#[0-9a-fA-F]{6}$/.test(color)) {
return true;
}
send(obj);
},
keys: [13]
}]
});
UI.openCustomModal(m);
};
var isReadOnly = function (id, teamId) {
var data = APP.calendars[id];
return data.readOnly || (data.roTeams && data.roTeams.indexOf(teamId) !== -1);
};
var makeEditDropdown = function (id, teamId) {
var options = [];
var privateData = metadataMgr.getPrivateData();
var cantRemove = teamId === 0 || (teamId !== 1 && privateData.teams[teamId].viewer);
var data = APP.calendars[id];
if (!data.readOnly) {
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-pencil',
},
content: h('span', Messages.tag_edit),
action: function (e) {
e.stopPropagation();
editCalendar(id);
return true;
}
});
}
if (data.teams.indexOf(1) === -1 || teamId === 0) {
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-clone',
},
content: h('span', Messages.calendar_import),
action: function (e) {
e.stopPropagation();
importCalendar({
id: id,
teamId: teamId
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(obj.error);
}
});
return true;
}
});
}
if (!data.restricted) {
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-shhare-alt',
},
content: h('span', Messages.shareButton),
action: function (e) {
e.stopPropagation();
var friends = common.getFriends();
var cal = APP.calendars[id];
var title = Util.find(cal, ['content', 'metadata', 'title']);
var color = Util.find(cal, ['content', 'metadata', 'color']);
Share.getShareModal(common, {
teamId: teamId === 1 ? undefined : teamId,
origin: APP.origin,
pathname: "/calendar/",
friends: friends,
title: title,
password: cal.password, // XXX support passwords
calendar: {
title: title,
color: color,
channel: id,
},
common: common,
hashes: cal.hashes
});
return true;
}
});
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-lock',
},
content: h('span', Messages.accessButton),
action: function (e) {
e.stopPropagation();
var cal = APP.calendars[id];
var title = Util.find(cal, ['content', 'metadata', 'title']);
var color = Util.find(cal, ['content', 'metadata', 'color']);
var h = cal.hashes || {};
var href = Hash.hashToHref(h.editHash || h.viewHash, 'calendar');
Access.getAccessModal(common, {
title: title,
password: cal.password, // XXX support passwords
calendar: {
title: title,
color: color,
channel: id,
},
common: common,
noExpiration: true,
noEditPassword: true,
channel: id,
href: href
});
return true;
}
});
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-info-circle',
},
content: h('span', Messages.propertiesButton),
action: function (e) {
e.stopPropagation();
var cal = APP.calendars[id];
var title = Util.find(cal, ['content', 'metadata', 'title']);
var color = Util.find(cal, ['content', 'metadata', 'color']);
var h = cal.hashes || {};
var href = Hash.hashToHref(h.editHash || h.viewHash, 'calendar');
Properties.getPropertiesModal(common, {
calendar: {
title: title,
color: color,
channel: id,
},
common: common,
channel: id,
href: href
});
return true;
}
});
}
if (!cantRemove) {
options.push({
tag: 'a',
attributes: {
'class': 'fa fa-trash-o',
},
content: h('span', Messages.kanban_delete),
action: function (e) {
e.stopPropagation();
var cal = APP.calendars[id];
var key = Messages.calendar_deleteConfirm;
var teams = (cal && cal.teams) || [];
if (teams.length === 1 && teams[0] !== 1) {
key = Messages.calendar_deleteTeamConfirm;
}
if (cal.owned) {
key += Messages.calendar_deleteOwned;
}
UI.confirm(Messages.calendar_deleteConfirm, function (yes) {
if (!yes) { return; }
deleteCalendar({
id: id,
teamId: teamId,
}, function (err) {
if (err) {
console.error(err);
UI.warn(Messages.error);
}
});
});
}
});
}
var dropdownConfig = {
text: '',
options: options, // Entries displayed in the menu
common: common,
buttonCls: 'btn btn-cancel fa fa-ellipsis-h small'
};
return UIElements.createDropdown(dropdownConfig)[0];
};
var makeCalendarEntry = function (id, teamId) {
// XXX handle RESTRICTED calendars (data.restricted)
var data = APP.calendars[id];
var edit;
if (data.loading) {
edit = h('i.fa.fa-spinner.fa-spin');
} else {
edit = makeEditDropdown(id, teamId);
}
var md = Util.find(data, ['content', 'metadata']);
if (!md) { return; }
var active = data.hidden ? '' : '.cp-active';
var restricted = data.restricted ? '.cp-restricted' : '';
var calendar = h('div.cp-calendar-entry'+active+restricted, {
'data-uid': id
}, [
h('span.cp-calendar-color', {
style: 'background-color: '+md.color+';'
}),
h('span.cp-calendar-title', md.title),
data.restricted ? h('i.fa.fa-ban', {title: Messages.fm_restricted}) :
(isReadOnly(id, teamId) ? h('i.fa.fa-eye', {title: Messages.readonly}) : undefined),
edit
]);
$(calendar).click(function () {
data.hidden = !data.hidden;
if (APP.$calendars) {
APP.$calendars.find('[data-uid="'+id+'"]').toggleClass('cp-active', !data.hidden);
} else {
$(calendar).toggleClass('cp-active', !data.hidden);
}
renderCalendar();
});
if (APP.$calendars) { APP.$calendars.append(calendar); }
return calendar;
};
var makeLeftside = function (calendar, $container) {
// Show calendars
var calendars = h('div.cp-calendar-list');
var $calendars = APP.$calendars = $(calendars).appendTo($container);
onCalendarsUpdate.reg(function () {
$calendars.empty();
var privateData = metadataMgr.getPrivateData();
var filter = function (teamId) {
return Object.keys(APP.calendars || {}).filter(function (id) {
var cal = APP.calendars[id] || {};
var teams = cal.teams || [];
return teams.indexOf(typeof(teamId) !== "undefined" ? teamId : 1) !== -1;
});
};
var tempCalendars = filter(0);
if (tempCalendars.length) {
APP.$calendars.append(h('div.cp-calendar-team', [
h('span', Messages.calendar_tempCalendar)
]));
makeCalendarEntry(tempCalendars[0], 0);
}
var myCalendars = filter(1);
if (myCalendars.length) {
APP.$calendars.append(h('div.cp-calendar-team', [
h('span', Messages.calendar_myCalendars)
]));
}
myCalendars.forEach(function (id) {
makeCalendarEntry(id, 1);
});
Object.keys(privateData.teams).forEach(function (teamId) {
var calendars = filter(teamId);
if (!calendars.length) { return; }
var team = privateData.teams[teamId];
var avatar = h('span.cp-avatar');
common.displayAvatar($(avatar), team.avatar, team.displayName);
APP.$calendars.append(h('div.cp-calendar-team', [
avatar,
h('span.cp-name', {title: team.name}, team.name)
]));
calendars.forEach(function (id) {
makeCalendarEntry(id, teamId);
});
});
// Add new button
var $newContainer = $(h('div.cp-calendar-entry.cp-ghost')).appendTo($calendars);
var newButton = h('button', [
h('i.fa.fa-plus'),
h('span', Messages.calendar_new),
h('span')
]);
$(newButton).click(function () {
editCalendar();
}).appendTo($newContainer);
});
onCalendarsUpdate.fire();
};
var updateDateRange = function () {
var range = APP.calendar._renderRange;
var start = range.start._date.toLocaleDateString();
var end = range.end._date.toLocaleDateString();
var date = [
h('b', start),
h('span', ' - '),
h('b', end),
];
if (APP.calendar._viewName === "day") {
date = h('b', start);
} else if (APP.calendar._viewName === "month") {
var month;
var mid = new Date(Math.floor(((+range.start._date) + (+range.end._date)) / 2));
try {
month = mid.toLocaleString('default', {
month: 'long',
year:'numeric'
});
month = month.replace(/^./, function (str) { return str.toUpperCase(); });
date = h('b', month);
} catch (e) {
// Use same as week range: first day of month to last day of month
}
}
APP.toolbar.$bottomM.empty().append(h('div', date));
};
var makeCalendar = function () {
var $container = $('#cp-sidebarlayout-container');
var leftside;
$container.append([
leftside = h('div#cp-sidebarlayout-leftside'),
h('div#cp-sidebarlayout-rightside')
]);
var cal = APP.calendar = new Calendar('#cp-sidebarlayout-rightside', {
defaultView: 'week', // weekly view option
taskView: false,
useCreationPopup: true,
useDetailPopup: true,
usageStatistics: false,
calendars: getCalendars(),
template: templates,
month: {
daynames: getWeekDays(),
startDayOfWeek: 1,
},
week: {
daynames: getWeekDays(),
startDayOfWeek: 1,
}
});
makeLeftside(cal, $(leftside));
cal.on('beforeCreateSchedule', function(event) {
// XXX Recurrence (later)
// On creation, select a recurrence rule (daily / weekly / monthly / more weird rules)
// then mark it under recurrence rule with a uid (the same for all the recurring events)
// ie: recurrenceRule: DAILY|{uid}
// Use template to hide "recurrenceRule" from the detailPopup or at least to use
// a non technical value
var schedule = {
id: Util.uid(),
calendarId: event.calendarId,
title: Util.fixHTML(event.title),
category: "time",
location: Util.fixHTML(event.location),
start: event.start,
isAllDay: event.isAllDay,
end: event.end,
};
newEvent(schedule, function (err) {
if (err) {
console.error(err);
return void UI.warn(err);
}
cal.createSchedules([schedule]);
});
});
cal.on('beforeUpdateSchedule', function(event) {
var changes = event.changes || {};
delete changes.state;
if (changes.end) { changes.end = +new Date(changes.end._date); }
if (changes.start) { changes.start = +new Date(changes.start._date); }
var old = event.schedule;
updateEvent({
ev: old,
changes: changes
}, function (err) {
if (err) {
console.error(err);
return void UI.warn(err);
}
cal.updateSchedule(old.id, old.calendarId, changes);
});
});
cal.on('beforeDeleteSchedule', function(event) {
var data = event.schedule;
deleteEvent(event.schedule, function (err) {
if (err) {
console.error(err);
return void UI.warn(err);
}
cal.deleteSchedule(data.id, data.calendarId);
});
});
updateDateRange();
renderCalendar();
// Toolbar
// Change view mode
var options = ['day', 'week', 'month'].map(function (k) {
return {
tag: 'a',
attributes: {
'class': 'cp-calendar-view',
'data-value': k,
'href': '#',
},
content: Messages['calendar_'+k]
// Messages.calendar_day
// Messages.calendar_week
// Messages.calendar_month
};
});
var dropdownConfig = {
text: Messages.calendar_week,
options: options, // Entries displayed in the menu
isSelect: true,
common: common,
caretDown: true,
left: true,
};
var $block = UIElements.createDropdown(dropdownConfig);
$block.setValue('week');
var $views = $block.find('a');
$views.click(function () {
var mode = $(this).attr('data-value');
cal.changeView(mode);
updateDateRange();
});
APP.toolbar.$bottomR.append($block);
// New event button
var newEventBtn = h('button', [
h('i.fa.fa-plus'),
h('span', Messages.calendar_newEvent)
]);
$(newEventBtn).click(function (e) {
e.preventDefault();
cal.openCreationPopup({isAllDay:false});
}).appendTo(APP.toolbar.$bottomL);
// Change page
var goLeft = h('button.fa.fa-chevron-left');
var goRight = h('button.fa.fa-chevron-right');
var goToday = h('button', Messages.calendar_today);
$(goLeft).click(function () {
cal.prev();
updateDateRange();
});
$(goRight).click(function () {
cal.next();
updateDateRange();
});
$(goToday).click(function () {
cal.today();
updateDateRange();
});
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
goLeft, goToday, goRight
]));
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.calendar,
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
var onEvent = function (obj) {
var ev = obj.ev;
var data = obj.data;
if (ev === 'UPDATE') {
onCalendarUpdate(data);
return;
}
};
nThen(function (waitFor) {
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
APP.$toolbar = $('#cp-toolbar');
sframeChan = common.getSframeChannel();
sframeChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
// Fix flatpickr selection
var MutationObserver = window.MutationObserver;
var onFlatPickr = function (el) {
// Don't close event creation popup when clicking on flatpickr
$(el).mousedown(function (e) {
e.stopPropagation();
});
};
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var node;
for (var i = 0; i < mutation.addedNodes.length; i++) {
node = mutation.addedNodes[i];
if (node.classList && node.classList.contains('flatpickr-calendar')) {
onFlatPickr(node);
}
}
});
});
observer.observe($('body')[0], {
childList: true,
subtree: false
});
// Customize creation/update popup
var onCalendarPopup = function (el) {
var $el = $(el);
$el.find('.tui-full-calendar-confirm').addClass('btn btn-primary').prepend(h('i.fa.fa-floppy-o'));
$el.find('input').attr('autocomplete', 'off');
$el.find('.tui-full-calendar-dropdown-button').addClass('btn btn-secondary');
$el.find('.tui-full-calendar-popup-close').addClass('btn btn-cancel fa fa-times cp-calendar-close').empty();
var calendars = APP.calendars || {};
var show = false;
$el.find('.tui-full-calendar-dropdown-menu li').each(function (i, li) {
var $li = $(li);
var id = $li.attr('data-calendar-id');
var c = calendars[id];
if (!c || c.readOnly) {
return void $li.remove();
}
// If at least one calendar is editable, show the popup
show = true;
});
if ($el.find('.tui-full-calendar-hide.tui-full-calendar-dropdown').length || !show) {
$el.hide();
UI.warn(Messages.calendar_errorNoCalendar);
return;
}
var isUpdate = Boolean($el.find('#tui-full-calendar-schedule-title').val());
if (!isUpdate) { $el.find('.tui-full-calendar-dropdown-menu li').first().click(); }
};
var onCalendarEditPopup = function (el) {
var $el = $(el);
$el.find('.tui-full-calendar-popup-edit').addClass('btn btn-primary');
$el.find('.tui-full-calendar-popup-edit .tui-full-calendar-icon').addClass('fa fa-pencil').removeClass('tui-full-calendar-icon');
$el.find('.tui-full-calendar-popup-delete').addClass('btn btn-danger');
$el.find('.tui-full-calendar-popup-delete .tui-full-calendar-icon').addClass('fa fa-trash').removeClass('tui-full-calendar-icon');
$el.find('.tui-full-calendar-content').removeClass('tui-full-calendar-content');
};
var onPopupRemoved = function () {
var start, end;
if (window.CP_startPickr) { start = window.CP_startPickr.calendarContainer; }
if (window.CP_endPickr) { end = window.CP_endPickr.calendarContainer; }
$('.flatpickr-calendar').each(function (i, el) {
if (el === start || el === end) { return; }
$(el).remove();
});
};
var observer2 = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var node, _node;
for (var i = 0; i < mutation.addedNodes.length; i++) {
node = mutation.addedNodes[i];
try {
if (node.classList && node.classList.contains('tui-full-calendar-popup')
&& !node.classList.contains('tui-full-calendar-popup-detail')) {
onCalendarPopup(node);
}
if (node.classList && node.classList.contains('tui-full-calendar-popup')
&& node.classList.contains('tui-full-calendar-popup-detail')) {
onCalendarEditPopup(node);
}
} catch (e) {}
}
for (var j = 0; j < mutation.removedNodes.length; j++) {
_node = mutation.addedNodes[j];
try {
if (_node.classList && _node.classList.contains('tui-full-calendar-popup')) {
onPopupRemoved();
}
} catch (e) {}
}
});
});
observer2.observe($('body')[0], {
childList: true,
subtree: true
});
APP.module = common.makeUniversal('calendar', {
onEvent: onEvent
});
APP.module.execCommand('SUBSCRIBE', null, function (obj) {
if (obj.empty && !privateData.calendarHash) {
// No calendar yet, create one
newCalendar({
teamId: 1,
initialCalendar: true,
color: user.color,
title: Messages.calendar_default
}, function (err) {
if (err) { return void UI.errorLoadingScreen(Messages.error); } // XXX
makeCalendar();
UI.removeLoadingScreen();
});
return;
}
if (privateData.calendarHash) {
APP.module.execCommand('OPEN', {
hash: privateData.hashes.editHash || privateData.hashes.viewHash,
password: privateData.password
}, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
}
makeCalendar();
UI.removeLoadingScreen();
});
APP.origin = privateData.origin;
});
});

@ -0,0 +1,24 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, SFCommonO) {
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
}).nThen(function (/*waitFor*/) {
var addData = function (meta) {
meta.calendarHash = Boolean(window.location.hash);
};
SFCommonO.start({
addData: addData,
noRealtime: true,
cache: true,
});
});
});

@ -12,7 +12,7 @@ define(function() {
* You should never remove the drive from this list. * You should never remove the drive from this list.
*/ */
config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
/*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts']; /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'calendar'];
/* The registered only types are apps restricted to registered users. /* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps * You should never remove apps from this list unless you know what you're doing. The apps
* listed here by default can't work without a user account. * listed here by default can't work without a user account.
@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access * users and these users will be redirected to the login page if they still try to access
* the app * the app
*/ */
config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support']; config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support', 'calendar'];
/* CryptPad is available is multiple languages, but only English and French are maintained /* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge * by the developers. The other languages may be outdated, and any missing string for a langauge

@ -15,6 +15,6 @@ define(['/customize/application_config.js'], function (AppConfig) {
MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5, MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5,
MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 5, MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 5,
// Apps // Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications'] criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications', 'calendar']
}; };
}); });

@ -1357,13 +1357,20 @@ define([
var $innerblock = $('<div>', {'class': 'cp-dropdown-content'}); var $innerblock = $('<div>', {'class': 'cp-dropdown-content'});
if (config.left) { $innerblock.addClass('cp-dropdown-left'); } if (config.left) { $innerblock.addClass('cp-dropdown-left'); }
var hide = function () {
window.setTimeout(function () { $innerblock.hide(); }, 0);
};
config.options.forEach(function (o) { config.options.forEach(function (o) {
if (!isValidOption(o)) { return; } if (!isValidOption(o)) { return; }
if (isElement(o)) { return $innerblock.append($(o)); } if (isElement(o)) { return $innerblock.append($(o)); }
var $el = $('<' + o.tag + '>', o.attributes || {}).html(o.content || ''); var $el = $('<' + o.tag + '>', o.attributes || {}).html(o.content || '');
$el.appendTo($innerblock); $el.appendTo($innerblock);
if (typeof(o.action) === 'function') { if (typeof(o.action) === 'function') {
$el.click(o.action); $el.click(function (e) {
var close = o.action(e);
if (close) { hide(); }
});
} }
}); });
@ -1383,10 +1390,6 @@ define([
} }
}; };
var hide = function () {
window.setTimeout(function () { $innerblock.hide(); }, 0);
};
var show = function () { var show = function () {
var wh = $(window).height(); var wh = $(window).height();
var button = $button[0].getBoundingClientRect(); var button = $button[0].getBoundingClientRect();
@ -2931,7 +2934,7 @@ define([
var dismiss = function () { var dismiss = function () {
common.mailbox.dismiss(data, function (err) { common.mailbox.dismiss(data, function (err) {
console.log(err); if (err) { console.log(err); }
}); });
}; };
var answer = function (yes) { var answer = function (yes) {
@ -2940,6 +2943,7 @@ define([
href: msg.content.href, href: msg.content.href,
password: msg.content.password, password: msg.content.password,
title: msg.content.title, title: msg.content.title,
calendar: msg.content.calendar,
answer: yes answer: yes
}, { }, {
channel: msg.content.user.notifications, channel: msg.content.user.notifications,
@ -2975,16 +2979,29 @@ define([
// Add the pad to your drive // Add the pad to your drive
// This command will also add your mailbox to the metadata log // This command will also add your mailbox to the metadata log
// The callback is called when the pad is stored, independantly of the metadata command // The callback is called when the pad is stored, independantly of the metadata command
sframeChan.query('Q_ACCEPT_OWNERSHIP', data, function (err, res) { if (data.calendar) {
if (err || (res && res.error)) { var calendarModule = common.makeUniversal('calendar');
return void console.error(err | res.error); var calendarData = data.calendar;
} calendarData.href = data.href;
UI.log(Messages.saved); calendarData.teamId = 1;
if (autoStoreModal[data.channel]) { calendarModule.execCommand('ADD', calendarData, function (obj) {
autoStoreModal[data.channel].delete(); if (obj && obj.error) {
delete autoStoreModal[data.channel]; console.error(obj.error);
} return void UI.warn(Messages.error);
}); }
});
} else {
sframeChan.query('Q_ACCEPT_OWNERSHIP', data, function (err, res) {
if (err || (res && res.error)) {
return void console.error(err | res.error);
}
UI.log(Messages.saved);
if (autoStoreModal[data.channel]) {
autoStoreModal[data.channel].delete();
delete autoStoreModal[data.channel];
}
});
}
// Remove yourself from the pending owners // Remove yourself from the pending owners
sframeChan.query('Q_SET_PAD_METADATA', { sframeChan.query('Q_SET_PAD_METADATA', {

@ -585,6 +585,18 @@
return isEmoji(emojis[0])? emojis[0]: str[0]; return isEmoji(emojis[0])? emojis[0]: str[0];
}; };
Util.getRandomColor = function (light) {
var getColor = function () {
if (light) {
return Math.floor(Math.random() * 156) + 70;
}
return Math.floor(Math.random() * 200) + 25;
};
return '#' + getColor().toString(16) +
getColor().toString(16) +
getColor().toString(16);
};
if (typeof(module) !== 'undefined' && module.exports) { if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Util; module.exports = Util;
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {

@ -336,6 +336,7 @@ define([
common.mailbox.sendTo("ADD_OWNER", { common.mailbox.sendTo("ADD_OWNER", {
channel: channel, channel: channel,
href: href, href: href,
calendar: opts.calendar,
password: data.password || priv.password, password: data.password || priv.password,
title: data.title || title title: data.title || title
}, { }, {
@ -841,7 +842,7 @@ define([
// In the properties, we should have the edit href if we know it. // In the properties, we should have the edit href if we know it.
// We should know it because the pad is stored, but it's better to check... // We should know it because the pad is stored, but it's better to check...
if (!data.noEditPassword && owned && data.href) { // FIXME SHEET fix password change for sheets if (!data.noEditPassword && !opts.noEditPassword && owned && data.href) { // FIXME SHEET fix password change for sheets
var isOO = parsed.type === 'sheet'; var isOO = parsed.type === 'sheet';
var isFile = parsed.hashData.type === 'file'; var isFile = parsed.hashData.type === 'file';
var isSharedFolder = parsed.type === 'drive'; var isSharedFolder = parsed.type === 'drive';
@ -984,7 +985,7 @@ define([
if (err || (obj && obj.error)) { UI.warn(Messages.error); } if (err || (obj && obj.error)) { UI.warn(Messages.error); }
}); });
}); });
$d.append(h('br')); if (!opts.noEditPassword) { $d.append(h('br')); }
$d.append(h('div', [ $d.append(h('div', [
h('label', Messages.access_destroyPad), h('label', Messages.access_destroyPad),
h('br'), h('br'),
@ -1016,7 +1017,7 @@ define([
var owned = Modal.isOwned(Env, data); var owned = Modal.isOwned(Env, data);
// Request edit access // Request edit access
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned) { if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar) {
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right', var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
Messages.requestEdit_button); Messages.requestEdit_button);
var requestBlock = h('p', requestButton); var requestBlock = h('p', requestButton);
@ -1054,7 +1055,7 @@ define([
var canMute = data.mailbox && owned === true && ( var canMute = data.mailbox && owned === true && (
(typeof (data.mailbox) === "string" && data.owners[0] === edPublic) || (typeof (data.mailbox) === "string" && data.owners[0] === edPublic) ||
data.mailbox[edPublic]); data.mailbox[edPublic]);
if (owned === true) { if (owned === true && !opts.calendar) {
var cbox = UI.createCheckbox('cp-access-mute', Messages.access_muteRequests, !canMute); var cbox = UI.createCheckbox('cp-access-mute', Messages.access_muteRequests, !canMute);
var $cbox = $(cbox); var $cbox = $(cbox);
var spinner = UI.makeSpinner($cbox); var spinner = UI.makeSpinner($cbox);

@ -112,6 +112,7 @@ define([
password: config.password, password: config.password,
isTemplate: config.isTemplate, isTemplate: config.isTemplate,
name: myName, name: myName,
isCalendar: Boolean(config.calendar),
title: title title: title
}, { }, {
viewed: team && team.id, viewed: team && team.id,
@ -123,6 +124,19 @@ define([
} }
// If it's a team with edit right, add the pad directly // If it's a team with edit right, add the pad directly
if (!team) { return; } if (!team) { return; }
if (config.calendar) {
var calendarModule = common.makeUniversal('calendar');
var calendarData = config.calendar;
calendarData.href = href;
calendarData.teamId = team.id;
calendarModule.execCommand('ADD', calendarData, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
});
return;
}
sframeChan.query('Q_STORE_IN_TEAM', { sframeChan.query('Q_STORE_IN_TEAM', {
href: href, href: href,
password: config.password, password: config.password,

@ -19,6 +19,7 @@ define([
'/common/outer/team.js', '/common/outer/team.js',
'/common/outer/messenger.js', '/common/outer/messenger.js',
'/common/outer/history.js', '/common/outer/history.js',
'/common/outer/calendar.js',
'/common/outer/network-config.js', '/common/outer/network-config.js',
'/customize/application_config.js', '/customize/application_config.js',
@ -32,7 +33,7 @@ define([
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Cache, Realtime, Messaging, Pinpad, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History, SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
NetConfig, AppConfig, Calendar, NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) { Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) {
var onReadyEvt = Util.mkEvent(true); var onReadyEvt = Util.mkEvent(true);
@ -178,6 +179,24 @@ define([
typeof(store.proxy.curvePublic) === 'string'; typeof(store.proxy.curvePublic) === 'string';
}; };
Store.isOwned = function (owners) {
var edPublic = store.proxy.edPublic;
// Not logged in? false
if (!edPublic) { return false; }
// No owners? false
if (!Array.isArray(owners) || !owners.length) { return false; }
if (owners.indexOf(edPublic) !== -1) { return true; }
// No team
var teams = store.proxy.teams;
if (!teams) { return false; }
return Object.keys(teams).some(function (id) {
var ed = Util.find(teams[id], ['keys', 'drive', 'edPublic']);
return ed && owners.indexOf(ed) !== -1;
});
};
var getUserChannelList = function () { var getUserChannelList = function () {
var userChannel = store.driveChannel; var userChannel = store.driveChannel;
if (!userChannel) { return null; } if (!userChannel) { return null; }
@ -212,6 +231,13 @@ define([
list = list.concat(mList); list = list.concat(mList);
} }
if (store.proxy.calendars) {
var cList = Object.keys(store.proxy.calendars).map(function (c) {
return store.proxy.calendars[c].channel;
});
list = list.concat(cList);
}
list.push(userChannel); list.push(userChannel);
list.sort(); list.sort();
@ -567,18 +593,10 @@ define([
}; };
// Get or create the user color for the cursor position // Get or create the user color for the cursor position
var getRandomColor = function () { Store.getUserColor = function () {
var getColor = function () {
return Math.floor(Math.random() * 156) + 70;
};
return '#' + getColor().toString(16) +
getColor().toString(16) +
getColor().toString(16);
};
var getUserColor = function () {
var color = Util.find(store, ['proxy', 'settings', 'general', 'cursor', 'color']); var color = Util.find(store, ['proxy', 'settings', 'general', 'cursor', 'color']);
if (!color) { if (!color) {
color = getRandomColor(); color = Util.getRandomColor(true);
Store.setAttribute(null, { Store.setAttribute(null, {
attr: ['general', 'cursor', 'color'], attr: ['general', 'cursor', 'color'],
value: color value: color
@ -602,7 +620,7 @@ define([
uid: proxy.uid || store.noDriveUid, // Random uid in nodrive mode uid: proxy.uid || store.noDriveUid, // Random uid in nodrive mode
avatar: Util.find(proxy, ['profile', 'avatar']), avatar: Util.find(proxy, ['profile', 'avatar']),
profile: Util.find(proxy, ['profile', 'view']), profile: Util.find(proxy, ['profile', 'view']),
color: getUserColor(), color: Store.getUserColor(),
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
curvePublic: proxy.curvePublic, curvePublic: proxy.curvePublic,
}, },
@ -1508,7 +1526,7 @@ define([
} }
}; };
// Teams support offline/cache mode // Teams support offline/cache mode
if (obj.type === "team") { return void todo(); } if (['team', 'calendar'].indexOf(obj.type) !== -1) { return void todo(); }
// If we're in "noDrive" mode // If we're in "noDrive" mode
if (!store.proxy) { return void todo(); } if (!store.proxy) { return void todo(); }
// Other modules should wait for the ready event // Other modules should wait for the ready event
@ -1528,6 +1546,7 @@ define([
broadcast([], "UPDATE_METADATA"); broadcast([], "UPDATE_METADATA");
}, },
pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
unpinPads: function (data, cb) { Store.unpinPads(null, data, cb); },
}, waitFor, function (ev, data, clients) { }, waitFor, function (ev, data, clients) {
clients.forEach(function (cId) { clients.forEach(function (cId) {
postMessage(cId, 'UNIVERSAL_EVENT', { postMessage(cId, 'UNIVERSAL_EVENT', {
@ -2665,6 +2684,8 @@ define([
}, true); }, true);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
loadUniversal(Team, 'team', waitFor, clientId); loadUniversal(Team, 'team', waitFor, clientId);
}).nThen(function (waitFor) {
loadUniversal(Calendar, 'calendar', waitFor);
}).nThen(function () { }).nThen(function () {
cb(); cb();
}); });
@ -2726,6 +2747,7 @@ define([
loadUniversal(Messenger, 'messenger', waitFor); loadUniversal(Messenger, 'messenger', waitFor);
store.messenger = store.modules['messenger']; store.messenger = store.modules['messenger'];
loadUniversal(Profile, 'profile', waitFor); loadUniversal(Profile, 'profile', waitFor);
loadUniversal(Calendar, 'calendar', waitFor);
if (store.modules['team']) { store.modules['team'].onReady(waitFor); } if (store.modules['team']) { store.modules['team'].onReady(waitFor); }
loadUniversal(History, 'history', waitFor); loadUniversal(History, 'history', waitFor);
}).nThen(function () { }).nThen(function () {

@ -0,0 +1,802 @@
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], 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;
}
var m = ctx.store.modules && ctx.store.modules.team;
if (!m) { return; }
return m.getTeam(id);
};
var makeCalendar = function () {
var hash = Hash.createRandomHash('calendar');
var secret = Hash.getSecrets('calendar', hash);
var roHash = Hash.getViewHashFromKeys(secret);
var href = Hash.hashToHref(hash, 'calendar');
var roHref = Hash.hashToHref(roHash, 'calendar');
return {
href: href,
roHref: roHref,
channel: secret.channel,
};
};
var initializeCalendars = function (ctx, cb) {
var proxy = ctx.store.proxy;
proxy.calendars = proxy.calendars || {};
setTimeout(cb);
};
var sendUpdate = function (ctx, c) {
ctx.emit('UPDATE', {
teams: c.stores,
roTeams: c.roStores,
id: c.channel,
loading: !c.ready && !c.cacheready,
readOnly: c.readOnly || (!c.ready && c.cacheready) || c.offline,
offline: c.offline,
deleted: !c.stores.length,
restricted: c.restricted,
owned: ctx.Store.isOwned(c.owners),
content: Util.clone(c.proxy),
hashes: c.hashes
}, ctx.clients);
};
var closeCalendar = function (ctx, id) {
var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; }
// If the calendar doesn't exist in any other team, stop it and delete it from ctx
if (!ctxCal.stores.length) {
ctxCal.lm.stop();
delete ctx.calendars[id];
}
};
var updateLocalCalendars = function (ctx, c, data) {
// Also update local data
c.stores.forEach(function (id) {
var s = getStore(ctx, id);
if (!s || !s.proxy) { return; }
if (!s.rpc) { return; } // team viewer
if (!s.proxy.calendars) { return; }
var cal = s.proxy.calendars[c.channel];
if (!cal) { return; }
if (cal.color !== data.color) { cal.color = data.color; }
if (cal.title !== data.title) { cal.title = data.title; }
});
};
var openChannel = function (ctx, cfg, _cb) {
var cb = Util.once(Util.mkAsync(_cb || function () {}));
var teamId = cfg.storeId;
var data = cfg.data;
var channel = data.channel;
console.error(cfg);
if (!channel) { return; }
var c = ctx.calendars[channel];
var update = function () {
sendUpdate(ctx, c);
};
if (c) {
if (c.readOnly && data.href) {
// Upgrade readOnly calendar to editable
var upgradeParsed = Hash.parsePadUrl(data.href);
var upgradeSecret = Hash.getSecrets('calendar', upgradeParsed.hash, data.password);
var upgradeCrypto = Crypto.createEncryptor(upgradeSecret.keys);
c.hashes.editHash = Hash.getEditHashFromKeys(upgradeSecret);
c.lm.setReadOnly(false, upgradeCrypto);
c.readOnly = false;
} else if (teamId === 0) {
// Existing calendars can't be "temp calendars" (unless they are an upgrade)
return void cb();
}
// Remove from roStores when upgrading this store
if (c.roStores.indexOf(teamId) !== -1 && data.href) {
c.roStores.splice(c.roStores.indexOf(teamId), 1);
// If we've upgraded a stored calendar, remove the temp calendar
if (c.stores.indexOf(0) !== -1) {
c.stores.splice(c.stores.indexOf(0), 1);
}
update();
}
// Don't store duplicates
if (c.stores && c.stores.indexOf(teamId) !== -1) { return void cb(); }
// If we store a temp calendar to our account or team, remove this "temp calendar"
if (c.stores.indexOf(0) !== -1) { c.stores.splice(c.stores.indexOf(0), 1); }
c.stores.push(teamId);
if (!data.href) {
c.roStores.push(teamId);
}
update();
return void cb();
}
// Multiple teams can have the same calendar. Make sure we remember the list of stores
// that know it so that we don't close the calendar when leaving/deleting a team.
c = ctx.calendars[channel] = {
ready: false,
channel: channel,
readOnly: !data.href,
stores: [teamId],
roStores: data.href ? [] : [teamId],
hashes: {}
};
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var secret = Hash.getSecrets('calendar', parsed.hash, data.password);
var crypto = Crypto.createEncryptor(secret.keys);
c.hashes.viewHash = Hash.getViewHashFromKeys(secret);
if (data.href) {
c.hashes.editHash = Hash.getEditHashFromKeys(secret);
}
c.proxy = {
metadata: {
color: data.color,
title: data.title
}
};
update();
var onDeleted = function () {
// Remove this calendar from all our teams
// XXX Maybe not? don't remove automatically so that we can tell the user to do so.
c.stores.forEach(function (storeId) {
var store = getStore(ctx, storeId);
if (!store || !store.rpc || !store.proxy.calendars) { return; }
delete store.proxy.calendars[channel];
// And unpin
var unpin = store.unpin || ctx.unpinPads;
unpin([channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
});
// Close listmap, update the UI and clear the memory
if (c.lm) { c.lm.stop(); }
c.stores = [];
sendUpdate(ctx, c);
delete ctx.calendars[channel];
};
nThen(function (waitFor) {
if (!ctx.store.network || cfg.isNew) { return; }
// This is supposed to be an existing channel. Make sure it exists on the server
// before trying to load it.
// NOTE: if we can't check (error), we can skip this step. On "ready", we have
// another check to make sure we won't make a new calendar
ctx.Store.isNewChannel(null, channel, waitFor(function (obj) {
if (obj && obj.error) {
// If we can't check, skip this part
return;
}
if (obj && typeof(obj.isNew) === "boolean") {
if (obj.isNew) {
onDeleted();
cb({error: 'EDELETED'});
waitFor.abort();
return;
}
}
}));
}).nThen(function () {
// Set the owners as the first store opening it. We don't know yet if it's a new or
// existing calendar. "owners' will be ignored if the calendar already exists.
var edPublic;
if (teamId === 1 || !teamId) {
edPublic = ctx.store.proxy.edPublic;
} else {
var teams = ctx.store.modules.team && ctx.store.modules.team.getTeamsData();
var team = teams && teams[teamId];
edPublic = team ? team.edPublic : undefined;
}
var config = {
data: {},
network: ctx.store.network || ctx.store.networkPromise,
channel: secret.channel,
crypto: crypto,
owners: [edPublic],
ChainPad: ChainPad,
validateKey: secret.keys.validateKey || undefined,
userName: 'calendar',
Cache: Cache,
classic: true,
onRejected: ctx.Store && ctx.Store.onRejected
};
var lm = Listmap.create(config);
c.lm = lm;
var proxy = c.proxy = lm.proxy;
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
}).on('ready', function (info) {
var md = info.metadata;
c.owners = md.owners || [];
c.ready = true;
if (!proxy.metadata) {
if (!cfg.isNew) {
// no metadata on an existing calendar: deleted calendar
return void onDeleted();
}
proxy.metadata = {
color: data.color,
title: data.title
};
}
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
}).on('change', ['metadata'], function () {
// if title or color have changed, update our local values
var md = proxy.metadata;
if (!md || !md.title || !md.color) { return; }
updateLocalCalendars(ctx, c, md);
}).on('disconnect', function () {
c.offline = true;
setTimeout(update);
}).on('reconnect', function () {
c.offline = false;
setTimeout(update);
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
return void onDeleted();
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
}
cb(info);
});
});
};
var decryptTeamCalendarHref = function (store, calData) {
if (!calData.href) { return; }
// Already decrypted? nothing to do
if (calData.href.indexOf('#') !== -1) { return; }
// href exists and is encrypted: decrypt if we can or ignore the href
if (store.secondaryKey) {
try {
calData.href = store.userObject.cryptor.decrypt(calData.href);
} catch (e) {
console.error(e);
delete calData.href;
}
} else {
delete calData.href;
}
};
var initializeStore = function (ctx, store) {
var c = store.proxy.calendars;
var storeId = store.id || 1;
// Add listeners
store.proxy.on('change', ['calendars'], function (o, n, p) {
if (p.length < 2) { return; }
// Handle deletions
if (o && !n) {
(function () {
var id = p[1];
var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; }
var idx = ctxCal.stores.indexOf(storeId);
// Check if the team has loaded this calendar in memory
if (idx === -1) { return; }
// Remove the team from memory
ctxCal.stores.splice(idx, 1);
var roIdx = ctxCal.roStores.indexOf(storeId);
if (roIdx !== -1) { ctxCal.roStores.splice(roIdx, 1); }
// Check if we need to close listmap and update the UI
closeCalendar(ctx, id);
sendUpdate(ctx, ctxCal);
})();
}
// Handle additions
// NOTE: this also upgrade from readOnly to edit (add an "href" to the calendar)
if (!o && n) {
(function () {
var id = p[1];
var _cal = store.proxy.calendars[id];
if (!_cal) { return; }
var cal = Util.clone(_cal);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: storeId,
data: cal
});
})();
}
});
// If this store contains existing calendars, open them
Object.keys(c || {}).forEach(function (channel) {
var cal = Util.clone(c[channel]);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: storeId,
data: cal
});
});
};
var openChannels = function (ctx) {
// Personal drive
initializeStore(ctx, ctx.store);
var teams = ctx.store.modules.team && ctx.store.modules.team.getTeamsData();
if (!teams) { return; }
Object.keys(teams).forEach(function (id) {
var store = getStore(ctx, id);
initializeStore(ctx, store);
});
};
var subscribe = function (ctx, data, cId, cb) {
// Subscribe to new notifications
var idx = ctx.clients.indexOf(cId);
if (idx === -1) {
ctx.clients.push(cId);
}
cb({
empty: !Object.keys(ctx.calendars).length
});
Object.keys(ctx.calendars).forEach(function (channel) {
var c = ctx.calendars[channel] || {};
sendUpdate(ctx, c);
});
};
var openCalendar = function (ctx, data, cId, cb) {
var secret = Hash.getSecrets('calendar', data.hash, data.password);
var hash = Hash.getEditHashFromKeys(secret);
var roHash = Hash.getViewHashFromKeys(secret);
var cal = {
href: hash && Hash.hashToHref(hash, 'calendar'),
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
channel: secret.channel,
color: Util.getRandomColor(),
title: '...'
};
openChannel(ctx, {
storeId: 0,
data: cal
}, cb);
};
var importCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
if (!Array.isArray(c.stores) || c.stores.indexOf(data.teamId) === -1) {
return void cb({error: 'EINVAL'});
}
// Add to my calendars
var store = ctx.store;
var calendars = store.proxy.calendars = store.proxy.calendars || {};
var hash = c.hashes.editHash;
var roHash = c.hashes.viewHash;
calendars[id] = {
href: hash && Hash.hashToHref(hash, 'calendar'),
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
channel: id,
color: Util.find(c,['proxy', 'metadata', 'color']) || Util.getRandomColor(),
title: Util.find(c,['proxy', 'metadata', 'title']) || '...'
};
ctx.Store.onSync(null, cb);
// Make the change in memory
openChannel(ctx, {
storeId: 1,
data: {
href: calendars[id].href,
toHref: calendars[id].roHref,
channel: id
}
});
};
var addCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
// Check team edit rights: viewers in teams don't have rpc
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
var c = store.proxy.calendars = store.proxy.calendars || {};
var parsed = Hash.parsePadUrl(data.href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
if (secret.channel !== data.channel) { return void cb({error: 'EINVAL'}); }
var hash = Hash.getEditHashFromKeys(secret);
var roHash = Hash.getViewHashFromKeys(secret);
var href = hash && Hash.hashToHref(hash, 'calendar');
var cal = {
href: href,
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
color: data.color,
title: data.title,
channel: data.channel
};
// If it already existed and it's not an upgrade, nothing to do
if (c[data.channel] && (c[data.channel].href || !cal.href)) { return void cb(); }
cal.color = data.color;
cal.title = data.title;
openChannel(ctx, {
storeId: store.id || 1,
data: Util.clone(cal)
}, function (err) {
if (err) {
// Can't open this channel, don't store it
console.error(err);
return void cb({error: err.error});
}
if (href && store.id && store.secondaryKey) {
try {
cal.href = store.userObject.cryptor.encrypt(href);
} catch (e) {
console.error(e);
}
}
// Add the calendar and call back
// If it already existed it means this is an upgrade
c[cal.channel] = cal;
var pin = store.pin || ctx.pinPads;
pin([cal.channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
ctx.Store.onSync(store.id, cb);
});
};
var createCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
// Check team edit rights: viewers in teams don't have rpc
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
var c = store.proxy.calendars = store.proxy.calendars || {};
var cal = makeCalendar();
cal.color = data.color;
cal.title = data.title;
openChannel(ctx, {
storeId: store.id || 1,
data: cal,
isNew: true
}, function (err) {
if (err) {
// Can't open this channel, don't store it
console.error(err);
return void cb({error: err.error});
}
// Add the calendar and call back
// Wait for the metadata to be stored (channel fully ready) before adding it
// to our store
var ctxCal = ctx.calendars[cal.channel];
Realtime.whenRealtimeSyncs(ctxCal.lm.realtime, function () {
c[cal.channel] = cal;
var pin = store.pin || ctx.pinPads;
pin([cal.channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
ctx.Store.onSync(store.id, cb);
});
});
};
var updateCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
var md = Util.find(c, ['proxy', 'metadata']);
if (!md) { return void cb({error: 'EINVAL'}); }
md.title = data.title;
md.color = data.color;
Realtime.whenRealtimeSyncs(c.lm.realtime, cb);
sendUpdate(ctx, c);
updateLocalCalendars(ctx, c, data);
};
var deleteCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
if (!store.proxy.calendars) { return; }
var id = data.id;
var cal = store.proxy.calendars[id];
if (!cal) { return void cb(); } // Already deleted
// Delete
delete store.proxy.calendars[id];
// Unpin
var unpin = store.unpin || ctx.unpinPads;
unpin([id], function (res) {
if (res && res.error) { console.error(res.error); }
});
// Clear/update ctx data
// Remove this store from the calendar's clients
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(store.id || 1);
ctxCal.stores.splice(idx, 1);
closeCalendar(ctx, id);
ctx.Store.onSync(store.id, function () {
sendUpdate(ctx, ctxCal);
cb();
});
};
var createEvent = function (ctx, data, cId, cb) {
var id = data.calendarId;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
c.proxy.content[data.id] = data;
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
sendUpdate(ctx, c);
cb();
});
};
var updateEvent = function (ctx, data, cId, cb) {
if (!data || !data.ev) { return void cb({error: 'EINVAL'}); }
var id = data.ev.calendarId;
var c = ctx.calendars[id];
if (!c || !c.proxy || !c.proxy.content) { return void cb({error: "ENOENT"}); }
// Find the event
var ev = c.proxy.content[data.ev.id];
if (!ev) { return void cb({error: "EINVAL"}); }
// update the event
var changes = data.changes || {};
var newC;
if (changes.calendarId) {
newC = ctx.calendars[changes.calendarId];
if (!newC || !newC.proxy) { return void cb({error: "ENOENT"}); }
newC.proxy.content = newC.proxy.content || {};
}
Object.keys(changes).forEach(function (key) {
ev[key] = changes[key];
});
// Move to a different calendar?
if (changes.calendarId && newC) {
newC.proxy.content[data.ev.id] = Util.clone(ev);
delete c.proxy.content[data.ev.id];
}
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
}).nThen(cb);
};
var deleteEvent = function (ctx, data, cId, cb) {
var id = data.calendarId;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
Realtime.whenRealtimeSyncs(c.lm.realtime, cb);
};
var removeClient = function (ctx, cId) {
var idx = ctx.clients.indexOf(cId);
ctx.clients.splice(idx, 1);
};
Calendar.init = function (cfg, waitFor, emit) {
var calendar = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; } // XXX logged in only?
var ctx = {
store: store,
Store: cfg.Store,
pinPads: cfg.pinPads,
unpinPads: cfg.unpinPads,
updateMetadata: cfg.updateMetadata,
emit: emit,
onReady: Util.mkEvent(true),
calendars: {},
clients: [],
};
initializeCalendars(ctx, waitFor(function (err) {
if (err) { return; }
openChannels(ctx);
}));
calendar.closeTeam = function (teamId) {
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(teamId);
if (idx === -1) { return; }
ctxCal.stores.splice(idx, 1);
var roIdx = ctxCal.roStores.indexOf(teamId);
if (roIdx !== -1) { ctxCal.roStores.splice(roIdx, 1); }
closeCalendar(ctx, id);
sendUpdate(ctx, ctxCal);
});
};
calendar.openTeam = function (teamId) {
var store = getStore(ctx, teamId);
if (!store) { return; }
initializeStore(ctx, store);
};
calendar.upgradeTeam = function (teamId) {
if (!teamId) { return; }
var store = getStore(ctx, teamId);
if (!store) { return; }
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(teamId);
if (idx === -1) { return; }
var _cal = store.proxy.calendars[id];
var cal = Util.clone(_cal);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: teamId,
data: cal
});
sendUpdate(ctx, ctxCal);
});
};
calendar.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
calendar.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'OPEN') {
ctx.Store.onReadyEvt.reg(function () {
openCalendar(ctx, data, clientId, cb);
});
return;
}
if (cmd === 'IMPORT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void importCalendar(ctx, data, clientId, cb);
}
if (cmd === 'ADD') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void addCalendar(ctx, data, clientId, cb);
}
if (cmd === 'CREATE') {
if (data.initialCalendar) {
return void ctx.Store.onReadyEvt.reg(function () {
createCalendar(ctx, data, clientId, cb);
});
}
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void createCalendar(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void updateCalendar(ctx, data, clientId, cb);
}
if (cmd === 'DELETE') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void deleteCalendar(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void createEvent(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void updateEvent(ctx, data, clientId, cb);
}
if (cmd === 'DELETE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void deleteEvent(ctx, data, clientId, cb);
}
};
return calendar;
};
return Calendar;
});

@ -186,7 +186,7 @@ define([
ChainPad: ChainPad, ChainPad: ChainPad,
classic: true, classic: true,
network: network, network: network,
Cache: Cache, // ICE shared-folder cache Cache: Cache, // shared-folder cache
metadata: { metadata: {
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
owners: owners owners: owners

@ -130,6 +130,9 @@ define([
delete ctx.store.proxy.teams[teamId]; delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients); ctx.emit('LEAVE_TEAM', teamId, team.clients);
ctx.updateMetadata(); ctx.updateMetadata();
if (ctx.store.calendar) {
ctx.store.calendar.closeTeam(teamId);
}
if (ctx.store.mailbox) { if (ctx.store.mailbox) {
ctx.store.mailbox.close('team-'+teamId, function () { ctx.store.mailbox.close('team-'+teamId, function () {
// Close team mailbox // Close team mailbox
@ -154,6 +157,13 @@ define([
if (membersChannel) { list.push(membersChannel); } if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); } if (mailboxChannel) { list.push(mailboxChannel); }
if (store.proxy.calendars) {
var cList = Object.keys(store.proxy.calendars).map(function (c) {
return store.proxy.calendars[c].channel;
});
list = list.concat(cList);
}
var state = store.roster.getState(); var state = store.roster.getState();
if (state.members) { if (state.members) {
Object.keys(state.members).forEach(function (curve) { Object.keys(state.members).forEach(function (curve) {
@ -338,6 +348,7 @@ define([
}); });
}, true); }, true);
}).nThen(function () { }).nThen(function () {
if (ctx.store.calendar) { ctx.store.calendar.openTeam(id); }
cb(); cb();
}); });
}; };
@ -1249,6 +1260,13 @@ define([
team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey);
} }
// Upgrade? update calendar rights
if (secret.keys.secondaryKey) {
try {
ctx.store.modules.calendar.upgradeTeam(teamId);
} catch (e) { console.error(e); }
}
if (!secret.keys.secondaryKey && team.rpc) { if (!secret.keys.secondaryKey && team.rpc) {
team.rpc.destroy(); team.rpc.destroy();
} }

@ -15,7 +15,9 @@ define([
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify", "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf", //"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker" //"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
cm: '/bower_components/codemirror' cm: '/bower_components/codemirror',
'tui-code-snippet': '/lib/calendar/tui-code-snippet.min',
'tui-date-picker': '/lib/calendar/date-picker',
}, },
map: { map: {
'*': { '*': {

@ -571,7 +571,7 @@ define([
var defaultTitle = Utils.UserObject.getDefaultName(parsed); var defaultTitle = Utils.UserObject.getDefaultName(parsed);
var edPublic, curvePublic, notifications, isTemplate; var edPublic, curvePublic, notifications, isTemplate;
var settings = {}; var settings = {};
var isSafe = ['debug', 'profile', 'drive', 'teams'].indexOf(currentPad.app) !== -1; var isSafe = ['debug', 'profile', 'drive', 'teams', 'calendar'].indexOf(currentPad.app) !== -1;
var isDeleted = isNewFile && currentPad.hash.length > 0; var isDeleted = isNewFile && currentPad.hash.length > 0;
if (isDeleted) { if (isDeleted) {

@ -0,0 +1,53 @@
define([
'jquery',
'/lib/datepicker/flatpickr.js',
'css!/lib/datepicker/flatpickr.min.css',
], function ($, Flatpickr) {
var createRangePicker = function (cfg) {
var start = cfg.startpicker;
var end = cfg.endpicker;
var is24h = false
try {
is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/);
} catch (e) {}
var e = $(end.input)[0];
var endPickr = Flatpickr(e, {
enableTime: true,
time_24hr: is24h,
minDate: start.date
});
endPickr.setDate(end.date);
var s = $(start.input)[0];
var startPickr = Flatpickr(s, {
enableTime: true,
time_24hr: is24h,
onChange: function () {
endPickr.set('minDate', startPickr.parseDate(s.value));
}
});
startPickr.setDate(start.date);
window.CP_startPickr = startPickr;
window.CP_endPickr = endPickr;
var getStartDate = function () {
setTimeout(function () { $(startPickr.calendarContainer).remove(); });
return startPickr.parseDate(s.value);
};
var getEndDate = function () {
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
return endPickr.parseDate(e.value);
};
return {
getStartDate: getStartDate,
getEndDate: getEndDate,
};
};
return {
createRangePicker: createRangePicker
};
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save