Basic calendar app

This commit is contained in:
yflory 2021-04-02 13:44:28 +02:00
parent d853466ad2
commit bbd35d0307
18 changed files with 713 additions and 9 deletions

View File

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

View File

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

View File

@ -13,9 +13,14 @@
color: @cp_forms-fg;
background-color: @cp_forms-bg;
border: 1px solid @cp_forms-border;
width: 100%;
font-size: 100%;
padding: @alertify_padding-base;
&:not(.tui-full-calendar-content) {
width: 100%;
}
&.tui-full-calendar-content {
font-size: @colortheme_app-font-size;
}
&[readonly] {
background-color: @cp_forms-readonly;
border-color: @cp_forms-readonly-border;

View File

@ -0,0 +1,66 @@
@import (reference) '../../customize/src/less2/include/framework.less';
&.cp-app-calendar {
.framework_min_main();
display: flex;
flex-flow: column;
#cp-calendar {
.tui-full-calendar-layout {
background-color: @cp_sidebar-right-bg !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-popup-section-item {
height: auto;
&:not(button) {
border: none;
display: inline-flex;
align-items: center;
padding-left: 0;
input { flex: 1; }
}
}
.tui-full-calendar-popup-section {
display: flex;
align-items: center;
}
.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;
background-color: @cp_dropdown-bg;
color: @cp_dropdown-fg;
}
.tui-full-calendar-section-state, #tui-full-calendar-schedule-private {
display: none;
}
.cp-calendar-close {
height: auto;
line-height: initial;
border: 1px solid;
&:not(:hover) {
background: transparent;
}
}
}
}

12
www/calendar/index.html Normal file
View File

@ -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>

18
www/calendar/inner.html Normal file
View File

@ -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-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>

231
www/calendar/inner.js Normal file
View File

@ -0,0 +1,231 @@
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',
'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
)
{
var APP = window.APP = {
};
console.log(Calendar);
var common;
var sframeChan;
Messages.calendar = "Calendar"; // XXX
var updateCalendar = function (data) {
console.log(data);
};
var templates = {
popupSave: function () {
return Messages.settings_save;
}
};
var makeCalendar = function (ctx) {
var $container = $('#cp-container');
$container.append([
h('div#menu', [
h('span#renderRange.render-range')
]),
h('div#cp-calendar')
]);
var cal = new Calendar('#cp-calendar', {
defaultView: 'week', // weekly view option
useCreationPopup: true,
useDetailPopup: true,
usageStatistics: false,
calendars: [{
id: '1',
name: 'My Calendar',
color: '#ffffff',
bgColor: '#9e5fff',
dragBgColor: '#9e5fff',
borderColor: '#9e5fff'
}, {
id: '2',
name: 'Company',
color: '#00a9ff',
bgColor: '#00a9ff',
dragBgColor: '#00a9ff',
borderColor: '#00a9ff'
}]
});
cal.on('beforeCreateSchedule', function(event) {
var startTime = event.start;
var endTime = event.end;
var isAllDay = event.isAllDay;
var guide = event.guide;
var triggerEventName = event.triggerEventName;
// 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,
end: event.end,
};
/*
if (triggerEventName === 'click') {
// open writing simple schedule popup
schedule = {
};
} else if (triggerEventName === 'dblclick') {
// open writing detail schedule popup
schedule = {
};
}
*/
cal.createSchedules([schedule]);
});
};
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') {
console.log('Update');
updateCalendar(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();
// 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++) {
var 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');
$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();
console.log(el);
};
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var node;
for (var i = 0; i < mutation.addedNodes.length; i++) {
var node = mutation.addedNodes[i];
if (node.classList && node.classList.contains('tui-full-calendar-popup')) {
onCalendarPopup(node);
}
}
});
});
observer.observe($('body')[0], {
childList: true,
subtree: true
});
APP.module = common.makeUniversal('calendar', {
onEvent: onEvent
});
APP.module.execCommand('SUBSCRIBE', null, function () {
console.error('subscribed');
// XXX build UI
makeCalendar();
UI.removeLoadingScreen();
});
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
APP.origin = privateData.origin;
});
});

32
www/calendar/main.js Normal file
View File

@ -0,0 +1,32 @@
// 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 getSecrets = function (Cryptpad, Utils, cb) {
// XXX open calendar from URL
};
var addData = function (meta, Cryptpad, user) {
// XXX flag when opening URL
meta.isOwnProfile = !window.location.hash ||
window.location.hash.slice(1) === user.profile;
};
SFCommonO.start({
//getSecrets: getSecrets,
//noHash: true, // Don't add the hash in the URL if it doesn't already exist
//addRpc: addRpc,
//addData: addData,
//owned: true,
noRealtime: true
});
});
});

View File

@ -12,7 +12,7 @@ define(function() {
* You should never remove the drive from this list.
*/
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.
* 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.
@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* 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
* by the developers. The other languages may be outdated, and any missing string for a langauge

View File

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

View File

@ -466,7 +466,6 @@ define([
return {
add: function(common, data) {
console.log(data);
var type = data.content.msg.type;
if (handlers[type]) {

View File

@ -19,6 +19,7 @@ define([
'/common/outer/team.js',
'/common/outer/messenger.js',
'/common/outer/history.js',
'/common/outer/calendar.js',
'/common/outer/network-config.js',
'/customize/application_config.js',
@ -32,7 +33,7 @@ define([
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
NetConfig, AppConfig,
Calendar, NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) {
var onReadyEvt = Util.mkEvent(true);
@ -574,7 +575,7 @@ define([
getColor().toString(16) +
getColor().toString(16);
};
var getUserColor = function () {
Store.getUserColor = function () {
var color = Util.find(store, ['proxy', 'settings', 'general', 'cursor', 'color']);
if (!color) {
color = getRandomColor();
@ -601,7 +602,7 @@ define([
uid: proxy.uid || store.noDriveUid, // Random uid in nodrive mode
avatar: Util.find(proxy, ['profile', 'avatar']),
profile: Util.find(proxy, ['profile', 'view']),
color: getUserColor(),
color: Store.getUserColor(),
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
curvePublic: proxy.curvePublic,
},
@ -2712,6 +2713,7 @@ define([
loadUniversal(Messenger, 'messenger', waitFor);
store.messenger = store.modules['messenger'];
loadUniversal(Profile, 'profile', waitFor);
loadUniversal(Calendar, 'calendar', waitFor);
if (store.modules['team']) { store.modules['team'].onReady(waitFor); }
loadUniversal(History, 'history', waitFor);
}).nThen(function () {

View File

@ -0,0 +1,261 @@
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/customize/messages.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, Messages, 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: {
own: calendar,
extra: {
uid: calendar,
uid: calendar
}
}
}
* Team drive
{
calendars: {
own: calendar,
extra: {
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 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;
var calendars = proxy.calendars = proxy.calendars || {};
if (!calendars.own) {
var own = calendars.own = makeCalendar(true);
own.color = ctx.Store.getUserColor();
}
setTimeout(cb);
// XXX for each team, if we have edit rights, create the team calendar?
// XXX or maybe do it from the team app?
};
var sendUpdate = function (ctx, c) {
ctx.emit('UPDATE', {
teams: c.stores,
id: c.channel,
readOnly: c.readOnly,
data: Util.clone(c.proxy)
}, ctx.clients);
};
var openChannel = function (ctx, cfg) {
var teamId = cfg.storeId;
var data = cfg.data;
var channel = data.channel;
if (!channel) { return; }
var c = ctx.calendars[channel];
if (c) {
if (c.stores && c.stores.indexOf(teamId) !== -1) { return; }
if (c.readOnly && data.href) {
// XXX UPGRADE
// XXX different cases if already ready or not?
}
c.stores.push(teamId);
return;
}
// 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]
};
var update = function () {
console.log(ctx.clients);
sendUpdate(ctx, c);
};
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var secret = Hash.getSecrets('calendar', parsed.hash);
var crypto = Crypto.createEncryptor(secret.keys);
// 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) {
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, // XXX offline
channel: secret.channel,
crypto: crypto,
owners: [edPublic],
ChainPad: ChainPad,
validateKey: secret.keys.validateKey || undefined,
userName: 'calendar',
classic: true
};
console.error(channel, config);
var lm = Listmap.create(config);
c.lm = lm;
c.proxy = lm.proxy;
lm.proxy.on('ready', function () {
c.ready = true;
console.warn('READY', channel);
setTimeout(update);
}).on('change', [], function () {
setTimeout(update);
});
};
var openChannels = function (ctx) {
var findFromStore = function (store) {
var c = store.proxy.calendars;
if (!c) { return; }
if (c.own) {
openChannel(ctx, {
storeId: store.id || 1,
data: c.own
});
}
if (c.extra) {
Object.keys(c.extra).forEach(function (channel) {
openChannel(ctx, {
storeId: store.id || 1,
data: c.extra[channel]
});
});
}
};
// Personal drive
findFromStore(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();
Object.keys(ctx.calendars).forEach(function (channel) {
var c = ctx.calendars[channel] || {};
console.log(channel, c);
if (!c.ready) { return; }
sendUpdate(ctx, c);
});
};
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,
updateMetadata: cfg.updateMetadata,
emit: emit,
onReady: Util.mkEvent(true),
calendars: {},
clients: [],
};
initializeCalendars(ctx, waitFor(function (err) {
if (err) { return; }
openChannels(ctx);
}));
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);
}
};
return calendar;
};
return Calendar;
});

View File

@ -15,7 +15,9 @@ define([
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
//"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: {
'*': {

View File

@ -0,0 +1,46 @@
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;
console.log(cfg);
console.error(start, end);
var e = $(end.input)[0];
var endPickr = Flatpickr(e, {
enableTime: true,
minDate: start.date
});
endPickr.setDate(end.date);
var s = $(start.input)[0];
var startPickr = Flatpickr(s, {
enableTime: true,
onChange: function () {
endPickr.set('minDate', startPickr.parseDate(s.value));
}
});
startPickr.setDate(start.date);
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
};
});

7
www/lib/calendar/tui-calendar.min.css vendored Normal file

File diff suppressed because one or more lines are too long

8
www/lib/calendar/tui-calendar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long